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

Revision 3180, 19.3 kB (checked in by jerome, 17 years ago)

Now the various delays are configurable when using hardware accounting,
through the newly introduced 'statusstabilizationdelay' and 'statusstabilizationloops'
directives in pykota.conf

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