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

Revision 3028, 19.3 kB (checked in by jerome, 18 years ago)

Printer Off should be reported as an error.

  • 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 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                                  ] 
92errorConditions = [ 'No Paper',
93                    # 'No Toner',
94                    'Door Open',
95                    'Jammed',
96                    'Offline',
97                    'Service Requested',
98                    'Input Tray Missing',
99                    'Output Tray Missing',
100                    # 'Marker Supply Missing',
101                    'Output Full',
102                    'Input Tray Empty',
103                  ]
104prtConsoleDisplayBufferTextOID = "1.3.6.1.2.1.43.16.5.1.2.1.1" # SNMPv2-SMI::mib-2.43.16.5.1.2.1.1
105
106class BaseHandler :
107    """A class for SNMP print accounting."""
108    def __init__(self, parent, printerhostname) :
109        self.parent = parent
110        self.printerHostname = printerhostname
111        try :
112            self.community = self.parent.arguments.split(":")[1].strip()
113        except IndexError :   
114            self.community = "public"
115        self.port = 161
116        self.initValues()
117       
118    def initValues(self) :   
119        """Initializes SNMP values."""
120        self.printerInternalPageCounter = None
121        self.printerStatus = None
122        self.deviceStatus = None
123        self.printerDetectedErrorState = None
124        self.consoleDisplayBufferText = None
125        self.timebefore = time.time()   # resets timer also in case of error
126       
127    def retrieveSNMPValues(self) :   
128        """Retrieves a printer's internal page counter and status via SNMP."""
129        raise RuntimeError, "You have to overload this method."
130       
131    def extractErrorStates(self, value) :   
132        """Returns a list of textual error states from a binary value."""
133        states = []
134        for i in range(min(len(value), len(printerDetectedErrorStateValues))) :
135            byte = ord(value[i])
136            bytedescription = printerDetectedErrorStateValues[i]
137            for (k, v) in bytedescription.items() :
138                if byte & k :
139                    states.append(v)
140        return states           
141       
142    def checkIfError(self, errorstates) :   
143        """Checks if any error state is fatal or not."""
144        if errorstates is None :
145            return True
146        else :
147            for err in errorstates :
148                if err in errorConditions :
149                    return True
150            return False   
151       
152    def waitPrinting(self) :
153        """Waits for printer status being 'printing'."""
154        try :
155            noprintingmaxdelay = int(self.parent.filter.config.getNoPrintingMaxDelay(self.parent.filter.PrinterName))
156        except (TypeError, AttributeError) : # NB : AttributeError in testing mode because I'm lazy !
157            noprintingmaxdelay = NOPRINTINGMAXDELAY
158            self.parent.filter.logdebug("No max delay defined for printer %s, using %i seconds." % (self.parent.filter.PrinterName, noprintingmaxdelay))
159        if not noprintingmaxdelay :
160            self.parent.filter.logdebug("Will wait indefinitely until printer %s is in 'printing' state." % self.parent.filter.PrinterName)
161        else :   
162            self.parent.filter.logdebug("Will wait until printer %s is in 'printing' state or %i seconds have elapsed." % (self.parent.filter.PrinterName, noprintingmaxdelay))
163        previousValue = self.parent.getLastPageCounter()
164        firstvalue = None
165        while 1:
166            self.retrieveSNMPValues()
167            statusAsString = printerStatusValues.get(self.printerStatus)
168            if statusAsString in ('printing', 'warmup') :
169                break
170            if self.printerInternalPageCounter is not None :   
171                if firstvalue is None :
172                    # first time we retrieved a page counter, save it
173                    firstvalue = self.printerInternalPageCounter
174                else :     
175                    # second time (or later)
176                    if firstvalue < self.printerInternalPageCounter :
177                        # Here we have a printer which lies :
178                        # it says it is not printing or warming up
179                        # BUT the page counter increases !!!
180                        # So we can probably quit being sure it is printing.
181                        self.parent.filter.printInfo("Printer %s is lying to us !!!" % self.parent.filter.PrinterName, "warn")
182                        break
183                    elif noprintingmaxdelay \
184                         and ((time.time() - self.timebefore) > noprintingmaxdelay) \
185                         and not self.checkIfError(self.printerDetectedErrorState) :
186                        # More than X seconds without the printer being in 'printing' mode
187                        # We can safely assume this won't change if printer is now 'idle'
188                        pstatusAsString = printerStatusValues.get(self.printerStatus)
189                        dstatusAsString = deviceStatusValues.get(self.deviceStatus)
190                        if (pstatusAsString == 'idle') or \
191                            ((pstatusAsString == 'other') and \
192                             (dstatusAsString == 'running')) :
193                            if self.printerInternalPageCounter == previousValue :
194                                # Here the job won't be printed, because probably
195                                # the printer rejected it for some reason.
196                                self.parent.filter.printInfo("Printer %s probably won't print this job !!!" % self.parent.filter.PrinterName, "warn")
197                            else :     
198                                # Here the job has already been entirely printed, and
199                                # the printer has already passed from 'idle' to 'printing' to 'idle' again.
200                                self.parent.filter.printInfo("Printer %s has probably already printed this job !!!" % self.parent.filter.PrinterName, "warn")
201                            break
202            self.parent.filter.logdebug(_("Waiting for printer %s to be printing...") % self.parent.filter.PrinterName)   
203            time.sleep(ITERATIONDELAY)
204       
205    def waitIdle(self) :
206        """Waits for printer status being 'idle'."""
207        idle_num = idle_flag = 0
208        while 1 :
209            self.retrieveSNMPValues()
210            pstatusAsString = printerStatusValues.get(self.printerStatus)
211            dstatusAsString = deviceStatusValues.get(self.deviceStatus)
212            idle_flag = 0
213            if (not self.checkIfError(self.printerDetectedErrorState)) \
214               and ((pstatusAsString == 'idle') or \
215                         ((pstatusAsString == 'other') and \
216                          (dstatusAsString == 'running'))) :
217                idle_flag = 1       # Standby / Powersave is considered idle
218            if idle_flag :   
219                idle_num += 1
220                if idle_num >= STABILIZATIONDELAY :
221                    # printer status is stable, we can exit
222                    break
223            else :   
224                idle_num = 0
225            self.parent.filter.logdebug(_("Waiting for printer %s's idle status to stabilize...") % self.parent.filter.PrinterName)   
226            time.sleep(ITERATIONDELAY)
227           
228    def retrieveInternalPageCounter(self) :
229        """Returns the page counter from the printer via internal SNMP handling."""
230        try :
231            if (os.environ.get("PYKOTASTATUS") != "CANCELLED") and \
232               (os.environ.get("PYKOTAACTION") == "ALLOW") and \
233               (os.environ.get("PYKOTAPHASE") == "AFTER") and \
234               self.parent.filter.JobSizeBytes :
235                self.waitPrinting()
236            self.waitIdle()   
237        except :   
238            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")
239            raise
240        return self.printerInternalPageCounter
241           
242if hasV4 :           
243    class Handler(BaseHandler) :
244        """A class for pysnmp v4.x"""
245        def retrieveSNMPValues(self) :
246            """Retrieves a printer's internal page counter and status via SNMP."""
247            errorIndication, errorStatus, errorIndex, varBinds = \
248                 cmdgen.CommandGenerator().getCmd(cmdgen.CommunityData("pykota", self.community, 0), \
249                                                  cmdgen.UdpTransportTarget((self.printerHostname, self.port)), \
250                                                  tuple([int(i) for i in pageCounterOID.split('.')]), \
251                                                  tuple([int(i) for i in hrPrinterStatusOID.split('.')]), \
252                                                  tuple([int(i) for i in hrDeviceStatusOID.split('.')]), \
253                                                  tuple([int(i) for i in hrPrinterDetectedErrorStateOID.split('.')]), \
254                                                  tuple([int(i) for i in prtConsoleDisplayBufferTextOID.split('.')]))
255            if errorIndication :                                                 
256                self.parent.filter.printInfo("SNMP Error : %s" % errorIndication, "error")
257                self.initValues()
258            elif errorStatus :   
259                self.parent.filter.printInfo("SNMP Error : %s at %s" % (errorStatus.prettyPrint(), \
260                                                                        varBinds[int(errorIndex)-1]), \
261                                             "error")
262                self.initValues()
263            else :                                 
264                self.printerInternalPageCounter = max(self.printerInternalPageCounter, int(varBinds[0][1].prettyPrint()))
265                self.printerStatus = int(varBinds[1][1].prettyPrint())
266                self.deviceStatus = int(varBinds[2][1].prettyPrint())
267                self.printerDetectedErrorState = self.extractErrorStates(str(varBinds[3][1]))
268                self.consoleDisplayBufferText = varBinds[4][1].prettyPrint()
269                self.parent.filter.logdebug("SNMP answer decoded : PageCounter : %s  PrinterStatus : '%s'  DeviceStatus : '%s'  PrinterErrorState : '%s'  ConsoleDisplayBuffer : '%s'" \
270                     % (self.printerInternalPageCounter, \
271                        printerStatusValues.get(self.printerStatus), \
272                        deviceStatusValues.get(self.deviceStatus), \
273                        self.printerDetectedErrorState, \
274                        self.consoleDisplayBufferText))
275else :
276    class Handler(BaseHandler) :
277        """A class for pysnmp v3.4.x"""
278        def retrieveSNMPValues(self) :   
279            """Retrieves a printer's internal page counter and status via SNMP."""
280            ver = alpha.protoVersions[alpha.protoVersionId1]
281            req = ver.Message()
282            req.apiAlphaSetCommunity(self.community)
283            req.apiAlphaSetPdu(ver.GetRequestPdu())
284            req.apiAlphaGetPdu().apiAlphaSetVarBindList((pageCounterOID, ver.Null()), \
285                                                        (hrPrinterStatusOID, ver.Null()), \
286                                                        (hrDeviceStatusOID, ver.Null()), \
287                                                        (hrPrinterDetectedErrorStateOID, ver.Null()), \
288                                                        (prtConsoleDisplayBufferTextOID, 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.consoleDisplayBufferText = self.values[4]
324                            self.parent.filter.logdebug("SNMP answer decoded : PageCounter : %s  PrinterStatus : '%s'  DeviceStatus : '%s'  PrinterErrorState : '%s'  ConsoleDisplayBuffer : '%s'" \
325                                 % (self.printerInternalPageCounter, \
326                                    printerStatusValues.get(self.printerStatus), \
327                                    deviceStatusValues.get(self.deviceStatus), \
328                                    self.printerDetectedErrorState, \
329                                    self.consoleDisplayBufferText))
330                        except IndexError :   
331                            self.parent.filter.logdebug("SNMP answer is incomplete : %s" % str(self.values))
332                            pass
333                        else :   
334                            return 1
335                   
336def main(hostname) :
337    """Tries SNMP accounting for a printer host."""
338    class fakeFilter :
339        """Fakes a filter for testing purposes."""
340        def __init__(self) :
341            """Initializes the fake filter."""
342            self.PrinterName = "FakePrintQueue"
343            self.JobSizeBytes = 1
344           
345        def printInfo(self, msg, level="info") :
346            """Prints informational message."""
347            sys.stderr.write("%s : %s\n" % (level.upper(), msg))
348            sys.stderr.flush()
349           
350        def logdebug(self, msg) :   
351            """Prints debug message."""
352            self.printInfo(msg, "debug")
353           
354    class fakeAccounter :       
355        """Fakes an accounter for testing purposes."""
356        def __init__(self) :
357            """Initializes fake accounter."""
358            self.arguments = "snmp:public"
359            self.filter = fakeFilter()
360            self.protocolHandler = Handler(self, hostname)
361           
362        def getLastPageCounter(self) :   
363            """Fakes the return of a page counter."""
364            return 0
365       
366    acc = fakeAccounter()           
367    return acc.protocolHandler.retrieveInternalPageCounter()
368       
369if __name__ == "__main__" :           
370    if len(sys.argv) != 2 :   
371        sys.stderr.write("Usage :  python  %s  printer_ip_address\n" % sys.argv[0])
372    else :   
373        def _(msg) :
374            return msg
375           
376        pagecounter = main(sys.argv[1])
377        print "Internal page counter's value is : %s" % pagecounter
Note: See TracBrowser for help on using the browser.