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

Revision 1739, 15.5 kB (checked in by jalet, 20 years ago)

Now loops when the external hardware accounter fails, until it returns a correct value

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