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

Revision 2830, 12.6 kB (checked in by jerome, 18 years ago)

Improved the code's quality a bit with pylint.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Auth Date Rev Id
Line 
1# PyKota
2# -*- coding: ISO-8859-15 -*-
3#
4# PyKota - Print Quotas for CUPS and LPRng
5#
6# (c) 2003, 2004, 2005, 2006 Jerome Alet <alet@librelogiciel.com>
7# This program is free software; you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by
9# the Free Software Foundation; either version 2 of the License, or
10# (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with this program; if not, write to the Free Software
19# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20#
21# $Id$
22#
23#
24
25ITERATIONDELAY = 1.5   # 1.5 Second
26STABILIZATIONDELAY = 3 # We must read three times the same value to consider it to be stable
27NOPRINTINGMAXDELAY = 60 # The printer must begin to print within 60 seconds.
28
29import sys
30import os
31import time
32import select
33
34try :
35    from pysnmp.asn1.encoding.ber.error import TypeMismatchError
36    from pysnmp.mapping.udp.error import SnmpOverUdpError
37    from pysnmp.mapping.udp.role import Manager
38    from pysnmp.proto.api import alpha
39except ImportError :
40    class Handler :
41        def __init__(self, parent, printerhostname) :
42            """Just there to raise an exception."""
43            raise RuntimeError, "The pysnmp module is not available. Download it from http://pysnmp.sf.net/"
44else :   
45    pageCounterOID = ".1.3.6.1.2.1.43.10.2.1.4.1.1"  # SNMPv2-SMI::mib-2.43.10.2.1.4.1.1
46    hrPrinterStatusOID = ".1.3.6.1.2.1.25.3.5.1.1.1" # SNMPv2-SMI::mib-2.25.3.5.1.1.1
47    printerStatusValues = { 1 : 'other',
48                            2 : 'unknown',
49                            3 : 'idle',
50                            4 : 'printing',
51                            5 : 'warmup',
52                          }
53    hrDeviceStatusOID = ".1.3.6.1.2.1.25.3.2.1.5.1" # SNMPv2-SMI::mib-2.25.3.2.1.5.1
54    deviceStatusValues = { 1 : 'unknown',
55                           2 : 'running',
56                           3 : 'warning',
57                           4 : 'testing',
58                           5 : 'down',
59                         } 
60    hrPrinterDetectedErrorStateOID = ".1.3.6.1.2.1.25.3.5.1.2.1" # SNMPv2-SMI::mib-2.25.3.5.1.2.1
61    prtConsoleDisplayBufferTextOID = ".1.3.6.1.2.1.43.16.5.1.2.1.1" # SNMPv2-SMI::mib-2.43.16.5.1.2.1.1
62                         
63    #                     
64    # Documentation taken from RFC 3805 (Printer MIB v2) and RFC 2790 (Host Resource MIB)
65    #
66    class Handler :
67        """A class for SNMP print accounting."""
68        def __init__(self, parent, printerhostname) :
69            self.parent = parent
70            self.printerHostname = printerhostname
71            try :
72                self.community = self.parent.arguments.split(":")[1].strip()
73            except IndexError :   
74                self.community = "public"
75            self.port = 161
76            self.printerInternalPageCounter = None
77            self.printerStatus = None
78            self.deviceStatus = None
79           
80        def retrieveSNMPValues(self) :   
81            """Retrieves a printer's internal page counter and status via SNMP."""
82            ver = alpha.protoVersions[alpha.protoVersionId1]
83            req = ver.Message()
84            req.apiAlphaSetCommunity(self.community)
85            req.apiAlphaSetPdu(ver.GetRequestPdu())
86            req.apiAlphaGetPdu().apiAlphaSetVarBindList((pageCounterOID, ver.Null()), \
87                                                        (hrPrinterStatusOID, ver.Null()), \
88                                                        (hrDeviceStatusOID, ver.Null()))
89            tsp = Manager()
90            try :
91                tsp.sendAndReceive(req.berEncode(), \
92                                   (self.printerHostname, self.port), \
93                                   (self.handleAnswer, req))
94            except (SnmpOverUdpError, select.error), msg :   
95                self.parent.filter.printInfo(_("Network error while doing SNMP queries on printer %s : %s") % (self.printerHostname, msg), "warn")
96            tsp.close()
97   
98        def handleAnswer(self, wholeMsg, notusedhere, req):
99            """Decodes and handles the SNMP answer."""
100            self.parent.filter.logdebug("SNMP answer : '%s'" % repr(wholeMsg))
101            ver = alpha.protoVersions[alpha.protoVersionId1]
102            rsp = ver.Message()
103            try :
104                rsp.berDecode(wholeMsg)
105            except TypeMismatchError, msg :   
106                self.parent.filter.printInfo(_("SNMP message decoding error for printer %s : %s") % (self.printerHostname, msg), "warn")
107            else :
108                if req.apiAlphaMatch(rsp):
109                    errorStatus = rsp.apiAlphaGetPdu().apiAlphaGetErrorStatus()
110                    if errorStatus:
111                        self.parent.filter.printInfo(_("Problem encountered while doing SNMP queries on printer %s : %s") % (self.printerHostname, errorStatus), "warn")
112                    else:
113                        self.values = []
114                        for varBind in rsp.apiAlphaGetPdu().apiAlphaGetVarBindList():
115                            self.values.append(varBind.apiAlphaGetOidVal()[1].rawAsn1Value)
116                        try :   
117                            # keep maximum value seen for printer's internal page counter
118                            self.printerInternalPageCounter = max(self.printerInternalPageCounter, self.values[0])
119                            self.printerStatus = self.values[1]
120                            self.deviceStatus = self.values[2]
121                            self.parent.filter.logdebug("SNMP answer decoded : PageCounter : %s  PrinterStatus : '%s'  DeviceStatus : '%s'" \
122                                 % (self.printerInternalPageCounter, \
123                                    printerStatusValues.get(self.printerStatus), \
124                                    deviceStatusValues.get(self.deviceStatus)))
125                        except IndexError :   
126                            self.parent.filter.logdebug("SNMP answer is incomplete : %s" % str(self.values))
127                            pass
128                        else :   
129                            return 1
130                       
131        def waitPrinting(self) :
132            """Waits for printer status being 'printing'."""
133            previousValue = self.parent.getLastPageCounter()
134            timebefore = time.time()
135            firstvalue = None
136            while 1:
137                self.retrieveSNMPValues()
138                statusAsString = printerStatusValues.get(self.printerStatus)
139                if statusAsString in ('printing', 'warmup') :
140                    break
141                if self.printerInternalPageCounter is not None :   
142                    if firstvalue is None :
143                        # first time we retrieved a page counter, save it
144                        firstvalue = self.printerInternalPageCounter
145                    else :     
146                        # second time (or later)
147                        if firstvalue < self.printerInternalPageCounter :
148                            # Here we have a printer which lies :
149                            # it says it is not printing or warming up
150                            # BUT the page counter increases !!!
151                            # So we can probably quit being sure it is printing.
152                            self.parent.filter.printInfo("Printer %s is lying to us !!!" % self.parent.filter.PrinterName, "warn")
153                            break
154                        elif (time.time() - timebefore) > NOPRINTINGMAXDELAY :
155                            # More than X seconds without the printer being in 'printing' mode
156                            # We can safely assume this won't change if printer is now 'idle'
157                            pstatusAsString = printerStatusValues.get(self.printerStatus)
158                            dstatusAsString = deviceStatusValues.get(self.deviceStatus)
159                            if (pstatusAsString == 'idle') or \
160                                ((pstatusAsString == 'other') and \
161                                 (dstatusAsString == 'running')) :
162                                if self.printerInternalPageCounter == previousValue :
163                                    # Here the job won't be printed, because probably
164                                    # the printer rejected it for some reason.
165                                    self.parent.filter.printInfo("Printer %s probably won't print this job !!!" % self.parent.filter.PrinterName, "warn")
166                                else :     
167                                    # Here the job has already been entirely printed, and
168                                    # the printer has already passed from 'idle' to 'printing' to 'idle' again.
169                                    self.parent.filter.printInfo("Printer %s has probably already printed this job !!!" % self.parent.filter.PrinterName, "warn")
170                                break
171                self.parent.filter.logdebug(_("Waiting for printer %s to be printing...") % self.parent.filter.PrinterName)   
172                time.sleep(ITERATIONDELAY)
173           
174        def waitIdle(self) :
175            """Waits for printer status being 'idle'."""
176            idle_num = idle_flag = 0
177            while 1 :
178                self.retrieveSNMPValues()
179                pstatusAsString = printerStatusValues.get(self.printerStatus)
180                dstatusAsString = deviceStatusValues.get(self.deviceStatus)
181                idle_flag = 0
182                if (pstatusAsString == 'idle') or \
183                   ((pstatusAsString == 'other') and \
184                    (dstatusAsString == 'running')) :
185                    idle_flag = 1       # Standby / Powersave is considered idle
186                if idle_flag :   
187                    idle_num += 1
188                    if idle_num >= STABILIZATIONDELAY :
189                        # printer status is stable, we can exit
190                        break
191                else :   
192                    idle_num = 0
193                self.parent.filter.logdebug(_("Waiting for printer %s's idle status to stabilize...") % self.parent.filter.PrinterName)   
194                time.sleep(ITERATIONDELAY)
195               
196        def retrieveInternalPageCounter(self) :
197            """Returns the page counter from the printer via internal SNMP handling."""
198            try :
199                if (os.environ.get("PYKOTASTATUS") != "CANCELLED") and \
200                   (os.environ.get("PYKOTAACTION") == "ALLOW") and \
201                   (os.environ.get("PYKOTAPHASE") == "AFTER") and \
202                   self.parent.filter.JobSizeBytes :
203                    self.waitPrinting()
204                self.waitIdle()   
205            except :   
206                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")
207                raise
208            return self.printerInternalPageCounter
209           
210def main(hostname) :
211    """Tries SNMP accounting for a printer host."""
212    class fakeFilter :
213        """Fakes a filter for testing purposes."""
214        def __init__(self) :
215            """Initializes the fake filter."""
216            self.PrinterName = "FakePrintQueue"
217            self.JobSizeBytes = 1
218           
219        def printInfo(self, msg, level="info") :
220            """Prints informational message."""
221            sys.stderr.write("%s : %s\n" % (level.upper(), msg))
222            sys.stderr.flush()
223           
224        def logdebug(self, msg) :   
225            """Prints debug message."""
226            self.printInfo(msg, "debug")
227           
228    class fakeAccounter :       
229        """Fakes an accounter for testing purposes."""
230        def __init__(self) :
231            """Initializes fake accounter."""
232            self.arguments = "snmp:public"
233            self.filter = fakeFilter()
234            self.protocolHandler = Handler(self, hostname)
235           
236        def getLastPageCounter(self) :   
237            """Fakes the return of a page counter."""
238            return 0
239       
240    acc = fakeAccounter()           
241    return acc.protocolHandler.retrieveInternalPageCounter()
242       
243if __name__ == "__main__" :           
244    if len(sys.argv) != 2 :   
245        sys.stderr.write("Usage :  python  %s  printer_ip_address\n" % sys.argv[0])
246    else :   
247        def _(msg) :
248            return msg
249           
250        pagecounter = main(sys.argv[1])
251        print "Internal page counter's value is : %s" % pagecounter
Note: See TracBrowser for help on using the browser.