root / pykota / trunk / bin / lprngpykota @ 2062

Revision 2062, 15.1 kB (checked in by jalet, 19 years ago)

Introduces the new 'trustjobsize' directive to workaround some printers
generating unstable internal page counter values when queried through SNMP.

  • Property svn:eol-style set to native
  • Property svn:executable set to *
  • Property svn:keywords set to Author Date Id Revision
RevLine 
[1601]1#! /usr/bin/env python
2# -*- coding: ISO-8859-15 -*-
3
4# LPRngPyKota accounting filter
5#
6# PyKota - Print Quotas for CUPS and LPRng
7#
[2028]8# (c) 2003, 2004, 2005 Jerome Alet <alet@librelogiciel.com>
[1601]9# This program is free software; you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation; either version 2 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program; if not, write to the Free Software
21# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
22#
23# $Id$
24#
25# $Log$
[2062]26# Revision 1.16  2005/02/14 23:39:50  jalet
27# Introduces the new 'trustjobsize' directive to workaround some printers
28# generating unstable internal page counter values when queried through SNMP.
29#
[2060]30# Revision 1.15  2005/02/14 22:53:44  jalet
31# Now always precomputes the job's size with the internal PDL parser, and not
32# only when 'enforcement: strict' was set in pykota.conf
33#
[2057]34# Revision 1.14  2005/02/13 22:48:37  jalet
35# Added the md5sum to the history
36#
[2028]37# Revision 1.13  2005/01/17 08:44:23  jalet
38# Modified copyright years
39#
[1878]40# Revision 1.12  2004/10/25 17:05:36  jalet
41# Another fix for LPRng support debug messages : I'm sure I'm completely stupid now.
42#
[1877]43# Revision 1.11  2004/10/25 15:14:59  jalet
44# Fixed typo in code added to debug LPRng problem
45#
[1872]46# Revision 1.10  2004/10/24 09:06:46  jalet
47# Added debug messages for LPRng support possible problem ???
48#
[1848]49# Revision 1.9  2004/10/19 21:37:57  jalet
50# Fixes recently introduced bug
51#
[1820]52# Revision 1.8  2004/10/13 20:51:27  jalet
53# Made debugging levels be the same in cupspykota and lprngpykota.
54# Now outputs more information in informational messages : user, printer, jobid
55#
[1713]56# Revision 1.7  2004/09/13 16:02:44  jalet
57# Added fix for incorrect job's size when hardware accounting fails
58#
[1696]59# Revision 1.6  2004/09/02 14:40:13  jalet
60# Another bunch of LPRng fixes
61#
[1629]62# Revision 1.5  2004/07/23 11:19:48  jalet
63# 1.19beta is out !
64#
[1624]65# Revision 1.4  2004/07/22 22:41:48  jalet
66# Hardware accounting for LPRng should be OK now. UNTESTED.
67#
[1611]68# Revision 1.3  2004/07/21 09:35:48  jalet
69# Software accounting seems to be OK with LPRng support now
70#
[1609]71# Revision 1.2  2004/07/20 22:47:38  jalet
72# Sanitizing
73#
[1601]74# Revision 1.1  2004/07/17 20:37:27  jalet
75# Missing file... Am I really stupid ?
76#
77#
78#
79
80import sys
81import os
82
83from pykota.tool import PyKotaFilterOrBackend, PyKotaToolError, crashed
84from pykota.config import PyKotaConfigError
85from pykota.storage import PyKotaStorageError
86from pykota.accounter import PyKotaAccounterError
87   
88# Exit codes
89JSUCC = 0       # filter succeeded
90JFAIL = 1       # filter failed, print server should retry later
91JABORT = 2      # filter failed, print server should suspend the queue
92JREMOVE = 3     # job will be removed from print queue
93JHOLD = 6       # job will be prevented from printing until lpc release
94
95# Environment variables
96# PRINTER = printer name
97# PRINTCAP_ENTRY = complete printcap entry for this printer
98# HF = job hold file contents
99# SPOOL_DIR = spool directory
100
101# HF contains df_name which is DataFile_Name created in SPOOL_DIR
102
103class PyKotaFilter(PyKotaFilterOrBackend) :       
104    """A class for the pykota filter for LPRng."""
105    def acceptJob(self) :       
106        """Returns the appropriate exit code to tell LPRng all is OK."""
107        return JSUCC
108           
109    def removeJob(self) :           
110        """Returns the appropriate exit code to tell LPRng job has to be removed."""   
111        return JREMOVE
112       
113    def getJobOriginatingHostname(self, printername, username, jobid) :
114        """Retrieves the job-originating-hostname if possible."""
115        try :
116            return [line[11:] for line in os.environ.get("HF", "").split() if line.startswith("remotehost=")][0]
117        except IndexError :   
[1696]118            try :
119                return [line[1:] for line in os.environ.get("CONTROL", "").split() if line.startswith("H")][0]
120            except IndexError :   
121                return None
[1601]122               
[1611]123    def firstPass(self, policy, printer, user, userpquota) :           
124        """First pass done here."""
125        # first we have to check if previous job was correctly accounted for
[1878]126        self.logdebug("First pass begins : POLICY_%s => %s => %s" % (policy, getattr(printer, "Name", None), getattr(user, "Name", None)))
[1629]127        if printer.LastJob.Exists and not printer.LastJob.JobSize :
[1611]128            # here we know that previous job wasn't accounted for correctly
129            # we are sure (?) that it was hardware accounting which was used
130            # and that the second pass didn't work or wasn't even launched
131            # we know have to act just as if we were in second pass
[1624]132            # for previous user on this printer, then we will continue
133            # with normal processing of current user.
134            self.secondPass(policy, printer, None, None)
[1611]135       
[1624]136        # export user info with initial values
137        self.exportUserInfo(userpquota)
138       
[1611]139        # tries to extract job-originating-hostname
140        clienthost = self.getJobOriginatingHostname(printer.Name, user.Name, self.jobid)
141        self.logdebug("Client Hostname : %s" % (clienthost or "Unknown"))   
142        os.environ["PYKOTAJOBORIGINATINGHOSTNAME"] = str(clienthost or "")
143       
144        # indicates first pass
145        os.environ["PYKOTAPHASE"] = "BEFORE"
146       
[2060]147        # precomputes the job's price
148        self.softwareJobPrice = userpquota.computeJobPrice(self.softwareJobSize)
[1611]149        os.environ["PYKOTAPRECOMPUTEDJOBPRICE"] = str(self.softwareJobPrice)
[2060]150        self.logdebug("Precomputed job's size is %s pages, price is %s units" % (self.softwareJobSize, self.softwareJobPrice))
[1611]151       
152        # if no data to pass to real backend, probably a filter
153        # higher in the chain failed because of a misconfiguration.
154        # we deny the job in this case (nothing to print anyway)
155        if not self.jobSizeBytes :
[1820]156            self.printMoreInfo(user, printer, _("Job contains no data. Printing is denied."), "warn")
[1611]157            action = "DENY"
158        else :   
159            # checks the user's quota
160            action = self.warnUserPQuota(userpquota)
161       
162        # exports some new environment variables
163        os.environ["PYKOTAACTION"] = action
164       
165        # launches the pre hook
166        self.prehook(userpquota)
167       
[1820]168        self.printMoreInfo(user, printer, _("Job accounting begins."))
[1624]169        self.accounter.beginJob(printer)
[1611]170       
171        jobsize = None
172        if self.accounter.isSoftware :
[1624]173            self.accounter.endJob(printer)
[1713]174            jobsize = self.accounter.getJobSize(printer)
[1820]175            self.printMoreInfo(user, printer, _("Job accounting ends."))
[1611]176           
177        if action == "DENY" :   
178            jobsize = 0
[1820]179            self.printMoreInfo(user, printer, _("Job size forced to 0 because printing is denied."))
[1611]180           
[1629]181        if (self.accounter.isSoftware) or (action == "DENY") :   
[1611]182            # update the quota for the current user on this printer
[1820]183            self.printMoreInfo(user, printer, _("Job size : %i") % jobsize)
[1848]184            self.printInfo(_("Updating user %s's quota on printer %s") % (user.Name, printer.Name))
[1611]185            jobprice = userpquota.increasePagesUsage(jobsize)
186           
[2057]187            printer.addJobToHistory(self.jobid, user, self.accounter.getLastPageCounter(), \
188                                    action, jobsize, jobprice, self.preserveinputfile, \
189                                    self.title, self.copies, self.options, clienthost, \
190                                    self.jobSizeBytes, self.checksum)
[1820]191            self.printMoreInfo(user, printer, _("Job added to history."))
[1611]192           
193            # exports some new environment variables
194            os.environ["PYKOTAPHASE"] = "AFTER"
195            os.environ["PYKOTAJOBSIZE"] = str(jobsize)
196            os.environ["PYKOTAJOBPRICE"] = str(jobprice)
197           
198            # then re-export user information with new value
199            self.exportUserInfo(userpquota)
200           
201            # Launches the post hook
202            self.posthook(userpquota)
203           
[1629]204            # here accounting was completed, either software, or hardware but over quota
[1611]205        else :
[2057]206            printer.addJobToHistory(self.jobid, user, self.accounter.getLastPageCounter(), \
207                                    action, filename=self.preserveinputfile, title=self.title, \
208                                    copies=self.copies, options=self.options, clienthost=clienthost, \
209                                    jobsizebytes=self.jobSizeBytes, self.checksum)
[1611]210            self.logdebug("Job added to history during first pass : Job's size and price are still unknown.")
211           
[1878]212        self.logdebug("First pass ends : ACTION_%s => %s => %s" % (action, getattr(printer, "Name", None), getattr(user, "Name", None)))
[1611]213        if action == "DENY" :
214            return self.removeJob()
215        else :   
216            return self.acceptJob()
217       
218    def secondPass(self, policy, printer, user, userpquota) :   
219        """Second pass done here."""
220        # Last job for current printer has the same JobId than
221        # the current job, so we know we are in the second pass
[1878]222        self.logdebug("Second pass begins : POLICY_%s => %s => %s" % (policy, getattr(printer, "Name", None), getattr(user, "Name", None)))
[1611]223        if self.accounter.isSoftware :
224            # Software accounting method was used, and we are
225            # in second pass, so all work is already done,
226            # now we just have to exit successfully
[1848]227            self.printInfo(_("Software accounting already done in first pass. Ignoring."))
[1629]228        elif printer.LastJob.JobAction == "DENY" :
229            # Hardware accounting method was used, but job
230            # was rejected during first pass, so nothing to do
[1848]231            self.printInfo(_("Hardware accounting already done in first pass. Ignoring."))
[1611]232        else :   
[1624]233            # here if user and userpquota are both None
234            # then it's a special second pass for a job
235            # which should have had one but didn't, so
236            # we need to get the last user, not the current one.
237            if (user is None) and (userpquota is None) :
238                user = printer.LastJob.User
239                userpquota = self.storage.getUserPQuota(user, printer)
240               
241            # exports user info for last user   
242            self.exportUserInfo(userpquota)
243           
[1611]244            # indicate phase change
245            os.environ["PYKOTAPHASE"] = "AFTER"
246           
[1624]247            # fakes beginning of job with old page counter
248            self.accounter.LastPageCounter = int(printer.LastJob.PrinterPageCounter or 0)
249            self.accounter.fakeBeginJob()
250            self.logdebug("Fakes beginning of job with LastPageCounter: %s" % self.accounter.getLastPageCounter())
251           
[1611]252            # stops accounting.
[1624]253            self.accounter.endJob(printer)
[1611]254            self.logdebug("Job accounting ends.")
255               
256            # retrieve the job size   
[1713]257            jobsize = self.accounter.getJobSize(printer)
[2060]258            if self.softwareJobSize and (jobsize != self.softwareJobSize) :
259                self.printInfo(_("Beware : computed job size (%s) != precomputed job size (%s)") % (jobsize, self.softwareJobSize), "error")
[2062]260                (limit, replacement) = self.config.getTrustJobSize(printer.Name)
261                if limit is None :
262                    self.printInfo(_("The job size will be trusted anyway according to the 'trustjobsize' directive"), "warn")
263                else :
264                    if jobsize <= limit :
265                        self.printInfo(_("The job size will be trusted because it is inferior to the 'trustjobsize' directive's limit %s") % limit, "warn")
266                    else :
267                        self.printInfo(_("The job size will be modified according to the 'trustjobsize' directive : %s") % replacement, "warn")
268                        if replacement == "PRECOMPUTED" :
269                            jobsize = self.softwareJobSize
270                        else :   
271                            jobsize = replacement
[1624]272           
[1820]273            self.printMoreInfo(user, printer, _("Job size : %i") % jobsize)
274            self.printInfo(_("Updating user %s's quota on printer %s") % (user.Name, printer.Name))
[1624]275            jobprice = userpquota.increasePagesUsage(jobsize)
276           
277            self.storage.writeLastJobSize(printer.LastJob, jobsize, jobprice)
[1820]278            self.printMoreInfo(user, printer, _("Job size and price now set in history."))
[1624]279           
280            # exports some new environment variables
281            os.environ["PYKOTAPHASE"] = "AFTER"
282            os.environ["PYKOTAJOBSIZE"] = str(jobsize)
283            os.environ["PYKOTAJOBPRICE"] = str(jobprice)
284           
285            # then re-export user information with new value
286            self.exportUserInfo(userpquota)
287           
288            # Launches the post hook
289            self.posthook(userpquota)
290           
291            # here hardware accounting was completed.
[1878]292        self.logdebug("Second pass ends : POLICY_%s => %s => %s" % (policy, getattr(printer, "Name", None), getattr(user, "Name", None)))
[1611]293        return self.acceptJob()
294       
[1601]295    def doWork(self, policy, printer, user, userpquota) :   
296        """Most of the work is done here."""
297        # Two different values possible for policy here :
298        # ALLOW means : Either printer, user or user print quota doesn't exist,
299        #               but the job should be allowed anyway.
300        # OK means : Both printer, user and user print quota exist, job should
301        #            be allowed if current user is allowed to print on this printer
302        if policy == "ALLOW" :
303            # nothing to do, just accept the job
304            return self.acceptJob()
305        else :   
[1696]306            if printer.LastJob.Exists and (printer.LastJob.JobId == self.jobid) :
307                # here we know we are in second pass.
308                return self.secondPass(policy, printer, user, userpquota)
309            else :   
[1601]310                # Last job for current printer has a different JobId than
[1611]311                # the current job, so we know we are in the first pass.
312                return self.firstPass(policy, printer, user, userpquota)
[1601]313           
314if __name__ == "__main__" :   
315    retcode = JSUCC
316    try :
317        try :
318            # Initializes the backend
319            kotabackend = PyKotaFilter()   
320        except SystemExit :   
321            retcode = JABORT
322        except :   
323            crashed("lprngpykota filter initialization failed")
324            retcode = JABORT
325        else :   
326            retcode = kotabackend.mainWork()
327            kotabackend.storage.close()
328            kotabackend.closeJobDataStream()   
329    except :
330        try :
331            kotabackend.crashed("lprngpykota filter failed")
332        except :   
333            crashed("lprngpykota filter failed")
334        retcode = JABORT
335       
336    sys.exit(retcode)   
Note: See TracBrowser for help on using the browser.