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

Revision 1737, 15.0 kB (checked in by jalet, 20 years ago)

Logs the looping message as debug instead of as info

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