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

Revision 1948, 21.6 kB (checked in by jalet, 19 years ago)

Catches TypeMismatchError? in SNMP answer handling code

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