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

Revision 1775, 21.1 kB (checked in by jalet, 20 years ago)

Removed misleading comments

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