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

Revision 1774, 21.2 kB (checked in by jalet, 20 years ago)

Reduced delay from 2 to 1 seconds in internal SNMP and PJL_over_TCP
handlers

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