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

Revision 1732, 14.5 kB (checked in by jalet, 20 years ago)

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