root / pykota / trunk / bin / pykota @ 1177

Revision 1177, 14.2 kB (checked in by jalet, 21 years ago)

CUPS backend added for people to experiment.

  • Property svn:eol-style set to native
  • Property svn:executable set to *
  • Property svn:keywords set to Author Date Id Revision
RevLine 
[695]1#! /usr/bin/env python
[1144]2# -*- coding: ISO-8859-15 -*-
[695]3
4# PyKota accounting filter
5#
[952]6# PyKota - Print Quotas for CUPS and LPRng
[695]7#
8# (c) 2003 Jerome Alet <alet@librelogiciel.com>
[873]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.
[695]13#
[873]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.
[695]22#
23# $Id$
24#
25# $Log$
[1177]26# Revision 1.44  2003/11/08 16:05:31  jalet
27# CUPS backend added for people to experiment.
28#
[1176]29# Revision 1.43  2003/11/06 22:33:25  jalet
30# French variable name
31#
[1152]32# Revision 1.42  2003/10/08 21:41:38  jalet
33# External policies for printers works !
34# We can now auto-add users on first print, and do other useful things if needed.
35#
[1144]36# Revision 1.41  2003/10/07 09:07:27  jalet
37# Character encoding added to please latest version of Python
38#
[1124]39# Revision 1.40  2003/09/04 08:38:56  jalet
40# Added an exception catch to ensure clean close of database even in
41# case of TypeError too.
42#
[1117]43# Revision 1.39  2003/08/18 16:35:28  jalet
44# New pychecker pass, on the tools this time.
45#
[1116]46# Revision 1.38  2003/08/18 16:20:59  jalet
47# Improvement of the printing system detection code.
48#
[1113]49# Revision 1.37  2003/07/29 20:55:17  jalet
50# 1.14 is out !
51#
[1080]52# Revision 1.36  2003/07/10 06:09:52  jalet
53# Incorrect documentation string
54#
[1041]55# Revision 1.35  2003/06/25 14:10:01  jalet
56# Hey, it may work (edpykota --reset excepted) !
57#
[1026]58# Revision 1.34  2003/06/13 18:54:17  jalet
59# Bug with remote jobs and LPRng fixed.
60#
[1004]61# Revision 1.33  2003/05/28 13:51:38  jalet
62# Better handling of errors
63#
[1000]64# Revision 1.32  2003/05/27 23:00:20  jalet
65# Big rewrite of external accounting methods.
66# Should work well now.
67#
[976]68# Revision 1.31  2003/04/30 13:36:39  jalet
69# Stupid accounting method was added.
70#
[975]71# Revision 1.30  2003/04/29 22:03:38  jalet
72# Better error handling.
73#
[973]74# Revision 1.29  2003/04/29 18:37:54  jalet
75# Pluggable accounting methods (actually doesn't support external scripts)
76#
[966]77# Revision 1.28  2003/04/26 08:41:24  jalet
78# Small code reorganisation (UNTESTED) to allow pluggable accounting
79# methods in the future.
80#
[963]81# Revision 1.27  2003/04/25 09:23:47  jalet
82# Debug message passed through !
83#
[962]84# Revision 1.26  2003/04/25 08:23:23  jalet
85# Multiple tries to get the printer's internal page counter, waits for
86# one minute maximum for the printer to warm up, actually.
87#
[956]88# Revision 1.25  2003/04/24 11:53:48  jalet
89# Default policy for unknown users/groups is to DENY printing instead
90# of the previous default to ALLOW printing. This is to solve an accuracy
91# problem. If you set the policy to ALLOW, jobs printed by in nexistant user
92# (from PyKota's POV) will be charged to the next user who prints on the
93# same printer.
94#
[952]95# Revision 1.24  2003/04/23 22:13:56  jalet
96# Preliminary support for LPRng added BUT STILL UNTESTED.
97#
[915]98# Revision 1.23  2003/04/15 11:30:57  jalet
99# More work done on money print charging.
100# Minor bugs corrected.
101# All tools now access to the storage as priviledged users, repykota excepted.
102#
[914]103# Revision 1.22  2003/04/15 11:09:04  jalet
104# Small bug was fixed when a printer was never used and its internal
105# page counter is not accessible.
106#
[909]107# Revision 1.21  2003/04/12 17:20:14  jalet
108# Better formula for HP workaround
109#
[908]110# Revision 1.20  2003/04/12 16:58:28  jalet
111# The workaround for HP printers was not correct, and there's probably no
112# correct way to workaround the problem because we can't save the internal
113# page counter in real time. The last job's size is unconditionnally set to
114# 5 pages in this case.
115#
[902]116# Revision 1.19  2003/04/11 08:56:49  jalet
117# Comment
118#
[901]119# Revision 1.18  2003/04/11 08:50:39  jalet
120# Workaround for the HP "feature" of saving the page counter to NVRAM
121# only every time 10 new pages are printed...
122# Workaround for printers with volatile page counters.
123#
[900]124# Revision 1.17  2003/04/10 21:47:20  jalet
125# Job history added. Upgrade script neutralized for now !
126#
[887]127# Revision 1.16  2003/04/08 20:38:08  jalet
128# The last job Id is saved now for each printer, this will probably
129# allow other accounting methods in the future.
130#
[873]131# Revision 1.15  2003/03/29 13:45:27  jalet
132# GPL paragraphs were incorrectly (from memory) copied into the sources.
133# Two README files were added.
134# Upgrade script for PostgreSQL pre 1.01 schema was added.
135#
[832]136# Revision 1.14  2003/03/07 22:16:57  jalet
137# Algorithmically incorrect : last user quota wasn't updated if current
138# user wasn't allowed to print.
139#
[826]140# Revision 1.13  2003/02/27 23:59:28  jalet
141# Stupid bug wrt exception handlingand value conversion
142#
[825]143# Revision 1.12  2003/02/27 23:48:41  jalet
144# Correctly maps PyKota's log levels to syslog log levels
145#
[824]146# Revision 1.11  2003/02/27 22:55:20  jalet
147# WARN log priority doesn't exist.
148#
[823]149# Revision 1.10  2003/02/27 22:43:21  jalet
150# Missing import
151#
[822]152# Revision 1.9  2003/02/27 22:40:26  jalet
153# Correctly handles cases where the printer is off.
154#
[772]155# Revision 1.8  2003/02/09 12:56:53  jalet
156# Internationalization begins...
157#
[740]158# Revision 1.7  2003/02/07 10:23:48  jalet
159# Avoid a possible future name clash
160#
[728]161# Revision 1.6  2003/02/06 22:54:33  jalet
162# warnpykota should be ok
163#
[704]164# Revision 1.5  2003/02/05 22:45:25  jalet
165# Forgotten import
166#
[703]167# Revision 1.4  2003/02/05 22:42:51  jalet
168# Typo
169#
[701]170# Revision 1.3  2003/02/05 22:38:39  jalet
171# Typo
172#
[699]173# Revision 1.2  2003/02/05 22:16:20  jalet
174# DEVICE_URI is undefined outside of CUPS, i.e. for normal command line tools
175#
[695]176# Revision 1.1  2003/02/05 21:28:17  jalet
177# Initial import into CVS
178#
179#
180#
181
182import sys
[704]183import os
[695]184
185from pykota.tool import PyKotaTool, PyKotaToolError
[975]186from pykota.config import PyKotaConfigError
[973]187from pykota.storage import PyKotaStorageError
188from pykota.accounter import openAccounter, PyKotaAccounterError
[695]189
[703]190class PyKotaFilter(PyKotaTool) :   
[695]191    """Class for the PyKota filter."""
[952]192    def __init__(self) :
[915]193        PyKotaTool.__init__(self)
[1000]194        (self.printingsystem, self.printerhostname, self.printername, self.username, self.jobid, self.inputfile, self.copies) = self.extractInfoFromCupsOrLprng()
[973]195        self.accounter = openAccounter(self)
[695]196   
[973]197    def extractInfoFromCupsOrLprng(self) :   
198        """Returns a tuple (printingsystem, printerhostname, printername, username, jobid, filename) depending on the printing system in use (as seen by the print filter).
199       
[1080]200           Returns (None, None, None, None, None, None, None) if no printing system is recognized.
[973]201        """
202        # Try to detect CUPS
203        if os.environ.has_key("CUPS_SERVERROOT") and os.path.isdir(os.environ.get("CUPS_SERVERROOT", "")) :
204            if len(sys.argv) == 7 :
205                inputfile = sys.argv[6]
206            else :   
207                inputfile = None
208               
209            device_uri = os.environ.get("DEVICE_URI", "")
210            # TODO : check this for more complex urls than ipp://myprinter.dot.com:631/printers/lp
211            try :
212                (backend, destination) = device_uri.split(":", 1) 
213            except ValueError :   
214                raise PyKotaToolError, "Invalid DEVICE_URI : %s\n" % device_uri
215            while destination.startswith("/") :
216                destination = destination[1:]
217            printerhostname = destination.split("/")[0].split(":")[0]
[1000]218            return ("CUPS", printerhostname, os.environ.get("PRINTER"), sys.argv[2].strip(), sys.argv[1].strip(), inputfile, int(sys.argv[4].strip()))
[973]219        else :   
220            # Try to detect LPRng
[1026]221            jseen = Pseen = nseen = rseen = Kseen = None
[973]222            for arg in sys.argv :
223                if arg.startswith("-j") :
224                    jseen = arg[2:].strip()
225                elif arg.startswith("-n") :     
226                    nseen = arg[2:].strip()
227                elif arg.startswith("-P") :   
228                    Pseen = arg[2:].strip()
229                elif arg.startswith("-r") :   
230                    rseen = arg[2:].strip()
[1000]231                elif arg.startswith("-K") or arg.startswith("-#") :   
232                    Kseen = int(arg[2:].strip())
233            if Kseen is None :       
234                Kseen = 1       # we assume the user wants at least one copy...
[1116]235            if (rseen is None) and jseen and Pseen and nseen :   
236                self.logger.log_message(_("Printer hostname undefined, set to 'localhost'"), "warn")
237                rseen = "localhost"
[973]238            if jseen and Pseen and nseen and rseen :       
[1026]239                # job is always in stdin (None)
240                return ("LPRNG", rseen, Pseen, nseen, jseen, None, Kseen)
[1116]241        self.logger.log_message(_("Printing system unknown, args=%s") % " ".join(sys.argv), "warn")
[1000]242        return (None, None, None, None, None, None, None)   # Unknown printing system         
[973]243       
[952]244    def acceptJob(self) :       
245        """Returns the exit code needed by the printing backend to accept the job and print it."""
246        if self.printingsystem == "CUPS" :
247            return 0
248        elif self.printingsystem == "LPRNG" :   
249            return 0
250        else :   
251            # UNKNOWN
252            return -1
253           
254    def removeJob(self) :           
255        """Returns the exit code needed by the printing backend to refuse the job and remove it."""
256        if self.printingsystem == "CUPS" :
257            return 1
258        elif self.printingsystem == "LPRNG" :   
259            return 3
260        else :   
261            # UNKNOWN
262            return -1
263           
[1152]264def format_commandline(prt, usr, cmdline) :           
265    """Passes printer and user names on the command line."""
266    printer = prt.Name
267    user = usr.Name
268    # we don't want the external command's standard
269    # output to break the print job's data, but we
270    # want to keep its standard error
271    return "%s >/dev/null" % (cmdline % locals())
272
[1113]273def main(thefilter) :   
[695]274    """Do it, and do it right !"""
[952]275    #
276    # If this is a CUPS filter, we should act and die like a CUPS filter when needed
[1113]277    if thefilter.printingsystem == "CUPS" :
[952]278        if len(sys.argv) not in (6, 7) :   
279            sys.stderr.write("ERROR: %s job-id user title copies options [file]\n" % sys.argv[0])
[1113]280            return thefilter.removeJob()
[695]281   
282    # Get the last page counter and last username from the Quota Storage backend
[1113]283    printer = thefilter.storage.getPrinter(thefilter.printername)
[1041]284    if not printer.Exists :
[695]285        # The printer is unknown from the Quota Storage perspective
286        # we let the job pass through, but log a warning message
[1113]287        thefilter.logger.log_message(_("Printer %s not registered in the PyKota system") % thefilter.printername, "warn")
[695]288    else :   
[1176]289        for dummy in range(2) :
[1152]290            user = thefilter.storage.getUser(thefilter.username)
291            if user.Exists :
292                break
[952]293            else :   
[1152]294                # The user is unknown from the Quota Storage perspective
295                # Depending on the default policy for this printer, we
296                # either let the job pass through or reject it, but we
297                # log a message in any case.
298                (policy, args) = thefilter.config.getPrinterPolicy(thefilter.printername)
299                if policy == "ALLOW" :
300                    action = "POLICY_ALLOW"
301                elif policy == "EXTERNAL" :   
302                    commandline = format_commandline(printer, user, args)
303                    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")
304                    if os.system(commandline) :
305                        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")
306                        return thefilter.removeJob()
307                    else :   
308                        # here we try a second time, because the goal
309                        # of the external action was to add the user
310                        # in the database.
311                        continue 
312                else :   
313                    action = "POLICY_DENY"
314                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")
315                if action == "POLICY_DENY" :
316                    return thefilter.removeJob()
317                # when we get there, the printer policy allows the job to pass
318                break   
319                   
320        # if user exists, do accounting
321        if user.Exists :
[973]322            # Now does the accounting and act depending on the result
[1113]323            action = thefilter.accounter.doAccounting(printer, user)
[1152]324       
[900]325            # if not allowed to print then die, else proceed.
326            if action == "DENY" :
327                # No, just die cleanly
[1113]328                return thefilter.removeJob()
[1152]329        elif policy == "EXTERNAL" :               
330            thefilter.logger.log_message(_("External policy %s for printer %s couldn't add user %s. Job rejected.") % (commandline, thefilter.printername, thefilter.username), "error")
331            return thefilter.removeJob()
[695]332       
333    # pass the job untouched to the underlying layer
[1113]334    thefilter.accounter.filterInput(thefilter.inputfile)     
[695]335   
[1113]336    return thefilter.acceptJob()
[695]337
338if __name__ == "__main__" :   
[1004]339    retcode = -1
[973]340    try :
[1113]341        # Initializes the current tool
342        kotafilter = PyKotaFilter()   
343        retcode = main(kotafilter)
[1124]344    except (PyKotaToolError, PyKotaConfigError, PyKotaStorageError, PyKotaAccounterError, AttributeError, KeyError, IndexError, ValueError, TypeError, IOError), msg :
[973]345        sys.stderr.write("ERROR : PyKota filter failed (%s)\n" % msg)
346        sys.stderr.flush()
[1177]347        try :
348            retcode = kotafilter.removeJob()
349        except :
350            retcode = -1
[1113]351
352    try :
353        kotafilter.storage.close()
354    except (TypeError, NameError, AttributeError) :   
355        pass
356       
[973]357    sys.exit(retcode)   
Note: See TracBrowser for help on using the browser.