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

Revision 1729, 14.3 kB (checked in by jalet, 20 years ago)

First try at full SNMP handling from the Python code.

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