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

Revision 2146, 23.1 kB (checked in by jerome, 19 years ago)

It seems that $Log$ is not implemented or doesn't work for some reason

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