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

Revision 2203, 21.9 kB (checked in by jerome, 19 years ago)

TODO is now done !

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