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
RevLine 
[3411]1# -*- coding: utf-8 -*-*-
[2205]2#
[3260]3# PyKota : Print Quotas for CUPS
[2205]4#
[3275]5# (c) 2003, 2004, 2005, 2006, 2007, 2008 Jerome Alet <alet@librelogiciel.com>
[3260]6# This program is free software: you can redistribute it and/or modify
[2205]7# it under the terms of the GNU General Public License as published by
[3260]8# the Free Software Foundation, either version 3 of the License, or
[2205]9# (at your option) any later version.
[3413]10#
[2205]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.
[3413]15#
[2205]16# You should have received a copy of the GNU General Public License
[3260]17# along with this program.  If not, see <http://www.gnu.org/licenses/>.
[2205]18#
19# $Id$
20#
21#
22
[3025]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
[2205]29
30import sys
31import os
32import time
[2319]33import select
[3161]34import socket
[2205]35
36try :
[2877]37    from pysnmp.entity.rfc3413.oneliner import cmdgen
[3413]38except ImportError :
[2877]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
[3175]50from pykota import constants
51
[3413]52#
[2877]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',
[3413]69                     }
[2877]70hrPrinterDetectedErrorStateOID = "1.3.6.1.2.1.25.3.5.1.2.1" # SNMPv2-SMI::mib-2.25.3.5.1.2.1
[3025]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                                    },
[3413]89                                  ]
90
[3190]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                          # ]
[3413]102
103# WARNING : some printers don't support this one :
[2877]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."""
[3162]107    def __init__(self, parent, printerhostname, skipinitialwait=False) :
[2877]108        self.parent = parent
109        self.printerHostname = printerhostname
[3162]110        self.skipinitialwait = skipinitialwait
[2877]111        try :
112            self.community = self.parent.arguments.split(":")[1].strip()
[3413]113        except IndexError :
[2877]114            self.community = "public"
115        self.port = 161
[3025]116        self.initValues()
[3413]117
118    def initValues(self) :
[3025]119        """Initializes SNMP values."""
[2877]120        self.printerInternalPageCounter = None
121        self.printerStatus = None
122        self.deviceStatus = None
[3025]123        self.printerDetectedErrorState = None
124        self.timebefore = time.time()   # resets timer also in case of error
[3413]125
126    def retrieveSNMPValues(self) :
[2877]127        """Retrieves a printer's internal page counter and status via SNMP."""
128        raise RuntimeError, "You have to overload this method."
[3413]129
130    def extractErrorStates(self, value) :
[3025]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)
[3413]139        return states
140
141    def checkIfError(self, errorstates) :
[3025]142        """Checks if any error state is fatal or not."""
[3028]143        if errorstates is None :
144            return True
145        else :
[3190]146            try :
147                errormask = self.parent.filter.config.getPrinterSNMPErrorMask(self.parent.filter.PrinterName)
[3413]148            except AttributeError : # debug mode
[3190]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))
[3028]158            for err in errorstates :
159                if err in errorConditions :
[3190]160                    self.parent.filter.logdebug("Error condition '%s' encountered. PyKota will wait until this problem is fixed." % err)
[3028]161                    return True
[3190]162            self.parent.filter.logdebug("No error condition matching mask 0x%04x" % errormask)
[3413]163            return False
164
[2877]165    def waitPrinting(self) :
166        """Waits for printer status being 'printing'."""
[3180]167        statusstabilizationdelay = constants.get(self.parent.filter, "StatusStabilizationDelay")
168        noprintingmaxdelay = constants.get(self.parent.filter, "NoPrintingMaxDelay")
[3025]169        if not noprintingmaxdelay :
170            self.parent.filter.logdebug("Will wait indefinitely until printer %s is in 'printing' state." % self.parent.filter.PrinterName)
[3413]171        else :
[3025]172            self.parent.filter.logdebug("Will wait until printer %s is in 'printing' state or %i seconds have elapsed." % (self.parent.filter.PrinterName, noprintingmaxdelay))
[2877]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
[3413]180            if self.printerInternalPageCounter is not None :
[2877]181                if firstvalue is None :
182                    # first time we retrieved a page counter, save it
183                    firstvalue = self.printerInternalPageCounter
[3413]184                else :
[2877]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
[3025]193                    elif noprintingmaxdelay \
194                         and ((time.time() - self.timebefore) > noprintingmaxdelay) \
195                         and not self.checkIfError(self.printerDetectedErrorState) :
[2877]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")
[3413]207                            else :
[2877]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
[3413]212            self.parent.filter.logdebug(_("Waiting for printer %s to be printing...") % self.parent.filter.PrinterName)
[3180]213            time.sleep(statusstabilizationdelay)
[3413]214
[2877]215    def waitIdle(self) :
216        """Waits for printer status being 'idle'."""
[3180]217        statusstabilizationdelay = constants.get(self.parent.filter, "StatusStabilizationDelay")
218        statusstabilizationloops = constants.get(self.parent.filter, "StatusStabilizationLoops")
[2877]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
[3025]225            if (not self.checkIfError(self.printerDetectedErrorState)) \
226               and ((pstatusAsString == 'idle') or \
227                         ((pstatusAsString == 'other') and \
228                          (dstatusAsString == 'running'))) :
[2877]229                idle_flag = 1       # Standby / Powersave is considered idle
[3413]230            if idle_flag :
[3163]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.")
[3413]235                    return
[2877]236                idle_num += 1
[3180]237                if idle_num >= statusstabilizationloops :
[2877]238                    # printer status is stable, we can exit
239                    break
[3413]240            else :
[2877]241                idle_num = 0
[3413]242            self.parent.filter.logdebug(_("Waiting for printer %s's idle status to stabilize...") % self.parent.filter.PrinterName)
[3180]243            time.sleep(statusstabilizationdelay)
[3413]244
[2877]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()
[3413]253            self.waitIdle()
254        except :
[2877]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
[3413]258
259if hasV4 :
[2877]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."""
[3160]264            try :
265                errorIndication, errorStatus, errorIndex, varBinds = \
[2877]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('.')]), \
[3025]270                                                  tuple([int(i) for i in hrDeviceStatusOID.split('.')]), \
[3039]271                                                  tuple([int(i) for i in hrPrinterDetectedErrorStateOID.split('.')]))
[3413]272            except socket.gaierror, msg :
[3161]273                errorIndication = repr(msg)
[3413]274            except :
[3160]275                errorIndication = "Unknown SNMP/Network error. Check your wires."
[3413]276            if errorIndication :
[2877]277                self.parent.filter.printInfo("SNMP Error : %s" % errorIndication, "error")
[3025]278                self.initValues()
[3413]279            elif errorStatus :
[2877]280                self.parent.filter.printInfo("SNMP Error : %s at %s" % (errorStatus.prettyPrint(), \
281                                                                        varBinds[int(errorIndex)-1]), \
282                                             "error")
[3027]283                self.initValues()
[3413]284            else :
[3371]285                self.printerInternalPageCounter = max(self.printerInternalPageCounter, int(varBinds[0][1].prettyPrint() or "0"))
[2877]286                self.printerStatus = int(varBinds[1][1].prettyPrint())
287                self.deviceStatus = int(varBinds[2][1].prettyPrint())
[3025]288                self.printerDetectedErrorState = self.extractErrorStates(str(varBinds[3][1]))
[3039]289                self.parent.filter.logdebug("SNMP answer decoded : PageCounter : %s  PrinterStatus : '%s'  DeviceStatus : '%s'  PrinterErrorState : '%s'" \
[2877]290                     % (self.printerInternalPageCounter, \
291                        printerStatusValues.get(self.printerStatus), \
[3025]292                        deviceStatusValues.get(self.deviceStatus), \
[3039]293                        self.printerDetectedErrorState))
[2877]294else :
295    class Handler(BaseHandler) :
296        """A class for pysnmp v3.4.x"""
[3413]297        def retrieveSNMPValues(self) :
[2205]298            """Retrieves a printer's internal page counter and status via SNMP."""
299            ver = alpha.protoVersions[alpha.protoVersionId1]
300            req = ver.Message()
[2423]301            req.apiAlphaSetCommunity(self.community)
[2205]302            req.apiAlphaSetPdu(ver.GetRequestPdu())
303            req.apiAlphaGetPdu().apiAlphaSetVarBindList((pageCounterOID, ver.Null()), \
304                                                        (hrPrinterStatusOID, ver.Null()), \
[3025]305                                                        (hrDeviceStatusOID, ver.Null()), \
[3039]306                                                        (hrPrinterDetectedErrorStateOID, ver.Null()))
[2205]307            tsp = Manager()
308            try :
[2423]309                tsp.sendAndReceive(req.berEncode(), \
310                                   (self.printerHostname, self.port), \
311                                   (self.handleAnswer, req))
[3413]312            except (SnmpOverUdpError, select.error), msg :
[2205]313                self.parent.filter.printInfo(_("Network error while doing SNMP queries on printer %s : %s") % (self.printerHostname, msg), "warn")
[3027]314                self.initValues()
[2205]315            tsp.close()
[3413]316
[2205]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)
[3413]323            except TypeMismatchError, msg :
[2205]324                self.parent.filter.printInfo(_("SNMP message decoding error for printer %s : %s") % (self.printerHostname, msg), "warn")
[3027]325                self.initValues()
[2205]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)
[3413]335                        try :
[2205]336                            # keep maximum value seen for printer's internal page counter
337                            self.printerInternalPageCounter = max(self.printerInternalPageCounter, self.values[0])
[2305]338                            self.printerStatus = self.values[1]
339                            self.deviceStatus = self.values[2]
[3025]340                            self.printerDetectedErrorState = self.extractErrorStates(self.values[3])
[3039]341                            self.parent.filter.logdebug("SNMP answer decoded : PageCounter : %s  PrinterStatus : '%s'  DeviceStatus : '%s'  PrinterErrorState : '%s'" \
[2615]342                                 % (self.printerInternalPageCounter, \
[2617]343                                    printerStatusValues.get(self.printerStatus), \
[3025]344                                    deviceStatusValues.get(self.deviceStatus), \
[3039]345                                    self.printerDetectedErrorState))
[3413]346                        except IndexError :
[2205]347                            self.parent.filter.logdebug("SNMP answer is incomplete : %s" % str(self.values))
348                            pass
[3413]349                        else :
[2205]350                            return 1
[3413]351
[2506]352def main(hostname) :
353    """Tries SNMP accounting for a printer host."""
354    class fakeFilter :
[2830]355        """Fakes a filter for testing purposes."""
[2506]356        def __init__(self) :
[2830]357            """Initializes the fake filter."""
[2506]358            self.PrinterName = "FakePrintQueue"
359            self.JobSizeBytes = 1
[3413]360
[2506]361        def printInfo(self, msg, level="info") :
[2830]362            """Prints informational message."""
[2506]363            sys.stderr.write("%s : %s\n" % (level.upper(), msg))
364            sys.stderr.flush()
[3413]365
366        def logdebug(self, msg) :
[2830]367            """Prints debug message."""
[2506]368            self.printInfo(msg, "debug")
[3413]369
370    class fakeAccounter :
[2830]371        """Fakes an accounter for testing purposes."""
[2506]372        def __init__(self) :
[2830]373            """Initializes fake accounter."""
[2506]374            self.arguments = "snmp:public"
375            self.filter = fakeFilter()
376            self.protocolHandler = Handler(self, hostname)
[3413]377
378        def getLastPageCounter(self) :
[2830]379            """Fakes the return of a page counter."""
[2615]380            return 0
[3413]381
382    acc = fakeAccounter()
[2506]383    return acc.protocolHandler.retrieveInternalPageCounter()
[3413]384
385if __name__ == "__main__" :
386    if len(sys.argv) != 2 :
[2205]387        sys.stderr.write("Usage :  python  %s  printer_ip_address\n" % sys.argv[0])
[3413]388    else :
[2205]389        def _(msg) :
390            return msg
[3413]391
[2506]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.