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

Revision 1745, 15.7 kB (checked in by jalet, 20 years ago)

Now includes printer's hostname in SNMP error messages

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