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

Revision 3133, 18.8 kB (checked in by jerome, 17 years ago)

Changed copyright years.

  • 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
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                  ]
104# WARNING : some printers don't support this one :                 
105prtConsoleDisplayBufferTextOID = "1.3.6.1.2.1.43.16.5.1.2.1.1" # SNMPv2-SMI::mib-2.43.16.5.1.2.1.1
106
107class BaseHandler :
108    """A class for SNMP print accounting."""
109    def __init__(self, parent, printerhostname) :
110        self.parent = parent
111        self.printerHostname = printerhostname
112        try :
113            self.community = self.parent.arguments.split(":")[1].strip()
114        except IndexError :   
115            self.community = "public"
116        self.port = 161
117        self.initValues()
118       
119    def initValues(self) :   
120        """Initializes SNMP values."""
121        self.printerInternalPageCounter = None
122        self.printerStatus = None
123        self.deviceStatus = None
124        self.printerDetectedErrorState = 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            if errorIndication :                                                 
255                self.parent.filter.printInfo("SNMP Error : %s" % errorIndication, "error")
256                self.initValues()
257            elif errorStatus :   
258                self.parent.filter.printInfo("SNMP Error : %s at %s" % (errorStatus.prettyPrint(), \
259                                                                        varBinds[int(errorIndex)-1]), \
260                                             "error")
261                self.initValues()
262            else :                                 
263                self.printerInternalPageCounter = max(self.printerInternalPageCounter, int(varBinds[0][1].prettyPrint()))
264                self.printerStatus = int(varBinds[1][1].prettyPrint())
265                self.deviceStatus = int(varBinds[2][1].prettyPrint())
266                self.printerDetectedErrorState = self.extractErrorStates(str(varBinds[3][1]))
267                self.parent.filter.logdebug("SNMP answer decoded : PageCounter : %s  PrinterStatus : '%s'  DeviceStatus : '%s'  PrinterErrorState : '%s'" \
268                     % (self.printerInternalPageCounter, \
269                        printerStatusValues.get(self.printerStatus), \
270                        deviceStatusValues.get(self.deviceStatus), \
271                        self.printerDetectedErrorState))
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            tsp = Manager()
286            try :
287                tsp.sendAndReceive(req.berEncode(), \
288                                   (self.printerHostname, self.port), \
289                                   (self.handleAnswer, req))
290            except (SnmpOverUdpError, select.error), msg :   
291                self.parent.filter.printInfo(_("Network error while doing SNMP queries on printer %s : %s") % (self.printerHostname, msg), "warn")
292                self.initValues()
293            tsp.close()
294       
295        def handleAnswer(self, wholeMsg, notusedhere, req):
296            """Decodes and handles the SNMP answer."""
297            ver = alpha.protoVersions[alpha.protoVersionId1]
298            rsp = ver.Message()
299            try :
300                rsp.berDecode(wholeMsg)
301            except TypeMismatchError, msg :   
302                self.parent.filter.printInfo(_("SNMP message decoding error for printer %s : %s") % (self.printerHostname, msg), "warn")
303                self.initValues()
304            else :
305                if req.apiAlphaMatch(rsp):
306                    errorStatus = rsp.apiAlphaGetPdu().apiAlphaGetErrorStatus()
307                    if errorStatus:
308                        self.parent.filter.printInfo(_("Problem encountered while doing SNMP queries on printer %s : %s") % (self.printerHostname, errorStatus), "warn")
309                    else:
310                        self.values = []
311                        for varBind in rsp.apiAlphaGetPdu().apiAlphaGetVarBindList():
312                            self.values.append(varBind.apiAlphaGetOidVal()[1].rawAsn1Value)
313                        try :   
314                            # keep maximum value seen for printer's internal page counter
315                            self.printerInternalPageCounter = max(self.printerInternalPageCounter, self.values[0])
316                            self.printerStatus = self.values[1]
317                            self.deviceStatus = self.values[2]
318                            self.printerDetectedErrorState = self.extractErrorStates(self.values[3])
319                            self.parent.filter.logdebug("SNMP answer decoded : PageCounter : %s  PrinterStatus : '%s'  DeviceStatus : '%s'  PrinterErrorState : '%s'" \
320                                 % (self.printerInternalPageCounter, \
321                                    printerStatusValues.get(self.printerStatus), \
322                                    deviceStatusValues.get(self.deviceStatus), \
323                                    self.printerDetectedErrorState))
324                        except IndexError :   
325                            self.parent.filter.logdebug("SNMP answer is incomplete : %s" % str(self.values))
326                            pass
327                        else :   
328                            return 1
329                   
330def main(hostname) :
331    """Tries SNMP accounting for a printer host."""
332    class fakeFilter :
333        """Fakes a filter for testing purposes."""
334        def __init__(self) :
335            """Initializes the fake filter."""
336            self.PrinterName = "FakePrintQueue"
337            self.JobSizeBytes = 1
338           
339        def printInfo(self, msg, level="info") :
340            """Prints informational message."""
341            sys.stderr.write("%s : %s\n" % (level.upper(), msg))
342            sys.stderr.flush()
343           
344        def logdebug(self, msg) :   
345            """Prints debug message."""
346            self.printInfo(msg, "debug")
347           
348    class fakeAccounter :       
349        """Fakes an accounter for testing purposes."""
350        def __init__(self) :
351            """Initializes fake accounter."""
352            self.arguments = "snmp:public"
353            self.filter = fakeFilter()
354            self.protocolHandler = Handler(self, hostname)
355           
356        def getLastPageCounter(self) :   
357            """Fakes the return of a page counter."""
358            return 0
359       
360    acc = fakeAccounter()           
361    return acc.protocolHandler.retrieveInternalPageCounter()
362       
363if __name__ == "__main__" :           
364    if len(sys.argv) != 2 :   
365        sys.stderr.write("Usage :  python  %s  printer_ip_address\n" % sys.argv[0])
366    else :   
367        def _(msg) :
368            return msg
369           
370        pagecounter = main(sys.argv[1])
371        print "Internal page counter's value is : %s" % pagecounter
Note: See TracBrowser for help on using the browser.