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

Revision 1736, 14.8 kB (checked in by jalet, 20 years ago)

Bad import statement

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