root / pykota / trunk / bin / pykota @ 1152

Revision 1152, 14.0 kB (checked in by jalet, 21 years ago)

External policies for printers works !
We can now auto-add users on first print, and do other useful things if needed.

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