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

Revision 1748, 21.0 kB (checked in by jalet, 20 years ago)

Typo

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