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

Revision 2877, 14.3 kB (checked in by jerome, 18 years ago)

Added support for pysnmp v4.x in addition to v3.4.x

  • 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.entity.rfc3413.oneliner import cmdgen
36except ImportError :   
37    hasV4 = False
38    try :
39        from pysnmp.asn1.encoding.ber.error import TypeMismatchError
40        from pysnmp.mapping.udp.error import SnmpOverUdpError
41        from pysnmp.mapping.udp.role import Manager
42        from pysnmp.proto.api import alpha
43    except ImportError :
44        raise RuntimeError, "The pysnmp module is not available. Download it from http://pysnmp.sf.net/"
45else :
46    hasV4 = True
47
48#                     
49# Documentation taken from RFC 3805 (Printer MIB v2) and RFC 2790 (Host Resource MIB)
50#
51pageCounterOID = "1.3.6.1.2.1.43.10.2.1.4.1.1"  # SNMPv2-SMI::mib-2.43.10.2.1.4.1.1
52hrPrinterStatusOID = "1.3.6.1.2.1.25.3.5.1.1.1" # SNMPv2-SMI::mib-2.25.3.5.1.1.1
53printerStatusValues = { 1 : 'other',
54                        2 : 'unknown',
55                        3 : 'idle',
56                        4 : 'printing',
57                        5 : 'warmup',
58                      }
59hrDeviceStatusOID = "1.3.6.1.2.1.25.3.2.1.5.1" # SNMPv2-SMI::mib-2.25.3.2.1.5.1
60deviceStatusValues = { 1 : 'unknown',
61                       2 : 'running',
62                       3 : 'warning',
63                       4 : 'testing',
64                       5 : 'down',
65                     } 
66hrPrinterDetectedErrorStateOID = "1.3.6.1.2.1.25.3.5.1.2.1" # SNMPv2-SMI::mib-2.25.3.5.1.2.1
67prtConsoleDisplayBufferTextOID = "1.3.6.1.2.1.43.16.5.1.2.1.1" # SNMPv2-SMI::mib-2.43.16.5.1.2.1.1
68class BaseHandler :
69    """A class for SNMP print accounting."""
70    def __init__(self, parent, printerhostname) :
71        self.parent = parent
72        self.printerHostname = printerhostname
73        try :
74            self.community = self.parent.arguments.split(":")[1].strip()
75        except IndexError :   
76            self.community = "public"
77        self.port = 161
78        self.printerInternalPageCounter = None
79        self.printerStatus = None
80        self.deviceStatus = None
81       
82    def retrieveSNMPValues(self) :   
83        """Retrieves a printer's internal page counter and status via SNMP."""
84        raise RuntimeError, "You have to overload this method."
85       
86    def waitPrinting(self) :
87        """Waits for printer status being 'printing'."""
88        previousValue = self.parent.getLastPageCounter()
89        timebefore = time.time()
90        firstvalue = None
91        while 1:
92            self.retrieveSNMPValues()
93            statusAsString = printerStatusValues.get(self.printerStatus)
94            if statusAsString in ('printing', 'warmup') :
95                break
96            if self.printerInternalPageCounter is not None :   
97                if firstvalue is None :
98                    # first time we retrieved a page counter, save it
99                    firstvalue = self.printerInternalPageCounter
100                else :     
101                    # second time (or later)
102                    if firstvalue < self.printerInternalPageCounter :
103                        # Here we have a printer which lies :
104                        # it says it is not printing or warming up
105                        # BUT the page counter increases !!!
106                        # So we can probably quit being sure it is printing.
107                        self.parent.filter.printInfo("Printer %s is lying to us !!!" % self.parent.filter.PrinterName, "warn")
108                        break
109                    elif (time.time() - timebefore) > NOPRINTINGMAXDELAY :
110                        # More than X seconds without the printer being in 'printing' mode
111                        # We can safely assume this won't change if printer is now 'idle'
112                        pstatusAsString = printerStatusValues.get(self.printerStatus)
113                        dstatusAsString = deviceStatusValues.get(self.deviceStatus)
114                        if (pstatusAsString == 'idle') or \
115                            ((pstatusAsString == 'other') and \
116                             (dstatusAsString == 'running')) :
117                            if self.printerInternalPageCounter == previousValue :
118                                # Here the job won't be printed, because probably
119                                # the printer rejected it for some reason.
120                                self.parent.filter.printInfo("Printer %s probably won't print this job !!!" % self.parent.filter.PrinterName, "warn")
121                            else :     
122                                # Here the job has already been entirely printed, and
123                                # the printer has already passed from 'idle' to 'printing' to 'idle' again.
124                                self.parent.filter.printInfo("Printer %s has probably already printed this job !!!" % self.parent.filter.PrinterName, "warn")
125                            break
126            self.parent.filter.logdebug(_("Waiting for printer %s to be printing...") % self.parent.filter.PrinterName)   
127            time.sleep(ITERATIONDELAY)
128       
129    def waitIdle(self) :
130        """Waits for printer status being 'idle'."""
131        idle_num = idle_flag = 0
132        while 1 :
133            self.retrieveSNMPValues()
134            pstatusAsString = printerStatusValues.get(self.printerStatus)
135            dstatusAsString = deviceStatusValues.get(self.deviceStatus)
136            idle_flag = 0
137            if (pstatusAsString == 'idle') or \
138               ((pstatusAsString == 'other') and \
139                (dstatusAsString == 'running')) :
140                idle_flag = 1       # Standby / Powersave is considered idle
141            if idle_flag :   
142                idle_num += 1
143                if idle_num >= STABILIZATIONDELAY :
144                    # printer status is stable, we can exit
145                    break
146            else :   
147                idle_num = 0
148            self.parent.filter.logdebug(_("Waiting for printer %s's idle status to stabilize...") % self.parent.filter.PrinterName)   
149            time.sleep(ITERATIONDELAY)
150           
151    def retrieveInternalPageCounter(self) :
152        """Returns the page counter from the printer via internal SNMP handling."""
153        try :
154            if (os.environ.get("PYKOTASTATUS") != "CANCELLED") and \
155               (os.environ.get("PYKOTAACTION") == "ALLOW") and \
156               (os.environ.get("PYKOTAPHASE") == "AFTER") and \
157               self.parent.filter.JobSizeBytes :
158                self.waitPrinting()
159            self.waitIdle()   
160        except :   
161            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")
162            raise
163        return self.printerInternalPageCounter
164           
165if hasV4 :           
166    class Handler(BaseHandler) :
167        """A class for pysnmp v4.x"""
168        def retrieveSNMPValues(self) :
169            """Retrieves a printer's internal page counter and status via SNMP."""
170            errorIndication, errorStatus, errorIndex, varBinds = \
171                 cmdgen.CommandGenerator().getCmd(cmdgen.CommunityData("pykota", self.community, 0), \
172                                                  cmdgen.UdpTransportTarget((self.printerHostname, self.port)), \
173                                                  tuple([int(i) for i in pageCounterOID.split('.')]), \
174                                                  tuple([int(i) for i in hrPrinterStatusOID.split('.')]), \
175                                                  tuple([int(i) for i in hrDeviceStatusOID.split('.')]))
176            if errorIndication :                                                 
177                self.parent.filter.printInfo("SNMP Error : %s" % errorIndication, "error")
178            elif errorStatus :   
179                self.parent.filter.printInfo("SNMP Error : %s at %s" % (errorStatus.prettyPrint(), \
180                                                                        varBinds[int(errorIndex)-1]), \
181                                             "error")
182            else :                                 
183                self.printerInternalPageCounter = max(self.printerInternalPageCounter, int(varBinds[0][1].prettyPrint()))
184                self.printerStatus = int(varBinds[1][1].prettyPrint())
185                self.deviceStatus = int(varBinds[2][1].prettyPrint())
186                self.parent.filter.logdebug("SNMP answer decoded : PageCounter : %s  PrinterStatus : '%s'  DeviceStatus : '%s'" \
187                     % (self.printerInternalPageCounter, \
188                        printerStatusValues.get(self.printerStatus), \
189                        deviceStatusValues.get(self.deviceStatus)))
190else :
191    class Handler(BaseHandler) :
192        """A class for pysnmp v3.4.x"""
193        def retrieveSNMPValues(self) :   
194            """Retrieves a printer's internal page counter and status via SNMP."""
195            ver = alpha.protoVersions[alpha.protoVersionId1]
196            req = ver.Message()
197            req.apiAlphaSetCommunity(self.community)
198            req.apiAlphaSetPdu(ver.GetRequestPdu())
199            req.apiAlphaGetPdu().apiAlphaSetVarBindList((pageCounterOID, ver.Null()), \
200                                                        (hrPrinterStatusOID, ver.Null()), \
201                                                        (hrDeviceStatusOID, ver.Null()))
202            tsp = Manager()
203            try :
204                tsp.sendAndReceive(req.berEncode(), \
205                                   (self.printerHostname, self.port), \
206                                   (self.handleAnswer, req))
207            except (SnmpOverUdpError, select.error), msg :   
208                self.parent.filter.printInfo(_("Network error while doing SNMP queries on printer %s : %s") % (self.printerHostname, msg), "warn")
209            tsp.close()
210       
211        def handleAnswer(self, wholeMsg, notusedhere, req):
212            """Decodes and handles the SNMP answer."""
213            self.parent.filter.logdebug("SNMP answer : '%s'" % repr(wholeMsg))
214            ver = alpha.protoVersions[alpha.protoVersionId1]
215            rsp = ver.Message()
216            try :
217                rsp.berDecode(wholeMsg)
218            except TypeMismatchError, msg :   
219                self.parent.filter.printInfo(_("SNMP message decoding error for printer %s : %s") % (self.printerHostname, msg), "warn")
220            else :
221                if req.apiAlphaMatch(rsp):
222                    errorStatus = rsp.apiAlphaGetPdu().apiAlphaGetErrorStatus()
223                    if errorStatus:
224                        self.parent.filter.printInfo(_("Problem encountered while doing SNMP queries on printer %s : %s") % (self.printerHostname, errorStatus), "warn")
225                    else:
226                        self.values = []
227                        for varBind in rsp.apiAlphaGetPdu().apiAlphaGetVarBindList():
228                            self.values.append(varBind.apiAlphaGetOidVal()[1].rawAsn1Value)
229                        try :   
230                            # keep maximum value seen for printer's internal page counter
231                            self.printerInternalPageCounter = max(self.printerInternalPageCounter, self.values[0])
232                            self.printerStatus = self.values[1]
233                            self.deviceStatus = self.values[2]
234                            self.parent.filter.logdebug("SNMP answer decoded : PageCounter : %s  PrinterStatus : '%s'  DeviceStatus : '%s'" \
235                                 % (self.printerInternalPageCounter, \
236                                    printerStatusValues.get(self.printerStatus), \
237                                    deviceStatusValues.get(self.deviceStatus)))
238                        except IndexError :   
239                            self.parent.filter.logdebug("SNMP answer is incomplete : %s" % str(self.values))
240                            pass
241                        else :   
242                            return 1
243                   
244def main(hostname) :
245    """Tries SNMP accounting for a printer host."""
246    class fakeFilter :
247        """Fakes a filter for testing purposes."""
248        def __init__(self) :
249            """Initializes the fake filter."""
250            self.PrinterName = "FakePrintQueue"
251            self.JobSizeBytes = 1
252           
253        def printInfo(self, msg, level="info") :
254            """Prints informational message."""
255            sys.stderr.write("%s : %s\n" % (level.upper(), msg))
256            sys.stderr.flush()
257           
258        def logdebug(self, msg) :   
259            """Prints debug message."""
260            self.printInfo(msg, "debug")
261           
262    class fakeAccounter :       
263        """Fakes an accounter for testing purposes."""
264        def __init__(self) :
265            """Initializes fake accounter."""
266            self.arguments = "snmp:public"
267            self.filter = fakeFilter()
268            self.protocolHandler = Handler(self, hostname)
269           
270        def getLastPageCounter(self) :   
271            """Fakes the return of a page counter."""
272            return 0
273       
274    acc = fakeAccounter()           
275    return acc.protocolHandler.retrieveInternalPageCounter()
276       
277if __name__ == "__main__" :           
278    if len(sys.argv) != 2 :   
279        sys.stderr.write("Usage :  python  %s  printer_ip_address\n" % sys.argv[0])
280    else :   
281        def _(msg) :
282            return msg
283           
284        pagecounter = main(sys.argv[1])
285        print "Internal page counter's value is : %s" % pagecounter
Note: See TracBrowser for help on using the browser.