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

Revision 3027, 19.2 kB (checked in by jerome, 18 years ago)

Improved the detection of a stable idle status in snmp accounting
stuff, because "lp *.pdf" would make only the first job to be
accounted correctly. PJL stuff needs some work.

  • 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        for err in errorstates :
145            if err in errorConditions :
146                return True
147        return False   
148       
149    def waitPrinting(self) :
150        """Waits for printer status being 'printing'."""
151        try :
152            noprintingmaxdelay = int(self.parent.filter.config.getNoPrintingMaxDelay(self.parent.filter.PrinterName))
153        except (TypeError, AttributeError) : # NB : AttributeError in testing mode because I'm lazy !
154            noprintingmaxdelay = NOPRINTINGMAXDELAY
155            self.parent.filter.logdebug("No max delay defined for printer %s, using %i seconds." % (self.parent.filter.PrinterName, noprintingmaxdelay))
156        if not noprintingmaxdelay :
157            self.parent.filter.logdebug("Will wait indefinitely until printer %s is in 'printing' state." % self.parent.filter.PrinterName)
158        else :   
159            self.parent.filter.logdebug("Will wait until printer %s is in 'printing' state or %i seconds have elapsed." % (self.parent.filter.PrinterName, noprintingmaxdelay))
160        previousValue = self.parent.getLastPageCounter()
161        firstvalue = None
162        while 1:
163            self.retrieveSNMPValues()
164            statusAsString = printerStatusValues.get(self.printerStatus)
165            if statusAsString in ('printing', 'warmup') :
166                break
167            if self.printerInternalPageCounter is not None :   
168                if firstvalue is None :
169                    # first time we retrieved a page counter, save it
170                    firstvalue = self.printerInternalPageCounter
171                else :     
172                    # second time (or later)
173                    if firstvalue < self.printerInternalPageCounter :
174                        # Here we have a printer which lies :
175                        # it says it is not printing or warming up
176                        # BUT the page counter increases !!!
177                        # So we can probably quit being sure it is printing.
178                        self.parent.filter.printInfo("Printer %s is lying to us !!!" % self.parent.filter.PrinterName, "warn")
179                        break
180                    elif noprintingmaxdelay \
181                         and ((time.time() - self.timebefore) > noprintingmaxdelay) \
182                         and not self.checkIfError(self.printerDetectedErrorState) :
183                        # More than X seconds without the printer being in 'printing' mode
184                        # We can safely assume this won't change if printer is now 'idle'
185                        pstatusAsString = printerStatusValues.get(self.printerStatus)
186                        dstatusAsString = deviceStatusValues.get(self.deviceStatus)
187                        if (pstatusAsString == 'idle') or \
188                            ((pstatusAsString == 'other') and \
189                             (dstatusAsString == 'running')) :
190                            if self.printerInternalPageCounter == previousValue :
191                                # Here the job won't be printed, because probably
192                                # the printer rejected it for some reason.
193                                self.parent.filter.printInfo("Printer %s probably won't print this job !!!" % self.parent.filter.PrinterName, "warn")
194                            else :     
195                                # Here the job has already been entirely printed, and
196                                # the printer has already passed from 'idle' to 'printing' to 'idle' again.
197                                self.parent.filter.printInfo("Printer %s has probably already printed this job !!!" % self.parent.filter.PrinterName, "warn")
198                            break
199            self.parent.filter.logdebug(_("Waiting for printer %s to be printing...") % self.parent.filter.PrinterName)   
200            time.sleep(ITERATIONDELAY)
201       
202    def waitIdle(self) :
203        """Waits for printer status being 'idle'."""
204        idle_num = idle_flag = 0
205        while 1 :
206            self.retrieveSNMPValues()
207            pstatusAsString = printerStatusValues.get(self.printerStatus)
208            dstatusAsString = deviceStatusValues.get(self.deviceStatus)
209            idle_flag = 0
210            if (not self.checkIfError(self.printerDetectedErrorState)) \
211               and ((pstatusAsString == 'idle') or \
212                         ((pstatusAsString == 'other') and \
213                          (dstatusAsString == 'running'))) :
214                idle_flag = 1       # Standby / Powersave is considered idle
215            if idle_flag :   
216                idle_num += 1
217                if idle_num >= STABILIZATIONDELAY :
218                    # printer status is stable, we can exit
219                    break
220            else :   
221                idle_num = 0
222            self.parent.filter.logdebug(_("Waiting for printer %s's idle status to stabilize...") % self.parent.filter.PrinterName)   
223            time.sleep(ITERATIONDELAY)
224           
225    def retrieveInternalPageCounter(self) :
226        """Returns the page counter from the printer via internal SNMP handling."""
227        try :
228            if (os.environ.get("PYKOTASTATUS") != "CANCELLED") and \
229               (os.environ.get("PYKOTAACTION") == "ALLOW") and \
230               (os.environ.get("PYKOTAPHASE") == "AFTER") and \
231               self.parent.filter.JobSizeBytes :
232                self.waitPrinting()
233            self.waitIdle()   
234        except :   
235            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")
236            raise
237        return self.printerInternalPageCounter
238           
239if hasV4 :           
240    class Handler(BaseHandler) :
241        """A class for pysnmp v4.x"""
242        def retrieveSNMPValues(self) :
243            """Retrieves a printer's internal page counter and status via SNMP."""
244            errorIndication, errorStatus, errorIndex, varBinds = \
245                 cmdgen.CommandGenerator().getCmd(cmdgen.CommunityData("pykota", self.community, 0), \
246                                                  cmdgen.UdpTransportTarget((self.printerHostname, self.port)), \
247                                                  tuple([int(i) for i in pageCounterOID.split('.')]), \
248                                                  tuple([int(i) for i in hrPrinterStatusOID.split('.')]), \
249                                                  tuple([int(i) for i in hrDeviceStatusOID.split('.')]), \
250                                                  tuple([int(i) for i in hrPrinterDetectedErrorStateOID.split('.')]), \
251                                                  tuple([int(i) for i in prtConsoleDisplayBufferTextOID.split('.')]))
252            if errorIndication :                                                 
253                self.parent.filter.printInfo("SNMP Error : %s" % errorIndication, "error")
254                self.initValues()
255            elif errorStatus :   
256                self.parent.filter.printInfo("SNMP Error : %s at %s" % (errorStatus.prettyPrint(), \
257                                                                        varBinds[int(errorIndex)-1]), \
258                                             "error")
259                self.initValues()
260            else :                                 
261                self.printerInternalPageCounter = max(self.printerInternalPageCounter, int(varBinds[0][1].prettyPrint()))
262                self.printerStatus = int(varBinds[1][1].prettyPrint())
263                self.deviceStatus = int(varBinds[2][1].prettyPrint())
264                self.printerDetectedErrorState = self.extractErrorStates(str(varBinds[3][1]))
265                self.consoleDisplayBufferText = varBinds[4][1].prettyPrint()
266                self.parent.filter.logdebug("SNMP answer decoded : PageCounter : %s  PrinterStatus : '%s'  DeviceStatus : '%s'  PrinterErrorState : '%s'  ConsoleDisplayBuffer : '%s'" \
267                     % (self.printerInternalPageCounter, \
268                        printerStatusValues.get(self.printerStatus), \
269                        deviceStatusValues.get(self.deviceStatus), \
270                        self.printerDetectedErrorState, \
271                        self.consoleDisplayBufferText))
272else :
273    class Handler(BaseHandler) :
274        """A class for pysnmp v3.4.x"""
275        def retrieveSNMPValues(self) :   
276            """Retrieves a printer's internal page counter and status via SNMP."""
277            ver = alpha.protoVersions[alpha.protoVersionId1]
278            req = ver.Message()
279            req.apiAlphaSetCommunity(self.community)
280            req.apiAlphaSetPdu(ver.GetRequestPdu())
281            req.apiAlphaGetPdu().apiAlphaSetVarBindList((pageCounterOID, ver.Null()), \
282                                                        (hrPrinterStatusOID, ver.Null()), \
283                                                        (hrDeviceStatusOID, ver.Null()), \
284                                                        (hrPrinterDetectedErrorStateOID, ver.Null()), \
285                                                        (prtConsoleDisplayBufferTextOID, ver.Null()))
286            tsp = Manager()
287            try :
288                tsp.sendAndReceive(req.berEncode(), \
289                                   (self.printerHostname, self.port), \
290                                   (self.handleAnswer, req))
291            except (SnmpOverUdpError, select.error), msg :   
292                self.parent.filter.printInfo(_("Network error while doing SNMP queries on printer %s : %s") % (self.printerHostname, msg), "warn")
293                self.initValues()
294            tsp.close()
295       
296        def handleAnswer(self, wholeMsg, notusedhere, req):
297            """Decodes and handles the SNMP answer."""
298            ver = alpha.protoVersions[alpha.protoVersionId1]
299            rsp = ver.Message()
300            try :
301                rsp.berDecode(wholeMsg)
302            except TypeMismatchError, msg :   
303                self.parent.filter.printInfo(_("SNMP message decoding error for printer %s : %s") % (self.printerHostname, msg), "warn")
304                self.initValues()
305            else :
306                if req.apiAlphaMatch(rsp):
307                    errorStatus = rsp.apiAlphaGetPdu().apiAlphaGetErrorStatus()
308                    if errorStatus:
309                        self.parent.filter.printInfo(_("Problem encountered while doing SNMP queries on printer %s : %s") % (self.printerHostname, errorStatus), "warn")
310                    else:
311                        self.values = []
312                        for varBind in rsp.apiAlphaGetPdu().apiAlphaGetVarBindList():
313                            self.values.append(varBind.apiAlphaGetOidVal()[1].rawAsn1Value)
314                        try :   
315                            # keep maximum value seen for printer's internal page counter
316                            self.printerInternalPageCounter = max(self.printerInternalPageCounter, self.values[0])
317                            self.printerStatus = self.values[1]
318                            self.deviceStatus = self.values[2]
319                            self.printerDetectedErrorState = self.extractErrorStates(self.values[3])
320                            self.consoleDisplayBufferText = self.values[4]
321                            self.parent.filter.logdebug("SNMP answer decoded : PageCounter : %s  PrinterStatus : '%s'  DeviceStatus : '%s'  PrinterErrorState : '%s'  ConsoleDisplayBuffer : '%s'" \
322                                 % (self.printerInternalPageCounter, \
323                                    printerStatusValues.get(self.printerStatus), \
324                                    deviceStatusValues.get(self.deviceStatus), \
325                                    self.printerDetectedErrorState, \
326                                    self.consoleDisplayBufferText))
327                        except IndexError :   
328                            self.parent.filter.logdebug("SNMP answer is incomplete : %s" % str(self.values))
329                            pass
330                        else :   
331                            return 1
332                   
333def main(hostname) :
334    """Tries SNMP accounting for a printer host."""
335    class fakeFilter :
336        """Fakes a filter for testing purposes."""
337        def __init__(self) :
338            """Initializes the fake filter."""
339            self.PrinterName = "FakePrintQueue"
340            self.JobSizeBytes = 1
341           
342        def printInfo(self, msg, level="info") :
343            """Prints informational message."""
344            sys.stderr.write("%s : %s\n" % (level.upper(), msg))
345            sys.stderr.flush()
346           
347        def logdebug(self, msg) :   
348            """Prints debug message."""
349            self.printInfo(msg, "debug")
350           
351    class fakeAccounter :       
352        """Fakes an accounter for testing purposes."""
353        def __init__(self) :
354            """Initializes fake accounter."""
355            self.arguments = "snmp:public"
356            self.filter = fakeFilter()
357            self.protocolHandler = Handler(self, hostname)
358           
359        def getLastPageCounter(self) :   
360            """Fakes the return of a page counter."""
361            return 0
362       
363    acc = fakeAccounter()           
364    return acc.protocolHandler.retrieveInternalPageCounter()
365       
366if __name__ == "__main__" :           
367    if len(sys.argv) != 2 :   
368        sys.stderr.write("Usage :  python  %s  printer_ip_address\n" % sys.argv[0])
369    else :   
370        def _(msg) :
371            return msg
372           
373        pagecounter = main(sys.argv[1])
374        print "Internal page counter's value is : %s" % pagecounter
Note: See TracBrowser for help on using the browser.