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

Revision 1743, 15.6 kB (checked in by jalet, 20 years ago)

Did a pass of PyChecker?

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