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

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

If skipinitialwait is set to 'yes' in pykota.conf, don't take it into
account unless the printer really is in 'idle' state. This doesn't cost
anything since we've already retrieved the page counter and idle status
a single time before the test, but will now correctly loop until the printer
is idle if it is not already for some strange reason (no paper, and
so on).

  • 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
39import socket
40
41try :
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
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                                  ] 
93                                 
94# TODO : make the following list configurable at runtime, possibly per printer.                                 
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                  ]
107# WARNING : some printers don't support this one :                 
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."""
111    def __init__(self, parent, printerhostname, skipinitialwait=False) :
112        self.parent = parent
113        self.printerHostname = printerhostname
114        self.skipinitialwait = skipinitialwait
115        try :
116            self.community = self.parent.arguments.split(":")[1].strip()
117        except IndexError :   
118            self.community = "public"
119        self.port = 161
120        self.initValues()
121       
122    def initValues(self) :   
123        """Initializes SNMP values."""
124        self.printerInternalPageCounter = None
125        self.printerStatus = None
126        self.deviceStatus = None
127        self.printerDetectedErrorState = None
128        self.timebefore = time.time()   # resets timer also in case of error
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       
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."""
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   
154       
155    def waitPrinting(self) :
156        """Waits for printer status being 'printing'."""
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))
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
186                    elif noprintingmaxdelay \
187                         and ((time.time() - self.timebefore) > noprintingmaxdelay) \
188                         and not self.checkIfError(self.printerDetectedErrorState) :
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()
213            pstatusAsString = printerStatusValues.get(self.printerStatus)
214            dstatusAsString = deviceStatusValues.get(self.deviceStatus)
215            idle_flag = 0
216            if (not self.checkIfError(self.printerDetectedErrorState)) \
217               and ((pstatusAsString == 'idle') or \
218                         ((pstatusAsString == 'other') and \
219                          (dstatusAsString == 'running'))) :
220                idle_flag = 1       # Standby / Powersave is considered idle
221            if idle_flag :   
222                if (self.printerInternalPageCounter is not None) \
223                   and self.skipinitialwait \
224                   and (os.environ.get("PYKOTAPHASE") == "BEFORE") :
225                    self.parent.filter.logdebug("No need to wait for the printer to be idle, it is the case already.")
226                    return 
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)
235           
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."""
255            try :
256                errorIndication, errorStatus, errorIndex, varBinds = \
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('.')]), \
261                                                  tuple([int(i) for i in hrDeviceStatusOID.split('.')]), \
262                                                  tuple([int(i) for i in hrPrinterDetectedErrorStateOID.split('.')]))
263            except socket.gaierror, msg :                                     
264                errorIndication = repr(msg)
265            except :                                     
266                errorIndication = "Unknown SNMP/Network error. Check your wires."
267            if errorIndication :                                                 
268                self.parent.filter.printInfo("SNMP Error : %s" % errorIndication, "error")
269                self.initValues()
270            elif errorStatus :   
271                self.parent.filter.printInfo("SNMP Error : %s at %s" % (errorStatus.prettyPrint(), \
272                                                                        varBinds[int(errorIndex)-1]), \
273                                             "error")
274                self.initValues()
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())
279                self.printerDetectedErrorState = self.extractErrorStates(str(varBinds[3][1]))
280                self.parent.filter.logdebug("SNMP answer decoded : PageCounter : %s  PrinterStatus : '%s'  DeviceStatus : '%s'  PrinterErrorState : '%s'" \
281                     % (self.printerInternalPageCounter, \
282                        printerStatusValues.get(self.printerStatus), \
283                        deviceStatusValues.get(self.deviceStatus), \
284                        self.printerDetectedErrorState))
285else :
286    class Handler(BaseHandler) :
287        """A class for pysnmp v3.4.x"""
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()
292            req.apiAlphaSetCommunity(self.community)
293            req.apiAlphaSetPdu(ver.GetRequestPdu())
294            req.apiAlphaGetPdu().apiAlphaSetVarBindList((pageCounterOID, ver.Null()), \
295                                                        (hrPrinterStatusOID, ver.Null()), \
296                                                        (hrDeviceStatusOID, ver.Null()), \
297                                                        (hrPrinterDetectedErrorStateOID, ver.Null()))
298            tsp = Manager()
299            try :
300                tsp.sendAndReceive(req.berEncode(), \
301                                   (self.printerHostname, self.port), \
302                                   (self.handleAnswer, req))
303            except (SnmpOverUdpError, select.error), msg :   
304                self.parent.filter.printInfo(_("Network error while doing SNMP queries on printer %s : %s") % (self.printerHostname, msg), "warn")
305                self.initValues()
306            tsp.close()
307       
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")
316                self.initValues()
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])
329                            self.printerStatus = self.values[1]
330                            self.deviceStatus = self.values[2]
331                            self.printerDetectedErrorState = self.extractErrorStates(self.values[3])
332                            self.parent.filter.logdebug("SNMP answer decoded : PageCounter : %s  PrinterStatus : '%s'  DeviceStatus : '%s'  PrinterErrorState : '%s'" \
333                                 % (self.printerInternalPageCounter, \
334                                    printerStatusValues.get(self.printerStatus), \
335                                    deviceStatusValues.get(self.deviceStatus), \
336                                    self.printerDetectedErrorState))
337                        except IndexError :   
338                            self.parent.filter.logdebug("SNMP answer is incomplete : %s" % str(self.values))
339                            pass
340                        else :   
341                            return 1
342                   
343def main(hostname) :
344    """Tries SNMP accounting for a printer host."""
345    class fakeFilter :
346        """Fakes a filter for testing purposes."""
347        def __init__(self) :
348            """Initializes the fake filter."""
349            self.PrinterName = "FakePrintQueue"
350            self.JobSizeBytes = 1
351           
352        def printInfo(self, msg, level="info") :
353            """Prints informational message."""
354            sys.stderr.write("%s : %s\n" % (level.upper(), msg))
355            sys.stderr.flush()
356           
357        def logdebug(self, msg) :   
358            """Prints debug message."""
359            self.printInfo(msg, "debug")
360           
361    class fakeAccounter :       
362        """Fakes an accounter for testing purposes."""
363        def __init__(self) :
364            """Initializes fake accounter."""
365            self.arguments = "snmp:public"
366            self.filter = fakeFilter()
367            self.protocolHandler = Handler(self, hostname)
368           
369        def getLastPageCounter(self) :   
370            """Fakes the return of a page counter."""
371            return 0
372       
373    acc = fakeAccounter()           
374    return acc.protocolHandler.retrieveInternalPageCounter()
375       
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           
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.