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

Revision 1734, 14.6 kB (checked in by jalet, 20 years ago)

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