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

Revision 3413, 19.5 kB (checked in by jerome, 16 years ago)

Removed unnecessary spaces at EOL.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Auth Date Rev Id
Line 
1# -*- coding: utf-8 -*-*-
2#
3# PyKota : Print Quotas for CUPS
4#
5# (c) 2003, 2004, 2005, 2006, 2007, 2008 Jerome Alet <alet@librelogiciel.com>
6# This program is free software: you can redistribute it and/or modify
7# it under the terms of the GNU General Public License as published by
8# the Free Software Foundation, either version 3 of the License, or
9# (at your option) any later version.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with this program.  If not, see <http://www.gnu.org/licenses/>.
18#
19# $Id$
20#
21#
22
23"""This module is used to extract printer's internal page counter
24and status informations using SNMP queries.
25
26The values extracted are defined at least in RFC3805 and RFC2970.
27"""
28
29
30import sys
31import os
32import time
33import select
34import socket
35
36try :
37    from pysnmp.entity.rfc3413.oneliner import cmdgen
38except ImportError :
39    hasV4 = False
40    try :
41        from pysnmp.asn1.encoding.ber.error import TypeMismatchError
42        from pysnmp.mapping.udp.error import SnmpOverUdpError
43        from pysnmp.mapping.udp.role import Manager
44        from pysnmp.proto.api import alpha
45    except ImportError :
46        raise RuntimeError, "The pysnmp module is not available. Download it from http://pysnmp.sf.net/"
47else :
48    hasV4 = True
49
50from pykota import constants
51
52#
53# Documentation taken from RFC 3805 (Printer MIB v2) and RFC 2790 (Host Resource MIB)
54#
55pageCounterOID = "1.3.6.1.2.1.43.10.2.1.4.1.1"  # SNMPv2-SMI::mib-2.43.10.2.1.4.1.1
56hrPrinterStatusOID = "1.3.6.1.2.1.25.3.5.1.1.1" # SNMPv2-SMI::mib-2.25.3.5.1.1.1
57printerStatusValues = { 1 : 'other',
58                        2 : 'unknown',
59                        3 : 'idle',
60                        4 : 'printing',
61                        5 : 'warmup',
62                      }
63hrDeviceStatusOID = "1.3.6.1.2.1.25.3.2.1.5.1" # SNMPv2-SMI::mib-2.25.3.2.1.5.1
64deviceStatusValues = { 1 : 'unknown',
65                       2 : 'running',
66                       3 : 'warning',
67                       4 : 'testing',
68                       5 : 'down',
69                     }
70hrPrinterDetectedErrorStateOID = "1.3.6.1.2.1.25.3.5.1.2.1" # SNMPv2-SMI::mib-2.25.3.5.1.2.1
71printerDetectedErrorStateValues = [ { 128 : 'Low Paper',
72                                       64 : 'No Paper',
73                                       32 : 'Low Toner',
74                                       16 : 'No Toner',
75                                        8 : 'Door Open',
76                                        4 : 'Jammed',
77                                        2 : 'Offline',
78                                        1 : 'Service Requested',
79                                    },
80                                    { 128 : 'Input Tray Missing',
81                                       64 : 'Output Tray Missing',
82                                       32 : 'Marker Supply Missing',
83                                       16 : 'Output Near Full',
84                                        8 : 'Output Full',
85                                        4 : 'Input Tray Empty',
86                                        2 : 'Overdue Preventive Maintainance',
87                                        1 : 'Not Assigned in RFC3805',
88                                    },
89                                  ]
90
91# The default error mask to use when checking error conditions.
92defaultErrorMask = 0x4fcc # [ 'No Paper',
93                          #   'Door Open',
94                          #   'Jammed',
95                          #   'Offline',
96                          #   'Service Requested',
97                          #   'Input Tray Missing',
98                          #   'Output Tray Missing',
99                          #   'Output Full',
100                          #   'Input Tray Empty',
101                          # ]
102
103# WARNING : some printers don't support this one :
104prtConsoleDisplayBufferTextOID = "1.3.6.1.2.1.43.16.5.1.2.1.1" # SNMPv2-SMI::mib-2.43.16.5.1.2.1.1
105class BaseHandler :
106    """A class for SNMP print accounting."""
107    def __init__(self, parent, printerhostname, skipinitialwait=False) :
108        self.parent = parent
109        self.printerHostname = printerhostname
110        self.skipinitialwait = skipinitialwait
111        try :
112            self.community = self.parent.arguments.split(":")[1].strip()
113        except IndexError :
114            self.community = "public"
115        self.port = 161
116        self.initValues()
117
118    def initValues(self) :
119        """Initializes SNMP values."""
120        self.printerInternalPageCounter = None
121        self.printerStatus = None
122        self.deviceStatus = None
123        self.printerDetectedErrorState = None
124        self.timebefore = time.time()   # resets timer also in case of error
125
126    def retrieveSNMPValues(self) :
127        """Retrieves a printer's internal page counter and status via SNMP."""
128        raise RuntimeError, "You have to overload this method."
129
130    def extractErrorStates(self, value) :
131        """Returns a list of textual error states from a binary value."""
132        states = []
133        for i in range(min(len(value), len(printerDetectedErrorStateValues))) :
134            byte = ord(value[i])
135            bytedescription = printerDetectedErrorStateValues[i]
136            for (k, v) in bytedescription.items() :
137                if byte & k :
138                    states.append(v)
139        return states
140
141    def checkIfError(self, errorstates) :
142        """Checks if any error state is fatal or not."""
143        if errorstates is None :
144            return True
145        else :
146            try :
147                errormask = self.parent.filter.config.getPrinterSNMPErrorMask(self.parent.filter.PrinterName)
148            except AttributeError : # debug mode
149                errormask = defaultErrorMask
150            if errormask is None :
151                errormask = defaultErrorMask
152            errormaskbytes = [ chr((errormask & 0xff00) >> 8),
153                               chr((errormask & 0x00ff)),
154                             ]
155            errorConditions = self.extractErrorStates(errormaskbytes)
156            self.parent.filter.logdebug("Error conditions for mask 0x%04x : %s" \
157                                               % (errormask, errorConditions))
158            for err in errorstates :
159                if err in errorConditions :
160                    self.parent.filter.logdebug("Error condition '%s' encountered. PyKota will wait until this problem is fixed." % err)
161                    return True
162            self.parent.filter.logdebug("No error condition matching mask 0x%04x" % errormask)
163            return False
164
165    def waitPrinting(self) :
166        """Waits for printer status being 'printing'."""
167        statusstabilizationdelay = constants.get(self.parent.filter, "StatusStabilizationDelay")
168        noprintingmaxdelay = constants.get(self.parent.filter, "NoPrintingMaxDelay")
169        if not noprintingmaxdelay :
170            self.parent.filter.logdebug("Will wait indefinitely until printer %s is in 'printing' state." % self.parent.filter.PrinterName)
171        else :
172            self.parent.filter.logdebug("Will wait until printer %s is in 'printing' state or %i seconds have elapsed." % (self.parent.filter.PrinterName, noprintingmaxdelay))
173        previousValue = self.parent.getLastPageCounter()
174        firstvalue = None
175        while 1:
176            self.retrieveSNMPValues()
177            statusAsString = printerStatusValues.get(self.printerStatus)
178            if statusAsString in ('printing', 'warmup') :
179                break
180            if self.printerInternalPageCounter is not None :
181                if firstvalue is None :
182                    # first time we retrieved a page counter, save it
183                    firstvalue = self.printerInternalPageCounter
184                else :
185                    # second time (or later)
186                    if firstvalue < self.printerInternalPageCounter :
187                        # Here we have a printer which lies :
188                        # it says it is not printing or warming up
189                        # BUT the page counter increases !!!
190                        # So we can probably quit being sure it is printing.
191                        self.parent.filter.printInfo("Printer %s is lying to us !!!" % self.parent.filter.PrinterName, "warn")
192                        break
193                    elif noprintingmaxdelay \
194                         and ((time.time() - self.timebefore) > noprintingmaxdelay) \
195                         and not self.checkIfError(self.printerDetectedErrorState) :
196                        # More than X seconds without the printer being in 'printing' mode
197                        # We can safely assume this won't change if printer is now 'idle'
198                        pstatusAsString = printerStatusValues.get(self.printerStatus)
199                        dstatusAsString = deviceStatusValues.get(self.deviceStatus)
200                        if (pstatusAsString == 'idle') or \
201                            ((pstatusAsString == 'other') and \
202                             (dstatusAsString == 'running')) :
203                            if self.printerInternalPageCounter == previousValue :
204                                # Here the job won't be printed, because probably
205                                # the printer rejected it for some reason.
206                                self.parent.filter.printInfo("Printer %s probably won't print this job !!!" % self.parent.filter.PrinterName, "warn")
207                            else :
208                                # Here the job has already been entirely printed, and
209                                # the printer has already passed from 'idle' to 'printing' to 'idle' again.
210                                self.parent.filter.printInfo("Printer %s has probably already printed this job !!!" % self.parent.filter.PrinterName, "warn")
211                            break
212            self.parent.filter.logdebug(_("Waiting for printer %s to be printing...") % self.parent.filter.PrinterName)
213            time.sleep(statusstabilizationdelay)
214
215    def waitIdle(self) :
216        """Waits for printer status being 'idle'."""
217        statusstabilizationdelay = constants.get(self.parent.filter, "StatusStabilizationDelay")
218        statusstabilizationloops = constants.get(self.parent.filter, "StatusStabilizationLoops")
219        idle_num = idle_flag = 0
220        while 1 :
221            self.retrieveSNMPValues()
222            pstatusAsString = printerStatusValues.get(self.printerStatus)
223            dstatusAsString = deviceStatusValues.get(self.deviceStatus)
224            idle_flag = 0
225            if (not self.checkIfError(self.printerDetectedErrorState)) \
226               and ((pstatusAsString == 'idle') or \
227                         ((pstatusAsString == 'other') and \
228                          (dstatusAsString == 'running'))) :
229                idle_flag = 1       # Standby / Powersave is considered idle
230            if idle_flag :
231                if (self.printerInternalPageCounter is not None) \
232                   and self.skipinitialwait \
233                   and (os.environ.get("PYKOTAPHASE") == "BEFORE") :
234                    self.parent.filter.logdebug("No need to wait for the printer to be idle, it is the case already.")
235                    return
236                idle_num += 1
237                if idle_num >= statusstabilizationloops :
238                    # printer status is stable, we can exit
239                    break
240            else :
241                idle_num = 0
242            self.parent.filter.logdebug(_("Waiting for printer %s's idle status to stabilize...") % self.parent.filter.PrinterName)
243            time.sleep(statusstabilizationdelay)
244
245    def retrieveInternalPageCounter(self) :
246        """Returns the page counter from the printer via internal SNMP handling."""
247        try :
248            if (os.environ.get("PYKOTASTATUS") != "CANCELLED") and \
249               (os.environ.get("PYKOTAACTION") == "ALLOW") and \
250               (os.environ.get("PYKOTAPHASE") == "AFTER") and \
251               self.parent.filter.JobSizeBytes :
252                self.waitPrinting()
253            self.waitIdle()
254        except :
255            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")
256            raise
257        return self.printerInternalPageCounter
258
259if hasV4 :
260    class Handler(BaseHandler) :
261        """A class for pysnmp v4.x"""
262        def retrieveSNMPValues(self) :
263            """Retrieves a printer's internal page counter and status via SNMP."""
264            try :
265                errorIndication, errorStatus, errorIndex, varBinds = \
266                 cmdgen.CommandGenerator().getCmd(cmdgen.CommunityData("pykota", self.community, 0), \
267                                                  cmdgen.UdpTransportTarget((self.printerHostname, self.port)), \
268                                                  tuple([int(i) for i in pageCounterOID.split('.')]), \
269                                                  tuple([int(i) for i in hrPrinterStatusOID.split('.')]), \
270                                                  tuple([int(i) for i in hrDeviceStatusOID.split('.')]), \
271                                                  tuple([int(i) for i in hrPrinterDetectedErrorStateOID.split('.')]))
272            except socket.gaierror, msg :
273                errorIndication = repr(msg)
274            except :
275                errorIndication = "Unknown SNMP/Network error. Check your wires."
276            if errorIndication :
277                self.parent.filter.printInfo("SNMP Error : %s" % errorIndication, "error")
278                self.initValues()
279            elif errorStatus :
280                self.parent.filter.printInfo("SNMP Error : %s at %s" % (errorStatus.prettyPrint(), \
281                                                                        varBinds[int(errorIndex)-1]), \
282                                             "error")
283                self.initValues()
284            else :
285                self.printerInternalPageCounter = max(self.printerInternalPageCounter, int(varBinds[0][1].prettyPrint() or "0"))
286                self.printerStatus = int(varBinds[1][1].prettyPrint())
287                self.deviceStatus = int(varBinds[2][1].prettyPrint())
288                self.printerDetectedErrorState = self.extractErrorStates(str(varBinds[3][1]))
289                self.parent.filter.logdebug("SNMP answer decoded : PageCounter : %s  PrinterStatus : '%s'  DeviceStatus : '%s'  PrinterErrorState : '%s'" \
290                     % (self.printerInternalPageCounter, \
291                        printerStatusValues.get(self.printerStatus), \
292                        deviceStatusValues.get(self.deviceStatus), \
293                        self.printerDetectedErrorState))
294else :
295    class Handler(BaseHandler) :
296        """A class for pysnmp v3.4.x"""
297        def retrieveSNMPValues(self) :
298            """Retrieves a printer's internal page counter and status via SNMP."""
299            ver = alpha.protoVersions[alpha.protoVersionId1]
300            req = ver.Message()
301            req.apiAlphaSetCommunity(self.community)
302            req.apiAlphaSetPdu(ver.GetRequestPdu())
303            req.apiAlphaGetPdu().apiAlphaSetVarBindList((pageCounterOID, ver.Null()), \
304                                                        (hrPrinterStatusOID, ver.Null()), \
305                                                        (hrDeviceStatusOID, ver.Null()), \
306                                                        (hrPrinterDetectedErrorStateOID, ver.Null()))
307            tsp = Manager()
308            try :
309                tsp.sendAndReceive(req.berEncode(), \
310                                   (self.printerHostname, self.port), \
311                                   (self.handleAnswer, req))
312            except (SnmpOverUdpError, select.error), msg :
313                self.parent.filter.printInfo(_("Network error while doing SNMP queries on printer %s : %s") % (self.printerHostname, msg), "warn")
314                self.initValues()
315            tsp.close()
316
317        def handleAnswer(self, wholeMsg, notusedhere, req):
318            """Decodes and handles the SNMP answer."""
319            ver = alpha.protoVersions[alpha.protoVersionId1]
320            rsp = ver.Message()
321            try :
322                rsp.berDecode(wholeMsg)
323            except TypeMismatchError, msg :
324                self.parent.filter.printInfo(_("SNMP message decoding error for printer %s : %s") % (self.printerHostname, msg), "warn")
325                self.initValues()
326            else :
327                if req.apiAlphaMatch(rsp):
328                    errorStatus = rsp.apiAlphaGetPdu().apiAlphaGetErrorStatus()
329                    if errorStatus:
330                        self.parent.filter.printInfo(_("Problem encountered while doing SNMP queries on printer %s : %s") % (self.printerHostname, errorStatus), "warn")
331                    else:
332                        self.values = []
333                        for varBind in rsp.apiAlphaGetPdu().apiAlphaGetVarBindList():
334                            self.values.append(varBind.apiAlphaGetOidVal()[1].rawAsn1Value)
335                        try :
336                            # keep maximum value seen for printer's internal page counter
337                            self.printerInternalPageCounter = max(self.printerInternalPageCounter, self.values[0])
338                            self.printerStatus = self.values[1]
339                            self.deviceStatus = self.values[2]
340                            self.printerDetectedErrorState = self.extractErrorStates(self.values[3])
341                            self.parent.filter.logdebug("SNMP answer decoded : PageCounter : %s  PrinterStatus : '%s'  DeviceStatus : '%s'  PrinterErrorState : '%s'" \
342                                 % (self.printerInternalPageCounter, \
343                                    printerStatusValues.get(self.printerStatus), \
344                                    deviceStatusValues.get(self.deviceStatus), \
345                                    self.printerDetectedErrorState))
346                        except IndexError :
347                            self.parent.filter.logdebug("SNMP answer is incomplete : %s" % str(self.values))
348                            pass
349                        else :
350                            return 1
351
352def main(hostname) :
353    """Tries SNMP accounting for a printer host."""
354    class fakeFilter :
355        """Fakes a filter for testing purposes."""
356        def __init__(self) :
357            """Initializes the fake filter."""
358            self.PrinterName = "FakePrintQueue"
359            self.JobSizeBytes = 1
360
361        def printInfo(self, msg, level="info") :
362            """Prints informational message."""
363            sys.stderr.write("%s : %s\n" % (level.upper(), msg))
364            sys.stderr.flush()
365
366        def logdebug(self, msg) :
367            """Prints debug message."""
368            self.printInfo(msg, "debug")
369
370    class fakeAccounter :
371        """Fakes an accounter for testing purposes."""
372        def __init__(self) :
373            """Initializes fake accounter."""
374            self.arguments = "snmp:public"
375            self.filter = fakeFilter()
376            self.protocolHandler = Handler(self, hostname)
377
378        def getLastPageCounter(self) :
379            """Fakes the return of a page counter."""
380            return 0
381
382    acc = fakeAccounter()
383    return acc.protocolHandler.retrieveInternalPageCounter()
384
385if __name__ == "__main__" :
386    if len(sys.argv) != 2 :
387        sys.stderr.write("Usage :  python  %s  printer_ip_address\n" % sys.argv[0])
388    else :
389        def _(msg) :
390            return msg
391
392        pagecounter = main(sys.argv[1])
393        print "Internal page counter's value is : %s" % pagecounter
Note: See TracBrowser for help on using the browser.