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

Revision 3529, 16.4 kB (checked in by jerome, 14 years ago)

Removed unneeded code.

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