#! /usr/bin/env python # -*- coding: ISO-8859-15 -*- # LPRngPyKota accounting filter # # PyKota - Print Quotas for CUPS and LPRng # # (c) 2003, 2004, 2005 Jerome Alet # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. # # $Id$ # # $Log$ # Revision 1.16 2005/02/14 23:39:50 jalet # Introduces the new 'trustjobsize' directive to workaround some printers # generating unstable internal page counter values when queried through SNMP. # # Revision 1.15 2005/02/14 22:53:44 jalet # Now always precomputes the job's size with the internal PDL parser, and not # only when 'enforcement: strict' was set in pykota.conf # # Revision 1.14 2005/02/13 22:48:37 jalet # Added the md5sum to the history # # Revision 1.13 2005/01/17 08:44:23 jalet # Modified copyright years # # Revision 1.12 2004/10/25 17:05:36 jalet # Another fix for LPRng support debug messages : I'm sure I'm completely stupid now. # # Revision 1.11 2004/10/25 15:14:59 jalet # Fixed typo in code added to debug LPRng problem # # Revision 1.10 2004/10/24 09:06:46 jalet # Added debug messages for LPRng support possible problem ??? # # Revision 1.9 2004/10/19 21:37:57 jalet # Fixes recently introduced bug # # Revision 1.8 2004/10/13 20:51:27 jalet # Made debugging levels be the same in cupspykota and lprngpykota. # Now outputs more information in informational messages : user, printer, jobid # # Revision 1.7 2004/09/13 16:02:44 jalet # Added fix for incorrect job's size when hardware accounting fails # # Revision 1.6 2004/09/02 14:40:13 jalet # Another bunch of LPRng fixes # # Revision 1.5 2004/07/23 11:19:48 jalet # 1.19beta is out ! # # Revision 1.4 2004/07/22 22:41:48 jalet # Hardware accounting for LPRng should be OK now. UNTESTED. # # Revision 1.3 2004/07/21 09:35:48 jalet # Software accounting seems to be OK with LPRng support now # # Revision 1.2 2004/07/20 22:47:38 jalet # Sanitizing # # Revision 1.1 2004/07/17 20:37:27 jalet # Missing file... Am I really stupid ? # # # import sys import os from pykota.tool import PyKotaFilterOrBackend, PyKotaToolError, crashed from pykota.config import PyKotaConfigError from pykota.storage import PyKotaStorageError from pykota.accounter import PyKotaAccounterError # Exit codes JSUCC = 0 # filter succeeded JFAIL = 1 # filter failed, print server should retry later JABORT = 2 # filter failed, print server should suspend the queue JREMOVE = 3 # job will be removed from print queue JHOLD = 6 # job will be prevented from printing until lpc release # Environment variables # PRINTER = printer name # PRINTCAP_ENTRY = complete printcap entry for this printer # HF = job hold file contents # SPOOL_DIR = spool directory # HF contains df_name which is DataFile_Name created in SPOOL_DIR class PyKotaFilter(PyKotaFilterOrBackend) : """A class for the pykota filter for LPRng.""" def acceptJob(self) : """Returns the appropriate exit code to tell LPRng all is OK.""" return JSUCC def removeJob(self) : """Returns the appropriate exit code to tell LPRng job has to be removed.""" return JREMOVE def getJobOriginatingHostname(self, printername, username, jobid) : """Retrieves the job-originating-hostname if possible.""" try : return [line[11:] for line in os.environ.get("HF", "").split() if line.startswith("remotehost=")][0] except IndexError : try : return [line[1:] for line in os.environ.get("CONTROL", "").split() if line.startswith("H")][0] except IndexError : return None def firstPass(self, policy, printer, user, userpquota) : """First pass done here.""" # first we have to check if previous job was correctly accounted for self.logdebug("First pass begins : POLICY_%s => %s => %s" % (policy, getattr(printer, "Name", None), getattr(user, "Name", None))) if printer.LastJob.Exists and not printer.LastJob.JobSize : # here we know that previous job wasn't accounted for correctly # we are sure (?) that it was hardware accounting which was used # and that the second pass didn't work or wasn't even launched # we know have to act just as if we were in second pass # for previous user on this printer, then we will continue # with normal processing of current user. self.secondPass(policy, printer, None, None) # export user info with initial values self.exportUserInfo(userpquota) # tries to extract job-originating-hostname clienthost = self.getJobOriginatingHostname(printer.Name, user.Name, self.jobid) self.logdebug("Client Hostname : %s" % (clienthost or "Unknown")) os.environ["PYKOTAJOBORIGINATINGHOSTNAME"] = str(clienthost or "") # indicates first pass os.environ["PYKOTAPHASE"] = "BEFORE" # precomputes the job's price self.softwareJobPrice = userpquota.computeJobPrice(self.softwareJobSize) os.environ["PYKOTAPRECOMPUTEDJOBPRICE"] = str(self.softwareJobPrice) self.logdebug("Precomputed job's size is %s pages, price is %s units" % (self.softwareJobSize, self.softwareJobPrice)) # if no data to pass to real backend, probably a filter # higher in the chain failed because of a misconfiguration. # we deny the job in this case (nothing to print anyway) if not self.jobSizeBytes : self.printMoreInfo(user, printer, _("Job contains no data. Printing is denied."), "warn") action = "DENY" else : # checks the user's quota action = self.warnUserPQuota(userpquota) # exports some new environment variables os.environ["PYKOTAACTION"] = action # launches the pre hook self.prehook(userpquota) self.printMoreInfo(user, printer, _("Job accounting begins.")) self.accounter.beginJob(printer) jobsize = None if self.accounter.isSoftware : self.accounter.endJob(printer) jobsize = self.accounter.getJobSize(printer) self.printMoreInfo(user, printer, _("Job accounting ends.")) if action == "DENY" : jobsize = 0 self.printMoreInfo(user, printer, _("Job size forced to 0 because printing is denied.")) if (self.accounter.isSoftware) or (action == "DENY") : # update the quota for the current user on this printer self.printMoreInfo(user, printer, _("Job size : %i") % jobsize) self.printInfo(_("Updating user %s's quota on printer %s") % (user.Name, printer.Name)) jobprice = userpquota.increasePagesUsage(jobsize) printer.addJobToHistory(self.jobid, user, self.accounter.getLastPageCounter(), \ action, jobsize, jobprice, self.preserveinputfile, \ self.title, self.copies, self.options, clienthost, \ self.jobSizeBytes, self.checksum) self.printMoreInfo(user, printer, _("Job added to history.")) # exports some new environment variables os.environ["PYKOTAPHASE"] = "AFTER" os.environ["PYKOTAJOBSIZE"] = str(jobsize) os.environ["PYKOTAJOBPRICE"] = str(jobprice) # then re-export user information with new value self.exportUserInfo(userpquota) # Launches the post hook self.posthook(userpquota) # here accounting was completed, either software, or hardware but over quota else : printer.addJobToHistory(self.jobid, user, self.accounter.getLastPageCounter(), \ action, filename=self.preserveinputfile, title=self.title, \ copies=self.copies, options=self.options, clienthost=clienthost, \ jobsizebytes=self.jobSizeBytes, self.checksum) self.logdebug("Job added to history during first pass : Job's size and price are still unknown.") self.logdebug("First pass ends : ACTION_%s => %s => %s" % (action, getattr(printer, "Name", None), getattr(user, "Name", None))) if action == "DENY" : return self.removeJob() else : return self.acceptJob() def secondPass(self, policy, printer, user, userpquota) : """Second pass done here.""" # Last job for current printer has the same JobId than # the current job, so we know we are in the second pass self.logdebug("Second pass begins : POLICY_%s => %s => %s" % (policy, getattr(printer, "Name", None), getattr(user, "Name", None))) if self.accounter.isSoftware : # Software accounting method was used, and we are # in second pass, so all work is already done, # now we just have to exit successfully self.printInfo(_("Software accounting already done in first pass. Ignoring.")) elif printer.LastJob.JobAction == "DENY" : # Hardware accounting method was used, but job # was rejected during first pass, so nothing to do self.printInfo(_("Hardware accounting already done in first pass. Ignoring.")) else : # here if user and userpquota are both None # then it's a special second pass for a job # which should have had one but didn't, so # we need to get the last user, not the current one. if (user is None) and (userpquota is None) : user = printer.LastJob.User userpquota = self.storage.getUserPQuota(user, printer) # exports user info for last user self.exportUserInfo(userpquota) # indicate phase change os.environ["PYKOTAPHASE"] = "AFTER" # fakes beginning of job with old page counter self.accounter.LastPageCounter = int(printer.LastJob.PrinterPageCounter or 0) self.accounter.fakeBeginJob() self.logdebug("Fakes beginning of job with LastPageCounter: %s" % self.accounter.getLastPageCounter()) # stops accounting. self.accounter.endJob(printer) self.logdebug("Job accounting ends.") # retrieve the job size jobsize = self.accounter.getJobSize(printer) if self.softwareJobSize and (jobsize != self.softwareJobSize) : self.printInfo(_("Beware : computed job size (%s) != precomputed job size (%s)") % (jobsize, self.softwareJobSize), "error") (limit, replacement) = self.config.getTrustJobSize(printer.Name) if limit is None : self.printInfo(_("The job size will be trusted anyway according to the 'trustjobsize' directive"), "warn") else : if jobsize <= limit : self.printInfo(_("The job size will be trusted because it is inferior to the 'trustjobsize' directive's limit %s") % limit, "warn") else : self.printInfo(_("The job size will be modified according to the 'trustjobsize' directive : %s") % replacement, "warn") if replacement == "PRECOMPUTED" : jobsize = self.softwareJobSize else : jobsize = replacement self.printMoreInfo(user, printer, _("Job size : %i") % jobsize) self.printInfo(_("Updating user %s's quota on printer %s") % (user.Name, printer.Name)) jobprice = userpquota.increasePagesUsage(jobsize) self.storage.writeLastJobSize(printer.LastJob, jobsize, jobprice) self.printMoreInfo(user, printer, _("Job size and price now set in history.")) # exports some new environment variables os.environ["PYKOTAPHASE"] = "AFTER" os.environ["PYKOTAJOBSIZE"] = str(jobsize) os.environ["PYKOTAJOBPRICE"] = str(jobprice) # then re-export user information with new value self.exportUserInfo(userpquota) # Launches the post hook self.posthook(userpquota) # here hardware accounting was completed. self.logdebug("Second pass ends : POLICY_%s => %s => %s" % (policy, getattr(printer, "Name", None), getattr(user, "Name", None))) return self.acceptJob() def doWork(self, policy, printer, user, userpquota) : """Most of the work is done here.""" # Two different values possible for policy here : # ALLOW means : Either printer, user or user print quota doesn't exist, # but the job should be allowed anyway. # OK means : Both printer, user and user print quota exist, job should # be allowed if current user is allowed to print on this printer if policy == "ALLOW" : # nothing to do, just accept the job return self.acceptJob() else : if printer.LastJob.Exists and (printer.LastJob.JobId == self.jobid) : # here we know we are in second pass. return self.secondPass(policy, printer, user, userpquota) else : # Last job for current printer has a different JobId than # the current job, so we know we are in the first pass. return self.firstPass(policy, printer, user, userpquota) if __name__ == "__main__" : retcode = JSUCC try : try : # Initializes the backend kotabackend = PyKotaFilter() except SystemExit : retcode = JABORT except : crashed("lprngpykota filter initialization failed") retcode = JABORT else : retcode = kotabackend.mainWork() kotabackend.storage.close() kotabackend.closeJobDataStream() except : try : kotabackend.crashed("lprngpykota filter failed") except : crashed("lprngpykota filter failed") retcode = JABORT sys.exit(retcode)