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

Revision 3161, 19.2 kB (checked in by jerome, 17 years ago)

Now catches socket related errors, like DNS server down and the like.

  • 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) :
112        self.parent = parent
113        self.printerHostname = printerhostname
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 = 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(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                idle_num += 1
222                if idle_num >= STABILIZATIONDELAY :
223                    # printer status is stable, we can exit
224                    break
225            else :   
226                idle_num = 0
227            self.parent.filter.logdebug(_("Waiting for printer %s's idle status to stabilize...") % self.parent.filter.PrinterName)   
228            time.sleep(ITERATIONDELAY)
229           
230    def retrieveInternalPageCounter(self) :
231        """Returns the page counter from the printer via internal SNMP handling."""
232        try :
233            if (os.environ.get("PYKOTASTATUS") != "CANCELLED") and \
234               (os.environ.get("PYKOTAACTION") == "ALLOW") and \
235               (os.environ.get("PYKOTAPHASE") == "AFTER") and \
236               self.parent.filter.JobSizeBytes :
237                self.waitPrinting()
238            self.waitIdle()   
239        except :   
240            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")
241            raise
242        return self.printerInternalPageCounter
243           
244if hasV4 :           
245    class Handler(BaseHandler) :
246        """A class for pysnmp v4.x"""
247        def retrieveSNMPValues(self) :
248            """Retrieves a printer's internal page counter and status via SNMP."""
249            try :
250                errorIndication, errorStatus, errorIndex, varBinds = \
251                 cmdgen.CommandGenerator().getCmd(cmdgen.CommunityData("pykota", self.community, 0), \
252                                                  cmdgen.UdpTransportTarget((self.printerHostname, self.port)), \
253                                                  tuple([int(i) for i in pageCounterOID.split('.')]), \
254                                                  tuple([int(i) for i in hrPrinterStatusOID.split('.')]), \
255                                                  tuple([int(i) for i in hrDeviceStatusOID.split('.')]), \
256                                                  tuple([int(i) for i in hrPrinterDetectedErrorStateOID.split('.')]))
257            except socket.gaierror, msg :                                     
258                errorIndication = repr(msg)
259            except :                                     
260                errorIndication = "Unknown SNMP/Network error. Check your wires."
261            if errorIndication :                                                 
262                self.parent.filter.printInfo("SNMP Error : %s" % errorIndication, "error")
263                self.initValues()
264            elif errorStatus :   
265                self.parent.filter.printInfo("SNMP Error : %s at %s" % (errorStatus.prettyPrint(), \
266                                                                        varBinds[int(errorIndex)-1]), \
267                                             "error")
268                self.initValues()
269            else :                                 
270                self.printerInternalPageCounter = max(self.printerInternalPageCounter, int(varBinds[0][1].prettyPrint()))
271                self.printerStatus = int(varBinds[1][1].prettyPrint())
272                self.deviceStatus = int(varBinds[2][1].prettyPrint())
273                self.printerDetectedErrorState = self.extractErrorStates(str(varBinds[3][1]))
274                self.parent.filter.logdebug("SNMP answer decoded : PageCounter : %s  PrinterStatus : '%s'  DeviceStatus : '%s'  PrinterErrorState : '%s'" \
275                     % (self.printerInternalPageCounter, \
276                        printerStatusValues.get(self.printerStatus), \
277                        deviceStatusValues.get(self.deviceStatus), \
278                        self.printerDetectedErrorState))
279else :
280    class Handler(BaseHandler) :
281        """A class for pysnmp v3.4.x"""
282        def retrieveSNMPValues(self) :   
283            """Retrieves a printer's internal page counter and status via SNMP."""
284            ver = alpha.protoVersions[alpha.protoVersionId1]
285            req = ver.Message()
286            req.apiAlphaSetCommunity(self.community)
287            req.apiAlphaSetPdu(ver.GetRequestPdu())
288            req.apiAlphaGetPdu().apiAlphaSetVarBindList((pageCounterOID, ver.Null()), \
289                                                        (hrPrinterStatusOID, ver.Null()), \
290                                                        (hrDeviceStatusOID, ver.Null()), \
291                                                        (hrPrinterDetectedErrorStateOID, ver.Null()))
292            tsp = Manager()
293            try :
294                tsp.sendAndReceive(req.berEncode(), \
295                                   (self.printerHostname, self.port), \
296                                   (self.handleAnswer, req))
297            except (SnmpOverUdpError, select.error), msg :   
298                self.parent.filter.printInfo(_("Network error while doing SNMP queries on printer %s : %s") % (self.printerHostname, msg), "warn")
299                self.initValues()
300            tsp.close()
301       
302        def handleAnswer(self, wholeMsg, notusedhere, req):
303            """Decodes and handles the SNMP answer."""
304            ver = alpha.protoVersions[alpha.protoVersionId1]
305            rsp = ver.Message()
306            try :
307                rsp.berDecode(wholeMsg)
308            except TypeMismatchError, msg :   
309                self.parent.filter.printInfo(_("SNMP message decoding error for printer %s : %s") % (self.printerHostname, msg), "warn")
310                self.initValues()
311            else :
312                if req.apiAlphaMatch(rsp):
313                    errorStatus = rsp.apiAlphaGetPdu().apiAlphaGetErrorStatus()
314                    if errorStatus:
315                        self.parent.filter.printInfo(_("Problem encountered while doing SNMP queries on printer %s : %s") % (self.printerHostname, errorStatus), "warn")
316                    else:
317                        self.values = []
318                        for varBind in rsp.apiAlphaGetPdu().apiAlphaGetVarBindList():
319                            self.values.append(varBind.apiAlphaGetOidVal()[1].rawAsn1Value)
320                        try :   
321                            # keep maximum value seen for printer's internal page counter
322                            self.printerInternalPageCounter = max(self.printerInternalPageCounter, self.values[0])
323                            self.printerStatus = self.values[1]
324                            self.deviceStatus = self.values[2]
325                            self.printerDetectedErrorState = self.extractErrorStates(self.values[3])
326                            self.parent.filter.logdebug("SNMP answer decoded : PageCounter : %s  PrinterStatus : '%s'  DeviceStatus : '%s'  PrinterErrorState : '%s'" \
327                                 % (self.printerInternalPageCounter, \
328                                    printerStatusValues.get(self.printerStatus), \
329                                    deviceStatusValues.get(self.deviceStatus), \
330                                    self.printerDetectedErrorState))
331                        except IndexError :   
332                            self.parent.filter.logdebug("SNMP answer is incomplete : %s" % str(self.values))
333                            pass
334                        else :   
335                            return 1
336                   
337def main(hostname) :
338    """Tries SNMP accounting for a printer host."""
339    class fakeFilter :
340        """Fakes a filter for testing purposes."""
341        def __init__(self) :
342            """Initializes the fake filter."""
343            self.PrinterName = "FakePrintQueue"
344            self.JobSizeBytes = 1
345           
346        def printInfo(self, msg, level="info") :
347            """Prints informational message."""
348            sys.stderr.write("%s : %s\n" % (level.upper(), msg))
349            sys.stderr.flush()
350           
351        def logdebug(self, msg) :   
352            """Prints debug message."""
353            self.printInfo(msg, "debug")
354           
355    class fakeAccounter :       
356        """Fakes an accounter for testing purposes."""
357        def __init__(self) :
358            """Initializes fake accounter."""
359            self.arguments = "snmp:public"
360            self.filter = fakeFilter()
361            self.protocolHandler = Handler(self, hostname)
362           
363        def getLastPageCounter(self) :   
364            """Fakes the return of a page counter."""
365            return 0
366       
367    acc = fakeAccounter()           
368    return acc.protocolHandler.retrieveInternalPageCounter()
369       
370if __name__ == "__main__" :           
371    if len(sys.argv) != 2 :   
372        sys.stderr.write("Usage :  python  %s  printer_ip_address\n" % sys.argv[0])
373    else :   
374        def _(msg) :
375            return msg
376           
377        pagecounter = main(sys.argv[1])
378        print "Internal page counter's value is : %s" % pagecounter
Note: See TracBrowser for help on using the browser.