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

Revision 1735, 14.7 kB (checked in by jalet, 20 years ago)

Just loop in case a network error occur

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