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

Revision 2183, 21.2 kB (checked in by jerome, 19 years ago)

Reported the SNMP warmup fix into PJL handling code as well.

  • 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                            self.parent.filter.logdebug("SNMP answer is decoded : PageCounter : %s     Status : %s" % (self.values[0], self.values[1]))
113                        except IndexError :   
114                            self.parent.filter.logdebug("SNMP answer is incomplete : %s" % str(self.values))
115                            pass
116                        else :   
117                            return 1
118                       
119        def waitPrinting(self) :
120            """Waits for printer status being 'printing'."""
121            firstvalue = None
122            while 1:
123                self.retrieveSNMPValues()
124                statusAsString = printerStatusValues.get(self.printerStatus)
125                if statusAsString in ('printing', 'warmup') :
126                    break
127                if self.printerInternalPageCounter is not None :   
128                    if firstvalue is None :
129                        # first time we retrieved a page counter, save it
130                        firstvalue = self.printerInternalPageCounter
131                    else :     
132                        # second time (or later)
133                        if firstvalue < self.printerInternalPageCounter :
134                            # Here we have a printer which lies :
135                            # it says it is not printing or warming up
136                            # BUT the page counter increases !!!
137                            # So we can probably quit being sure it is printing.
138                            self.parent.filter.printInfo("Printer %s is lying to us !!!" % self.parent.filter.printername, "warn")
139                            break
140                self.parent.filter.logdebug(_("Waiting for printer %s to be printing...") % self.parent.filter.printername)   
141                time.sleep(ITERATIONDELAY)
142           
143        def waitIdle(self) :
144            """Waits for printer status being 'idle'."""
145            idle_num = idle_flag = 0
146            while 1 :
147                self.retrieveSNMPValues()
148                statusAsString = printerStatusValues.get(self.printerStatus)
149                idle_flag = 0
150                if statusAsString in ('idle',) :
151                    idle_flag = 1
152                if idle_flag :   
153                    idle_num += 1
154                    if idle_num > STABILIZATIONDELAY :
155                        # printer status is stable, we can exit
156                        break
157                else :   
158                    idle_num = 0
159                self.parent.filter.logdebug(_("Waiting for printer %s's idle status to stabilize...") % self.parent.filter.printername)   
160                time.sleep(ITERATIONDELAY)
161               
162pjlMessage = "\033%-12345X@PJL USTATUSOFF\r\n@PJL INFO STATUS\r\n@PJL INFO PAGECOUNT\r\n\033%-12345X"
163pjlStatusValues = {
164                    "10000" : "Powersave Mode",
165                    "10001" : "Ready Online",
166                    "10002" : "Ready Offline",
167                    "10003" : "Warming Up",
168                    "10004" : "Self Test",
169                    "10005" : "Reset",
170                    "10023" : "Printing",
171                    "35078" : "Powersave Mode",         # 10000 is ALSO powersave !!!
172                  }
173class PJLAccounter :
174    """A class for PJL print accounting."""
175    def __init__(self, parent, printerhostname) :
176        self.parent = parent
177        self.printerHostname = printerhostname
178        self.printerInternalPageCounter = self.printerStatus = None
179        self.printerInternalPageCounter = self.printerStatus = None
180        self.timedout = 0
181       
182    def alarmHandler(self, signum, frame) :   
183        """Query has timedout, handle this."""
184        self.timedout = 1
185        raise IOError, "Waiting for PJL answer timed out. Please try again later."
186       
187    def retrievePJLValues(self) :   
188        """Retrieves a printer's internal page counter and status via PJL."""
189        port = 9100
190        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
191        try :
192            sock.connect((self.printerHostname, port))
193        except socket.error, msg :
194            self.parent.filter.printInfo(_("Problem during connection to %s:%s : %s") % (self.printerHostname, port, msg), "warn")
195        else :
196            try :
197                sock.send(pjlMessage)
198            except socket.error, msg :
199                self.parent.filter.printInfo(_("Problem while sending PJL query to %s:%s : %s") % (self.printerHostname, port, msg), "warn")
200            else :   
201                actualpagecount = self.printerStatus = None
202                self.timedout = 0
203                while (self.timedout == 0) or (actualpagecount is None) or (self.printerStatus is None) :
204                    signal.signal(signal.SIGALRM, self.alarmHandler)
205                    signal.alarm(3)
206                    try :
207                        answer = sock.recv(1024)
208                    except IOError, msg :   
209                        break   # our alarm handler was launched, probably
210                    else :   
211                        readnext = 0
212                        for line in [l.strip() for l in answer.split()] : 
213                            if line.startswith("CODE=") :
214                                self.printerStatus = line.split("=")[1]
215                            elif line.startswith("PAGECOUNT") :   
216                                readnext = 1 # page counter is on next line
217                            elif readnext :   
218                                actualpagecount = int(line.strip())
219                                readnext = 0
220                    signal.alarm(0)
221                self.printerInternalPageCounter = max(actualpagecount, self.printerInternalPageCounter)
222        sock.close()
223       
224    def waitPrinting(self) :
225        """Waits for printer status being 'printing'."""
226        firstvalue = None
227        while 1:
228            self.retrievePJLValues()
229            if self.printerStatus in ('10023', '10003') :
230                break
231            if self.printerInternalPageCounter is not None :   
232                if firstvalue is None :
233                    # first time we retrieved a page counter, save it
234                    firstvalue = self.printerInternalPageCounter
235                else :     
236                    # second time (or later)
237                    if firstvalue < self.printerInternalPageCounter :
238                        # Here we have a printer which lies :
239                        # it says it is not printing or warming up
240                        # BUT the page counter increases !!!
241                        # So we can probably quit being sure it is printing.
242                        self.parent.filter.printInfo("Printer %s is lying to us !!!" % self.parent.filter.printername, "warn")
243                        break
244            self.parent.filter.logdebug(_("Waiting for printer %s to be printing...") % self.parent.filter.printername)
245            time.sleep(ITERATIONDELAY)
246       
247    def waitIdle(self) :
248        """Waits for printer status being 'idle'."""
249        idle_num = idle_flag = 0
250        while 1 :
251            self.retrievePJLValues()
252            idle_flag = 0
253            if self.printerStatus in ('10000', '10001', '35078') :
254                idle_flag = 1
255            if idle_flag :   
256                idle_num += 1
257                if idle_num > STABILIZATIONDELAY :
258                    # printer status is stable, we can exit
259                    break
260            else :   
261                idle_num = 0
262            self.parent.filter.logdebug(_("Waiting for printer %s's idle status to stabilize...") % self.parent.filter.printername)
263            time.sleep(ITERATIONDELAY)
264   
265class Accounter(AccounterBase) :
266    def __init__(self, kotabackend, arguments) :
267        """Initializes querying accounter."""
268        AccounterBase.__init__(self, kotabackend, arguments)
269        self.isSoftware = 0
270       
271    def getPrinterInternalPageCounter(self) :   
272        """Returns the printer's internal page counter."""
273        self.filter.logdebug("Reading printer %s's internal page counter..." % self.filter.printername)
274        counter = self.askPrinterPageCounter(self.filter.printerhostname)
275        self.filter.logdebug("Printer %s's internal page counter value is : %s" % (self.filter.printername, str(counter)))
276        return counter   
277       
278    def beginJob(self, printer) :   
279        """Saves printer internal page counter at start of job."""
280        # save page counter before job
281        self.LastPageCounter = self.getPrinterInternalPageCounter()
282        self.fakeBeginJob()
283       
284    def fakeBeginJob(self) :   
285        """Fakes a begining of a job."""
286        self.counterbefore = self.getLastPageCounter()
287       
288    def endJob(self, printer) :   
289        """Saves printer internal page counter at end of job."""
290        # save page counter after job
291        self.LastPageCounter = self.counterafter = self.getPrinterInternalPageCounter()
292       
293    def getJobSize(self, printer) :   
294        """Returns the actual job size."""
295        if (not self.counterbefore) or (not self.counterafter) :
296            # there was a problem retrieving page counter
297            self.filter.printInfo(_("A problem occured while reading printer %s's internal page counter.") % printer.Name, "warn")
298            if printer.LastJob.Exists :
299                # if there's a previous job, use the last value from database
300                self.filter.printInfo(_("Retrieving printer %s's page counter from database instead.") % printer.Name, "warn")
301                if not self.counterbefore : 
302                    self.counterbefore = printer.LastJob.PrinterPageCounter or 0
303                if not self.counterafter :
304                    self.counterafter = printer.LastJob.PrinterPageCounter or 0
305                before = min(self.counterbefore, self.counterafter)   
306                after = max(self.counterbefore, self.counterafter)   
307                self.counterbefore = before
308                self.counterafter = after
309                if (not self.counterbefore) or (not self.counterafter) or (self.counterbefore == self.counterafter) :
310                    self.filter.printInfo(_("Couldn't retrieve printer %s's internal page counter either before or after printing.") % printer.Name, "warn")
311                    self.filter.printInfo(_("Job's size forced to 1 page for printer %s.") % printer.Name, "warn")
312                    self.counterbefore = 0
313                    self.counterafter = 1
314            else :
315                self.filter.printInfo(_("No previous job in database for printer %s.") % printer.Name, "warn")
316                self.filter.printInfo(_("Job's size forced to 1 page for printer %s.") % printer.Name, "warn")
317                self.counterbefore = 0
318                self.counterafter = 1
319               
320        jobsize = (self.counterafter - self.counterbefore)   
321        if jobsize < 0 :
322            # Try to take care of HP printers
323            # Their internal page counter is saved to NVRAM
324            # only every 10 pages. If the printer was switched
325            # off then back on during the job, and that the
326            # counters difference is negative, we know
327            # the formula (we can't know if more than eleven
328            # pages were printed though) :
329            if jobsize > -10 :
330                jobsize += 10
331            else :   
332                # here we may have got a printer being replaced
333                # DURING the job. This is HIGHLY improbable !
334                self.filter.printInfo(_("Inconsistent values for printer %s's internal page counter.") % printer.Name, "warn")
335                self.filter.printInfo(_("Job's size forced to 1 page for printer %s.") % printer.Name, "warn")
336                jobsize = 1
337        return jobsize
338       
339    def askPrinterPageCounter(self, printer) :
340        """Returns the page counter from the printer via an external command.
341       
342           The external command must report the life time page number of the printer on stdout.
343        """
344        commandline = self.arguments.strip() % locals()
345        cmdlower = commandline.lower()
346        if cmdlower == "snmp" :
347            if hasSNMP :
348                return self.askWithSNMP(printer)
349            else :   
350                raise PyKotaAccounterError, _("Internal SNMP accounting asked, but Python-SNMP is not available. Please download it from http://pysnmp.sourceforge.net")
351        elif cmdlower == "pjl" :
352            return self.askWithPJL(printer)
353           
354        if printer is None :
355            raise PyKotaAccounterError, _("Unknown printer address in HARDWARE(%s) for printer %s") % (commandline, self.filter.printername)
356        while 1 :   
357            self.filter.printInfo(_("Launching HARDWARE(%s)...") % commandline)
358            pagecounter = None
359            child = popen2.Popen4(commandline)   
360            try :
361                answer = child.fromchild.read()
362            except IOError :   
363                # we were interrupted by a signal, certainely a SIGTERM
364                # caused by the user cancelling the current job
365                try :
366                    os.kill(child.pid, signal.SIGTERM)
367                except :   
368                    pass # already killed ?
369                self.filter.printInfo(_("SIGTERM was sent to hardware accounter %s (pid: %s)") % (commandline, child.pid))
370            else :   
371                lines = [l.strip() for l in answer.split("\n")]
372                for i in range(len(lines)) : 
373                    try :
374                        pagecounter = int(lines[i])
375                    except (AttributeError, ValueError) :
376                        self.filter.printInfo(_("Line [%s] skipped in accounter's output. Trying again...") % lines[i])
377                    else :   
378                        break
379            child.fromchild.close()   
380            child.tochild.close()
381            try :
382                status = child.wait()
383            except OSError, msg :   
384                self.filter.logdebug("Error while waiting for hardware accounter pid %s : %s" % (child.pid, msg))
385            else :   
386                if os.WIFEXITED(status) :
387                    status = os.WEXITSTATUS(status)
388                self.filter.printInfo(_("Hardware accounter %s exit code is %s") % (self.arguments, str(status)))
389               
390            if pagecounter is None :
391                message = _("Unable to query printer %s via HARDWARE(%s)") % (printer, commandline)
392                if self.onerror == "CONTINUE" :
393                    self.filter.printInfo(message, "error")
394                else :
395                    raise PyKotaAccounterError, message 
396            else :       
397                return pagecounter       
398       
399    def askWithSNMP(self, printer) :
400        """Returns the page counter from the printer via internal SNMP handling."""
401        acc = SNMPAccounter(self, printer)
402        try :
403            if (os.environ.get("PYKOTASTATUS") != "CANCELLED") and \
404               (os.environ.get("PYKOTAACTION") != "DENY") and \
405               (os.environ.get("PYKOTAPHASE") == "AFTER") and \
406               self.filter.jobSizeBytes :
407                acc.waitPrinting()
408            acc.waitIdle()   
409        except :   
410            if acc.printerInternalPageCounter is None :
411                raise
412            else :   
413                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")
414        return acc.printerInternalPageCounter
415       
416    def askWithPJL(self, printer) :
417        """Returns the page counter from the printer via internal PJL handling."""
418        acc = PJLAccounter(self, printer)
419        try :
420            if (os.environ.get("PYKOTASTATUS") != "CANCELLED") and \
421               (os.environ.get("PYKOTAACTION") != "DENY") and \
422               (os.environ.get("PYKOTAPHASE") == "AFTER") and \
423               self.filter.jobSizeBytes :
424                acc.waitPrinting()
425            acc.waitIdle()   
426        except :   
427            if acc.printerInternalPageCounter is None :
428                raise
429            else :   
430                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")
431        return acc.printerInternalPageCounter
Note: See TracBrowser for help on using the browser.