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

Revision 1730, 14.4 kB (checked in by jalet, 20 years ago)

Typo

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