root / pykota / branches / 1.26_fixes / pykota / accounters / snmp.py @ 3518

Revision 3518, 20.3 kB (checked in by jerome, 14 years ago)

Backported some code from latest development tree to fix #59.

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