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

Revision 1949, 22.0 kB (checked in by jalet, 19 years ago)

Modified the SNMP fix as hinted by pysnmp's maintainer

  • 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# $Log$
24# Revision 1.34  2004/11/19 11:57:51  jalet
25# Modified the SNMP fix as hinted by pysnmp's maintainer
26#
27# Revision 1.33  2004/11/19 10:35:37  jalet
28# Catches TypeMismatchError in SNMP answer handling code
29#
30# Revision 1.32  2004/11/16 23:23:40  jalet
31# Fixed internal PJL handling wrt the 35078 PowerSave mode.
32#
33# Revision 1.31  2004/11/01 14:32:26  jalet
34# Fix for unneeded out of band status in pjl_over_tcp/9100
35#
36# Revision 1.30  2004/10/05 09:21:34  jalet
37# Removed misleading comments
38#
39# Revision 1.29  2004/10/05 09:20:07  jalet
40# Reduced delay from 2 to 1 seconds in internal SNMP and PJL_over_TCP
41# handlers
42#
43# Revision 1.28  2004/09/27 20:09:30  jalet
44# Lowered timeout delay for PJL queries
45#
46# Revision 1.27  2004/09/27 20:00:35  jalet
47# Typo
48#
49# Revision 1.26  2004/09/27 19:56:27  jalet
50# Added internal handling for PJL queries over port tcp/9100. Now waits
51# for printer being idle before asking, just like with SNMP.
52#
53# Revision 1.25  2004/09/27 09:21:37  jalet
54# Now includes printer's hostname in SNMP error messages
55#
56# Revision 1.24  2004/09/24 21:19:48  jalet
57# Did a pass of PyChecker
58#
59# Revision 1.23  2004/09/23 19:18:12  jalet
60# Now loops when the external hardware accounter fails, until it returns a correct value
61#
62# Revision 1.22  2004/09/22 19:48:01  jalet
63# Logs the looping message as debug instead of as info
64#
65# Revision 1.21  2004/09/22 19:27:41  jalet
66# Bad import statement
67#
68# Revision 1.20  2004/09/22 19:22:27  jalet
69# Just loop in case a network error occur
70#
71# Revision 1.19  2004/09/22 14:29:01  jalet
72# Fixed nasty typo
73#
74# Revision 1.18  2004/09/21 16:00:46  jalet
75# More informational messages
76#
77# Revision 1.17  2004/09/21 13:42:18  jalet
78# Typo
79#
80# Revision 1.16  2004/09/21 13:30:53  jalet
81# First try at full SNMP handling from the Python code.
82#
83# Revision 1.15  2004/09/14 11:38:59  jalet
84# Minor fix
85#
86# Revision 1.14  2004/09/14 06:53:53  jalet
87# Small test added
88#
89# Revision 1.13  2004/09/13 16:02:45  jalet
90# Added fix for incorrect job's size when hardware accounting fails
91#
92# Revision 1.12  2004/09/06 15:42:34  jalet
93# Fix missing import statement for the signal module
94#
95# Revision 1.11  2004/08/31 23:29:53  jalet
96# Introduction of the new 'onaccountererror' configuration directive.
97# Small fix for software accounter's return code which can't be None anymore.
98# Make software and hardware accounting code look similar : will be factorized
99# later.
100#
101# Revision 1.10  2004/08/27 22:49:04  jalet
102# No answer from subprocess now is really a fatal error. Waiting for some
103# time to make this configurable...
104#
105# Revision 1.9  2004/08/25 22:34:39  jalet
106# Now both software and hardware accounting raise an exception when no valid
107# result can be extracted from the subprocess' output.
108# Hardware accounting now reads subprocess' output until an integer is read
109# or data is exhausted : it now behaves just like software accounting in this
110# aspect.
111#
112# Revision 1.8  2004/07/22 22:41:48  jalet
113# Hardware accounting for LPRng should be OK now. UNTESTED.
114#
115# Revision 1.7  2004/07/16 12:22:47  jalet
116# LPRng support early version
117#
118# Revision 1.6  2004/07/01 19:56:42  jalet
119# Better dispatching of error messages
120#
121# Revision 1.5  2004/06/10 22:42:06  jalet
122# Better messages in logs
123#
124# Revision 1.4  2004/05/24 22:45:49  jalet
125# New 'enforcement' directive added
126# Polling loop improvements
127#
128# Revision 1.3  2004/05/24 14:36:40  jalet
129# Revert to old polling loop. Will need optimisations
130#
131# Revision 1.2  2004/05/18 14:49:22  jalet
132# Big code changes to completely remove the need for "requester" directives,
133# jsut use "hardware(... your previous requester directive's content ...)"
134#
135# Revision 1.1  2004/05/13 13:59:30  jalet
136# Code simplifications
137#
138#
139#
140
141import os
142import socket
143import time
144import signal
145import popen2
146
147from pykota.accounter import AccounterBase, PyKotaAccounterError
148
149ITERATIONDELAY = 1.0   # 1 Second
150STABILIZATIONDELAY = 3 # We must read three times the same value to consider it to be stable
151
152try :
153    from pysnmp.asn1.encoding.ber.error import TypeMismatchError
154    from pysnmp.mapping.udp.error import SnmpOverUdpError
155    from pysnmp.mapping.udp.role import Manager
156    from pysnmp.proto.api import alpha
157except ImportError :
158    hasSNMP = 0
159else :   
160    hasSNMP = 1
161    pageCounterOID = ".1.3.6.1.2.1.43.10.2.1.4.1.1"
162    hrPrinterStatusOID = ".1.3.6.1.2.1.25.3.5.1.1.1"
163    printerStatusValues = { 1 : 'other',
164                            2 : 'unknown',
165                            3 : 'idle',
166                            4 : 'printing',
167                            5 : 'warmup',
168                          }
169                         
170    class SNMPAccounter :
171        """A class for SNMP print accounting."""
172        def __init__(self, parent, printerhostname) :
173            self.parent = parent
174            self.printerHostname = printerhostname
175            self.printerInternalPageCounter = self.printerStatus = None
176           
177        def retrieveSNMPValues(self) :   
178            """Retrieves a printer's internal page counter and status via SNMP."""
179            ver = alpha.protoVersions[alpha.protoVersionId1]
180            req = ver.Message()
181            req.apiAlphaSetCommunity('public')
182            req.apiAlphaSetPdu(ver.GetRequestPdu())
183            req.apiAlphaGetPdu().apiAlphaSetVarBindList((pageCounterOID, ver.Null()), (hrPrinterStatusOID, ver.Null()))
184            tsp = Manager()
185            try :
186                tsp.sendAndReceive(req.berEncode(), (self.printerHostname, 161), (self.handleAnswer, req))
187            except SnmpOverUdpError, msg :   
188                self.parent.filter.printInfo(_("Network error while doing SNMP queries on printer %s : %s") % (self.printerHostname, msg), "warn")
189            tsp.close()
190   
191        def handleAnswer(self, wholeMsg, notusedhere, req):
192            """Decodes and handles the SNMP answer."""
193            self.parent.filter.logdebug("SNMP message : '%s'" % repr(wholeMsg))
194            ver = alpha.protoVersions[alpha.protoVersionId1]
195            rsp = ver.Message()
196            try :
197                rsp.berDecode(wholeMsg)
198            except TypeMismatchError, msg :   
199                self.parent.filter.printInfo(_("SNMP message decoding error for printer %s : %s") % (self.printerHostname, msg), "warn")
200            else :
201                if req.apiAlphaMatch(rsp):
202                    errorStatus = rsp.apiAlphaGetPdu().apiAlphaGetErrorStatus()
203                    if errorStatus:
204                        self.parent.filter.printInfo(_("Problem encountered while doing SNMP queries on printer %s : %s") % (self.printerHostname, errorStatus), "warn")
205                    else:
206                        self.values = []
207                        for varBind in rsp.apiAlphaGetPdu().apiAlphaGetVarBindList():
208                            self.values.append(varBind.apiAlphaGetOidVal()[1].rawAsn1Value)
209                        try :   
210                            # keep maximum value seen for printer's internal page counter
211                            self.printerInternalPageCounter = max(self.printerInternalPageCounter, self.values[0])
212                            self.printerStatus = self.values[1]
213                        except IndexError :   
214                            pass
215                        else :   
216                            return 1
217                       
218        def waitPrinting(self) :
219            """Waits for printer status being 'printing'."""
220            while 1:
221                self.retrieveSNMPValues()
222                statusAsString = printerStatusValues.get(self.printerStatus)
223                if statusAsString in ('idle', 'printing') :
224                    break
225                self.parent.filter.logdebug(_("Waiting for printer %s to be idle or printing...") % self.parent.filter.printername)   
226                time.sleep(ITERATIONDELAY)
227           
228        def waitIdle(self) :
229            """Waits for printer status being 'idle'."""
230            idle_num = idle_flag = 0
231            while 1 :
232                self.retrieveSNMPValues()
233                statusAsString = printerStatusValues.get(self.printerStatus)
234                idle_flag = 0
235                if statusAsString in ('idle',) :
236                    idle_flag = 1
237                if idle_flag :   
238                    idle_num += 1
239                    if idle_num > STABILIZATIONDELAY :
240                        # printer status is stable, we can exit
241                        break
242                else :   
243                    idle_num = 0
244                self.parent.filter.logdebug(_("Waiting for printer %s's idle status to stabilize...") % self.parent.filter.printername)   
245                time.sleep(ITERATIONDELAY)
246               
247pjlMessage = "\033%-12345X@PJL USTATUSOFF\r\n@PJL INFO STATUS\r\n@PJL INFO PAGECOUNT\r\n\033%-12345X"
248pjlStatusValues = {
249                    "10000" : "Powersave Mode",
250                    "10001" : "Ready Online",
251                    "10002" : "Ready Offline",
252                    "10003" : "Warming Up",
253                    "10004" : "Self Test",
254                    "10005" : "Reset",
255                    "10023" : "Printing",
256                    "35078" : "Powersave Mode",         # 10000 is ALSO powersave !!!
257                  }
258class PJLAccounter :
259    """A class for PJL print accounting."""
260    def __init__(self, parent, printerhostname) :
261        self.parent = parent
262        self.printerHostname = printerhostname
263        self.printerInternalPageCounter = self.printerStatus = None
264        self.printerInternalPageCounter = self.printerStatus = None
265        self.timedout = 0
266       
267    def alarmHandler(self, signum, frame) :   
268        """Query has timedout, handle this."""
269        self.timedout = 1
270        raise IOError, "Waiting for PJL answer timed out. Please try again later."
271       
272    def retrievePJLValues(self) :   
273        """Retrieves a printer's internal page counter and status via PJL."""
274        port = 9100
275        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
276        try :
277            sock.connect((self.printerHostname, port))
278        except socket.error, msg :
279            self.parent.filter.printInfo(_("Problem during connection to %s:%s : %s") % (self.printerHostname, port, msg), "warn")
280        else :
281            try :
282                sock.send(pjlMessage)
283            except socket.error, msg :
284                self.parent.filter.printInfo(_("Problem while sending PJL query to %s:%s : %s") % (self.printerHostname, port, msg), "warn")
285            else :   
286                actualpagecount = self.printerStatus = None
287                self.timedout = 0
288                while (self.timedout == 0) or (actualpagecount is None) or (self.printerStatus is None) :
289                    signal.signal(signal.SIGALRM, self.alarmHandler)
290                    signal.alarm(3)
291                    try :
292                        answer = sock.recv(1024)
293                    except IOError, msg :   
294                        break   # our alarm handler was launched, probably
295                    else :   
296                        readnext = 0
297                        for line in [l.strip() for l in answer.split()] : 
298                            if line.startswith("CODE=") :
299                                self.printerStatus = line.split("=")[1]
300                            elif line.startswith("PAGECOUNT") :   
301                                readnext = 1 # page counter is on next line
302                            elif readnext :   
303                                actualpagecount = int(line.strip())
304                                readnext = 0
305                    signal.alarm(0)
306                self.printerInternalPageCounter = max(actualpagecount, self.printerInternalPageCounter)
307        sock.close()
308       
309    def waitPrinting(self) :
310        """Waits for printer status being 'printing'."""
311        while 1:
312            self.retrievePJLValues()
313            if self.printerStatus in ('10000', '10001', '10023', '35078') :
314                break
315            self.parent.filter.logdebug(_("Waiting for printer %s to be idle or printing...") % self.parent.filter.printername)
316            time.sleep(ITERATIONDELAY)
317       
318    def waitIdle(self) :
319        """Waits for printer status being 'idle'."""
320        idle_num = idle_flag = 0
321        while 1 :
322            self.retrievePJLValues()
323            idle_flag = 0
324            if self.printerStatus in ('10000', '10001', '35078') :
325                idle_flag = 1
326            if idle_flag :   
327                idle_num += 1
328                if idle_num > STABILIZATIONDELAY :
329                    # printer status is stable, we can exit
330                    break
331            else :   
332                idle_num = 0
333            self.parent.filter.logdebug(_("Waiting for printer %s's idle status to stabilize...") % self.parent.filter.printername)
334            time.sleep(ITERATIONDELAY)
335   
336class Accounter(AccounterBase) :
337    def __init__(self, kotabackend, arguments) :
338        """Initializes querying accounter."""
339        AccounterBase.__init__(self, kotabackend, arguments)
340        self.isSoftware = 0
341       
342    def getPrinterInternalPageCounter(self) :   
343        """Returns the printer's internal page counter."""
344        self.filter.logdebug("Reading printer %s's internal page counter..." % self.filter.printername)
345        counter = self.askPrinterPageCounter(self.filter.printerhostname)
346        self.filter.logdebug("Printer %s's internal page counter value is : %s" % (self.filter.printername, str(counter)))
347        return counter   
348       
349    def beginJob(self, printer) :   
350        """Saves printer internal page counter at start of job."""
351        # save page counter before job
352        self.LastPageCounter = self.getPrinterInternalPageCounter()
353        self.fakeBeginJob()
354       
355    def fakeBeginJob(self) :   
356        """Fakes a begining of a job."""
357        self.counterbefore = self.getLastPageCounter()
358       
359    def endJob(self, printer) :   
360        """Saves printer internal page counter at end of job."""
361        # save page counter after job
362        self.LastPageCounter = self.counterafter = self.getPrinterInternalPageCounter()
363       
364    def getJobSize(self, printer) :   
365        """Returns the actual job size."""
366        if (not self.counterbefore) or (not self.counterafter) :
367            # there was a problem retrieving page counter
368            self.filter.printInfo(_("A problem occured while reading printer %s's internal page counter.") % printer.Name, "warn")
369            if printer.LastJob.Exists :
370                # if there's a previous job, use the last value from database
371                self.filter.printInfo(_("Retrieving printer %s's page counter from database instead.") % printer.Name, "warn")
372                if not self.counterbefore : 
373                    self.counterbefore = printer.LastJob.PrinterPageCounter or 0
374                if not self.counterafter :
375                    self.counterafter = printer.LastJob.PrinterPageCounter or 0
376                before = min(self.counterbefore, self.counterafter)   
377                after = max(self.counterbefore, self.counterafter)   
378                self.counterbefore = before
379                self.counterafter = after
380                if (not self.counterbefore) or (not self.counterafter) or (self.counterbefore == self.counterafter) :
381                    self.filter.printInfo(_("Couldn't retrieve printer %s's internal page counter either before or after printing.") % printer.Name, "warn")
382                    self.filter.printInfo(_("Job's size forced to 1 page for printer %s.") % printer.Name, "warn")
383                    self.counterbefore = 0
384                    self.counterafter = 1
385            else :
386                self.filter.printInfo(_("No previous job in database for printer %s.") % printer.Name, "warn")
387                self.filter.printInfo(_("Job's size forced to 1 page for printer %s.") % printer.Name, "warn")
388                self.counterbefore = 0
389                self.counterafter = 1
390               
391        jobsize = (self.counterafter - self.counterbefore)   
392        if jobsize < 0 :
393            # Try to take care of HP printers
394            # Their internal page counter is saved to NVRAM
395            # only every 10 pages. If the printer was switched
396            # off then back on during the job, and that the
397            # counters difference is negative, we know
398            # the formula (we can't know if more than eleven
399            # pages were printed though) :
400            if jobsize > -10 :
401                jobsize += 10
402            else :   
403                # here we may have got a printer being replaced
404                # DURING the job. This is HIGHLY improbable !
405                self.filter.printInfo(_("Inconsistent values for printer %s's internal page counter.") % printer.Name, "warn")
406                self.filter.printInfo(_("Job's size forced to 1 page for printer %s.") % printer.Name, "warn")
407                jobsize = 1
408        return jobsize
409       
410    def askPrinterPageCounter(self, printer) :
411        """Returns the page counter from the printer via an external command.
412       
413           The external command must report the life time page number of the printer on stdout.
414        """
415        commandline = self.arguments.strip() % locals()
416        cmdlower = commandline.lower()
417        if cmdlower == "snmp" :
418            if hasSNMP :
419                return self.askWithSNMP(printer)
420            else :   
421                raise PyKotaAccounterError, _("Internal SNMP accounting asked, but Python-SNMP is not available. Please download it from http://pysnmp.sourceforge.net")
422        elif cmdlower == "pjl" :
423            return self.askWithPJL(printer)
424           
425        if printer is None :
426            raise PyKotaAccounterError, _("Unknown printer address in HARDWARE(%s) for printer %s") % (commandline, self.filter.printername)
427        while 1 :   
428            self.filter.printInfo(_("Launching HARDWARE(%s)...") % commandline)
429            pagecounter = None
430            child = popen2.Popen4(commandline)   
431            try :
432                answer = child.fromchild.read()
433            except IOError :   
434                # we were interrupted by a signal, certainely a SIGTERM
435                # caused by the user cancelling the current job
436                try :
437                    os.kill(child.pid, signal.SIGTERM)
438                except :   
439                    pass # already killed ?
440                self.filter.printInfo(_("SIGTERM was sent to hardware accounter %s (pid: %s)") % (commandline, child.pid))
441            else :   
442                lines = [l.strip() for l in answer.split("\n")]
443                for i in range(len(lines)) : 
444                    try :
445                        pagecounter = int(lines[i])
446                    except (AttributeError, ValueError) :
447                        self.filter.printInfo(_("Line [%s] skipped in accounter's output. Trying again...") % lines[i])
448                    else :   
449                        break
450            child.fromchild.close()   
451            child.tochild.close()
452            try :
453                status = child.wait()
454            except OSError, msg :   
455                self.filter.logdebug("Error while waiting for hardware accounter pid %s : %s" % (child.pid, msg))
456            else :   
457                if os.WIFEXITED(status) :
458                    status = os.WEXITSTATUS(status)
459                self.filter.printInfo(_("Hardware accounter %s exit code is %s") % (self.arguments, str(status)))
460               
461            if pagecounter is None :
462                message = _("Unable to query printer %s via HARDWARE(%s)") % (printer, commandline)
463                if self.onerror == "CONTINUE" :
464                    self.filter.printInfo(message, "error")
465                else :
466                    raise PyKotaAccounterError, message 
467            else :       
468                return pagecounter       
469       
470    def askWithSNMP(self, printer) :
471        """Returns the page counter from the printer via internal SNMP handling."""
472        acc = SNMPAccounter(self, printer)
473        try :
474            if (os.environ.get("PYKOTASTATUS") != "CANCELLED") and \
475               (os.environ.get("PYKOTAACTION") != "DENY") and \
476               (os.environ.get("PYKOTAPHASE") == "AFTER") :
477                acc.waitPrinting()
478            acc.waitIdle()   
479        except :   
480            if acc.printerInternalPageCounter is None :
481                raise
482            else :   
483                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")
484        return acc.printerInternalPageCounter
485       
486    def askWithPJL(self, printer) :
487        """Returns the page counter from the printer via internal PJL handling."""
488        acc = PJLAccounter(self, printer)
489        try :
490            if (os.environ.get("PYKOTASTATUS") != "CANCELLED") and \
491               (os.environ.get("PYKOTAACTION") != "DENY") and \
492               (os.environ.get("PYKOTAPHASE") == "AFTER") :
493                acc.waitPrinting()
494            acc.waitIdle()   
495        except :   
496            if acc.printerInternalPageCounter is None :
497                raise
498            else :   
499                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")
500        return acc.printerInternalPageCounter
Note: See TracBrowser for help on using the browser.