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

Revision 3175, 19.4 kB (checked in by jerome, 17 years ago)

Rewrote PJL accounter using threads and non-blocking I/O.
Move shared constants to a dedicated module.

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