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

Revision 3517, 16.5 kB (checked in by jerome, 12 years ago)

Reworked the existing code to log informational messages and prepare for
backport.

  • 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        noprintingmaxdelay = constants.get(self.parent.filter, "NoPrintingMaxDelay")
160        if not noprintingmaxdelay :
161            self.parent.filter.logdebug("Will wait indefinitely until printer %s is in 'printing' state." % self.parent.filter.PrinterName)
162        else :
163            self.parent.filter.logdebug("Will wait until printer %s is in 'printing' state or %i seconds have elapsed." % (self.parent.filter.PrinterName, noprintingmaxdelay))
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
171            if self.printerInternalPageCounter is not None :
172                if firstvalue is None :
173                    # first time we retrieved a page counter, save it
174                    firstvalue = self.printerInternalPageCounter
175                else :
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
184                    elif noprintingmaxdelay \
185                         and ((time.time() - self.timebefore) > noprintingmaxdelay) \
186                         and not self.checkIfError(self.printerDetectedErrorState) :
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")
198                            else :
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
203            self.parent.filter.logdebug(_("Waiting for printer %s to be printing...") % self.parent.filter.PrinterName)
204            time.sleep(statusstabilizationdelay)
205
206    def waitIdle(self) :
207        """Waits for printer status being 'idle'."""
208        statusstabilizationdelay = constants.get(self.parent.filter, "StatusStabilizationDelay")
209        statusstabilizationloops = constants.get(self.parent.filter, "StatusStabilizationLoops")
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
216            if (not self.checkIfError(self.printerDetectedErrorState)) \
217               and ((pstatusAsString == 'idle') or \
218                         ((pstatusAsString == 'other') and \
219                          (dstatusAsString == 'running'))) :
220                idle_flag = 1       # Standby / Powersave is considered idle
221            if idle_flag :
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.")
226                    return
227                idle_num += 1
228                if idle_num >= statusstabilizationloops :
229                    # printer status is stable, we can exit
230                    break
231            else :
232                idle_num = 0
233            self.parent.filter.logdebug(_("Waiting for printer %s's idle status to stabilize...") % self.parent.filter.PrinterName)
234            time.sleep(statusstabilizationdelay)
235
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()
244            self.waitIdle()
245        except :
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
249
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(), \
277                                                                        varBinds[int(errorIndex)-1]), \
278                                             "error")
279            self.initValues()
280        else :
281            self.printerInternalPageCounter = max(self.printerInternalPageCounter, int(varBinds[0][1].prettyPrint() or "0"))
282            try :
283                self.printerStatus = int(varBinds[1][1].prettyPrint())
284            except ValueError :
285                self parent.filter.logdebug("The printer reported a non-integer printer status, it will be converted to 2 ('unknown')")
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
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))
298
299def main(hostname) :
300    """Tries SNMP accounting for a printer host."""
301    class fakeFilter :
302        """Fakes a filter for testing purposes."""
303        def __init__(self) :
304            """Initializes the fake filter."""
305            self.PrinterName = "FakePrintQueue"
306            self.JobSizeBytes = 1
307
308        def printInfo(self, msg, level="info") :
309            """Prints informational message."""
310            sys.stderr.write("%s : %s\n" % (level.upper(), msg))
311            sys.stderr.flush()
312
313        def logdebug(self, msg) :
314            """Prints debug message."""
315            self.printInfo(msg, "debug")
316
317    class fakeAccounter :
318        """Fakes an accounter for testing purposes."""
319        def __init__(self) :
320            """Initializes fake accounter."""
321            self.arguments = "snmp:public"
322            self.filter = fakeFilter()
323            self.protocolHandler = Handler(self, hostname)
324
325        def getLastPageCounter(self) :
326            """Fakes the return of a page counter."""
327            return 0
328
329    acc = fakeAccounter()
330    return acc.protocolHandler.retrieveInternalPageCounter()
331
332if __name__ == "__main__" :
333    if len(sys.argv) != 2 :
334        sys.stderr.write("Usage :  python  %s  printer_ip_address\n" % sys.argv[0])
335    else :
336        def _(msg) :
337            return msg
338
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.