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

Revision 3546, 17.1 kB (checked in by jerome, 14 years ago)

Fixes #65.

  • 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-2009 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    raise RuntimeError, "The pysnmp v4.x module is not available. Download it from http://pysnmp.sf.net/\nPyKota doesn't support earlier releases anymore."
40
41from pykota import constants
42
43#
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',
60                     }
61hrPrinterDetectedErrorStateOID = "1.3.6.1.2.1.25.3.5.1.2.1" # SNMPv2-SMI::mib-2.25.3.5.1.2.1
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                                    },
80                                  ]
81
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                          # ]
93
94# WARNING : some printers don't support this one :
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."""
98    def __init__(self, parent, printerhostname, skipinitialwait=False) :
99        self.parent = parent
100        self.printerHostname = printerhostname
101        self.skipinitialwait = skipinitialwait
102        try :
103            self.community = self.parent.arguments.split(":")[1].strip()
104        except IndexError :
105            self.community = "public"
106        self.port = 161
107        self.initValues()
108
109    def initValues(self) :
110        """Initializes SNMP values."""
111        self.printerInternalPageCounter = None
112        self.printerStatus = None
113        self.deviceStatus = None
114        self.printerDetectedErrorState = None
115        self.timebefore = time.time()   # resets timer also in case of error
116
117    def retrieveSNMPValues(self) :
118        """Retrieves a printer's internal page counter and status via SNMP."""
119        raise RuntimeError, "You have to overload this method."
120
121    def extractErrorStates(self, value) :
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)
130        return states
131
132    def checkIfError(self, errorstates) :
133        """Checks if any error state is fatal or not."""
134        if errorstates is None :
135            return True
136        else :
137            try :
138                errormask = self.parent.filter.config.getPrinterSNMPErrorMask(self.parent.filter.PrinterName)
139            except AttributeError : # debug mode
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))
149            for err in errorstates :
150                if err in errorConditions :
151                    self.parent.filter.logdebug("Error condition '%s' encountered. PyKota will wait until this problem is fixed." % err)
152                    return True
153            self.parent.filter.logdebug("No error condition matching mask 0x%04x" % errormask)
154            return False
155
156    def waitPrinting(self) :
157        """Waits for printer status being 'printing'."""
158        statusstabilizationdelay = constants.get(self.parent.filter, "StatusStabilizationDelay")
159        increment = 1
160        noprintingmaxdelay = constants.get(self.parent.filter, "NoPrintingMaxDelay")
161        if not noprintingmaxdelay :
162            self.parent.filter.logdebug("Will wait indefinitely until printer %s is in 'printing' state." % self.parent.filter.PrinterName)
163        else :
164            self.parent.filter.logdebug("Will wait until printer %s is in 'printing' state or %i seconds have elapsed." % (self.parent.filter.PrinterName, noprintingmaxdelay))
165        previousValue = self.parent.getLastPageCounter()
166        firstvalue = None
167        while True :
168            self.retrieveSNMPValues()
169            waitdelay = statusstabilizationdelay * increment
170            error = self.checkIfError(self.printerDetectedErrorState)
171            pstatusAsString = printerStatusValues.get(self.printerStatus)
172            dstatusAsString = deviceStatusValues.get(self.deviceStatus)
173            if pstatusAsString in ('printing', 'warmup') :
174                break
175            if self.printerInternalPageCounter is not None :
176                if firstvalue is None :
177                    # first time we retrieved a page counter, save it
178                    firstvalue = self.printerInternalPageCounter
179                else :
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
188                    elif noprintingmaxdelay \
189                         and ((time.time() - self.timebefore) > noprintingmaxdelay) \
190                         and not error :
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 \
195                             (dstatusAsString in ('running', 'warning'))) :
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")
200                            else :
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
205                    if error or (dstatusAsString == "down") :
206                        if waitdelay < constants.FIVEMINUTES :
207                            increment *= 2
208            self.parent.filter.logdebug("Waiting %s seconds for printer %s to be printing..." % (waitdelay, self.parent.filter.PrinterName))
209            time.sleep(waitdelay)
210
211    def waitIdle(self) :
212        """Waits for printer status being 'idle'."""
213        statusstabilizationdelay = constants.get(self.parent.filter, "StatusStabilizationDelay")
214        statusstabilizationloops = constants.get(self.parent.filter, "StatusStabilizationLoops")
215        increment = 1
216        idle_num = 0
217        while True :
218            self.retrieveSNMPValues()
219            waitdelay = statusstabilizationdelay * increment
220            error = self.checkIfError(self.printerDetectedErrorState)
221            pstatusAsString = printerStatusValues.get(self.printerStatus)
222            dstatusAsString = deviceStatusValues.get(self.deviceStatus)
223            idle_flag = False
224            if (not error) and ((pstatusAsString == 'idle') or \
225                                    ((pstatusAsString == 'other') and \
226                                         (dstatusAsString in ('running', 'warning')))) :
227                idle_flag = True # Standby / Powersave is considered idle
228                increment = 1 # Reset initial stabilization delay
229            if idle_flag :
230                if (self.printerInternalPageCounter is not None) \
231                   and self.skipinitialwait \
232                   and (os.environ.get("PYKOTAPHASE") == "BEFORE") :
233                    self.parent.filter.logdebug("No need to wait for the printer to be idle, it is the case already.")
234                    return
235                idle_num += 1
236                if idle_num >= statusstabilizationloops :
237                    # printer status is stable, we can exit
238                    break
239            else :
240                idle_num = 0
241            if error or (dstatusAsString == "down") :
242                if waitdelay < constants.FIVEMINUTES :
243                    increment *= 2
244            self.parent.filter.logdebug("Waiting %s seconds for printer %s's idle status to stabilize..." % (waitdelay,
245                                                                                                             self.parent.filter.PrinterName))
246            time.sleep(waitdelay)
247
248    def retrieveInternalPageCounter(self) :
249        """Returns the page counter from the printer via internal SNMP handling."""
250        try :
251            if (os.environ.get("PYKOTASTATUS") != "CANCELLED") and \
252               (os.environ.get("PYKOTAACTION") == "ALLOW") and \
253               (os.environ.get("PYKOTAPHASE") == "AFTER") and \
254               self.parent.filter.JobSizeBytes :
255                self.waitPrinting()
256            self.waitIdle()
257        except :
258            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")
259            raise
260        return self.printerInternalPageCounter
261
262class Handler(BaseHandler) :
263    """A class for pysnmp v4.x, PyKota doesn't support earlier releases of pysnmp anymore.'"""
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))
310
311def main(hostname) :
312    """Tries SNMP accounting for a printer host."""
313    class fakeFilter :
314        """Fakes a filter for testing purposes."""
315        def __init__(self) :
316            """Initializes the fake filter."""
317            self.PrinterName = "FakePrintQueue"
318            self.JobSizeBytes = 1
319
320        def printInfo(self, msg, level="info") :
321            """Prints informational message."""
322            sys.stderr.write("%s : %s\n" % (level.upper(), msg))
323            sys.stderr.flush()
324
325        def logdebug(self, msg) :
326            """Prints debug message."""
327            self.printInfo(msg, "debug")
328
329    class fakeAccounter :
330        """Fakes an accounter for testing purposes."""
331        def __init__(self) :
332            """Initializes fake accounter."""
333            self.arguments = "snmp:public"
334            self.filter = fakeFilter()
335            self.protocolHandler = Handler(self, hostname)
336
337        def getLastPageCounter(self) :
338            """Fakes the return of a page counter."""
339            return 0
340
341    acc = fakeAccounter()
342    return acc.protocolHandler.retrieveInternalPageCounter()
343
344if __name__ == "__main__" :
345    if len(sys.argv) != 2 :
346        sys.stderr.write("Usage :  python  %s  printer_ip_address\n" % sys.argv[0])
347    else :
348        pagecounter = main(sys.argv[1])
349        print "Internal page counter's value is : %s" % pagecounter
Note: See TracBrowser for help on using the browser.