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

Revision 2147, 19.3 kB (checked in by jerome, 19 years ago)

Removed all references to $Log$

  • 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#
24
25import os
26import socket
27import time
28import signal
29import popen2
30
31from pykota.accounter import AccounterBase, PyKotaAccounterError
32
33ITERATIONDELAY = 1.0   # 1 Second
34STABILIZATIONDELAY = 3 # We must read three times the same value to consider it to be stable
35
36try :
37    from pysnmp.asn1.encoding.ber.error import TypeMismatchError
38    from pysnmp.mapping.udp.error import SnmpOverUdpError
39    from pysnmp.mapping.udp.role import Manager
40    from pysnmp.proto.api import alpha
41except ImportError :
42    hasSNMP = 0
43else :   
44    hasSNMP = 1
45    pageCounterOID = ".1.3.6.1.2.1.43.10.2.1.4.1.1"  # SNMPv2-SMI::mib-2.43.10.2.1.4.1.1
46    hrPrinterStatusOID = ".1.3.6.1.2.1.25.3.5.1.1.1" # SNMPv2-SMI::mib-2.25.3.5.1.1.1
47    printerStatusValues = { 1 : 'other',
48                            2 : 'unknown',
49                            3 : 'idle',
50                            4 : 'printing',
51                            5 : 'warmup',
52                          }
53    hrDeviceStatusOID = ".1.3.6.1.2.1.25.3.2.1.5.1" # SNMPv2-SMI::mib-2.25.3.2.1.5.1
54    deviceStatusValues = { 1 : 'unknown',
55                           2 : 'running',
56                           3 : 'warning',
57                           4 : 'testing',
58                           5 : 'down',
59                         } 
60    hrPrinterDetectedErrorStateOID = ".1.3.6.1.2.1.25.3.5.1.2.1" # SNMPv2-SMI::mib-2.25.3.5.1.2.1
61    prtConsoleDisplayBufferTextOID = ".1.3.6.1.2.1.43.16.5.1.2.1.1" # SNMPv2-SMI::mib-2.43.16.5.1.2.1.1
62                         
63    #                     
64    # Documentation taken from RFC 3805 (Printer MIB v2) and RFC 2790 (Host Resource MIB)
65    #
66    # TODO : if hrDeviceStatus==2 and hrPrinterStatus==1 then it's powersave mode.
67    #
68    class SNMPAccounter :
69        """A class for SNMP print accounting."""
70        def __init__(self, parent, printerhostname) :
71            self.parent = parent
72            self.printerHostname = printerhostname
73            self.printerInternalPageCounter = self.printerStatus = None
74           
75        def retrieveSNMPValues(self) :   
76            """Retrieves a printer's internal page counter and status via SNMP."""
77            ver = alpha.protoVersions[alpha.protoVersionId1]
78            req = ver.Message()
79            req.apiAlphaSetCommunity('public')
80            req.apiAlphaSetPdu(ver.GetRequestPdu())
81            req.apiAlphaGetPdu().apiAlphaSetVarBindList((pageCounterOID, ver.Null()), \
82                                                        (hrPrinterStatusOID, ver.Null()))
83            tsp = Manager()
84            try :
85                tsp.sendAndReceive(req.berEncode(), (self.printerHostname, 161), (self.handleAnswer, req))
86            except SnmpOverUdpError, msg :   
87                self.parent.filter.printInfo(_("Network error while doing SNMP queries on printer %s : %s") % (self.printerHostname, msg), "warn")
88            tsp.close()
89   
90        def handleAnswer(self, wholeMsg, notusedhere, req):
91            """Decodes and handles the SNMP answer."""
92            self.parent.filter.logdebug("SNMP message : '%s'" % repr(wholeMsg))
93            ver = alpha.protoVersions[alpha.protoVersionId1]
94            rsp = ver.Message()
95            try :
96                rsp.berDecode(wholeMsg)
97            except TypeMismatchError, msg :   
98                self.parent.filter.printInfo(_("SNMP message decoding error for printer %s : %s") % (self.printerHostname, msg), "warn")
99            else :
100                if req.apiAlphaMatch(rsp):
101                    errorStatus = rsp.apiAlphaGetPdu().apiAlphaGetErrorStatus()
102                    if errorStatus:
103                        self.parent.filter.printInfo(_("Problem encountered while doing SNMP queries on printer %s : %s") % (self.printerHostname, errorStatus), "warn")
104                    else:
105                        self.values = []
106                        for varBind in rsp.apiAlphaGetPdu().apiAlphaGetVarBindList():
107                            self.values.append(varBind.apiAlphaGetOidVal()[1].rawAsn1Value)
108                        try :   
109                            # keep maximum value seen for printer's internal page counter
110                            self.printerInternalPageCounter = max(self.printerInternalPageCounter, self.values[0])
111                            self.printerStatus = self.values[1]
112                        except IndexError :   
113                            pass
114                        else :   
115                            return 1
116                       
117        def waitPrinting(self) :
118            """Waits for printer status being 'printing'."""
119            while 1:
120                self.retrieveSNMPValues()
121                statusAsString = printerStatusValues.get(self.printerStatus)
122                if statusAsString in ('printing',) :
123                    break
124                self.parent.filter.logdebug(_("Waiting for printer %s to be printing...") % self.parent.filter.printername)   
125                time.sleep(ITERATIONDELAY)
126           
127        def waitIdle(self) :
128            """Waits for printer status being 'idle'."""
129            idle_num = idle_flag = 0
130            while 1 :
131                self.retrieveSNMPValues()
132                statusAsString = printerStatusValues.get(self.printerStatus)
133                idle_flag = 0
134                if statusAsString in ('idle',) :
135                    idle_flag = 1
136                if idle_flag :   
137                    idle_num += 1
138                    if idle_num > STABILIZATIONDELAY :
139                        # printer status is stable, we can exit
140                        break
141                else :   
142                    idle_num = 0
143                self.parent.filter.logdebug(_("Waiting for printer %s's idle status to stabilize...") % self.parent.filter.printername)   
144                time.sleep(ITERATIONDELAY)
145               
146pjlMessage = "\033%-12345X@PJL USTATUSOFF\r\n@PJL INFO STATUS\r\n@PJL INFO PAGECOUNT\r\n\033%-12345X"
147pjlStatusValues = {
148                    "10000" : "Powersave Mode",
149                    "10001" : "Ready Online",
150                    "10002" : "Ready Offline",
151                    "10003" : "Warming Up",
152                    "10004" : "Self Test",
153                    "10005" : "Reset",
154                    "10023" : "Printing",
155                    "35078" : "Powersave Mode",         # 10000 is ALSO powersave !!!
156                  }
157class PJLAccounter :
158    """A class for PJL print accounting."""
159    def __init__(self, parent, printerhostname) :
160        self.parent = parent
161        self.printerHostname = printerhostname
162        self.printerInternalPageCounter = self.printerStatus = None
163        self.printerInternalPageCounter = self.printerStatus = None
164        self.timedout = 0
165       
166    def alarmHandler(self, signum, frame) :   
167        """Query has timedout, handle this."""
168        self.timedout = 1
169        raise IOError, "Waiting for PJL answer timed out. Please try again later."
170       
171    def retrievePJLValues(self) :   
172        """Retrieves a printer's internal page counter and status via PJL."""
173        port = 9100
174        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
175        try :
176            sock.connect((self.printerHostname, port))
177        except socket.error, msg :
178            self.parent.filter.printInfo(_("Problem during connection to %s:%s : %s") % (self.printerHostname, port, msg), "warn")
179        else :
180            try :
181                sock.send(pjlMessage)
182            except socket.error, msg :
183                self.parent.filter.printInfo(_("Problem while sending PJL query to %s:%s : %s") % (self.printerHostname, port, msg), "warn")
184            else :   
185                actualpagecount = self.printerStatus = None
186                self.timedout = 0
187                while (self.timedout == 0) or (actualpagecount is None) or (self.printerStatus is None) :
188                    signal.signal(signal.SIGALRM, self.alarmHandler)
189                    signal.alarm(3)
190                    try :
191                        answer = sock.recv(1024)
192                    except IOError, msg :   
193                        break   # our alarm handler was launched, probably
194                    else :   
195                        readnext = 0
196                        for line in [l.strip() for l in answer.split()] : 
197                            if line.startswith("CODE=") :
198                                self.printerStatus = line.split("=")[1]
199                            elif line.startswith("PAGECOUNT") :   
200                                readnext = 1 # page counter is on next line
201                            elif readnext :   
202                                actualpagecount = int(line.strip())
203                                readnext = 0
204                    signal.alarm(0)
205                self.printerInternalPageCounter = max(actualpagecount, self.printerInternalPageCounter)
206        sock.close()
207       
208    def waitPrinting(self) :
209        """Waits for printer status being 'printing'."""
210        while 1:
211            self.retrievePJLValues()
212            if self.printerStatus in ('10023',) :
213                break
214            self.parent.filter.logdebug(_("Waiting for printer %s to be printing...") % self.parent.filter.printername)
215            time.sleep(ITERATIONDELAY)
216       
217    def waitIdle(self) :
218        """Waits for printer status being 'idle'."""
219        idle_num = idle_flag = 0
220        while 1 :
221            self.retrievePJLValues()
222            idle_flag = 0
223            if self.printerStatus in ('10000', '10001', '35078') :
224                idle_flag = 1
225            if idle_flag :   
226                idle_num += 1
227                if idle_num > STABILIZATIONDELAY :
228                    # printer status is stable, we can exit
229                    break
230            else :   
231                idle_num = 0
232            self.parent.filter.logdebug(_("Waiting for printer %s's idle status to stabilize...") % self.parent.filter.printername)
233            time.sleep(ITERATIONDELAY)
234   
235class Accounter(AccounterBase) :
236    def __init__(self, kotabackend, arguments) :
237        """Initializes querying accounter."""
238        AccounterBase.__init__(self, kotabackend, arguments)
239        self.isSoftware = 0
240       
241    def getPrinterInternalPageCounter(self) :   
242        """Returns the printer's internal page counter."""
243        self.filter.logdebug("Reading printer %s's internal page counter..." % self.filter.printername)
244        counter = self.askPrinterPageCounter(self.filter.printerhostname)
245        self.filter.logdebug("Printer %s's internal page counter value is : %s" % (self.filter.printername, str(counter)))
246        return counter   
247       
248    def beginJob(self, printer) :   
249        """Saves printer internal page counter at start of job."""
250        # save page counter before job
251        self.LastPageCounter = self.getPrinterInternalPageCounter()
252        self.fakeBeginJob()
253       
254    def fakeBeginJob(self) :   
255        """Fakes a begining of a job."""
256        self.counterbefore = self.getLastPageCounter()
257       
258    def endJob(self, printer) :   
259        """Saves printer internal page counter at end of job."""
260        # save page counter after job
261        self.LastPageCounter = self.counterafter = self.getPrinterInternalPageCounter()
262       
263    def getJobSize(self, printer) :   
264        """Returns the actual job size."""
265        if (not self.counterbefore) or (not self.counterafter) :
266            # there was a problem retrieving page counter
267            self.filter.printInfo(_("A problem occured while reading printer %s's internal page counter.") % printer.Name, "warn")
268            if printer.LastJob.Exists :
269                # if there's a previous job, use the last value from database
270                self.filter.printInfo(_("Retrieving printer %s's page counter from database instead.") % printer.Name, "warn")
271                if not self.counterbefore : 
272                    self.counterbefore = printer.LastJob.PrinterPageCounter or 0
273                if not self.counterafter :
274                    self.counterafter = printer.LastJob.PrinterPageCounter or 0
275                before = min(self.counterbefore, self.counterafter)   
276                after = max(self.counterbefore, self.counterafter)   
277                self.counterbefore = before
278                self.counterafter = after
279                if (not self.counterbefore) or (not self.counterafter) or (self.counterbefore == self.counterafter) :
280                    self.filter.printInfo(_("Couldn't retrieve printer %s's internal page counter either before or after printing.") % printer.Name, "warn")
281                    self.filter.printInfo(_("Job's size forced to 1 page for printer %s.") % printer.Name, "warn")
282                    self.counterbefore = 0
283                    self.counterafter = 1
284            else :
285                self.filter.printInfo(_("No previous job in database for printer %s.") % printer.Name, "warn")
286                self.filter.printInfo(_("Job's size forced to 1 page for printer %s.") % printer.Name, "warn")
287                self.counterbefore = 0
288                self.counterafter = 1
289               
290        jobsize = (self.counterafter - self.counterbefore)   
291        if jobsize < 0 :
292            # Try to take care of HP printers
293            # Their internal page counter is saved to NVRAM
294            # only every 10 pages. If the printer was switched
295            # off then back on during the job, and that the
296            # counters difference is negative, we know
297            # the formula (we can't know if more than eleven
298            # pages were printed though) :
299            if jobsize > -10 :
300                jobsize += 10
301            else :   
302                # here we may have got a printer being replaced
303                # DURING the job. This is HIGHLY improbable !
304                self.filter.printInfo(_("Inconsistent values for printer %s's internal page counter.") % printer.Name, "warn")
305                self.filter.printInfo(_("Job's size forced to 1 page for printer %s.") % printer.Name, "warn")
306                jobsize = 1
307        return jobsize
308       
309    def askPrinterPageCounter(self, printer) :
310        """Returns the page counter from the printer via an external command.
311       
312           The external command must report the life time page number of the printer on stdout.
313        """
314        commandline = self.arguments.strip() % locals()
315        cmdlower = commandline.lower()
316        if cmdlower == "snmp" :
317            if hasSNMP :
318                return self.askWithSNMP(printer)
319            else :   
320                raise PyKotaAccounterError, _("Internal SNMP accounting asked, but Python-SNMP is not available. Please download it from http://pysnmp.sourceforge.net")
321        elif cmdlower == "pjl" :
322            return self.askWithPJL(printer)
323           
324        if printer is None :
325            raise PyKotaAccounterError, _("Unknown printer address in HARDWARE(%s) for printer %s") % (commandline, self.filter.printername)
326        while 1 :   
327            self.filter.printInfo(_("Launching HARDWARE(%s)...") % commandline)
328            pagecounter = None
329            child = popen2.Popen4(commandline)   
330            try :
331                answer = child.fromchild.read()
332            except IOError :   
333                # we were interrupted by a signal, certainely a SIGTERM
334                # caused by the user cancelling the current job
335                try :
336                    os.kill(child.pid, signal.SIGTERM)
337                except :   
338                    pass # already killed ?
339                self.filter.printInfo(_("SIGTERM was sent to hardware accounter %s (pid: %s)") % (commandline, child.pid))
340            else :   
341                lines = [l.strip() for l in answer.split("\n")]
342                for i in range(len(lines)) : 
343                    try :
344                        pagecounter = int(lines[i])
345                    except (AttributeError, ValueError) :
346                        self.filter.printInfo(_("Line [%s] skipped in accounter's output. Trying again...") % lines[i])
347                    else :   
348                        break
349            child.fromchild.close()   
350            child.tochild.close()
351            try :
352                status = child.wait()
353            except OSError, msg :   
354                self.filter.logdebug("Error while waiting for hardware accounter pid %s : %s" % (child.pid, msg))
355            else :   
356                if os.WIFEXITED(status) :
357                    status = os.WEXITSTATUS(status)
358                self.filter.printInfo(_("Hardware accounter %s exit code is %s") % (self.arguments, str(status)))
359               
360            if pagecounter is None :
361                message = _("Unable to query printer %s via HARDWARE(%s)") % (printer, commandline)
362                if self.onerror == "CONTINUE" :
363                    self.filter.printInfo(message, "error")
364                else :
365                    raise PyKotaAccounterError, message 
366            else :       
367                return pagecounter       
368       
369    def askWithSNMP(self, printer) :
370        """Returns the page counter from the printer via internal SNMP handling."""
371        acc = SNMPAccounter(self, printer)
372        try :
373            if (os.environ.get("PYKOTASTATUS") != "CANCELLED") and \
374               (os.environ.get("PYKOTAACTION") != "DENY") and \
375               (os.environ.get("PYKOTAPHASE") == "AFTER") and \
376               self.filter.jobSizeBytes :
377                acc.waitPrinting()
378            acc.waitIdle()   
379        except :   
380            if acc.printerInternalPageCounter is None :
381                raise
382            else :   
383                self.filter.printInfo(_("SNMP querying stage interrupted. Using latest value seen for internal page counter (%s) on printer %s.") % (acc.printerInternalPageCounter, self.filter.printername), "warn")
384        return acc.printerInternalPageCounter
385       
386    def askWithPJL(self, printer) :
387        """Returns the page counter from the printer via internal PJL handling."""
388        acc = PJLAccounter(self, printer)
389        try :
390            if (os.environ.get("PYKOTASTATUS") != "CANCELLED") and \
391               (os.environ.get("PYKOTAACTION") != "DENY") and \
392               (os.environ.get("PYKOTAPHASE") == "AFTER") and \
393               self.filter.jobSizeBytes :
394                acc.waitPrinting()
395            acc.waitIdle()   
396        except :   
397            if acc.printerInternalPageCounter is None :
398                raise
399            else :   
400                self.filter.printInfo(_("PJL querying stage interrupted. Using latest value seen for internal page counter (%s) on printer %s.") % (acc.printerInternalPageCounter, self.filter.printername), "warn")
401        return acc.printerInternalPageCounter
Note: See TracBrowser for help on using the browser.