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

Revision 1746, 20.9 kB (checked in by jalet, 20 years ago)

Added internal handling for PJL queries over port tcp/9100. Now waits
for printer being idle before asking, just like with SNMP.

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