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

Revision 3160, 19.1 kB (checked in by jerome, 17 years ago)

Doesn't fail anymore on strange network errors ("SubstrateUnderrunError?: 31-octet
short" received during intermittent power outages).

  • Property svn:eol-style set to native
  • Property svn:keywords set to Auth Date Rev Id
Line 
1# PyKota
2# -*- coding: ISO-8859-15 -*-
3#
4# PyKota - Print Quotas for CUPS and LPRng
5#
6# (c) 2003, 2004, 2005, 2006, 2007 Jerome Alet <alet@librelogiciel.com>
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
19# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20#
21# $Id$
22#
23#
24
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
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.
34
35import sys
36import os
37import time
38import select
39
40try :
41    from pysnmp.entity.rfc3413.oneliner import cmdgen
42except ImportError :   
43    hasV4 = False
44    try :
45        from pysnmp.asn1.encoding.ber.error import TypeMismatchError
46        from pysnmp.mapping.udp.error import SnmpOverUdpError
47        from pysnmp.mapping.udp.role import Manager
48        from pysnmp.proto.api import alpha
49    except ImportError :
50        raise RuntimeError, "The pysnmp module is not available. Download it from http://pysnmp.sf.net/"
51else :
52    hasV4 = True
53
54#                     
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',
71                     } 
72hrPrinterDetectedErrorStateOID = "1.3.6.1.2.1.25.3.5.1.2.1" # SNMPv2-SMI::mib-2.25.3.5.1.2.1
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                                    },
91                                  ] 
92                                 
93# TODO : make the following list configurable at runtime, possibly per printer.                                 
94errorConditions = [ 'No Paper',
95                    # 'No Toner',
96                    'Door Open',
97                    'Jammed',
98                    'Offline',
99                    'Service Requested',
100                    'Input Tray Missing',
101                    'Output Tray Missing',
102                    # 'Marker Supply Missing',
103                    'Output Full',
104                    'Input Tray Empty',
105                  ]
106# WARNING : some printers don't support this one :                 
107prtConsoleDisplayBufferTextOID = "1.3.6.1.2.1.43.16.5.1.2.1.1" # SNMPv2-SMI::mib-2.43.16.5.1.2.1.1
108class BaseHandler :
109    """A class for SNMP print accounting."""
110    def __init__(self, parent, printerhostname) :
111        self.parent = parent
112        self.printerHostname = printerhostname
113        try :
114            self.community = self.parent.arguments.split(":")[1].strip()
115        except IndexError :   
116            self.community = "public"
117        self.port = 161
118        self.initValues()
119       
120    def initValues(self) :   
121        """Initializes SNMP values."""
122        self.printerInternalPageCounter = None
123        self.printerStatus = None
124        self.deviceStatus = None
125        self.printerDetectedErrorState = None
126        self.timebefore = time.time()   # resets timer also in case of error
127       
128    def retrieveSNMPValues(self) :   
129        """Retrieves a printer's internal page counter and status via SNMP."""
130        raise RuntimeError, "You have to overload this method."
131       
132    def extractErrorStates(self, value) :   
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)
141        return states           
142       
143    def checkIfError(self, errorstates) :   
144        """Checks if any error state is fatal or not."""
145        if errorstates is None :
146            return True
147        else :
148            for err in errorstates :
149                if err in errorConditions :
150                    return True
151            return False   
152       
153    def waitPrinting(self) :
154        """Waits for printer status being 'printing'."""
155        try :
156            noprintingmaxdelay = int(self.parent.filter.config.getNoPrintingMaxDelay(self.parent.filter.PrinterName))
157        except (TypeError, AttributeError) : # NB : AttributeError in testing mode because I'm lazy !
158            noprintingmaxdelay = NOPRINTINGMAXDELAY
159            self.parent.filter.logdebug("No max delay defined for printer %s, using %i seconds." % (self.parent.filter.PrinterName, noprintingmaxdelay))
160        if not noprintingmaxdelay :
161            self.parent.filter.logdebug("Will wait indefinitely until printer %s is in 'printing' state." % self.parent.filter.PrinterName)
162        else :   
163            self.parent.filter.logdebug("Will wait until printer %s is in 'printing' state or %i seconds have elapsed." % (self.parent.filter.PrinterName, noprintingmaxdelay))
164        previousValue = self.parent.getLastPageCounter()
165        firstvalue = None
166        while 1:
167            self.retrieveSNMPValues()
168            statusAsString = printerStatusValues.get(self.printerStatus)
169            if statusAsString in ('printing', 'warmup') :
170                break
171            if self.printerInternalPageCounter is not None :   
172                if firstvalue is None :
173                    # first time we retrieved a page counter, save it
174                    firstvalue = self.printerInternalPageCounter
175                else :     
176                    # second time (or later)
177                    if firstvalue < self.printerInternalPageCounter :
178                        # Here we have a printer which lies :
179                        # it says it is not printing or warming up
180                        # BUT the page counter increases !!!
181                        # So we can probably quit being sure it is printing.
182                        self.parent.filter.printInfo("Printer %s is lying to us !!!" % self.parent.filter.PrinterName, "warn")
183                        break
184                    elif noprintingmaxdelay \
185                         and ((time.time() - self.timebefore) > noprintingmaxdelay) \
186                         and not self.checkIfError(self.printerDetectedErrorState) :
187                        # More than X seconds without the printer being in 'printing' mode
188                        # We can safely assume this won't change if printer is now 'idle'
189                        pstatusAsString = printerStatusValues.get(self.printerStatus)
190                        dstatusAsString = deviceStatusValues.get(self.deviceStatus)
191                        if (pstatusAsString == 'idle') or \
192                            ((pstatusAsString == 'other') and \
193                             (dstatusAsString == 'running')) :
194                            if self.printerInternalPageCounter == previousValue :
195                                # Here the job won't be printed, because probably
196                                # the printer rejected it for some reason.
197                                self.parent.filter.printInfo("Printer %s probably won't print this job !!!" % self.parent.filter.PrinterName, "warn")
198                            else :     
199                                # Here the job has already been entirely printed, and
200                                # the printer has already passed from 'idle' to 'printing' to 'idle' again.
201                                self.parent.filter.printInfo("Printer %s has probably already printed this job !!!" % self.parent.filter.PrinterName, "warn")
202                            break
203            self.parent.filter.logdebug(_("Waiting for printer %s to be printing...") % self.parent.filter.PrinterName)   
204            time.sleep(ITERATIONDELAY)
205       
206    def waitIdle(self) :
207        """Waits for printer status being 'idle'."""
208        idle_num = idle_flag = 0
209        while 1 :
210            self.retrieveSNMPValues()
211            pstatusAsString = printerStatusValues.get(self.printerStatus)
212            dstatusAsString = deviceStatusValues.get(self.deviceStatus)
213            idle_flag = 0
214            if (not self.checkIfError(self.printerDetectedErrorState)) \
215               and ((pstatusAsString == 'idle') or \
216                         ((pstatusAsString == 'other') and \
217                          (dstatusAsString == 'running'))) :
218                idle_flag = 1       # Standby / Powersave is considered idle
219            if idle_flag :   
220                idle_num += 1
221                if idle_num >= STABILIZATIONDELAY :
222                    # printer status is stable, we can exit
223                    break
224            else :   
225                idle_num = 0
226            self.parent.filter.logdebug(_("Waiting for printer %s's idle status to stabilize...") % self.parent.filter.PrinterName)   
227            time.sleep(ITERATIONDELAY)
228           
229    def retrieveInternalPageCounter(self) :
230        """Returns the page counter from the printer via internal SNMP handling."""
231        try :
232            if (os.environ.get("PYKOTASTATUS") != "CANCELLED") and \
233               (os.environ.get("PYKOTAACTION") == "ALLOW") and \
234               (os.environ.get("PYKOTAPHASE") == "AFTER") and \
235               self.parent.filter.JobSizeBytes :
236                self.waitPrinting()
237            self.waitIdle()   
238        except :   
239            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")
240            raise
241        return self.printerInternalPageCounter
242           
243if hasV4 :           
244    class Handler(BaseHandler) :
245        """A class for pysnmp v4.x"""
246        def retrieveSNMPValues(self) :
247            """Retrieves a printer's internal page counter and status via SNMP."""
248            try :
249                errorIndication, errorStatus, errorIndex, varBinds = \
250                 cmdgen.CommandGenerator().getCmd(cmdgen.CommunityData("pykota", self.community, 0), \
251                                                  cmdgen.UdpTransportTarget((self.printerHostname, self.port)), \
252                                                  tuple([int(i) for i in pageCounterOID.split('.')]), \
253                                                  tuple([int(i) for i in hrPrinterStatusOID.split('.')]), \
254                                                  tuple([int(i) for i in hrDeviceStatusOID.split('.')]), \
255                                                  tuple([int(i) for i in hrPrinterDetectedErrorStateOID.split('.')]))
256            except :                                     
257                errorIndication = "Unknown SNMP/Network error. Check your wires."
258            if errorIndication :                                                 
259                self.parent.filter.printInfo("SNMP Error : %s" % errorIndication, "error")
260                self.initValues()
261            elif errorStatus :   
262                self.parent.filter.printInfo("SNMP Error : %s at %s" % (errorStatus.prettyPrint(), \
263                                                                        varBinds[int(errorIndex)-1]), \
264                                             "error")
265                self.initValues()
266            else :                                 
267                self.printerInternalPageCounter = max(self.printerInternalPageCounter, int(varBinds[0][1].prettyPrint()))
268                self.printerStatus = int(varBinds[1][1].prettyPrint())
269                self.deviceStatus = int(varBinds[2][1].prettyPrint())
270                self.printerDetectedErrorState = self.extractErrorStates(str(varBinds[3][1]))
271                self.parent.filter.logdebug("SNMP answer decoded : PageCounter : %s  PrinterStatus : '%s'  DeviceStatus : '%s'  PrinterErrorState : '%s'" \
272                     % (self.printerInternalPageCounter, \
273                        printerStatusValues.get(self.printerStatus), \
274                        deviceStatusValues.get(self.deviceStatus), \
275                        self.printerDetectedErrorState))
276else :
277    class Handler(BaseHandler) :
278        """A class for pysnmp v3.4.x"""
279        def retrieveSNMPValues(self) :   
280            """Retrieves a printer's internal page counter and status via SNMP."""
281            ver = alpha.protoVersions[alpha.protoVersionId1]
282            req = ver.Message()
283            req.apiAlphaSetCommunity(self.community)
284            req.apiAlphaSetPdu(ver.GetRequestPdu())
285            req.apiAlphaGetPdu().apiAlphaSetVarBindList((pageCounterOID, ver.Null()), \
286                                                        (hrPrinterStatusOID, ver.Null()), \
287                                                        (hrDeviceStatusOID, ver.Null()), \
288                                                        (hrPrinterDetectedErrorStateOID, ver.Null()))
289            tsp = Manager()
290            try :
291                tsp.sendAndReceive(req.berEncode(), \
292                                   (self.printerHostname, self.port), \
293                                   (self.handleAnswer, req))
294            except (SnmpOverUdpError, select.error), msg :   
295                self.parent.filter.printInfo(_("Network error while doing SNMP queries on printer %s : %s") % (self.printerHostname, msg), "warn")
296                self.initValues()
297            tsp.close()
298       
299        def handleAnswer(self, wholeMsg, notusedhere, req):
300            """Decodes and handles the SNMP answer."""
301            ver = alpha.protoVersions[alpha.protoVersionId1]
302            rsp = ver.Message()
303            try :
304                rsp.berDecode(wholeMsg)
305            except TypeMismatchError, msg :   
306                self.parent.filter.printInfo(_("SNMP message decoding error for printer %s : %s") % (self.printerHostname, msg), "warn")
307                self.initValues()
308            else :
309                if req.apiAlphaMatch(rsp):
310                    errorStatus = rsp.apiAlphaGetPdu().apiAlphaGetErrorStatus()
311                    if errorStatus:
312                        self.parent.filter.printInfo(_("Problem encountered while doing SNMP queries on printer %s : %s") % (self.printerHostname, errorStatus), "warn")
313                    else:
314                        self.values = []
315                        for varBind in rsp.apiAlphaGetPdu().apiAlphaGetVarBindList():
316                            self.values.append(varBind.apiAlphaGetOidVal()[1].rawAsn1Value)
317                        try :   
318                            # keep maximum value seen for printer's internal page counter
319                            self.printerInternalPageCounter = max(self.printerInternalPageCounter, self.values[0])
320                            self.printerStatus = self.values[1]
321                            self.deviceStatus = self.values[2]
322                            self.printerDetectedErrorState = self.extractErrorStates(self.values[3])
323                            self.parent.filter.logdebug("SNMP answer decoded : PageCounter : %s  PrinterStatus : '%s'  DeviceStatus : '%s'  PrinterErrorState : '%s'" \
324                                 % (self.printerInternalPageCounter, \
325                                    printerStatusValues.get(self.printerStatus), \
326                                    deviceStatusValues.get(self.deviceStatus), \
327                                    self.printerDetectedErrorState))
328                        except IndexError :   
329                            self.parent.filter.logdebug("SNMP answer is incomplete : %s" % str(self.values))
330                            pass
331                        else :   
332                            return 1
333                   
334def main(hostname) :
335    """Tries SNMP accounting for a printer host."""
336    class fakeFilter :
337        """Fakes a filter for testing purposes."""
338        def __init__(self) :
339            """Initializes the fake filter."""
340            self.PrinterName = "FakePrintQueue"
341            self.JobSizeBytes = 1
342           
343        def printInfo(self, msg, level="info") :
344            """Prints informational message."""
345            sys.stderr.write("%s : %s\n" % (level.upper(), msg))
346            sys.stderr.flush()
347           
348        def logdebug(self, msg) :   
349            """Prints debug message."""
350            self.printInfo(msg, "debug")
351           
352    class fakeAccounter :       
353        """Fakes an accounter for testing purposes."""
354        def __init__(self) :
355            """Initializes fake accounter."""
356            self.arguments = "snmp:public"
357            self.filter = fakeFilter()
358            self.protocolHandler = Handler(self, hostname)
359           
360        def getLastPageCounter(self) :   
361            """Fakes the return of a page counter."""
362            return 0
363       
364    acc = fakeAccounter()           
365    return acc.protocolHandler.retrieveInternalPageCounter()
366       
367if __name__ == "__main__" :           
368    if len(sys.argv) != 2 :   
369        sys.stderr.write("Usage :  python  %s  printer_ip_address\n" % sys.argv[0])
370    else :   
371        def _(msg) :
372            return msg
373           
374        pagecounter = main(sys.argv[1])
375        print "Internal page counter's value is : %s" % pagecounter
Note: See TracBrowser for help on using the browser.