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

Revision 3548, 17.4 kB (checked in by jerome, 14 years ago)

During SNMP accounting, I forgot to decrease the waiting delay at the
time the printer went back to a normal status, when it
was increased because the printer was down.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Auth Date Rev Id
RevLine 
[3489]1# -*- coding: utf-8 -*-
[2205]2#
[3260]3# PyKota : Print Quotas for CUPS
[2205]4#
[3481]5# (c) 2003-2009 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 :
[3506]39    raise RuntimeError, "The pysnmp v4.x module is not available. Download it from http://pysnmp.sf.net/\nPyKota doesn't support earlier releases anymore."
[2877]40
[3175]41from pykota import constants
42
[3413]43#
[2877]44# Documentation taken from RFC 3805 (Printer MIB v2) and RFC 2790 (Host Resource MIB)
45#
46pageCounterOID = "1.3.6.1.2.1.43.10.2.1.4.1.1"  # SNMPv2-SMI::mib-2.43.10.2.1.4.1.1
47hrPrinterStatusOID = "1.3.6.1.2.1.25.3.5.1.1.1" # SNMPv2-SMI::mib-2.25.3.5.1.1.1
48printerStatusValues = { 1 : 'other',
49                        2 : 'unknown',
50                        3 : 'idle',
51                        4 : 'printing',
52                        5 : 'warmup',
53                      }
54hrDeviceStatusOID = "1.3.6.1.2.1.25.3.2.1.5.1" # SNMPv2-SMI::mib-2.25.3.2.1.5.1
55deviceStatusValues = { 1 : 'unknown',
56                       2 : 'running',
57                       3 : 'warning',
58                       4 : 'testing',
59                       5 : 'down',
[3413]60                     }
[2877]61hrPrinterDetectedErrorStateOID = "1.3.6.1.2.1.25.3.5.1.2.1" # SNMPv2-SMI::mib-2.25.3.5.1.2.1
[3025]62printerDetectedErrorStateValues = [ { 128 : 'Low Paper',
63                                       64 : 'No Paper',
64                                       32 : 'Low Toner',
65                                       16 : 'No Toner',
66                                        8 : 'Door Open',
67                                        4 : 'Jammed',
68                                        2 : 'Offline',
69                                        1 : 'Service Requested',
70                                    },
71                                    { 128 : 'Input Tray Missing',
72                                       64 : 'Output Tray Missing',
73                                       32 : 'Marker Supply Missing',
74                                       16 : 'Output Near Full',
75                                        8 : 'Output Full',
76                                        4 : 'Input Tray Empty',
77                                        2 : 'Overdue Preventive Maintainance',
78                                        1 : 'Not Assigned in RFC3805',
79                                    },
[3413]80                                  ]
81
[3190]82# The default error mask to use when checking error conditions.
83defaultErrorMask = 0x4fcc # [ 'No Paper',
84                          #   'Door Open',
85                          #   'Jammed',
86                          #   'Offline',
87                          #   'Service Requested',
88                          #   'Input Tray Missing',
89                          #   'Output Tray Missing',
90                          #   'Output Full',
91                          #   'Input Tray Empty',
92                          # ]
[3413]93
94# WARNING : some printers don't support this one :
[2877]95prtConsoleDisplayBufferTextOID = "1.3.6.1.2.1.43.16.5.1.2.1.1" # SNMPv2-SMI::mib-2.43.16.5.1.2.1.1
96class BaseHandler :
97    """A class for SNMP print accounting."""
[3162]98    def __init__(self, parent, printerhostname, skipinitialwait=False) :
[2877]99        self.parent = parent
100        self.printerHostname = printerhostname
[3162]101        self.skipinitialwait = skipinitialwait
[2877]102        try :
103            self.community = self.parent.arguments.split(":")[1].strip()
[3413]104        except IndexError :
[2877]105            self.community = "public"
106        self.port = 161
[3025]107        self.initValues()
[3413]108
109    def initValues(self) :
[3025]110        """Initializes SNMP values."""
[2877]111        self.printerInternalPageCounter = None
112        self.printerStatus = None
113        self.deviceStatus = None
[3025]114        self.printerDetectedErrorState = None
115        self.timebefore = time.time()   # resets timer also in case of error
[3413]116
117    def retrieveSNMPValues(self) :
[2877]118        """Retrieves a printer's internal page counter and status via SNMP."""
119        raise RuntimeError, "You have to overload this method."
[3413]120
121    def extractErrorStates(self, value) :
[3025]122        """Returns a list of textual error states from a binary value."""
123        states = []
124        for i in range(min(len(value), len(printerDetectedErrorStateValues))) :
125            byte = ord(value[i])
126            bytedescription = printerDetectedErrorStateValues[i]
127            for (k, v) in bytedescription.items() :
128                if byte & k :
129                    states.append(v)
[3413]130        return states
131
132    def checkIfError(self, errorstates) :
[3025]133        """Checks if any error state is fatal or not."""
[3028]134        if errorstates is None :
135            return True
136        else :
[3190]137            try :
138                errormask = self.parent.filter.config.getPrinterSNMPErrorMask(self.parent.filter.PrinterName)
[3413]139            except AttributeError : # debug mode
[3190]140                errormask = defaultErrorMask
141            if errormask is None :
142                errormask = defaultErrorMask
143            errormaskbytes = [ chr((errormask & 0xff00) >> 8),
144                               chr((errormask & 0x00ff)),
145                             ]
146            errorConditions = self.extractErrorStates(errormaskbytes)
147            self.parent.filter.logdebug("Error conditions for mask 0x%04x : %s" \
148                                               % (errormask, errorConditions))
[3028]149            for err in errorstates :
150                if err in errorConditions :
[3190]151                    self.parent.filter.logdebug("Error condition '%s' encountered. PyKota will wait until this problem is fixed." % err)
[3028]152                    return True
[3190]153            self.parent.filter.logdebug("No error condition matching mask 0x%04x" % errormask)
[3413]154            return False
155
[2877]156    def waitPrinting(self) :
157        """Waits for printer status being 'printing'."""
[3180]158        statusstabilizationdelay = constants.get(self.parent.filter, "StatusStabilizationDelay")
159        noprintingmaxdelay = constants.get(self.parent.filter, "NoPrintingMaxDelay")
[3025]160        if not noprintingmaxdelay :
161            self.parent.filter.logdebug("Will wait indefinitely until printer %s is in 'printing' state." % self.parent.filter.PrinterName)
[3413]162        else :
[3025]163            self.parent.filter.logdebug("Will wait until printer %s is in 'printing' state or %i seconds have elapsed." % (self.parent.filter.PrinterName, noprintingmaxdelay))
[2877]164        previousValue = self.parent.getLastPageCounter()
165        firstvalue = None
[3548]166        increment = 1
167        waitdelay = statusstabilizationdelay * increment
[3529]168        while True :
[2877]169            self.retrieveSNMPValues()
[3530]170            error = self.checkIfError(self.printerDetectedErrorState)
[3529]171            pstatusAsString = printerStatusValues.get(self.printerStatus)
[3530]172            dstatusAsString = deviceStatusValues.get(self.deviceStatus)
[3529]173            if pstatusAsString in ('printing', 'warmup') :
[2877]174                break
[3413]175            if self.printerInternalPageCounter is not None :
[2877]176                if firstvalue is None :
177                    # first time we retrieved a page counter, save it
178                    firstvalue = self.printerInternalPageCounter
[3413]179                else :
[2877]180                    # second time (or later)
181                    if firstvalue < self.printerInternalPageCounter :
182                        # Here we have a printer which lies :
183                        # it says it is not printing or warming up
184                        # BUT the page counter increases !!!
185                        # So we can probably quit being sure it is printing.
186                        self.parent.filter.printInfo("Printer %s is lying to us !!!" % self.parent.filter.PrinterName, "warn")
187                        break
[3025]188                    elif noprintingmaxdelay \
189                         and ((time.time() - self.timebefore) > noprintingmaxdelay) \
[3530]190                         and not error :
[2877]191                        # More than X seconds without the printer being in 'printing' mode
192                        # We can safely assume this won't change if printer is now 'idle'
193                        if (pstatusAsString == 'idle') or \
194                            ((pstatusAsString == 'other') and \
[3546]195                             (dstatusAsString in ('running', 'warning'))) :
[2877]196                            if self.printerInternalPageCounter == previousValue :
197                                # Here the job won't be printed, because probably
198                                # the printer rejected it for some reason.
199                                self.parent.filter.printInfo("Printer %s probably won't print this job !!!" % self.parent.filter.PrinterName, "warn")
[3413]200                            else :
[2877]201                                # Here the job has already been entirely printed, and
202                                # the printer has already passed from 'idle' to 'printing' to 'idle' again.
203                                self.parent.filter.printInfo("Printer %s has probably already printed this job !!!" % self.parent.filter.PrinterName, "warn")
204                            break
[3530]205                    if error or (dstatusAsString == "down") :
206                        if waitdelay < constants.FIVEMINUTES :
207                            increment *= 2
[3548]208                    else :
209                        increment = 1
210            self.parent.filter.logdebug("Waiting %s seconds for printer %s to be printing..." % (waitdelay,
211                                                                                                 self.parent.filter.PrinterName))
[3530]212            time.sleep(waitdelay)
[3548]213            waitdelay = statusstabilizationdelay * increment
[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")
[3530]219        increment = 1
[3548]220        waitdelay = statusstabilizationdelay * increment
[3530]221        idle_num = 0
[3529]222        while True :
[2877]223            self.retrieveSNMPValues()
[3530]224            error = self.checkIfError(self.printerDetectedErrorState)
[2877]225            pstatusAsString = printerStatusValues.get(self.printerStatus)
226            dstatusAsString = deviceStatusValues.get(self.deviceStatus)
[3530]227            idle_flag = False
228            if (not error) and ((pstatusAsString == 'idle') or \
229                                    ((pstatusAsString == 'other') and \
[3546]230                                         (dstatusAsString in ('running', 'warning')))) :
[3530]231                idle_flag = True # Standby / Powersave is considered idle
232                increment = 1 # Reset initial stabilization delay
[3413]233            if idle_flag :
[3163]234                if (self.printerInternalPageCounter is not None) \
235                   and self.skipinitialwait \
236                   and (os.environ.get("PYKOTAPHASE") == "BEFORE") :
237                    self.parent.filter.logdebug("No need to wait for the printer to be idle, it is the case already.")
[3413]238                    return
[2877]239                idle_num += 1
[3180]240                if idle_num >= statusstabilizationloops :
[2877]241                    # printer status is stable, we can exit
242                    break
[3413]243            else :
[2877]244                idle_num = 0
[3530]245            if error or (dstatusAsString == "down") :
246                if waitdelay < constants.FIVEMINUTES :
247                    increment *= 2
[3548]248            else :
249                increment = 1
[3530]250            self.parent.filter.logdebug("Waiting %s seconds for printer %s's idle status to stabilize..." % (waitdelay,
251                                                                                                             self.parent.filter.PrinterName))
252            time.sleep(waitdelay)
[3548]253            waitdelay = statusstabilizationdelay * increment
[3413]254
[2877]255    def retrieveInternalPageCounter(self) :
256        """Returns the page counter from the printer via internal SNMP handling."""
257        try :
258            if (os.environ.get("PYKOTASTATUS") != "CANCELLED") and \
259               (os.environ.get("PYKOTAACTION") == "ALLOW") and \
260               (os.environ.get("PYKOTAPHASE") == "AFTER") and \
261               self.parent.filter.JobSizeBytes :
262                self.waitPrinting()
[3413]263            self.waitIdle()
264        except :
[3530]265            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")
[2877]266            raise
267        return self.printerInternalPageCounter
[3413]268
[3506]269class Handler(BaseHandler) :
270    """A class for pysnmp v4.x, PyKota doesn't support earlier releases of pysnmp anymore.'"""
271    def __init__(self, *args):
272        BaseHandler.__init__(self, *args)
273        self.snmpEngine = cmdgen.CommandGenerator()
274        self.snmpAuth = cmdgen.CommunityData("pykota", self.community, 0)
275        self.snmpTarget = cmdgen.UdpTransportTarget((self.printerHostname, self.port))
276
277    def retrieveSNMPValues(self) :
278        """Retrieves a printer's internal page counter and status via SNMP."""
279        try :
280            errorIndication, errorStatus, errorIndex, varBinds = \
281                self.snmpEngine.getCmd(self.snmpAuth, \
282                                       self.snmpTarget, \
283                                       tuple([int(i) for i in pageCounterOID.split('.')]), \
284                                       tuple([int(i) for i in hrPrinterStatusOID.split('.')]), \
285                                       tuple([int(i) for i in hrDeviceStatusOID.split('.')]), \
286                                       tuple([int(i) for i in hrPrinterDetectedErrorStateOID.split('.')]))
287        except socket.gaierror, msg :
288            errorIndication = repr(msg)
289        except :
290            errorIndication = "Unknown SNMP/Network error. Check your wires."
291        if errorIndication :
292            self.parent.filter.printInfo("SNMP Error : %s" % errorIndication, "error")
293            self.initValues()
294        elif errorStatus :
295            self.parent.filter.printInfo("SNMP Error : %s at %s" % (errorStatus.prettyPrint(), \
[2877]296                                                                        varBinds[int(errorIndex)-1]), \
297                                             "error")
[3506]298            self.initValues()
299        else :
300            self.printerInternalPageCounter = max(self.printerInternalPageCounter, int(varBinds[0][1].prettyPrint() or "0"))
[3517]301            try :
302                self.printerStatus = int(varBinds[1][1].prettyPrint())
303            except ValueError :
[3519]304                self.parent.filter.logdebug("The printer reported a non-integer printer status, it will be converted to 2 ('unknown')")
[3517]305                self.printerStatus = 2
306            try :
307                self.deviceStatus = int(varBinds[2][1].prettyPrint())
308            except ValueError :
309                self.parent.filter.logdebug("The printer reported a non-integer device status, it will be converted to 1 ('unknown')")
310                self.deviceStatus = 1
[3506]311            self.printerDetectedErrorState = self.extractErrorStates(str(varBinds[3][1]))
312            self.parent.filter.logdebug("SNMP answer decoded : PageCounter : %s  PrinterStatus : '%s'  DeviceStatus : '%s'  PrinterErrorState : '%s'" \
313                                            % (self.printerInternalPageCounter, \
314                                                   printerStatusValues.get(self.printerStatus), \
315                                                   deviceStatusValues.get(self.deviceStatus), \
316                                                   self.printerDetectedErrorState))
[3413]317
[2506]318def main(hostname) :
319    """Tries SNMP accounting for a printer host."""
320    class fakeFilter :
[2830]321        """Fakes a filter for testing purposes."""
[2506]322        def __init__(self) :
[2830]323            """Initializes the fake filter."""
[2506]324            self.PrinterName = "FakePrintQueue"
325            self.JobSizeBytes = 1
[3413]326
[2506]327        def printInfo(self, msg, level="info") :
[2830]328            """Prints informational message."""
[2506]329            sys.stderr.write("%s : %s\n" % (level.upper(), msg))
330            sys.stderr.flush()
[3413]331
332        def logdebug(self, msg) :
[2830]333            """Prints debug message."""
[2506]334            self.printInfo(msg, "debug")
[3413]335
336    class fakeAccounter :
[2830]337        """Fakes an accounter for testing purposes."""
[2506]338        def __init__(self) :
[2830]339            """Initializes fake accounter."""
[2506]340            self.arguments = "snmp:public"
341            self.filter = fakeFilter()
342            self.protocolHandler = Handler(self, hostname)
[3413]343
344        def getLastPageCounter(self) :
[2830]345            """Fakes the return of a page counter."""
[2615]346            return 0
[3413]347
348    acc = fakeAccounter()
[2506]349    return acc.protocolHandler.retrieveInternalPageCounter()
[3413]350
351if __name__ == "__main__" :
352    if len(sys.argv) != 2 :
[2205]353        sys.stderr.write("Usage :  python  %s  printer_ip_address\n" % sys.argv[0])
[3413]354    else :
[2506]355        pagecounter = main(sys.argv[1])
356        print "Internal page counter's value is : %s" % pagecounter
Note: See TracBrowser for help on using the browser.