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

Revision 3561, 17.4 kB (checked in by jerome, 11 years ago)

Changed copyright years.

  • 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-2013 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        increment = 1
167        waitdelay = statusstabilizationdelay * increment
168        while True :
169            self.retrieveSNMPValues()
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                    else :
209                        increment = 1
210            self.parent.filter.logdebug("Waiting %s seconds for printer %s to be printing..." % (waitdelay,
211                                                                                                 self.parent.filter.PrinterName))
212            time.sleep(waitdelay)
213            waitdelay = statusstabilizationdelay * increment
214
215    def waitIdle(self) :
216        """Waits for printer status being 'idle'."""
217        statusstabilizationdelay = constants.get(self.parent.filter, "StatusStabilizationDelay")
218        statusstabilizationloops = constants.get(self.parent.filter, "StatusStabilizationLoops")
219        increment = 1
220        waitdelay = statusstabilizationdelay * increment
221        idle_num = 0
222        while True :
223            self.retrieveSNMPValues()
224            error = self.checkIfError(self.printerDetectedErrorState)
225            pstatusAsString = printerStatusValues.get(self.printerStatus)
226            dstatusAsString = deviceStatusValues.get(self.deviceStatus)
227            idle_flag = False
228            if (not error) and ((pstatusAsString == 'idle') or \
229                                    ((pstatusAsString == 'other') and \
230                                         (dstatusAsString in ('running', 'warning')))) :
231                idle_flag = True # Standby / Powersave is considered idle
232                increment = 1 # Reset initial stabilization delay
233            if idle_flag :
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.")
238                    return
239                idle_num += 1
240                if idle_num >= statusstabilizationloops :
241                    # printer status is stable, we can exit
242                    break
243            else :
244                idle_num = 0
245            if error or (dstatusAsString == "down") :
246                if waitdelay < constants.FIVEMINUTES :
247                    increment *= 2
248            else :
249                increment = 1
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)
253            waitdelay = statusstabilizationdelay * increment
254
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()
263            self.waitIdle()
264        except :
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")
266            raise
267        return self.printerInternalPageCounter
268
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(), \
296                                                                        varBinds[int(errorIndex)-1]), \
297                                             "error")
298            self.initValues()
299        else :
300            self.printerInternalPageCounter = max(self.printerInternalPageCounter, int(varBinds[0][1].prettyPrint() or "0"))
301            try :
302                self.printerStatus = int(varBinds[1][1].prettyPrint())
303            except ValueError :
304                self.parent.filter.logdebug("The printer reported a non-integer printer status, it will be converted to 2 ('unknown')")
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
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))
317
318def main(hostname) :
319    """Tries SNMP accounting for a printer host."""
320    class fakeFilter :
321        """Fakes a filter for testing purposes."""
322        def __init__(self) :
323            """Initializes the fake filter."""
324            self.PrinterName = "FakePrintQueue"
325            self.JobSizeBytes = 1
326
327        def printInfo(self, msg, level="info") :
328            """Prints informational message."""
329            sys.stderr.write("%s : %s\n" % (level.upper(), msg))
330            sys.stderr.flush()
331
332        def logdebug(self, msg) :
333            """Prints debug message."""
334            self.printInfo(msg, "debug")
335
336    class fakeAccounter :
337        """Fakes an accounter for testing purposes."""
338        def __init__(self) :
339            """Initializes fake accounter."""
340            self.arguments = "snmp:public"
341            self.filter = fakeFilter()
342            self.protocolHandler = Handler(self, hostname)
343
344        def getLastPageCounter(self) :
345            """Fakes the return of a page counter."""
346            return 0
347
348    acc = fakeAccounter()
349    return acc.protocolHandler.retrieveInternalPageCounter()
350
351if __name__ == "__main__" :
352    if len(sys.argv) != 2 :
353        sys.stderr.write("Usage :  python  %s  printer_ip_address\n" % sys.argv[0])
354    else :
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.