root / pykota / trunk / pykota / accounters / hardware.py @ 2202

Revision 2202, 22.0 kB (checked in by jerome, 19 years ago)

Now SNMP handling should correctly detect PowerSave/StandBy? mode

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
Line 
1# PyKota
2# -*- coding: ISO-8859-15 -*-
3#
4# PyKota - Print Quotas for CUPS and LPRng
5#
6# (c) 2003-2004 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
20#
21# $Id$
22#
23#
24
25import os
26import socket
27import time
28import signal
29import popen2
30
31from pykota.accounter import AccounterBase, PyKotaAccounterError
32
33ITERATIONDELAY = 1.0   # 1 Second
34STABILIZATIONDELAY = 3 # We must read three times the same value to consider it to be stable
35
36try :
37    from pysnmp.asn1.encoding.ber.error import TypeMismatchError
38    from pysnmp.mapping.udp.error import SnmpOverUdpError
39    from pysnmp.mapping.udp.role import Manager
40    from pysnmp.proto.api import alpha
41except ImportError :
42    hasSNMP = 0
43else :   
44    hasSNMP = 1
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    pageCounterOID2 = ".1.3.6.1.2.1.43.10.2.1.5.1.1"  # SNMPv2-SMI::mib-2.43.10.2.1.5.1.1
47    hrPrinterStatusOID = ".1.3.6.1.2.1.25.3.5.1.1.1" # SNMPv2-SMI::mib-2.25.3.5.1.1.1
48    printerStatusValues = { 1 : 'other',
49                            2 : 'unknown',
50                            3 : 'idle',
51                            4 : 'printing',
52                            5 : 'warmup',
53                          }
54    hrDeviceStatusOID = ".1.3.6.1.2.1.25.3.2.1.5.1" # SNMPv2-SMI::mib-2.25.3.2.1.5.1
55    deviceStatusValues = { 1 : 'unknown',
56                           2 : 'running',
57                           3 : 'warning',
58                           4 : 'testing',
59                           5 : 'down',
60                         } 
61    hrPrinterDetectedErrorStateOID = ".1.3.6.1.2.1.25.3.5.1.2.1" # SNMPv2-SMI::mib-2.25.3.5.1.2.1
62    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
63                         
64    #                     
65    # Documentation taken from RFC 3805 (Printer MIB v2) and RFC 2790 (Host Resource MIB)
66    #
67    # TODO : if hrDeviceStatus==2 and hrPrinterStatus==1 then it's powersave mode.
68    #
69    class SNMPAccounter :
70        """A class for SNMP print accounting."""
71        def __init__(self, parent, printerhostname) :
72            self.parent = parent
73            self.printerHostname = printerhostname
74            self.printerInternalPageCounter = None
75            self.printerInternalPageCounter2 = None
76            self.printerStatus = None
77            self.deviceStatus = None
78           
79        def retrieveSNMPValues(self) :   
80            """Retrieves a printer's internal page counter and status via SNMP."""
81            ver = alpha.protoVersions[alpha.protoVersionId1]
82            req = ver.Message()
83            req.apiAlphaSetCommunity('public')
84            req.apiAlphaSetPdu(ver.GetRequestPdu())
85            req.apiAlphaGetPdu().apiAlphaSetVarBindList((pageCounterOID, ver.Null()), \
86                                                        (pageCounterOID2, ver.Null()), \
87                                                        (hrPrinterStatusOID, ver.Null()), \
88                                                        (hrDeviceStatusOID, ver.Null()))
89            tsp = Manager()
90            try :
91                tsp.sendAndReceive(req.berEncode(), (self.printerHostname, 161), (self.handleAnswer, req))
92            except SnmpOverUdpError, msg :   
93                self.parent.filter.printInfo(_("Network error while doing SNMP queries on printer %s : %s") % (self.printerHostname, msg), "warn")
94            tsp.close()
95   
96        def handleAnswer(self, wholeMsg, notusedhere, req):
97            """Decodes and handles the SNMP answer."""
98            self.parent.filter.logdebug("SNMP message : '%s'" % repr(wholeMsg))
99            ver = alpha.protoVersions[alpha.protoVersionId1]
100            rsp = ver.Message()
101            try :
102                rsp.berDecode(wholeMsg)
103            except TypeMismatchError, msg :   
104                self.parent.filter.printInfo(_("SNMP message decoding error for printer %s : %s") % (self.printerHostname, msg), "warn")
105            else :
106                if req.apiAlphaMatch(rsp):
107                    errorStatus = rsp.apiAlphaGetPdu().apiAlphaGetErrorStatus()
108                    if errorStatus:
109                        self.parent.filter.printInfo(_("Problem encountered while doing SNMP queries on printer %s : %s") % (self.printerHostname, errorStatus), "warn")
110                    else:
111                        self.values = []
112                        for varBind in rsp.apiAlphaGetPdu().apiAlphaGetVarBindList():
113                            self.values.append(varBind.apiAlphaGetOidVal()[1].rawAsn1Value)
114                        try :   
115                            # keep maximum value seen for printer's internal page counter
116                            self.printerInternalPageCounter = max(self.printerInternalPageCounter, self.values[0])
117                            self.printerInternalPageCounter2 = max(self.printerInternalPageCounter2, self.values[1])
118                            self.printerStatus = self.values[2]
119                            self.deviceStatus = self.values[3]
120                            self.parent.filter.logdebug("SNMP answer is decoded : PageCounters : (%s, %s)  PrinterStatus : %s  DeviceStatus : %s" % tuple(self.values))
121                        except IndexError :   
122                            self.parent.filter.logdebug("SNMP answer is incomplete : %s" % str(self.values))
123                            pass
124                        else :   
125                            return 1
126                       
127        def waitPrinting(self) :
128            """Waits for printer status being 'printing'."""
129            firstvalue = None
130            while 1:
131                self.retrieveSNMPValues()
132                statusAsString = printerStatusValues.get(self.printerStatus)
133                if statusAsString in ('printing', 'warmup') :
134                    break
135                if self.printerInternalPageCounter is not None :   
136                    if firstvalue is None :
137                        # first time we retrieved a page counter, save it
138                        firstvalue = self.printerInternalPageCounter
139                    else :     
140                        # second time (or later)
141                        if firstvalue < self.printerInternalPageCounter :
142                            # Here we have a printer which lies :
143                            # it says it is not printing or warming up
144                            # BUT the page counter increases !!!
145                            # So we can probably quit being sure it is printing.
146                            self.parent.filter.printInfo("Printer %s is lying to us !!!" % self.parent.filter.printername, "warn")
147                            break
148                self.parent.filter.logdebug(_("Waiting for printer %s to be printing...") % self.parent.filter.printername)   
149                time.sleep(ITERATIONDELAY)
150           
151        def waitIdle(self) :
152            """Waits for printer status being 'idle'."""
153            idle_num = idle_flag = 0
154            while 1 :
155                self.retrieveSNMPValues()
156                pstatusAsString = printerStatusValues.get(self.printerStatus)
157                dstatusAsString = deviceStatusValues.get(self.deviceStatus)
158                idle_flag = 0
159                if (pstatusAsString == 'idle') or \
160                   ((pstatusAsString == 'other') and \
161                    (dstatusAsString == 'running')) :
162                    idle_flag = 1       # Standby / Powersave is considered idle
163                if idle_flag :   
164                    idle_num += 1
165                    if idle_num > STABILIZATIONDELAY :
166                        # printer status is stable, we can exit
167                        break
168                else :   
169                    idle_num = 0
170                self.parent.filter.logdebug(_("Waiting for printer %s's idle status to stabilize...") % self.parent.filter.printername)   
171                time.sleep(ITERATIONDELAY)
172               
173pjlMessage = "\033%-12345X@PJL USTATUSOFF\r\n@PJL INFO STATUS\r\n@PJL INFO PAGECOUNT\r\n\033%-12345X"
174pjlStatusValues = {
175                    "10000" : "Powersave Mode",
176                    "10001" : "Ready Online",
177                    "10002" : "Ready Offline",
178                    "10003" : "Warming Up",
179                    "10004" : "Self Test",
180                    "10005" : "Reset",
181                    "10023" : "Printing",
182                    "35078" : "Powersave Mode",         # 10000 is ALSO powersave !!!
183                    "40000" : "Sleep Mode",             # Standby
184                  }
185class PJLAccounter :
186    """A class for PJL print accounting."""
187    def __init__(self, parent, printerhostname) :
188        self.parent = parent
189        self.printerHostname = printerhostname
190        self.printerInternalPageCounter = self.printerStatus = None
191        self.timedout = 0
192       
193    def alarmHandler(self, signum, frame) :   
194        """Query has timedout, handle this."""
195        self.timedout = 1
196        raise IOError, "Waiting for PJL answer timed out. Please try again later."
197       
198    def retrievePJLValues(self) :   
199        """Retrieves a printer's internal page counter and status via PJL."""
200        port = 9100
201        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
202        try :
203            sock.connect((self.printerHostname, port))
204        except socket.error, msg :
205            self.parent.filter.printInfo(_("Problem during connection to %s:%s : %s") % (self.printerHostname, port, msg), "warn")
206        else :
207            try :
208                sock.send(pjlMessage)
209            except socket.error, msg :
210                self.parent.filter.printInfo(_("Problem while sending PJL query to %s:%s : %s") % (self.printerHostname, port, msg), "warn")
211            else :   
212                actualpagecount = self.printerStatus = None
213                self.timedout = 0
214                while (self.timedout == 0) or (actualpagecount is None) or (self.printerStatus is None) :
215                    signal.signal(signal.SIGALRM, self.alarmHandler)
216                    signal.alarm(3)
217                    try :
218                        answer = sock.recv(1024)
219                    except IOError, msg :   
220                        break   # our alarm handler was launched, probably
221                    else :   
222                        readnext = 0
223                        for line in [l.strip() for l in answer.split()] : 
224                            if line.startswith("CODE=") :
225                                self.printerStatus = line.split("=")[1]
226                            elif line.startswith("PAGECOUNT") :   
227                                readnext = 1 # page counter is on next line
228                            elif readnext :   
229                                actualpagecount = int(line.strip())
230                                readnext = 0
231                    signal.alarm(0)
232                self.printerInternalPageCounter = max(actualpagecount, self.printerInternalPageCounter)
233        sock.close()
234       
235    def waitPrinting(self) :
236        """Waits for printer status being 'printing'."""
237        firstvalue = None
238        while 1:
239            self.retrievePJLValues()
240            if self.printerStatus in ('10023', '10003') :
241                break
242            if self.printerInternalPageCounter is not None :   
243                if firstvalue is None :
244                    # first time we retrieved a page counter, save it
245                    firstvalue = self.printerInternalPageCounter
246                else :     
247                    # second time (or later)
248                    if firstvalue < self.printerInternalPageCounter :
249                        # Here we have a printer which lies :
250                        # it says it is not printing or warming up
251                        # BUT the page counter increases !!!
252                        # So we can probably quit being sure it is printing.
253                        self.parent.filter.printInfo("Printer %s is lying to us !!!" % self.parent.filter.printername, "warn")
254                        break
255            self.parent.filter.logdebug(_("Waiting for printer %s to be printing...") % self.parent.filter.printername)
256            time.sleep(ITERATIONDELAY)
257       
258    def waitIdle(self) :
259        """Waits for printer status being 'idle'."""
260        idle_num = idle_flag = 0
261        while 1 :
262            self.retrievePJLValues()
263            idle_flag = 0
264            if self.printerStatus in ('10000', '10001', '35078', '40000') :
265                idle_flag = 1
266            if idle_flag :   
267                idle_num += 1
268                if idle_num > STABILIZATIONDELAY :
269                    # printer status is stable, we can exit
270                    break
271            else :   
272                idle_num = 0
273            self.parent.filter.logdebug(_("Waiting for printer %s's idle status to stabilize...") % self.parent.filter.printername)
274            time.sleep(ITERATIONDELAY)
275   
276class Accounter(AccounterBase) :
277    def __init__(self, kotabackend, arguments) :
278        """Initializes querying accounter."""
279        AccounterBase.__init__(self, kotabackend, arguments)
280        self.isSoftware = 0
281       
282    def getPrinterInternalPageCounter(self) :   
283        """Returns the printer's internal page counter."""
284        self.filter.logdebug("Reading printer %s's internal page counter..." % self.filter.printername)
285        counter = self.askPrinterPageCounter(self.filter.printerhostname)
286        self.filter.logdebug("Printer %s's internal page counter value is : %s" % (self.filter.printername, str(counter)))
287        return counter   
288       
289    def beginJob(self, printer) :   
290        """Saves printer internal page counter at start of job."""
291        # save page counter before job
292        self.LastPageCounter = self.getPrinterInternalPageCounter()
293        self.fakeBeginJob()
294       
295    def fakeBeginJob(self) :   
296        """Fakes a begining of a job."""
297        self.counterbefore = self.getLastPageCounter()
298       
299    def endJob(self, printer) :   
300        """Saves printer internal page counter at end of job."""
301        # save page counter after job
302        self.LastPageCounter = self.counterafter = self.getPrinterInternalPageCounter()
303       
304    def getJobSize(self, printer) :   
305        """Returns the actual job size."""
306        if (not self.counterbefore) or (not self.counterafter) :
307            # there was a problem retrieving page counter
308            self.filter.printInfo(_("A problem occured while reading printer %s's internal page counter.") % printer.Name, "warn")
309            if printer.LastJob.Exists :
310                # if there's a previous job, use the last value from database
311                self.filter.printInfo(_("Retrieving printer %s's page counter from database instead.") % printer.Name, "warn")
312                if not self.counterbefore : 
313                    self.counterbefore = printer.LastJob.PrinterPageCounter or 0
314                if not self.counterafter :
315                    self.counterafter = printer.LastJob.PrinterPageCounter or 0
316                before = min(self.counterbefore, self.counterafter)   
317                after = max(self.counterbefore, self.counterafter)   
318                self.counterbefore = before
319                self.counterafter = after
320                if (not self.counterbefore) or (not self.counterafter) or (self.counterbefore == self.counterafter) :
321                    self.filter.printInfo(_("Couldn't retrieve printer %s's internal page counter either before or after printing.") % printer.Name, "warn")
322                    self.filter.printInfo(_("Job's size forced to 1 page for printer %s.") % printer.Name, "warn")
323                    self.counterbefore = 0
324                    self.counterafter = 1
325            else :
326                self.filter.printInfo(_("No previous job in database for printer %s.") % printer.Name, "warn")
327                self.filter.printInfo(_("Job's size forced to 1 page for printer %s.") % printer.Name, "warn")
328                self.counterbefore = 0
329                self.counterafter = 1
330               
331        jobsize = (self.counterafter - self.counterbefore)   
332        if jobsize < 0 :
333            # Try to take care of HP printers
334            # Their internal page counter is saved to NVRAM
335            # only every 10 pages. If the printer was switched
336            # off then back on during the job, and that the
337            # counters difference is negative, we know
338            # the formula (we can't know if more than eleven
339            # pages were printed though) :
340            if jobsize > -10 :
341                jobsize += 10
342            else :   
343                # here we may have got a printer being replaced
344                # DURING the job. This is HIGHLY improbable (but already happened) !
345                self.filter.printInfo(_("Inconsistent values for printer %s's internal page counter.") % printer.Name, "warn")
346                self.filter.printInfo(_("Job's size forced to 1 page for printer %s.") % printer.Name, "warn")
347                jobsize = 1
348        return jobsize
349       
350    def askPrinterPageCounter(self, printer) :
351        """Returns the page counter from the printer via an external command.
352       
353           The external command must report the life time page number of the printer on stdout.
354        """
355        commandline = self.arguments.strip() % locals()
356        cmdlower = commandline.lower()
357        if cmdlower == "snmp" :
358            if hasSNMP :
359                return self.askWithSNMP(printer)
360            else :   
361                raise PyKotaAccounterError, _("Internal SNMP accounting asked, but Python-SNMP is not available. Please download it from http://pysnmp.sourceforge.net")
362        elif cmdlower == "pjl" :
363            return self.askWithPJL(printer)
364           
365        if printer is None :
366            raise PyKotaAccounterError, _("Unknown printer address in HARDWARE(%s) for printer %s") % (commandline, self.filter.printername)
367        while 1 :   
368            self.filter.printInfo(_("Launching HARDWARE(%s)...") % commandline)
369            pagecounter = None
370            child = popen2.Popen4(commandline)   
371            try :
372                answer = child.fromchild.read()
373            except IOError :   
374                # we were interrupted by a signal, certainely a SIGTERM
375                # caused by the user cancelling the current job
376                try :
377                    os.kill(child.pid, signal.SIGTERM)
378                except :   
379                    pass # already killed ?
380                self.filter.printInfo(_("SIGTERM was sent to hardware accounter %s (pid: %s)") % (commandline, child.pid))
381            else :   
382                lines = [l.strip() for l in answer.split("\n")]
383                for i in range(len(lines)) : 
384                    try :
385                        pagecounter = int(lines[i])
386                    except (AttributeError, ValueError) :
387                        self.filter.printInfo(_("Line [%s] skipped in accounter's output. Trying again...") % lines[i])
388                    else :   
389                        break
390            child.fromchild.close()   
391            child.tochild.close()
392            try :
393                status = child.wait()
394            except OSError, msg :   
395                self.filter.logdebug("Error while waiting for hardware accounter pid %s : %s" % (child.pid, msg))
396            else :   
397                if os.WIFEXITED(status) :
398                    status = os.WEXITSTATUS(status)
399                self.filter.printInfo(_("Hardware accounter %s exit code is %s") % (self.arguments, str(status)))
400               
401            if pagecounter is None :
402                message = _("Unable to query printer %s via HARDWARE(%s)") % (printer, commandline)
403                if self.onerror == "CONTINUE" :
404                    self.filter.printInfo(message, "error")
405                else :
406                    raise PyKotaAccounterError, message 
407            else :       
408                return pagecounter       
409       
410    def askWithSNMP(self, printer) :
411        """Returns the page counter from the printer via internal SNMP handling."""
412        acc = SNMPAccounter(self, printer)
413        try :
414            if (os.environ.get("PYKOTASTATUS") != "CANCELLED") and \
415               (os.environ.get("PYKOTAACTION") != "DENY") and \
416               (os.environ.get("PYKOTAPHASE") == "AFTER") and \
417               self.filter.jobSizeBytes :
418                acc.waitPrinting()
419            acc.waitIdle()   
420        except :   
421            if acc.printerInternalPageCounter is None :
422                raise
423            else :   
424                self.filter.printInfo(_("SNMP querying stage interrupted. Using latest value seen for internal page counter (%s) on printer %s.") % (acc.printerInternalPageCounter, self.filter.printername), "warn")
425        return acc.printerInternalPageCounter
426       
427    def askWithPJL(self, printer) :
428        """Returns the page counter from the printer via internal PJL handling."""
429        acc = PJLAccounter(self, printer)
430        try :
431            if (os.environ.get("PYKOTASTATUS") != "CANCELLED") and \
432               (os.environ.get("PYKOTAACTION") != "DENY") and \
433               (os.environ.get("PYKOTAPHASE") == "AFTER") and \
434               self.filter.jobSizeBytes :
435                acc.waitPrinting()
436            acc.waitIdle()   
437        except :   
438            if acc.printerInternalPageCounter is None :
439                raise
440            else :   
441                self.filter.printInfo(_("PJL querying stage interrupted. Using latest value seen for internal page counter (%s) on printer %s.") % (acc.printerInternalPageCounter, self.filter.printername), "warn")
442        return acc.printerInternalPageCounter
Note: See TracBrowser for help on using the browser.