#! /usr/bin/env python # -*- coding: ISO-8859-15 -*- # CUPSPyKota accounting backend # # 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.5 2003/11/14 18:31:27 jalet # Not perfect, but seems to work with the poll() loop. # # Revision 1.4 2003/11/14 17:04:15 jalet # More (untested) work on the CUPS backend. # # Revision 1.3 2003/11/12 23:27:44 jalet # More work on new backend. This commit may be unstable. # # Revision 1.2 2003/11/12 09:33:34 jalet # New CUPS backend supports device enumeration # # Revision 1.1 2003/11/08 16:05:31 jalet # CUPS backend added for people to experiment. # # # import sys import os import popen2 import time import cStringIO import shlex import select import signal from pykota.tool import PyKotaTool, PyKotaToolError from pykota.config import PyKotaConfigError from pykota.storage import PyKotaStorageError from pykota.accounter import openAccounter, PyKotaAccounterError from pykota.requester import openRequester, PyKotaRequesterError class PyKotaPopen3(popen2.Popen3) : """Our own class to execute real backends. Their first argument is different from their path so using native popen2.Popen3 would not be feasible. """ def __init__(self, cmd, capturestderr=False, bufsize=-1, arg0=None) : self.arg0 = arg0 popen2.Popen3.__init__(self, cmd, capturestderr, bufsize) def _run_child(self, cmd): for i in range(3, 256): # TODO : MAXFD in original popen2 module try: os.close(i) except OSError: pass try: os.execvpe(cmd[0], [self.arg0 or cmd[0]] + cmd[1:], os.environ) finally: os._exit(1) class PyKotaBackend(PyKotaTool) : """Class for the PyKota backend.""" def __init__(self) : PyKotaTool.__init__(self) (self.printingsystem, \ self.printerhostname, \ self.printername, \ self.username, \ self.jobid, \ self.inputfile, \ self.copies, \ self.title, \ self.options, \ self.originalbackend) = self.extractCUPSInfo() self.accounter = openAccounter(self) def extractCUPSInfo(self) : """Returns a tuple (printingsystem, printerhostname, printername, username, jobid, filename, title, options, backend). Returns (None, None, None, 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 # the DEVICE_URI environment variable's value is # prefixed with "cupspykota:" otherwise we wouldn't # be called. We have to remove this from the environment # before launching the real backend. fulldevice_uri = os.environ.get("DEVICE_URI", "") device_uri = fulldevice_uri[len("cupspykota:"):] os.environ["DEVICE_URI"] = 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()), \ sys.argv[3], \ sys.argv[5], \ backend) else : self.logger.log_message(_("Printing system unknown, args=%s") % " ".join(sys.argv), "warn") return (None, None, None, None, None, None, None, None, None, None) # Unknown printing system 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 sigterm_handler(signum, frame) : """Handler for SIGTERM.""" sys.exit(1) def main(thebackend) : """Do it, and do it right !""" # first deal with signals # CUPS backends ignore SIGPIPE and exit(1) on SIGTERM # Fortunately SIGPIPE is already ignored by Python # It's there just in case this changes in the future signal.signal(signal.SIGPIPE, signal.SIG_IGN) signal.signal(signal.SIGTERM, sigterm_handler) # # Get the last page counter and last username from the Quota Storage backend printer = thebackend.storage.getPrinter(thebackend.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 thebackend.logger.log_message(_("Printer %s not registered in the PyKota system") % thebackend.printername, "warn") action = "ALLOW" else : for dummy in range(2) : user = thebackend.storage.getUser(thebackend.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) = thebackend.config.getPrinterPolicy(thebackend.printername) if policy == "ALLOW" : action = "POLICY_ALLOW" elif policy == "EXTERNAL" : commandline = format_commandline(printer, user, args) thebackend.logger.log_message(_("User %s not registered in the PyKota system, applying external policy (%s) for printer %s") % (thebackend.username, commandline, thebackend.printername), "info") if os.system(commandline) : # if an error occured, we die without error, # so that the job doesn't stop the print queue. thebackend.logger.log_message(_("External policy %s for printer %s produced an error. Job rejected. Please check PyKota's configuration files.") % (commandline, thebackend.printername), "error") return 0 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" thebackend.logger.log_message(_("User %s not registered in the PyKota system, applying default policy (%s) for printer %s") % (thebackend.username, action, thebackend.printername), "warn") if action == "POLICY_DENY" : # if not allowed to print then die, else proceed. # we die without error, so that the job doesn't # stop the print queue. return 0 # when we get there, the printer policy allows the job to pass break if user.Exists : # Is the current user allowed to print at all ? action = thebackend.warnUserPQuota(thebackend.storage.getUserPQuota(user, printer)) elif policy == "EXTERNAL" : # if the extenal policy produced no error, but the # user still doesn't exist, we die without error, # so that the job doesn't stop the print queue. thebackend.logger.log_message(_("External policy %s for printer %s couldn't add user %s. Job rejected.") % (commandline, thebackend.printername, thebackend.username), "error") return 0 if action not in ["ALLOW", "WARN"] : # if not allowed to print then die, else proceed. # we die without error, so that the job doesn't # stop the print queue. retcode = 0 else : # pass the job untouched to the underlying layer # and starts accounting at the same time thebackend.accounter.beginJob(printer, user) # executes original backend mustclose = 0 if thebackend.inputfile is not None : if hasattr(thebackend.inputfile, "read") : infile = thebackend.inputfile else : infile = open(thebackend.inputfile, "rb") mustclose = 1 else : infile = sys.stdin realbackend = os.path.join(os.path.split(sys.argv[0])[0], thebackend.originalbackend) subprocess = PyKotaPopen3([realbackend] + sys.argv[1:], capturestderr=1, arg0=os.environ["DEVICE_URI"]) pollster = select.poll() pollster.register(infile.fileno(), select.POLLIN) pollster.register(subprocess.fromchild.fileno(), select.POLLIN) pollster.register(subprocess.childerr.fileno(), select.POLLIN) pollster.register(subprocess.tochild.fileno(), select.POLLOUT) inputdata = "" end = 0 while not end : availablestreams = pollster.poll() for (stream, mask) in availablestreams : if mask & select.POLLHUP : if stream == infile.fileno() : end = 1 if mask & select.POLLOUT : if stream == subprocess.tochild.fileno() : if inputdata : os.write(stream, inputdata) inputdata = "" if mask & select.POLLIN : data = os.read(stream, 256 * 1024) if stream == infile.fileno() : inputdata += data elif stream == subprocess.fromchild.fileno() : sys.stdout.write(data) elif stream == subprocess.childerr.fileno() : sys.stderr.write(data) if inputdata : try : os.write(subprocess.tochild.fileno(), inputdata) except : pass sys.stdout.flush() sys.stderr.flush() subprocess.fromchild.close() subprocess.childerr.close() subprocess.tochild.close() if mustclose : infile.close() status = subprocess.wait() if os.WIFEXITED(status) : retcode = os.WEXITSTATUS(status) else : thebackend.logger.log_message(_("CUPS backend %s died abnormally.") % realbackend, "error") retcode = -1 # stops accounting. thebackend.accounter.endJob(printer, user) # retrieve the job size jobsize = thebackend.accounter.getJobSize() # update the quota for the current user on this printer if printer.Exists : if jobsize : userquota = thebackend.storage.getUserPQuota(user, printer) if userquota.Exists : userquota.increasePagesUsage(jobsize) # adds the current job to history printer.addJobToHistory(thebackend.jobid, user, thebackend.accounter.getLastPageCounter(), action, jobsize) return retcode if __name__ == "__main__" : # This is a CUPS backend, we should act and die like a CUPS backend if len(sys.argv) == 1 : # we will execute each existing backend in device enumeration mode # and generate their PyKota accounting counterpart (directory, myname) = os.path.split(sys.argv[0]) for backend in [os.path.join(directory, b) for b in os.listdir(directory) if os.path.isfile(os.path.join(directory, b)) and (b != myname)] : answer = os.popen(backend, "r") try : devices = [line.strip() for line in answer.readlines()] except : devices = [] status = answer.close() if status is None : for d in devices : # each line is of the form : 'xxxx xxxx "xxxx xxx" "xxxx xxx"' # so we have to decompose it carefully fdevice = cStringIO.StringIO("%s" % d) tokenizer = shlex.shlex(fdevice) tokenizer.wordchars = tokenizer.wordchars + r".:,?!~/\_$*-+={}[]()#" arguments = [] while 1 : token = tokenizer.get_token() if token : arguments.append(token) else : break fdevice.close() try : (devicetype, device, name, fullname) = arguments except ValueError : pass # ignore this 'bizarre' device else : if name.startswith('"') and name.endswith('"') : name = name[1:-1] if fullname.startswith('"') and fullname.endswith('"') : fullname = fullname[1:-1] print '%s cupspykota:%s "PyKota+%s" "PyKota managed %s"' % (devicetype, device, name, fullname) retcode = 0 elif len(sys.argv) not in (6, 7) : sys.stderr.write("ERROR: %s job-id user title copies options [file]\n" % sys.argv[0]) retcode = 1 else : try : # Initializes the backend kotabackend = PyKotaBackend() retcode = main(kotabackend) except (PyKotaToolError, PyKotaConfigError, PyKotaStorageError, PyKotaAccounterError, AttributeError, KeyError, IndexError, ValueError, TypeError, IOError), msg : sys.stderr.write("ERROR : cupspykota backend failed (%s)\n" % msg) sys.stderr.flush() retcode = 1 try : kotabackend.storage.close() except (TypeError, NameError, AttributeError) : pass sys.exit(retcode)