root / pykota / branches / 1.26_fixes / pykota / accounters / snmp.py @ 3518

Revision 3518, 20.3 kB (checked in by jerome, 14 years ago)

Backported some code from latest development tree to fix #59.

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