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

Revision 3162, 19.6 kB (checked in by jerome, 17 years ago)

Added the 'skipinitialwait' directive to pykota.conf.
This halves the inter-job delay when using hardware accounting
when set.

  • 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.
16#
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
[3027]31ITERATIONDELAY = 4      # time to sleep between two loops
32STABILIZATIONDELAY = 5  # number of consecutive times the idle status must be seen before we consider it to be stable
33NOPRINTINGMAXDELAY = 60 # The printer must begin to print within 60 seconds by default.
[2205]34
35import sys
36import os
37import time
[2319]38import select
[3161]39import socket
[2205]40
41try :
[2877]42    from pysnmp.entity.rfc3413.oneliner import cmdgen
43except ImportError :   
44    hasV4 = False
45    try :
46        from pysnmp.asn1.encoding.ber.error import TypeMismatchError
47        from pysnmp.mapping.udp.error import SnmpOverUdpError
48        from pysnmp.mapping.udp.role import Manager
49        from pysnmp.proto.api import alpha
50    except ImportError :
51        raise RuntimeError, "The pysnmp module is not available. Download it from http://pysnmp.sf.net/"
52else :
53    hasV4 = True
54
55#                     
56# Documentation taken from RFC 3805 (Printer MIB v2) and RFC 2790 (Host Resource MIB)
57#
58pageCounterOID = "1.3.6.1.2.1.43.10.2.1.4.1.1"  # SNMPv2-SMI::mib-2.43.10.2.1.4.1.1
59hrPrinterStatusOID = "1.3.6.1.2.1.25.3.5.1.1.1" # SNMPv2-SMI::mib-2.25.3.5.1.1.1
60printerStatusValues = { 1 : 'other',
61                        2 : 'unknown',
62                        3 : 'idle',
63                        4 : 'printing',
64                        5 : 'warmup',
65                      }
66hrDeviceStatusOID = "1.3.6.1.2.1.25.3.2.1.5.1" # SNMPv2-SMI::mib-2.25.3.2.1.5.1
67deviceStatusValues = { 1 : 'unknown',
68                       2 : 'running',
69                       3 : 'warning',
70                       4 : 'testing',
71                       5 : 'down',
72                     } 
73hrPrinterDetectedErrorStateOID = "1.3.6.1.2.1.25.3.5.1.2.1" # SNMPv2-SMI::mib-2.25.3.5.1.2.1
[3025]74printerDetectedErrorStateValues = [ { 128 : 'Low Paper',
75                                       64 : 'No Paper',
76                                       32 : 'Low Toner',
77                                       16 : 'No Toner',
78                                        8 : 'Door Open',
79                                        4 : 'Jammed',
80                                        2 : 'Offline',
81                                        1 : 'Service Requested',
82                                    },
83                                    { 128 : 'Input Tray Missing',
84                                       64 : 'Output Tray Missing',
85                                       32 : 'Marker Supply Missing',
86                                       16 : 'Output Near Full',
87                                        8 : 'Output Full',
88                                        4 : 'Input Tray Empty',
89                                        2 : 'Overdue Preventive Maintainance',
90                                        1 : 'Not Assigned in RFC3805',
91                                    },
92                                  ] 
[3145]93                                 
94# TODO : make the following list configurable at runtime, possibly per printer.                                 
[3025]95errorConditions = [ 'No Paper',
96                    # 'No Toner',
97                    'Door Open',
98                    'Jammed',
99                    'Offline',
100                    'Service Requested',
101                    'Input Tray Missing',
102                    'Output Tray Missing',
103                    # 'Marker Supply Missing',
104                    'Output Full',
105                    'Input Tray Empty',
106                  ]
[3039]107# WARNING : some printers don't support this one :                 
[2877]108prtConsoleDisplayBufferTextOID = "1.3.6.1.2.1.43.16.5.1.2.1.1" # SNMPv2-SMI::mib-2.43.16.5.1.2.1.1
109class BaseHandler :
110    """A class for SNMP print accounting."""
[3162]111    def __init__(self, parent, printerhostname, skipinitialwait=False) :
[2877]112        self.parent = parent
113        self.printerHostname = printerhostname
[3162]114        self.skipinitialwait = skipinitialwait
[2877]115        try :
116            self.community = self.parent.arguments.split(":")[1].strip()
117        except IndexError :   
118            self.community = "public"
119        self.port = 161
[3025]120        self.initValues()
121       
122    def initValues(self) :   
123        """Initializes SNMP values."""
[2877]124        self.printerInternalPageCounter = None
125        self.printerStatus = None
126        self.deviceStatus = None
[3025]127        self.printerDetectedErrorState = None
128        self.timebefore = time.time()   # resets timer also in case of error
[2877]129       
130    def retrieveSNMPValues(self) :   
131        """Retrieves a printer's internal page counter and status via SNMP."""
132        raise RuntimeError, "You have to overload this method."
133       
[3025]134    def extractErrorStates(self, value) :   
135        """Returns a list of textual error states from a binary value."""
136        states = []
137        for i in range(min(len(value), len(printerDetectedErrorStateValues))) :
138            byte = ord(value[i])
139            bytedescription = printerDetectedErrorStateValues[i]
140            for (k, v) in bytedescription.items() :
141                if byte & k :
142                    states.append(v)
143        return states           
144       
145    def checkIfError(self, errorstates) :   
146        """Checks if any error state is fatal or not."""
[3028]147        if errorstates is None :
148            return True
149        else :
150            for err in errorstates :
151                if err in errorConditions :
152                    return True
153            return False   
[3025]154       
[2877]155    def waitPrinting(self) :
156        """Waits for printer status being 'printing'."""
[3025]157        try :
158            noprintingmaxdelay = int(self.parent.filter.config.getNoPrintingMaxDelay(self.parent.filter.PrinterName))
159        except (TypeError, AttributeError) : # NB : AttributeError in testing mode because I'm lazy !
160            noprintingmaxdelay = NOPRINTINGMAXDELAY
161            self.parent.filter.logdebug("No max delay defined for printer %s, using %i seconds." % (self.parent.filter.PrinterName, noprintingmaxdelay))
162        if not noprintingmaxdelay :
163            self.parent.filter.logdebug("Will wait indefinitely until printer %s is in 'printing' state." % self.parent.filter.PrinterName)
164        else :   
165            self.parent.filter.logdebug("Will wait until printer %s is in 'printing' state or %i seconds have elapsed." % (self.parent.filter.PrinterName, noprintingmaxdelay))
[2877]166        previousValue = self.parent.getLastPageCounter()
167        firstvalue = None
168        while 1:
169            self.retrieveSNMPValues()
170            statusAsString = printerStatusValues.get(self.printerStatus)
171            if statusAsString in ('printing', 'warmup') :
172                break
173            if self.printerInternalPageCounter is not None :   
174                if firstvalue is None :
175                    # first time we retrieved a page counter, save it
176                    firstvalue = self.printerInternalPageCounter
177                else :     
178                    # second time (or later)
179                    if firstvalue < self.printerInternalPageCounter :
180                        # Here we have a printer which lies :
181                        # it says it is not printing or warming up
182                        # BUT the page counter increases !!!
183                        # So we can probably quit being sure it is printing.
184                        self.parent.filter.printInfo("Printer %s is lying to us !!!" % self.parent.filter.PrinterName, "warn")
185                        break
[3025]186                    elif noprintingmaxdelay \
187                         and ((time.time() - self.timebefore) > noprintingmaxdelay) \
188                         and not self.checkIfError(self.printerDetectedErrorState) :
[2877]189                        # More than X seconds without the printer being in 'printing' mode
190                        # We can safely assume this won't change if printer is now 'idle'
191                        pstatusAsString = printerStatusValues.get(self.printerStatus)
192                        dstatusAsString = deviceStatusValues.get(self.deviceStatus)
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")
200                            else :     
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
205            self.parent.filter.logdebug(_("Waiting for printer %s to be printing...") % self.parent.filter.PrinterName)   
206            time.sleep(ITERATIONDELAY)
207       
208    def waitIdle(self) :
209        """Waits for printer status being 'idle'."""
210        idle_num = idle_flag = 0
211        while 1 :
212            self.retrieveSNMPValues()
[3162]213            if (self.printerInternalPageCounter is not None) \
214               and self.skipinitialwait \
215               and (os.environ.get("PYKOTAPHASE") == "BEFORE") :
216                self.parent.filter.logdebug("No need to wait for the printer to be idle, this should be the case already.")
217                return 
[2877]218            pstatusAsString = printerStatusValues.get(self.printerStatus)
219            dstatusAsString = deviceStatusValues.get(self.deviceStatus)
220            idle_flag = 0
[3025]221            if (not self.checkIfError(self.printerDetectedErrorState)) \
222               and ((pstatusAsString == 'idle') or \
223                         ((pstatusAsString == 'other') and \
224                          (dstatusAsString == 'running'))) :
[2877]225                idle_flag = 1       # Standby / Powersave is considered idle
226            if idle_flag :   
227                idle_num += 1
228                if idle_num >= STABILIZATIONDELAY :
229                    # printer status is stable, we can exit
230                    break
231            else :   
232                idle_num = 0
233            self.parent.filter.logdebug(_("Waiting for printer %s's idle status to stabilize...") % self.parent.filter.PrinterName)   
234            time.sleep(ITERATIONDELAY)
[2205]235           
[2877]236    def retrieveInternalPageCounter(self) :
237        """Returns the page counter from the printer via internal SNMP handling."""
238        try :
239            if (os.environ.get("PYKOTASTATUS") != "CANCELLED") and \
240               (os.environ.get("PYKOTAACTION") == "ALLOW") and \
241               (os.environ.get("PYKOTAPHASE") == "AFTER") and \
242               self.parent.filter.JobSizeBytes :
243                self.waitPrinting()
244            self.waitIdle()   
245        except :   
246            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")
247            raise
248        return self.printerInternalPageCounter
249           
250if hasV4 :           
251    class Handler(BaseHandler) :
252        """A class for pysnmp v4.x"""
253        def retrieveSNMPValues(self) :
254            """Retrieves a printer's internal page counter and status via SNMP."""
[3160]255            try :
256                errorIndication, errorStatus, errorIndex, varBinds = \
[2877]257                 cmdgen.CommandGenerator().getCmd(cmdgen.CommunityData("pykota", self.community, 0), \
258                                                  cmdgen.UdpTransportTarget((self.printerHostname, self.port)), \
259                                                  tuple([int(i) for i in pageCounterOID.split('.')]), \
260                                                  tuple([int(i) for i in hrPrinterStatusOID.split('.')]), \
[3025]261                                                  tuple([int(i) for i in hrDeviceStatusOID.split('.')]), \
[3039]262                                                  tuple([int(i) for i in hrPrinterDetectedErrorStateOID.split('.')]))
[3161]263            except socket.gaierror, msg :                                     
264                errorIndication = repr(msg)
[3160]265            except :                                     
266                errorIndication = "Unknown SNMP/Network error. Check your wires."
[2877]267            if errorIndication :                                                 
268                self.parent.filter.printInfo("SNMP Error : %s" % errorIndication, "error")
[3025]269                self.initValues()
[2877]270            elif errorStatus :   
271                self.parent.filter.printInfo("SNMP Error : %s at %s" % (errorStatus.prettyPrint(), \
272                                                                        varBinds[int(errorIndex)-1]), \
273                                             "error")
[3027]274                self.initValues()
[2877]275            else :                                 
276                self.printerInternalPageCounter = max(self.printerInternalPageCounter, int(varBinds[0][1].prettyPrint()))
277                self.printerStatus = int(varBinds[1][1].prettyPrint())
278                self.deviceStatus = int(varBinds[2][1].prettyPrint())
[3025]279                self.printerDetectedErrorState = self.extractErrorStates(str(varBinds[3][1]))
[3039]280                self.parent.filter.logdebug("SNMP answer decoded : PageCounter : %s  PrinterStatus : '%s'  DeviceStatus : '%s'  PrinterErrorState : '%s'" \
[2877]281                     % (self.printerInternalPageCounter, \
282                        printerStatusValues.get(self.printerStatus), \
[3025]283                        deviceStatusValues.get(self.deviceStatus), \
[3039]284                        self.printerDetectedErrorState))
[2877]285else :
286    class Handler(BaseHandler) :
287        """A class for pysnmp v3.4.x"""
[2205]288        def retrieveSNMPValues(self) :   
289            """Retrieves a printer's internal page counter and status via SNMP."""
290            ver = alpha.protoVersions[alpha.protoVersionId1]
291            req = ver.Message()
[2423]292            req.apiAlphaSetCommunity(self.community)
[2205]293            req.apiAlphaSetPdu(ver.GetRequestPdu())
294            req.apiAlphaGetPdu().apiAlphaSetVarBindList((pageCounterOID, ver.Null()), \
295                                                        (hrPrinterStatusOID, ver.Null()), \
[3025]296                                                        (hrDeviceStatusOID, ver.Null()), \
[3039]297                                                        (hrPrinterDetectedErrorStateOID, ver.Null()))
[2205]298            tsp = Manager()
299            try :
[2423]300                tsp.sendAndReceive(req.berEncode(), \
301                                   (self.printerHostname, self.port), \
302                                   (self.handleAnswer, req))
[2319]303            except (SnmpOverUdpError, select.error), msg :   
[2205]304                self.parent.filter.printInfo(_("Network error while doing SNMP queries on printer %s : %s") % (self.printerHostname, msg), "warn")
[3027]305                self.initValues()
[2205]306            tsp.close()
[2877]307       
[2205]308        def handleAnswer(self, wholeMsg, notusedhere, req):
309            """Decodes and handles the SNMP answer."""
310            ver = alpha.protoVersions[alpha.protoVersionId1]
311            rsp = ver.Message()
312            try :
313                rsp.berDecode(wholeMsg)
314            except TypeMismatchError, msg :   
315                self.parent.filter.printInfo(_("SNMP message decoding error for printer %s : %s") % (self.printerHostname, msg), "warn")
[3027]316                self.initValues()
[2205]317            else :
318                if req.apiAlphaMatch(rsp):
319                    errorStatus = rsp.apiAlphaGetPdu().apiAlphaGetErrorStatus()
320                    if errorStatus:
321                        self.parent.filter.printInfo(_("Problem encountered while doing SNMP queries on printer %s : %s") % (self.printerHostname, errorStatus), "warn")
322                    else:
323                        self.values = []
324                        for varBind in rsp.apiAlphaGetPdu().apiAlphaGetVarBindList():
325                            self.values.append(varBind.apiAlphaGetOidVal()[1].rawAsn1Value)
326                        try :   
327                            # keep maximum value seen for printer's internal page counter
328                            self.printerInternalPageCounter = max(self.printerInternalPageCounter, self.values[0])
[2305]329                            self.printerStatus = self.values[1]
330                            self.deviceStatus = self.values[2]
[3025]331                            self.printerDetectedErrorState = self.extractErrorStates(self.values[3])
[3039]332                            self.parent.filter.logdebug("SNMP answer decoded : PageCounter : %s  PrinterStatus : '%s'  DeviceStatus : '%s'  PrinterErrorState : '%s'" \
[2615]333                                 % (self.printerInternalPageCounter, \
[2617]334                                    printerStatusValues.get(self.printerStatus), \
[3025]335                                    deviceStatusValues.get(self.deviceStatus), \
[3039]336                                    self.printerDetectedErrorState))
[2205]337                        except IndexError :   
338                            self.parent.filter.logdebug("SNMP answer is incomplete : %s" % str(self.values))
339                            pass
340                        else :   
341                            return 1
[2877]342                   
[2506]343def main(hostname) :
344    """Tries SNMP accounting for a printer host."""
345    class fakeFilter :
[2830]346        """Fakes a filter for testing purposes."""
[2506]347        def __init__(self) :
[2830]348            """Initializes the fake filter."""
[2506]349            self.PrinterName = "FakePrintQueue"
350            self.JobSizeBytes = 1
351           
352        def printInfo(self, msg, level="info") :
[2830]353            """Prints informational message."""
[2506]354            sys.stderr.write("%s : %s\n" % (level.upper(), msg))
355            sys.stderr.flush()
356           
357        def logdebug(self, msg) :   
[2830]358            """Prints debug message."""
[2506]359            self.printInfo(msg, "debug")
360           
361    class fakeAccounter :       
[2830]362        """Fakes an accounter for testing purposes."""
[2506]363        def __init__(self) :
[2830]364            """Initializes fake accounter."""
[2506]365            self.arguments = "snmp:public"
366            self.filter = fakeFilter()
367            self.protocolHandler = Handler(self, hostname)
[2615]368           
369        def getLastPageCounter(self) :   
[2830]370            """Fakes the return of a page counter."""
[2615]371            return 0
[2506]372       
373    acc = fakeAccounter()           
374    return acc.protocolHandler.retrieveInternalPageCounter()
375       
[2205]376if __name__ == "__main__" :           
377    if len(sys.argv) != 2 :   
378        sys.stderr.write("Usage :  python  %s  printer_ip_address\n" % sys.argv[0])
379    else :   
380        def _(msg) :
381            return msg
382           
[2506]383        pagecounter = main(sys.argv[1])
384        print "Internal page counter's value is : %s" % pagecounter
Note: See TracBrowser for help on using the browser.