root / pykota / trunk / pykota / accounters / snmp.py @ 3530

Revision 3530, 17.0 kB (checked in by jerome, 14 years ago)

Removed gettext marker for debugging messages.
Reworked bse's patch in order to fix #54 with suggestions from mhyclak.
This code is currently UNTESTED.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Auth Date Rev Id
RevLine 
[3489]1# -*- coding: utf-8 -*-
[2205]2#
[3260]3# PyKota : Print Quotas for CUPS
[2205]4#
[3481]5# (c) 2003-2009 Jerome Alet <alet@librelogiciel.com>
[3260]6# This program is free software: you can redistribute it and/or modify
[2205]7# it under the terms of the GNU General Public License as published by
[3260]8# the Free Software Foundation, either version 3 of the License, or
[2205]9# (at your option) any later version.
[3413]10#
[2205]11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14# GNU General Public License for more details.
[3413]15#
[2205]16# You should have received a copy of the GNU General Public License
[3260]17# along with this program.  If not, see <http://www.gnu.org/licenses/>.
[2205]18#
19# $Id$
20#
21#
22
[3025]23"""This module is used to extract printer's internal page counter
24and status informations using SNMP queries.
25
26The values extracted are defined at least in RFC3805 and RFC2970.
27"""
28
[2205]29
30import sys
31import os
32import time
[2319]33import select
[3161]34import socket
[2205]35
36try :
[2877]37    from pysnmp.entity.rfc3413.oneliner import cmdgen
[3413]38except ImportError :
[3506]39    raise RuntimeError, "The pysnmp v4.x module is not available. Download it from http://pysnmp.sf.net/\nPyKota doesn't support earlier releases anymore."
[2877]40
[3175]41from pykota import constants
42
[3413]43#
[2877]44# Documentation taken from RFC 3805 (Printer MIB v2) and RFC 2790 (Host Resource MIB)
45#
46pageCounterOID = "1.3.6.1.2.1.43.10.2.1.4.1.1"  # SNMPv2-SMI::mib-2.43.10.2.1.4.1.1
47hrPrinterStatusOID = "1.3.6.1.2.1.25.3.5.1.1.1" # SNMPv2-SMI::mib-2.25.3.5.1.1.1
48printerStatusValues = { 1 : 'other',
49                        2 : 'unknown',
50                        3 : 'idle',
51                        4 : 'printing',
52                        5 : 'warmup',
53                      }
54hrDeviceStatusOID = "1.3.6.1.2.1.25.3.2.1.5.1" # SNMPv2-SMI::mib-2.25.3.2.1.5.1
55deviceStatusValues = { 1 : 'unknown',
56                       2 : 'running',
57                       3 : 'warning',
58                       4 : 'testing',
59                       5 : 'down',
[3413]60                     }
[2877]61hrPrinterDetectedErrorStateOID = "1.3.6.1.2.1.25.3.5.1.2.1" # SNMPv2-SMI::mib-2.25.3.5.1.2.1
[3025]62printerDetectedErrorStateValues = [ { 128 : 'Low Paper',
63                                       64 : 'No Paper',
64                                       32 : 'Low Toner',
65                                       16 : 'No Toner',
66                                        8 : 'Door Open',
67                                        4 : 'Jammed',
68                                        2 : 'Offline',
69                                        1 : 'Service Requested',
70                                    },
71                                    { 128 : 'Input Tray Missing',
72                                       64 : 'Output Tray Missing',
73                                       32 : 'Marker Supply Missing',
74                                       16 : 'Output Near Full',
75                                        8 : 'Output Full',
76                                        4 : 'Input Tray Empty',
77                                        2 : 'Overdue Preventive Maintainance',
78                                        1 : 'Not Assigned in RFC3805',
79                                    },
[3413]80                                  ]
81
[3190]82# The default error mask to use when checking error conditions.
83defaultErrorMask = 0x4fcc # [ 'No Paper',
84                          #   'Door Open',
85                          #   'Jammed',
86                          #   'Offline',
87                          #   'Service Requested',
88                          #   'Input Tray Missing',
89                          #   'Output Tray Missing',
90                          #   'Output Full',
91                          #   'Input Tray Empty',
92                          # ]
[3413]93
94# WARNING : some printers don't support this one :
[2877]95prtConsoleDisplayBufferTextOID = "1.3.6.1.2.1.43.16.5.1.2.1.1" # SNMPv2-SMI::mib-2.43.16.5.1.2.1.1
96class BaseHandler :
97    """A class for SNMP print accounting."""
[3162]98    def __init__(self, parent, printerhostname, skipinitialwait=False) :
[2877]99        self.parent = parent
100        self.printerHostname = printerhostname
[3162]101        self.skipinitialwait = skipinitialwait
[2877]102        try :
103            self.community = self.parent.arguments.split(":")[1].strip()
[3413]104        except IndexError :
[2877]105            self.community = "public"
106        self.port = 161
[3025]107        self.initValues()
[3413]108
109    def initValues(self) :
[3025]110        """Initializes SNMP values."""
[2877]111        self.printerInternalPageCounter = None
112        self.printerStatus = None
113        self.deviceStatus = None
[3025]114        self.printerDetectedErrorState = None
115        self.timebefore = time.time()   # resets timer also in case of error
[3413]116
117    def retrieveSNMPValues(self) :
[2877]118        """Retrieves a printer's internal page counter and status via SNMP."""
119        raise RuntimeError, "You have to overload this method."
[3413]120
121    def extractErrorStates(self, value) :
[3025]122        """Returns a list of textual error states from a binary value."""
123        states = []
124        for i in range(min(len(value), len(printerDetectedErrorStateValues))) :
125            byte = ord(value[i])
126            bytedescription = printerDetectedErrorStateValues[i]
127            for (k, v) in bytedescription.items() :
128                if byte & k :
129                    states.append(v)
[3413]130        return states
131
132    def checkIfError(self, errorstates) :
[3025]133        """Checks if any error state is fatal or not."""
[3028]134        if errorstates is None :
135            return True
136        else :
[3190]137            try :
138                errormask = self.parent.filter.config.getPrinterSNMPErrorMask(self.parent.filter.PrinterName)
[3413]139            except AttributeError : # debug mode
[3190]140                errormask = defaultErrorMask
141            if errormask is None :
142                errormask = defaultErrorMask
143            errormaskbytes = [ chr((errormask & 0xff00) >> 8),
144                               chr((errormask & 0x00ff)),
145                             ]
146            errorConditions = self.extractErrorStates(errormaskbytes)
147            self.parent.filter.logdebug("Error conditions for mask 0x%04x : %s" \
148                                               % (errormask, errorConditions))
[3028]149            for err in errorstates :
150                if err in errorConditions :
[3190]151                    self.parent.filter.logdebug("Error condition '%s' encountered. PyKota will wait until this problem is fixed." % err)
[3028]152                    return True
[3190]153            self.parent.filter.logdebug("No error condition matching mask 0x%04x" % errormask)
[3413]154            return False
155
[2877]156    def waitPrinting(self) :
157        """Waits for printer status being 'printing'."""
[3180]158        statusstabilizationdelay = constants.get(self.parent.filter, "StatusStabilizationDelay")
[3530]159        increment = 1
[3180]160        noprintingmaxdelay = constants.get(self.parent.filter, "NoPrintingMaxDelay")
[3025]161        if not noprintingmaxdelay :
162            self.parent.filter.logdebug("Will wait indefinitely until printer %s is in 'printing' state." % self.parent.filter.PrinterName)
[3413]163        else :
[3025]164            self.parent.filter.logdebug("Will wait until printer %s is in 'printing' state or %i seconds have elapsed." % (self.parent.filter.PrinterName, noprintingmaxdelay))
[2877]165        previousValue = self.parent.getLastPageCounter()
166        firstvalue = None
[3529]167        while True :
[2877]168            self.retrieveSNMPValues()
[3530]169            waitdelay = statusstabilizationdelay * increment
170            error = self.checkIfError(self.printerDetectedErrorState)
[3529]171            pstatusAsString = printerStatusValues.get(self.printerStatus)
[3530]172            dstatusAsString = deviceStatusValues.get(self.deviceStatus)
[3529]173            if pstatusAsString in ('printing', 'warmup') :
[2877]174                break
[3413]175            if self.printerInternalPageCounter is not None :
[2877]176                if firstvalue is None :
177                    # first time we retrieved a page counter, save it
178                    firstvalue = self.printerInternalPageCounter
[3413]179                else :
[2877]180                    # second time (or later)
181                    if firstvalue < self.printerInternalPageCounter :
182                        # Here we have a printer which lies :
183                        # it says it is not printing or warming up
184                        # BUT the page counter increases !!!
185                        # So we can probably quit being sure it is printing.
186                        self.parent.filter.printInfo("Printer %s is lying to us !!!" % self.parent.filter.PrinterName, "warn")
187                        break
[3025]188                    elif noprintingmaxdelay \
189                         and ((time.time() - self.timebefore) > noprintingmaxdelay) \
[3530]190                         and not error :
[2877]191                        # More than X seconds without the printer being in 'printing' mode
192                        # We can safely assume this won't change if printer is now 'idle'
193                        if (pstatusAsString == 'idle') or \
194                            ((pstatusAsString == 'other') and \
195                             (dstatusAsString == 'running')) :
196                            if self.printerInternalPageCounter == previousValue :
197                                # Here the job won't be printed, because probably
198                                # the printer rejected it for some reason.
199                                self.parent.filter.printInfo("Printer %s probably won't print this job !!!" % self.parent.filter.PrinterName, "warn")
[3413]200                            else :
[2877]201                                # Here the job has already been entirely printed, and
202                                # the printer has already passed from 'idle' to 'printing' to 'idle' again.
203                                self.parent.filter.printInfo("Printer %s has probably already printed this job !!!" % self.parent.filter.PrinterName, "warn")
204                            break
[3530]205                    if error or (dstatusAsString == "down") :
206                        if waitdelay < constants.FIVEMINUTES :
207                            increment *= 2
208            self.parent.filter.logdebug("Waiting %s seconds for printer %s to be printing..." % (waitdelay, self.parent.filter.PrinterName))
209            time.sleep(waitdelay)
[3413]210
[2877]211    def waitIdle(self) :
212        """Waits for printer status being 'idle'."""
[3180]213        statusstabilizationdelay = constants.get(self.parent.filter, "StatusStabilizationDelay")
214        statusstabilizationloops = constants.get(self.parent.filter, "StatusStabilizationLoops")
[3530]215        increment = 1
216        idle_num = 0
[3529]217        while True :
[2877]218            self.retrieveSNMPValues()
[3530]219            waitdelay = statusstabilizationdelay * increment
220            error = self.checkIfError(self.printerDetectedErrorState)
[2877]221            pstatusAsString = printerStatusValues.get(self.printerStatus)
222            dstatusAsString = deviceStatusValues.get(self.deviceStatus)
[3530]223            idle_flag = False
224            if (not error) and ((pstatusAsString == 'idle') or \
225                                    ((pstatusAsString == 'other') and \
226                                         (dstatusAsString == 'running'))) :
227                idle_flag = True # Standby / Powersave is considered idle
228                increment = 1 # Reset initial stabilization delay
[3413]229            if idle_flag :
[3163]230                if (self.printerInternalPageCounter is not None) \
231                   and self.skipinitialwait \
232                   and (os.environ.get("PYKOTAPHASE") == "BEFORE") :
233                    self.parent.filter.logdebug("No need to wait for the printer to be idle, it is the case already.")
[3413]234                    return
[2877]235                idle_num += 1
[3180]236                if idle_num >= statusstabilizationloops :
[2877]237                    # printer status is stable, we can exit
238                    break
[3413]239            else :
[2877]240                idle_num = 0
[3530]241            if error or (dstatusAsString == "down") :
242                if waitdelay < constants.FIVEMINUTES :
243                    increment *= 2
244            self.parent.filter.logdebug("Waiting %s seconds for printer %s's idle status to stabilize..." % (waitdelay,
245                                                                                                             self.parent.filter.PrinterName))
246            time.sleep(waitdelay)
[3413]247
[2877]248    def retrieveInternalPageCounter(self) :
249        """Returns the page counter from the printer via internal SNMP handling."""
250        try :
251            if (os.environ.get("PYKOTASTATUS") != "CANCELLED") and \
252               (os.environ.get("PYKOTAACTION") == "ALLOW") and \
253               (os.environ.get("PYKOTAPHASE") == "AFTER") and \
254               self.parent.filter.JobSizeBytes :
255                self.waitPrinting()
[3413]256            self.waitIdle()
257        except :
[3530]258            self.parent.filter.printInfo("SNMP querying stage interrupted. Using latest value seen for internal page counter (%s) on printer %s." % (self.printerInternalPageCounter, self.parent.filter.PrinterName), "warn")
[2877]259            raise
260        return self.printerInternalPageCounter
[3413]261
[3506]262class Handler(BaseHandler) :
263    """A class for pysnmp v4.x, PyKota doesn't support earlier releases of pysnmp anymore.'"""
264    def __init__(self, *args):
265        BaseHandler.__init__(self, *args)
266        self.snmpEngine = cmdgen.CommandGenerator()
267        self.snmpAuth = cmdgen.CommunityData("pykota", self.community, 0)
268        self.snmpTarget = cmdgen.UdpTransportTarget((self.printerHostname, self.port))
269
270    def retrieveSNMPValues(self) :
271        """Retrieves a printer's internal page counter and status via SNMP."""
272        try :
273            errorIndication, errorStatus, errorIndex, varBinds = \
274                self.snmpEngine.getCmd(self.snmpAuth, \
275                                       self.snmpTarget, \
276                                       tuple([int(i) for i in pageCounterOID.split('.')]), \
277                                       tuple([int(i) for i in hrPrinterStatusOID.split('.')]), \
278                                       tuple([int(i) for i in hrDeviceStatusOID.split('.')]), \
279                                       tuple([int(i) for i in hrPrinterDetectedErrorStateOID.split('.')]))
280        except socket.gaierror, msg :
281            errorIndication = repr(msg)
282        except :
283            errorIndication = "Unknown SNMP/Network error. Check your wires."
284        if errorIndication :
285            self.parent.filter.printInfo("SNMP Error : %s" % errorIndication, "error")
286            self.initValues()
287        elif errorStatus :
288            self.parent.filter.printInfo("SNMP Error : %s at %s" % (errorStatus.prettyPrint(), \
[2877]289                                                                        varBinds[int(errorIndex)-1]), \
290                                             "error")
[3506]291            self.initValues()
292        else :
293            self.printerInternalPageCounter = max(self.printerInternalPageCounter, int(varBinds[0][1].prettyPrint() or "0"))
[3517]294            try :
295                self.printerStatus = int(varBinds[1][1].prettyPrint())
296            except ValueError :
[3519]297                self.parent.filter.logdebug("The printer reported a non-integer printer status, it will be converted to 2 ('unknown')")
[3517]298                self.printerStatus = 2
299            try :
300                self.deviceStatus = int(varBinds[2][1].prettyPrint())
301            except ValueError :
302                self.parent.filter.logdebug("The printer reported a non-integer device status, it will be converted to 1 ('unknown')")
303                self.deviceStatus = 1
[3506]304            self.printerDetectedErrorState = self.extractErrorStates(str(varBinds[3][1]))
305            self.parent.filter.logdebug("SNMP answer decoded : PageCounter : %s  PrinterStatus : '%s'  DeviceStatus : '%s'  PrinterErrorState : '%s'" \
306                                            % (self.printerInternalPageCounter, \
307                                                   printerStatusValues.get(self.printerStatus), \
308                                                   deviceStatusValues.get(self.deviceStatus), \
309                                                   self.printerDetectedErrorState))
[3413]310
[2506]311def main(hostname) :
312    """Tries SNMP accounting for a printer host."""
313    class fakeFilter :
[2830]314        """Fakes a filter for testing purposes."""
[2506]315        def __init__(self) :
[2830]316            """Initializes the fake filter."""
[2506]317            self.PrinterName = "FakePrintQueue"
318            self.JobSizeBytes = 1
[3413]319
[2506]320        def printInfo(self, msg, level="info") :
[2830]321            """Prints informational message."""
[2506]322            sys.stderr.write("%s : %s\n" % (level.upper(), msg))
323            sys.stderr.flush()
[3413]324
325        def logdebug(self, msg) :
[2830]326            """Prints debug message."""
[2506]327            self.printInfo(msg, "debug")
[3413]328
329    class fakeAccounter :
[2830]330        """Fakes an accounter for testing purposes."""
[2506]331        def __init__(self) :
[2830]332            """Initializes fake accounter."""
[2506]333            self.arguments = "snmp:public"
334            self.filter = fakeFilter()
335            self.protocolHandler = Handler(self, hostname)
[3413]336
337        def getLastPageCounter(self) :
[2830]338            """Fakes the return of a page counter."""
[2615]339            return 0
[3413]340
341    acc = fakeAccounter()
[2506]342    return acc.protocolHandler.retrieveInternalPageCounter()
[3413]343
344if __name__ == "__main__" :
345    if len(sys.argv) != 2 :
[2205]346        sys.stderr.write("Usage :  python  %s  printer_ip_address\n" % sys.argv[0])
[3413]347    else :
[2506]348        pagecounter = main(sys.argv[1])
349        print "Internal page counter's value is : %s" % pagecounter
Note: See TracBrowser for help on using the browser.