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

Revision 3519, 16.5 kB (checked in by jerome, 14 years ago)

Fuck !

  • 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
166        while 1:
167            self.retrieveSNMPValues()
168            statusAsString = printerStatusValues.get(self.printerStatus)
169            if statusAsString in ('printing', 'warmup') :
170                break
[3413]171            if self.printerInternalPageCounter is not None :
[2877]172                if firstvalue is None :
173                    # first time we retrieved a page counter, save it
174                    firstvalue = self.printerInternalPageCounter
[3413]175                else :
[2877]176                    # second time (or later)
177                    if firstvalue < self.printerInternalPageCounter :
178                        # Here we have a printer which lies :
179                        # it says it is not printing or warming up
180                        # BUT the page counter increases !!!
181                        # So we can probably quit being sure it is printing.
182                        self.parent.filter.printInfo("Printer %s is lying to us !!!" % self.parent.filter.PrinterName, "warn")
183                        break
[3025]184                    elif noprintingmaxdelay \
185                         and ((time.time() - self.timebefore) > noprintingmaxdelay) \
186                         and not self.checkIfError(self.printerDetectedErrorState) :
[2877]187                        # More than X seconds without the printer being in 'printing' mode
188                        # We can safely assume this won't change if printer is now 'idle'
189                        pstatusAsString = printerStatusValues.get(self.printerStatus)
190                        dstatusAsString = deviceStatusValues.get(self.deviceStatus)
191                        if (pstatusAsString == 'idle') or \
192                            ((pstatusAsString == 'other') and \
193                             (dstatusAsString == 'running')) :
194                            if self.printerInternalPageCounter == previousValue :
195                                # Here the job won't be printed, because probably
196                                # the printer rejected it for some reason.
197                                self.parent.filter.printInfo("Printer %s probably won't print this job !!!" % self.parent.filter.PrinterName, "warn")
[3413]198                            else :
[2877]199                                # Here the job has already been entirely printed, and
200                                # the printer has already passed from 'idle' to 'printing' to 'idle' again.
201                                self.parent.filter.printInfo("Printer %s has probably already printed this job !!!" % self.parent.filter.PrinterName, "warn")
202                            break
[3413]203            self.parent.filter.logdebug(_("Waiting for printer %s to be printing...") % self.parent.filter.PrinterName)
[3180]204            time.sleep(statusstabilizationdelay)
[3413]205
[2877]206    def waitIdle(self) :
207        """Waits for printer status being 'idle'."""
[3180]208        statusstabilizationdelay = constants.get(self.parent.filter, "StatusStabilizationDelay")
209        statusstabilizationloops = constants.get(self.parent.filter, "StatusStabilizationLoops")
[2877]210        idle_num = idle_flag = 0
211        while 1 :
212            self.retrieveSNMPValues()
213            pstatusAsString = printerStatusValues.get(self.printerStatus)
214            dstatusAsString = deviceStatusValues.get(self.deviceStatus)
215            idle_flag = 0
[3025]216            if (not self.checkIfError(self.printerDetectedErrorState)) \
217               and ((pstatusAsString == 'idle') or \
218                         ((pstatusAsString == 'other') and \
219                          (dstatusAsString == 'running'))) :
[2877]220                idle_flag = 1       # Standby / Powersave is considered idle
[3413]221            if idle_flag :
[3163]222                if (self.printerInternalPageCounter is not None) \
223                   and self.skipinitialwait \
224                   and (os.environ.get("PYKOTAPHASE") == "BEFORE") :
225                    self.parent.filter.logdebug("No need to wait for the printer to be idle, it is the case already.")
[3413]226                    return
[2877]227                idle_num += 1
[3180]228                if idle_num >= statusstabilizationloops :
[2877]229                    # printer status is stable, we can exit
230                    break
[3413]231            else :
[2877]232                idle_num = 0
[3413]233            self.parent.filter.logdebug(_("Waiting for printer %s's idle status to stabilize...") % self.parent.filter.PrinterName)
[3180]234            time.sleep(statusstabilizationdelay)
[3413]235
[2877]236    def retrieveInternalPageCounter(self) :
237        """Returns the page counter from the printer via internal SNMP handling."""
238        try :
239            if (os.environ.get("PYKOTASTATUS") != "CANCELLED") and \
240               (os.environ.get("PYKOTAACTION") == "ALLOW") and \
241               (os.environ.get("PYKOTAPHASE") == "AFTER") and \
242               self.parent.filter.JobSizeBytes :
243                self.waitPrinting()
[3413]244            self.waitIdle()
245        except :
[2877]246            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")
247            raise
248        return self.printerInternalPageCounter
[3413]249
[3506]250class Handler(BaseHandler) :
251    """A class for pysnmp v4.x, PyKota doesn't support earlier releases of pysnmp anymore.'"""
252    def __init__(self, *args):
253        BaseHandler.__init__(self, *args)
254        self.snmpEngine = cmdgen.CommandGenerator()
255        self.snmpAuth = cmdgen.CommunityData("pykota", self.community, 0)
256        self.snmpTarget = cmdgen.UdpTransportTarget((self.printerHostname, self.port))
257
258    def retrieveSNMPValues(self) :
259        """Retrieves a printer's internal page counter and status via SNMP."""
260        try :
261            errorIndication, errorStatus, errorIndex, varBinds = \
262                self.snmpEngine.getCmd(self.snmpAuth, \
263                                       self.snmpTarget, \
264                                       tuple([int(i) for i in pageCounterOID.split('.')]), \
265                                       tuple([int(i) for i in hrPrinterStatusOID.split('.')]), \
266                                       tuple([int(i) for i in hrDeviceStatusOID.split('.')]), \
267                                       tuple([int(i) for i in hrPrinterDetectedErrorStateOID.split('.')]))
268        except socket.gaierror, msg :
269            errorIndication = repr(msg)
270        except :
271            errorIndication = "Unknown SNMP/Network error. Check your wires."
272        if errorIndication :
273            self.parent.filter.printInfo("SNMP Error : %s" % errorIndication, "error")
274            self.initValues()
275        elif errorStatus :
276            self.parent.filter.printInfo("SNMP Error : %s at %s" % (errorStatus.prettyPrint(), \
[2877]277                                                                        varBinds[int(errorIndex)-1]), \
278                                             "error")
[3506]279            self.initValues()
280        else :
281            self.printerInternalPageCounter = max(self.printerInternalPageCounter, int(varBinds[0][1].prettyPrint() or "0"))
[3517]282            try :
283                self.printerStatus = int(varBinds[1][1].prettyPrint())
284            except ValueError :
[3519]285                self.parent.filter.logdebug("The printer reported a non-integer printer status, it will be converted to 2 ('unknown')")
[3517]286                self.printerStatus = 2
287            try :
288                self.deviceStatus = int(varBinds[2][1].prettyPrint())
289            except ValueError :
290                self.parent.filter.logdebug("The printer reported a non-integer device status, it will be converted to 1 ('unknown')")
291                self.deviceStatus = 1
[3506]292            self.printerDetectedErrorState = self.extractErrorStates(str(varBinds[3][1]))
293            self.parent.filter.logdebug("SNMP answer decoded : PageCounter : %s  PrinterStatus : '%s'  DeviceStatus : '%s'  PrinterErrorState : '%s'" \
294                                            % (self.printerInternalPageCounter, \
295                                                   printerStatusValues.get(self.printerStatus), \
296                                                   deviceStatusValues.get(self.deviceStatus), \
297                                                   self.printerDetectedErrorState))
[3413]298
[2506]299def main(hostname) :
300    """Tries SNMP accounting for a printer host."""
301    class fakeFilter :
[2830]302        """Fakes a filter for testing purposes."""
[2506]303        def __init__(self) :
[2830]304            """Initializes the fake filter."""
[2506]305            self.PrinterName = "FakePrintQueue"
306            self.JobSizeBytes = 1
[3413]307
[2506]308        def printInfo(self, msg, level="info") :
[2830]309            """Prints informational message."""
[2506]310            sys.stderr.write("%s : %s\n" % (level.upper(), msg))
311            sys.stderr.flush()
[3413]312
313        def logdebug(self, msg) :
[2830]314            """Prints debug message."""
[2506]315            self.printInfo(msg, "debug")
[3413]316
317    class fakeAccounter :
[2830]318        """Fakes an accounter for testing purposes."""
[2506]319        def __init__(self) :
[2830]320            """Initializes fake accounter."""
[2506]321            self.arguments = "snmp:public"
322            self.filter = fakeFilter()
323            self.protocolHandler = Handler(self, hostname)
[3413]324
325        def getLastPageCounter(self) :
[2830]326            """Fakes the return of a page counter."""
[2615]327            return 0
[3413]328
329    acc = fakeAccounter()
[2506]330    return acc.protocolHandler.retrieveInternalPageCounter()
[3413]331
332if __name__ == "__main__" :
333    if len(sys.argv) != 2 :
[2205]334        sys.stderr.write("Usage :  python  %s  printer_ip_address\n" % sys.argv[0])
[3413]335    else :
[2205]336        def _(msg) :
337            return msg
[3413]338
[2506]339        pagecounter = main(sys.argv[1])
340        print "Internal page counter's value is : %s" % pagecounter
Note: See TracBrowser for help on using the browser.