#! /usr/bin/env python # -*- coding: ISO-8859-15 -*- # PyKota accounting filter # # PyKota - Print Quotas for CUPS and LPRng # # (c) 2003 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.42 2003/10/08 21:41:38 jalet # External policies for printers works ! # We can now auto-add users on first print, and do other useful things if needed. # # Revision 1.41 2003/10/07 09:07:27 jalet # Character encoding added to please latest version of Python # # Revision 1.40 2003/09/04 08:38:56 jalet # Added an exception catch to ensure clean close of database even in # case of TypeError too. # # Revision 1.39 2003/08/18 16:35:28 jalet # New pychecker pass, on the tools this time. # # Revision 1.38 2003/08/18 16:20:59 jalet # Improvement of the printing system detection code. # # Revision 1.37 2003/07/29 20:55:17 jalet # 1.14 is out ! # # Revision 1.36 2003/07/10 06:09:52 jalet # Incorrect documentation string # # Revision 1.35 2003/06/25 14:10:01 jalet # Hey, it may work (edpykota --reset excepted) ! # # Revision 1.34 2003/06/13 18:54:17 jalet # Bug with remote jobs and LPRng fixed. # # Revision 1.33 2003/05/28 13:51:38 jalet # Better handling of errors # # Revision 1.32 2003/05/27 23:00:20 jalet # Big rewrite of external accounting methods. # Should work well now. # # Revision 1.31 2003/04/30 13:36:39 jalet # Stupid accounting method was added. # # Revision 1.30 2003/04/29 22:03:38 jalet # Better error handling. # # Revision 1.29 2003/04/29 18:37:54 jalet # Pluggable accounting methods (actually doesn't support external scripts) # # Revision 1.28 2003/04/26 08:41:24 jalet # Small code reorganisation (UNTESTED) to allow pluggable accounting # methods in the future. # # Revision 1.27 2003/04/25 09:23:47 jalet # Debug message passed through ! # # Revision 1.26 2003/04/25 08:23:23 jalet # Multiple tries to get the printer's internal page counter, waits for # one minute maximum for the printer to warm up, actually. # # Revision 1.25 2003/04/24 11:53:48 jalet # Default policy for unknown users/groups is to DENY printing instead # of the previous default to ALLOW printing. This is to solve an accuracy # problem. If you set the policy to ALLOW, jobs printed by in nexistant user # (from PyKota's POV) will be charged to the next user who prints on the # same printer. # # Revision 1.24 2003/04/23 22:13:56 jalet # Preliminary support for LPRng added BUT STILL UNTESTED. # # Revision 1.23 2003/04/15 11:30:57 jalet # More work done on money print charging. # Minor bugs corrected. # All tools now access to the storage as priviledged users, repykota excepted. # # Revision 1.22 2003/04/15 11:09:04 jalet # Small bug was fixed when a printer was never used and its internal # page counter is not accessible. # # Revision 1.21 2003/04/12 17:20:14 jalet # Better formula for HP workaround # # Revision 1.20 2003/04/12 16:58:28 jalet # The workaround for HP printers was not correct, and there's probably no # correct way to workaround the problem because we can't save the internal # page counter in real time. The last job's size is unconditionnally set to # 5 pages in this case. # # Revision 1.19 2003/04/11 08:56:49 jalet # Comment # # Revision 1.18 2003/04/11 08:50:39 jalet # Workaround for the HP "feature" of saving the page counter to NVRAM # only every time 10 new pages are printed... # Workaround for printers with volatile page counters. # # Revision 1.17 2003/04/10 21:47:20 jalet # Job history added. Upgrade script neutralized for now ! # # Revision 1.16 2003/04/08 20:38:08 jalet # The last job Id is saved now for each printer, this will probably # allow other accounting methods in the future. # # Revision 1.15 2003/03/29 13:45:27 jalet # GPL paragraphs were incorrectly (from memory) copied into the sources. # Two README files were added. # Upgrade script for PostgreSQL pre 1.01 schema was added. # # Revision 1.14 2003/03/07 22:16:57 jalet # Algorithmically incorrect : last user quota wasn't updated if current # user wasn't allowed to print. # # Revision 1.13 2003/02/27 23:59:28 jalet # Stupid bug wrt exception handlingand value conversion # # Revision 1.12 2003/02/27 23:48:41 jalet # Correctly maps PyKota's log levels to syslog log levels # # Revision 1.11 2003/02/27 22:55:20 jalet # WARN log priority doesn't exist. # # Revision 1.10 2003/02/27 22:43:21 jalet # Missing import # # Revision 1.9 2003/02/27 22:40:26 jalet # Correctly handles cases where the printer is off. # # Revision 1.8 2003/02/09 12:56:53 jalet # Internationalization begins... # # Revision 1.7 2003/02/07 10:23:48 jalet # Avoid a possible future name clash # # Revision 1.6 2003/02/06 22:54:33 jalet # warnpykota should be ok # # Revision 1.5 2003/02/05 22:45:25 jalet # Forgotten import # # Revision 1.4 2003/02/05 22:42:51 jalet # Typo # # Revision 1.3 2003/02/05 22:38:39 jalet # Typo # # Revision 1.2 2003/02/05 22:16:20 jalet # DEVICE_URI is undefined outside of CUPS, i.e. for normal command line tools # # Revision 1.1 2003/02/05 21:28:17 jalet # Initial import into CVS # # # import sys import os from pykota.tool import PyKotaTool, PyKotaToolError from pykota.config import PyKotaConfigError from pykota.storage import PyKotaStorageError from pykota.accounter import openAccounter, PyKotaAccounterError class PyKotaFilter(PyKotaTool) : """Class for the PyKota filter.""" def __init__(self) : PyKotaTool.__init__(self) (self.printingsystem, self.printerhostname, self.printername, self.username, self.jobid, self.inputfile, self.copies) = self.extractInfoFromCupsOrLprng() self.accounter = openAccounter(self) def extractInfoFromCupsOrLprng(self) : """Returns a tuple (printingsystem, printerhostname, printername, username, jobid, filename) depending on the printing system in use (as seen by the print filter). Returns (None, None, None, None, None, None, None) if no printing system is recognized. """ # Try to detect CUPS if os.environ.has_key("CUPS_SERVERROOT") and os.path.isdir(os.environ.get("CUPS_SERVERROOT", "")) : if len(sys.argv) == 7 : inputfile = sys.argv[6] else : inputfile = None device_uri = os.environ.get("DEVICE_URI", "") # TODO : check this for more complex urls than ipp://myprinter.dot.com:631/printers/lp try : (backend, destination) = device_uri.split(":", 1) except ValueError : raise PyKotaToolError, "Invalid DEVICE_URI : %s\n" % device_uri while destination.startswith("/") : destination = destination[1:] printerhostname = destination.split("/")[0].split(":")[0] return ("CUPS", printerhostname, os.environ.get("PRINTER"), sys.argv[2].strip(), sys.argv[1].strip(), inputfile, int(sys.argv[4].strip())) else : # Try to detect LPRng jseen = Pseen = nseen = rseen = Kseen = None for arg in sys.argv : if arg.startswith("-j") : jseen = arg[2:].strip() elif arg.startswith("-n") : nseen = arg[2:].strip() elif arg.startswith("-P") : Pseen = arg[2:].strip() elif arg.startswith("-r") : rseen = arg[2:].strip() elif arg.startswith("-K") or arg.startswith("-#") : Kseen = int(arg[2:].strip()) if Kseen is None : Kseen = 1 # we assume the user wants at least one copy... if (rseen is None) and jseen and Pseen and nseen : self.logger.log_message(_("Printer hostname undefined, set to 'localhost'"), "warn") rseen = "localhost" if jseen and Pseen and nseen and rseen : # job is always in stdin (None) return ("LPRNG", rseen, Pseen, nseen, jseen, None, Kseen) self.logger.log_message(_("Printing system unknown, args=%s") % " ".join(sys.argv), "warn") return (None, None, None, None, None, None, None) # Unknown printing system def acceptJob(self) : """Returns the exit code needed by the printing backend to accept the job and print it.""" if self.printingsystem == "CUPS" : return 0 elif self.printingsystem == "LPRNG" : return 0 else : # UNKNOWN return -1 def removeJob(self) : """Returns the exit code needed by the printing backend to refuse the job and remove it.""" if self.printingsystem == "CUPS" : return 1 elif self.printingsystem == "LPRNG" : return 3 else : # UNKNOWN return -1 def format_commandline(prt, usr, cmdline) : """Passes printer and user names on the command line.""" printer = prt.Name user = usr.Name # we don't want the external command's standard # output to break the print job's data, but we # want to keep its standard error return "%s >/dev/null" % (cmdline % locals()) def main(thefilter) : """Do it, and do it right !""" # # If this is a CUPS filter, we should act and die like a CUPS filter when needed if thefilter.printingsystem == "CUPS" : if len(sys.argv) not in (6, 7) : sys.stderr.write("ERROR: %s job-id user title copies options [file]\n" % sys.argv[0]) return thefilter.removeJob() # Get the last page counter and last username from the Quota Storage backend printer = thefilter.storage.getPrinter(thefilter.printername) if not printer.Exists : # The printer is unknown from the Quota Storage perspective # we let the job pass through, but log a warning message thefilter.logger.log_message(_("Printer %s not registered in the PyKota system") % thefilter.printername, "warn") else : for compteur in range(2) : user = thefilter.storage.getUser(thefilter.username) if user.Exists : break else : # The user is unknown from the Quota Storage perspective # Depending on the default policy for this printer, we # either let the job pass through or reject it, but we # log a message in any case. (policy, args) = thefilter.config.getPrinterPolicy(thefilter.printername) if policy == "ALLOW" : action = "POLICY_ALLOW" elif policy == "EXTERNAL" : commandline = format_commandline(printer, user, args) thefilter.logger.log_message(_("User %s not registered in the PyKota system, applying external policy (%s) for printer %s") % (thefilter.username, commandline, thefilter.printername), "info") if os.system(commandline) : thefilter.logger.log_message(_("External policy %s for printer %s produced an error. Job rejected. Please check PyKota's configuration files.") % (commandline, thefilter.printername), "error") return thefilter.removeJob() else : # here we try a second time, because the goal # of the external action was to add the user # in the database. continue else : action = "POLICY_DENY" thefilter.logger.log_message(_("User %s not registered in the PyKota system, applying default policy (%s) for printer %s") % (thefilter.username, action, thefilter.printername), "warn") if action == "POLICY_DENY" : return thefilter.removeJob() # when we get there, the printer policy allows the job to pass break # if user exists, do accounting if user.Exists : # Now does the accounting and act depending on the result action = thefilter.accounter.doAccounting(printer, user) # if not allowed to print then die, else proceed. if action == "DENY" : # No, just die cleanly return thefilter.removeJob() elif policy == "EXTERNAL" : thefilter.logger.log_message(_("External policy %s for printer %s couldn't add user %s. Job rejected.") % (commandline, thefilter.printername, thefilter.username), "error") return thefilter.removeJob() # pass the job untouched to the underlying layer thefilter.accounter.filterInput(thefilter.inputfile) return thefilter.acceptJob() if __name__ == "__main__" : retcode = -1 try : # Initializes the current tool kotafilter = PyKotaFilter() retcode = main(kotafilter) except (PyKotaToolError, PyKotaConfigError, PyKotaStorageError, PyKotaAccounterError, AttributeError, KeyError, IndexError, ValueError, TypeError, IOError), msg : sys.stderr.write("ERROR : PyKota filter failed (%s)\n" % msg) sys.stderr.flush() retcode = -1 try : kotafilter.storage.close() except (TypeError, NameError, AttributeError) : pass sys.exit(retcode)