root / pykota / trunk / pykota / tool.py @ 1565

Revision 1565, 50.0 kB (checked in by jalet, 20 years ago)

Also prints read size on last block

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
Line 
1# PyKota
2# -*- coding: ISO-8859-15 -*-
3
4# PyKota - Print Quotas for CUPS and LPRng
5#
6# (c) 2003-2004 Jerome Alet <alet@librelogiciel.com>
7# This program is free software; you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by
9# the Free Software Foundation; either version 2 of the License, or
10# (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with this program; if not, write to the Free Software
19# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
20#
21# $Id$
22#
23# $Log$
24# Revision 1.108  2004/06/24 23:09:30  jalet
25# Also prints read size on last block
26#
27# Revision 1.107  2004/06/23 13:03:28  jalet
28# Catches accounter configuration errors earlier
29#
30# Revision 1.106  2004/06/22 09:31:18  jalet
31# Always send some debug info to CUPS' back channel stream (stderr) as
32# informationnal messages.
33#
34# Revision 1.105  2004/06/21 08:17:38  jalet
35# Added version number in subject message for directive crashrecipient.
36#
37# Revision 1.104  2004/06/18 13:34:49  jalet
38# Now all tracebacks include PyKota's version number
39#
40# Revision 1.103  2004/06/18 13:17:26  jalet
41# Now includes PyKota's version number in messages sent by the crashrecipient
42# directive.
43#
44# Revision 1.102  2004/06/17 13:26:51  jalet
45# Better exception handling code
46#
47# Revision 1.101  2004/06/16 20:56:34  jalet
48# Smarter initialisation code
49#
50# Revision 1.100  2004/06/11 08:16:03  jalet
51# More exceptions catched in case of very early failure.
52#
53# Revision 1.99  2004/06/11 07:07:38  jalet
54# Now detects and logs configuration syntax errors instead of failing without
55# any notice message.
56#
57# Revision 1.98  2004/06/08 19:27:12  jalet
58# Doesn't ignore SIGCHLD anymore
59#
60# Revision 1.97  2004/06/07 22:45:35  jalet
61# Now accepts a job when enforcement is STRICT and predicted account balance
62# is equal to 0.0 : since the job hasn't been printed yet, only its printing
63# will really render balance equal to 0.0, so we should be allowed to print.
64#
65# Revision 1.96  2004/06/05 22:18:04  jalet
66# Now catches some exceptions earlier.
67# storage.py and ldapstorage.py : removed old comments
68#
69# Revision 1.95  2004/06/03 21:50:34  jalet
70# Improved error logging.
71# crashrecipient directive added.
72# Now exports the job's size in bytes too.
73#
74# Revision 1.94  2004/06/03 08:51:03  jalet
75# logs job's size in bytes now
76#
77# Revision 1.93  2004/06/02 21:51:02  jalet
78# Moved the sigterm capturing elsewhere
79#
80# Revision 1.92  2004/06/02 13:21:38  jalet
81# Debug message added
82#
83# Revision 1.91  2004/05/25 05:17:52  jalet
84# Now precomputes the job's size only if current printer's enforcement
85# is "STRICT"
86#
87# Revision 1.90  2004/05/24 22:45:49  jalet
88# New 'enforcement' directive added
89# Polling loop improvements
90#
91# Revision 1.89  2004/05/21 22:02:52  jalet
92# Preliminary work on pre-accounting
93#
94# Revision 1.88  2004/05/18 14:49:20  jalet
95# Big code changes to completely remove the need for "requester" directives,
96# jsut use "hardware(... your previous requester directive's content ...)"
97#
98# Revision 1.87  2004/05/17 19:14:59  jalet
99# Now catches SIGPIPE and SIGCHLD
100#
101# Revision 1.86  2004/05/13 13:59:28  jalet
102# Code simplifications
103#
104# Revision 1.85  2004/05/11 08:26:27  jalet
105# Now catches connection problems to SMTP server
106#
107# Revision 1.84  2004/04/21 08:36:32  jalet
108# Exports the PYKOTASTATUS environment variable when SIGTERM is received.
109#
110# Revision 1.83  2004/04/16 17:03:49  jalet
111# The list of printers groups the current printer is a member of is
112# now exported in the PYKOTAPGROUPS environment variable
113#
114# Revision 1.82  2004/04/13 09:38:03  jalet
115# More work on correct child processes handling
116#
117# Revision 1.81  2004/04/09 22:24:47  jalet
118# Began work on correct handling of child processes when jobs are cancelled by
119# the user. Especially important when an external requester is running for a
120# long time.
121#
122# Revision 1.80  2004/04/06 12:00:21  jalet
123# uninitialized values caused problems
124#
125# Revision 1.79  2004/03/28 21:01:29  jalet
126# PYKOTALIMITBY environment variable is now exported too
127#
128# Revision 1.78  2004/03/08 20:13:25  jalet
129# Allow names to begin with a digit
130#
131# Revision 1.77  2004/03/03 13:10:35  jalet
132# Now catches all smtplib exceptions when there's a problem sending messages
133#
134# Revision 1.76  2004/03/01 14:34:15  jalet
135# PYKOTAPHASE wasn't set at the right time at the end of data transmission
136# to underlying layer (real backend)
137#
138# Revision 1.75  2004/03/01 11:23:25  jalet
139# Pre and Post hooks to external commands are available in the cupspykota
140# backend. Forthe pykota filter they will be implemented real soon now.
141#
142# Revision 1.74  2004/02/26 14:18:07  jalet
143# Should fix the remaining bugs wrt printers groups and users groups.
144#
145# Revision 1.73  2004/02/19 14:20:21  jalet
146# maildomain pykota.conf directive added.
147# Small improvements on mail headers quality.
148#
149# Revision 1.72  2004/01/14 15:51:19  jalet
150# Docstring added.
151#
152# Revision 1.71  2004/01/11 23:22:42  jalet
153# Major code refactoring, it's way cleaner, and now allows automated addition
154# of printers on first print.
155#
156# Revision 1.70  2004/01/08 14:10:32  jalet
157# Copyright year changed.
158#
159# Revision 1.69  2004/01/05 16:02:18  jalet
160# Dots in user, groups and printer names should be allowed.
161#
162# Revision 1.68  2004/01/02 17:38:40  jalet
163# This time it should be better...
164#
165# Revision 1.67  2004/01/02 17:37:09  jalet
166# I'm completely stupid !!! Better to not talk while coding !
167#
168# Revision 1.66  2004/01/02 17:31:26  jalet
169# Forgot to remove some code
170#
171# Revision 1.65  2003/12/06 08:14:38  jalet
172# Added support for CUPS device uris which contain authentication information.
173#
174# Revision 1.64  2003/11/29 22:03:17  jalet
175# Some code refactoring work. New code is not used at this time.
176#
177# Revision 1.63  2003/11/29 20:06:20  jalet
178# Added 'utolower' configuration option to convert all usernames to
179# lowercase when printing. All database accesses are still and will
180# remain case sensitive though.
181#
182# Revision 1.62  2003/11/25 22:37:22  jalet
183# Small code move
184#
185# Revision 1.61  2003/11/25 22:03:28  jalet
186# No more message on stderr when the translation is not available.
187#
188# Revision 1.60  2003/11/25 21:54:05  jalet
189# updated FAQ
190#
191# Revision 1.59  2003/11/25 13:33:43  jalet
192# Puts 'root' instead of '' when printing from CUPS web interface (which
193# gives an empty username)
194#
195# Revision 1.58  2003/11/21 14:28:45  jalet
196# More complete job history.
197#
198# Revision 1.57  2003/11/19 23:19:38  jalet
199# Code refactoring work.
200# Explicit redirection to /dev/null has to be set in external policy now, just
201# like in external mailto.
202#
203# Revision 1.56  2003/11/19 07:40:20  jalet
204# Missing import statement.
205# Better documentation for mailto: external(...)
206#
207# Revision 1.55  2003/11/18 23:43:12  jalet
208# Mailto can be any external command now, as usual.
209#
210# Revision 1.54  2003/10/24 21:52:46  jalet
211# Now can force language when coming from CGI script.
212#
213# Revision 1.53  2003/10/08 21:41:38  jalet
214# External policies for printers works !
215# We can now auto-add users on first print, and do other useful things if needed.
216#
217# Revision 1.52  2003/10/07 09:07:28  jalet
218# Character encoding added to please latest version of Python
219#
220# Revision 1.51  2003/10/06 14:21:41  jalet
221# Test reversed to not retrieve group members when no messages for them.
222#
223# Revision 1.50  2003/10/02 20:23:18  jalet
224# Storage caching mechanism added.
225#
226# Revision 1.49  2003/07/29 20:55:17  jalet
227# 1.14 is out !
228#
229# Revision 1.48  2003/07/21 23:01:56  jalet
230# Modified some messages aout soft limit
231#
232# Revision 1.47  2003/07/16 21:53:08  jalet
233# Really big modifications wrt new configuration file's location and content.
234#
235# Revision 1.46  2003/07/09 20:17:07  jalet
236# Email field added to PostgreSQL schema
237#
238# Revision 1.45  2003/07/08 19:43:51  jalet
239# Configurable warning messages.
240# Poor man's treshold value added.
241#
242# Revision 1.44  2003/07/07 11:49:24  jalet
243# Lots of small fixes with the help of PyChecker
244#
245# Revision 1.43  2003/07/04 09:06:32  jalet
246# Small bug fix wrt undefined "LimitBy" field.
247#
248# Revision 1.42  2003/06/30 12:46:15  jalet
249# Extracted reporting code.
250#
251# Revision 1.41  2003/06/25 14:10:01  jalet
252# Hey, it may work (edpykota --reset excepted) !
253#
254# Revision 1.40  2003/06/10 16:37:54  jalet
255# Deletion of the second user which is not needed anymore.
256# Added a debug configuration field in /etc/pykota.conf
257# All queries can now be sent to the logger in debug mode, this will
258# greatly help improve performance when time for this will come.
259#
260# Revision 1.39  2003/04/29 18:37:54  jalet
261# Pluggable accounting methods (actually doesn't support external scripts)
262#
263# Revision 1.38  2003/04/24 11:53:48  jalet
264# Default policy for unknown users/groups is to DENY printing instead
265# of the previous default to ALLOW printing. This is to solve an accuracy
266# problem. If you set the policy to ALLOW, jobs printed by in nexistant user
267# (from PyKota's POV) will be charged to the next user who prints on the
268# same printer.
269#
270# Revision 1.37  2003/04/24 08:08:27  jalet
271# Debug message forgotten
272#
273# Revision 1.36  2003/04/24 07:59:40  jalet
274# LPRng support now works !
275#
276# Revision 1.35  2003/04/23 22:13:57  jalet
277# Preliminary support for LPRng added BUT STILL UNTESTED.
278#
279# Revision 1.34  2003/04/17 09:26:21  jalet
280# repykota now reports account balances too.
281#
282# Revision 1.33  2003/04/16 12:35:49  jalet
283# Groups quota work now !
284#
285# Revision 1.32  2003/04/16 08:53:14  jalet
286# Printing can now be limited either by user's account balance or by
287# page quota (the default). Quota report doesn't include account balance
288# yet, though.
289#
290# Revision 1.31  2003/04/15 11:30:57  jalet
291# More work done on money print charging.
292# Minor bugs corrected.
293# All tools now access to the storage as priviledged users, repykota excepted.
294#
295# Revision 1.30  2003/04/10 21:47:20  jalet
296# Job history added. Upgrade script neutralized for now !
297#
298# Revision 1.29  2003/03/29 13:45:27  jalet
299# GPL paragraphs were incorrectly (from memory) copied into the sources.
300# Two README files were added.
301# Upgrade script for PostgreSQL pre 1.01 schema was added.
302#
303# Revision 1.28  2003/03/29 13:08:28  jalet
304# Configuration is now expected to be found in /etc/pykota.conf instead of
305# in /etc/cups/pykota.conf
306# Installation script can move old config files to the new location if needed.
307# Better error handling if configuration file is absent.
308#
309# Revision 1.27  2003/03/15 23:01:28  jalet
310# New mailto option in configuration file added.
311# No time to test this tonight (although it should work).
312#
313# Revision 1.26  2003/03/09 23:58:16  jalet
314# Comment
315#
316# Revision 1.25  2003/03/07 22:56:14  jalet
317# 0.99 is out with some bug fixes.
318#
319# Revision 1.24  2003/02/27 23:48:41  jalet
320# Correctly maps PyKota's log levels to syslog log levels
321#
322# Revision 1.23  2003/02/27 22:55:20  jalet
323# WARN log priority doesn't exist.
324#
325# Revision 1.22  2003/02/27 09:09:20  jalet
326# Added a method to match strings against wildcard patterns
327#
328# Revision 1.21  2003/02/17 23:01:56  jalet
329# Typos
330#
331# Revision 1.20  2003/02/17 22:55:01  jalet
332# More options can now be set per printer or globally :
333#
334#       admin
335#       adminmail
336#       gracedelay
337#       requester
338#
339# the printer option has priority when both are defined.
340#
341# Revision 1.19  2003/02/10 11:28:45  jalet
342# Localization
343#
344# Revision 1.18  2003/02/10 01:02:17  jalet
345# External requester is about to work, but I must sleep
346#
347# Revision 1.17  2003/02/09 13:05:43  jalet
348# Internationalization continues...
349#
350# Revision 1.16  2003/02/09 12:56:53  jalet
351# Internationalization begins...
352#
353# Revision 1.15  2003/02/08 22:09:52  jalet
354# Name check method moved here
355#
356# Revision 1.14  2003/02/07 10:42:45  jalet
357# Indentation problem
358#
359# Revision 1.13  2003/02/07 08:34:16  jalet
360# Test wrt date limit was wrong
361#
362# Revision 1.12  2003/02/06 23:20:02  jalet
363# warnpykota doesn't need any user/group name argument, mimicing the
364# warnquota disk quota tool.
365#
366# Revision 1.11  2003/02/06 22:54:33  jalet
367# warnpykota should be ok
368#
369# Revision 1.10  2003/02/06 15:03:11  jalet
370# added a method to set the limit date
371#
372# Revision 1.9  2003/02/06 10:39:23  jalet
373# Preliminary edpykota work.
374#
375# Revision 1.8  2003/02/06 09:19:02  jalet
376# More robust behavior (hopefully) when the user or printer is not managed
377# correctly by the Quota System : e.g. cupsFilter added in ppd file, but
378# printer and/or user not 'yet?' in storage.
379#
380# Revision 1.7  2003/02/06 00:00:45  jalet
381# Now includes the printer name in email messages
382#
383# Revision 1.6  2003/02/05 23:55:02  jalet
384# Cleaner email messages
385#
386# Revision 1.5  2003/02/05 23:45:09  jalet
387# Better DateTime manipulation wrt grace delay
388#
389# Revision 1.4  2003/02/05 23:26:22  jalet
390# Incorrect handling of grace delay
391#
392# Revision 1.3  2003/02/05 22:16:20  jalet
393# DEVICE_URI is undefined outside of CUPS, i.e. for normal command line tools
394#
395# Revision 1.2  2003/02/05 22:10:29  jalet
396# Typos
397#
398# Revision 1.1  2003/02/05 21:28:17  jalet
399# Initial import into CVS
400#
401#
402#
403
404import sys
405import os
406import fnmatch
407import getopt
408import smtplib
409import gettext
410import locale
411import signal
412import socket
413import tempfile
414import ConfigParser
415
416from mx import DateTime
417
418from pykota import version, config, storage, logger, accounter, pdlanalyzer
419
420class PyKotaToolError(Exception):
421    """An exception for PyKota config related stuff."""
422    def __init__(self, message = ""):
423        self.message = message
424        Exception.__init__(self, message)
425    def __repr__(self):
426        return self.message
427    __str__ = __repr__
428   
429def crashed(message) :   
430    """Minimal crash method."""
431    import traceback
432    lines = []
433    for line in traceback.format_exception(*sys.exc_info()) :
434        lines.extend([l for l in line.split("\n") if l])
435    msg = "ERROR: ".join(["%s\n" % l for l in (["ERROR: PyKota v%s" % version.__version__, message] + lines)])
436    sys.stderr.write(msg)
437    sys.stderr.flush()
438    return msg
439
440class PyKotaTool :   
441    """Base class for all PyKota command line tools."""
442    def __init__(self, lang=None, doc="PyKota %s (c) 2003-2004 %s" % (version.__version__, version.__author__)) :
443        """Initializes the command line tool."""
444        # locale stuff
445        try :
446            locale.setlocale(locale.LC_ALL, lang)
447            gettext.install("pykota")
448        except (locale.Error, IOError) :
449            gettext.NullTranslations().install()
450            # sys.stderr.write("PyKota : Error while loading translations\n")
451   
452        # pykota specific stuff
453        self.documentation = doc
454        try :
455            self.config = config.PyKotaConfig("/etc/pykota")
456        except ConfigParser.ParsingError, msg :   
457            sys.stderr.write("ERROR: Problem encountered while parsing configuration file : %s\n" % msg)
458            sys.stderr.flush()
459            sys.exit(-1)
460           
461        try :
462            self.debug = self.config.getDebug()
463            self.smtpserver = self.config.getSMTPServer()
464            self.maildomain = self.config.getMailDomain()
465            self.logger = logger.openLogger(self.config.getLoggingBackend())
466            self.storage = storage.openConnection(self)
467        except (config.PyKotaConfigError, logger.PyKotaLoggingError, storage.PyKotaStorageError), msg :
468            self.crashed(msg)
469            raise
470        self.softwareJobSize = 0
471        self.softwareJobPrice = 0.0
472       
473    def logdebug(self, message) :   
474        """Logs something to debug output if debug is enabled."""
475        if self.debug :
476            self.logger.log_message(message, "debug")
477       
478    def clean(self) :   
479        """Ensures that the database is closed."""
480        try :
481            self.storage.close()
482        except (TypeError, NameError, AttributeError) :   
483            pass
484           
485    def display_version_and_quit(self) :
486        """Displays version number, then exists successfully."""
487        self.clean()
488        print version.__version__
489        sys.exit(0)
490   
491    def display_usage_and_quit(self) :
492        """Displays command line usage, then exists successfully."""
493        self.clean()
494        print self.documentation
495        sys.exit(0)
496       
497    def crashed(self, message) :   
498        """Outputs a crash message, and optionally sends it to software author."""
499        msg = crashed(message)
500        try :
501            crashrecipient = self.config.getCrashRecipient()
502            if crashrecipient :
503                admin = self.config.getAdminMail("global") # Nice trick, isn't it ?
504                fullmessage = "========== Traceback :\n\n%s\n\n========== sys.argv :\n\n%s\n\n========== Environment :\n\n%s\n" % \
505                                (msg, \
506                                 "\n".join(["    %s" % repr(a) for a in sys.argv]), \
507                                 "\n".join(["    %s=%s" % (k, v) for (k, v) in os.environ.items()]))
508                server = smtplib.SMTP(self.smtpserver)
509                server.sendmail(admin, [admin, crashrecipient], \
510                                       "From: %s\nTo: %s\nCc: %s\nSubject: PyKota v%s crash traceback !\n\n%s" % \
511                                       (admin, crashrecipient, admin, version.__version__, fullmessage))
512                server.quit()
513        except :
514            pass
515       
516    def parseCommandline(self, argv, short, long, allownothing=0) :
517        """Parses the command line, controlling options."""
518        # split options in two lists: those which need an argument, those which don't need any
519        withoutarg = []
520        witharg = []
521        lgs = len(short)
522        i = 0
523        while i < lgs :
524            ii = i + 1
525            if (ii < lgs) and (short[ii] == ':') :
526                # needs an argument
527                witharg.append(short[i])
528                ii = ii + 1 # skip the ':'
529            else :
530                # doesn't need an argument
531                withoutarg.append(short[i])
532            i = ii
533               
534        for option in long :
535            if option[-1] == '=' :
536                # needs an argument
537                witharg.append(option[:-1])
538            else :
539                # doesn't need an argument
540                withoutarg.append(option)
541       
542        # we begin with all possible options unset
543        parsed = {}
544        for option in withoutarg + witharg :
545            parsed[option] = None
546       
547        # then we parse the command line
548        args = []       # to not break if something unexpected happened
549        try :
550            options, args = getopt.getopt(argv, short, long)
551            if options :
552                for (o, v) in options :
553                    # we skip the '-' chars
554                    lgo = len(o)
555                    i = 0
556                    while (i < lgo) and (o[i] == '-') :
557                        i = i + 1
558                    o = o[i:]
559                    if o in witharg :
560                        # needs an argument : set it
561                        parsed[o] = v
562                    elif o in withoutarg :
563                        # doesn't need an argument : boolean
564                        parsed[o] = 1
565                    else :
566                        # should never occur
567                        raise PyKotaToolError, "Unexpected problem when parsing command line"
568            elif (not args) and (not allownothing) and sys.stdin.isatty() : # no option and no argument, we display help if we are a tty
569                self.display_usage_and_quit()
570        except getopt.error, msg :
571            sys.stderr.write("%s\n" % msg)
572            sys.stderr.flush()
573            self.display_usage_and_quit()
574        return (parsed, args)
575   
576    def isValidName(self, name) :
577        """Checks if a user or printer name is valid."""
578        # unfortunately Python 2.1 string modules doesn't define ascii_letters...
579        asciiletters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
580        digits = '0123456789'
581        validchars = asciiletters + digits + "-_."
582        for c in name :
583            if c not in validchars :
584                return 0
585        return 1       
586       
587    def matchString(self, s, patterns) :
588        """Returns 1 if the string s matches one of the patterns, else 0."""
589        for pattern in patterns :
590            if fnmatch.fnmatchcase(s, pattern) :
591                return 1
592        return 0
593       
594    def sendMessage(self, adminmail, touser, fullmessage) :
595        """Sends an email message containing headers to some user."""
596        if "@" not in touser :
597            touser = "%s@%s" % (touser, self.maildomain or self.smtpserver)
598        try :   
599            server = smtplib.SMTP(self.smtpserver)
600        except socket.error, msg :   
601            self.logger.log_message(_("Impossible to connect to SMTP server : %s") % msg, "error")
602        else :
603            try :
604                server.sendmail(adminmail, [touser], "From: %s\nTo: %s\n%s" % (adminmail, touser, fullmessage))
605            except smtplib.SMTPException, answer :   
606                for (k, v) in answer.recipients.items() :
607                    self.logger.log_message(_("Impossible to send mail to %s, error %s : %s") % (k, v[0], v[1]), "error")
608            server.quit()
609       
610    def sendMessageToUser(self, admin, adminmail, user, subject, message) :
611        """Sends an email message to a user."""
612        message += _("\n\nPlease contact your system administrator :\n\n\t%s - <%s>\n") % (admin, adminmail)
613        self.sendMessage(adminmail, user.Email or user.Name, "Subject: %s\n\n%s" % (subject, message))
614       
615    def sendMessageToAdmin(self, adminmail, subject, message) :
616        """Sends an email message to the Print Quota administrator."""
617        self.sendMessage(adminmail, adminmail, "Subject: %s\n\n%s" % (subject, message))
618       
619    def _checkUserPQuota(self, userpquota) :           
620        """Checks the user quota on a printer and deny or accept the job."""
621        # then we check the user's own quota
622        # if we get there we are sure that policy is not EXTERNAL
623        user = userpquota.User
624        printer = userpquota.Printer
625        enforcement = self.config.getPrinterEnforcement(printer.Name)
626        self.logdebug("Checking user %s's quota on printer %s" % (user.Name, printer.Name))
627        (policy, dummy) = self.config.getPrinterPolicy(userpquota.Printer.Name)
628        if not userpquota.Exists :
629            # Unknown userquota
630            if policy == "ALLOW" :
631                action = "POLICY_ALLOW"
632            else :   
633                action = "POLICY_DENY"
634            self.logger.log_message(_("Unable to match user %s on printer %s, applying default policy (%s)") % (user.Name, printer.Name, action))
635        else :   
636            pagecounter = int(userpquota.PageCounter or 0)
637            if enforcement == "STRICT" :
638                pagecounter += self.softwareJobSize
639            if userpquota.SoftLimit is not None :
640                softlimit = int(userpquota.SoftLimit)
641                if pagecounter < softlimit :
642                    action = "ALLOW"
643                else :   
644                    if userpquota.HardLimit is None :
645                        # only a soft limit, this is equivalent to having only a hard limit
646                        action = "DENY"
647                    else :   
648                        hardlimit = int(userpquota.HardLimit)
649                        if softlimit <= pagecounter < hardlimit :   
650                            now = DateTime.now()
651                            if userpquota.DateLimit is not None :
652                                datelimit = DateTime.ISO.ParseDateTime(userpquota.DateLimit)
653                            else :
654                                datelimit = now + self.config.getGraceDelay(printer.Name)
655                                userpquota.setDateLimit(datelimit)
656                            if now < datelimit :
657                                action = "WARN"
658                            else :   
659                                action = "DENY"
660                        else :         
661                            action = "DENY"
662            else :       
663                if userpquota.HardLimit is not None :
664                    # no soft limit, only a hard one.
665                    hardlimit = int(userpquota.HardLimit)
666                    if pagecounter < hardlimit :
667                        action = "ALLOW"
668                    else :     
669                        action = "DENY"
670                else :
671                    # Both are unset, no quota, i.e. accounting only
672                    action = "ALLOW"
673        return action
674   
675    def checkGroupPQuota(self, grouppquota) :   
676        """Checks the group quota on a printer and deny or accept the job."""
677        group = grouppquota.Group
678        printer = grouppquota.Printer
679        enforcement = self.config.getPrinterEnforcement(printer.Name)
680        self.logdebug("Checking group %s's quota on printer %s" % (group.Name, printer.Name))
681        if group.LimitBy and (group.LimitBy.lower() == "balance") : 
682            val = group.AccountBalance
683            if enforcement == "STRICT" : 
684                val -= self.softwareJobPrice # use precomputed size.
685            if val <= 0.0 :
686                action = "DENY"
687            elif val <= self.config.getPoorMan() :   
688                action = "WARN"
689            else :   
690                action = "ALLOW"
691            if (enforcement == "STRICT") and (val == 0.0) :
692                action = "WARN" # we can still print until account is 0
693        else :
694            val = grouppquota.PageCounter
695            if enforcement == "STRICT" :
696                val += self.softwareJobSize
697            if grouppquota.SoftLimit is not None :
698                softlimit = int(grouppquota.SoftLimit)
699                if val < softlimit :
700                    action = "ALLOW"
701                else :   
702                    if grouppquota.HardLimit is None :
703                        # only a soft limit, this is equivalent to having only a hard limit
704                        action = "DENY"
705                    else :   
706                        hardlimit = int(grouppquota.HardLimit)
707                        if softlimit <= val < hardlimit :   
708                            now = DateTime.now()
709                            if grouppquota.DateLimit is not None :
710                                datelimit = DateTime.ISO.ParseDateTime(grouppquota.DateLimit)
711                            else :
712                                datelimit = now + self.config.getGraceDelay(printer.Name)
713                                grouppquota.setDateLimit(datelimit)
714                            if now < datelimit :
715                                action = "WARN"
716                            else :   
717                                action = "DENY"
718                        else :         
719                            action = "DENY"
720            else :       
721                if grouppquota.HardLimit is not None :
722                    # no soft limit, only a hard one.
723                    hardlimit = int(grouppquota.HardLimit)
724                    if val < hardlimit :
725                        action = "ALLOW"
726                    else :     
727                        action = "DENY"
728                else :
729                    # Both are unset, no quota, i.e. accounting only
730                    action = "ALLOW"
731        return action
732   
733    def checkUserPQuota(self, userpquota) :
734        """Checks the user quota on a printer and all its parents and deny or accept the job."""
735        user = userpquota.User
736        printer = userpquota.Printer
737       
738        # indicates that a warning needs to be sent
739        warned = 0               
740       
741        # first we check any group the user is a member of
742        for group in self.storage.getUserGroups(user) :
743            grouppquota = self.storage.getGroupPQuota(group, printer)
744            # for the printer and all its parents
745            for gpquota in [ grouppquota ] + grouppquota.ParentPrintersGroupPQuota :
746                if gpquota.Exists :
747                    action = self.checkGroupPQuota(gpquota)
748                    if action == "DENY" :
749                        return action
750                    elif action == "WARN" :   
751                        warned = 1
752                       
753        # Then we check the user's account balance
754        # if we get there we are sure that policy is not EXTERNAL
755        (policy, dummy) = self.config.getPrinterPolicy(printer.Name)
756        if user.LimitBy and (user.LimitBy.lower() == "balance") : 
757            self.logdebug("Checking account balance for user %s" % user.Name)
758            if user.AccountBalance is None :
759                if policy == "ALLOW" :
760                    action = "POLICY_ALLOW"
761                else :   
762                    action = "POLICY_DENY"
763                self.logger.log_message(_("Unable to find user %s's account balance, applying default policy (%s) for printer %s") % (user.Name, action, printer.Name))
764                return action       
765            else :   
766                val = float(user.AccountBalance or 0.0)
767                enforcement = self.config.getPrinterEnforcement(printer.Name)
768                if enforcement == "STRICT" : 
769                    val -= self.softwareJobPrice # use precomputed size.
770                if val <= 0.0 :
771                    action = "DENY"
772                elif val <= self.config.getPoorMan() :   
773                    action = "WARN"
774                else :
775                    action = "ALLOW"
776                if (enforcement == "STRICT") and (val == 0.0) :
777                    action = "WARN" # we can still print until account is 0
778                return action   
779        else :
780            # Then check the user quota on current printer and all its parents.               
781            policyallowed = 0
782            for upquota in [ userpquota ] + userpquota.ParentPrintersUserPQuota :               
783                action = self._checkUserPQuota(upquota)
784                if action in ("DENY", "POLICY_DENY") :
785                    return action
786                elif action == "WARN" :   
787                    warned = 1
788                elif action == "POLICY_ALLOW" :   
789                    policyallowed = 1
790            if warned :       
791                return "WARN"
792            elif policyallowed :   
793                return "POLICY_ALLOW" 
794            else :   
795                return "ALLOW"
796               
797    def externalMailTo(self, cmd, action, user, printer, message) :
798        """Warns the user with an external command."""
799        username = user.Name
800        printername = printer.Name
801        email = user.Email or user.Name
802        if "@" not in email :
803            email = "%s@%s" % (email, self.maildomain or self.smtpserver)
804        os.system(cmd % locals())
805   
806    def formatCommandLine(self, cmd, user, printer) :
807        """Executes an external command."""
808        username = user.Name
809        printername = printer.Name
810        return cmd % locals()
811       
812    def warnGroupPQuota(self, grouppquota) :
813        """Checks a group quota and send messages if quota is exceeded on current printer."""
814        group = grouppquota.Group
815        printer = grouppquota.Printer
816        admin = self.config.getAdmin(printer.Name)
817        adminmail = self.config.getAdminMail(printer.Name)
818        (mailto, arguments) = self.config.getMailTo(printer.Name)
819        action = self.checkGroupPQuota(grouppquota)
820        if action.startswith("POLICY_") :
821            action = action[7:]
822        if action == "DENY" :
823            adminmessage = _("Print Quota exceeded for group %s on printer %s") % (group.Name, printer.Name)
824            self.logger.log_message(adminmessage)
825            if mailto in [ "BOTH", "ADMIN" ] :
826                self.sendMessageToAdmin(adminmail, _("Print Quota"), adminmessage)
827            if mailto in [ "BOTH", "USER", "EXTERNAL" ] :
828                for user in self.storage.getGroupMembers(group) :
829                    if mailto != "EXTERNAL" :
830                        self.sendMessageToUser(admin, adminmail, user, _("Print Quota Exceeded"), self.config.getHardWarn(printer.Name))
831                    else :   
832                        self.externalMailTo(arguments, action, user, printer, message)
833        elif action == "WARN" :   
834            adminmessage = _("Print Quota low for group %s on printer %s") % (group.Name, printer.Name)
835            self.logger.log_message(adminmessage)
836            if mailto in [ "BOTH", "ADMIN" ] :
837                self.sendMessageToAdmin(adminmail, _("Print Quota"), adminmessage)
838            if group.LimitBy and (group.LimitBy.lower() == "balance") : 
839                message = self.config.getPoorWarn()
840            else :     
841                message = self.config.getSoftWarn(printer.Name)
842            if mailto in [ "BOTH", "USER", "EXTERNAL" ] :
843                for user in self.storage.getGroupMembers(group) :
844                    if mailto != "EXTERNAL" :
845                        self.sendMessageToUser(admin, adminmail, user, _("Print Quota Exceeded"), message)
846                    else :   
847                        self.externalMailTo(arguments, action, user, printer, message)
848        return action       
849       
850    def warnUserPQuota(self, userpquota) :
851        """Checks a user quota and send him a message if quota is exceeded on current printer."""
852        user = userpquota.User
853        printer = userpquota.Printer
854        admin = self.config.getAdmin(printer.Name)
855        adminmail = self.config.getAdminMail(printer.Name)
856        (mailto, arguments) = self.config.getMailTo(printer.Name)
857        action = self.checkUserPQuota(userpquota)
858        if action.startswith("POLICY_") :
859            action = action[7:]
860        if action == "DENY" :
861            adminmessage = _("Print Quota exceeded for user %s on printer %s") % (user.Name, printer.Name)
862            self.logger.log_message(adminmessage)
863            if mailto in [ "BOTH", "USER", "EXTERNAL" ] :
864                message = self.config.getHardWarn(printer.Name)
865                if mailto != "EXTERNAL" :
866                    self.sendMessageToUser(admin, adminmail, user, _("Print Quota Exceeded"), message)
867                else :   
868                    self.externalMailTo(arguments, action, user, printer, message)
869            if mailto in [ "BOTH", "ADMIN" ] :
870                self.sendMessageToAdmin(adminmail, _("Print Quota"), adminmessage)
871        elif action == "WARN" :   
872            adminmessage = _("Print Quota low for user %s on printer %s") % (user.Name, printer.Name)
873            self.logger.log_message(adminmessage)
874            if mailto in [ "BOTH", "USER", "EXTERNAL" ] :
875                if user.LimitBy and (user.LimitBy.lower() == "balance") : 
876                    message = self.config.getPoorWarn()
877                else :     
878                    message = self.config.getSoftWarn(printer.Name)
879                if mailto != "EXTERNAL" :   
880                    self.sendMessageToUser(admin, adminmail, user, _("Print Quota Low"), message)
881                else :   
882                    self.externalMailTo(arguments, action, user, printer, message)
883            if mailto in [ "BOTH", "ADMIN" ] :
884                self.sendMessageToAdmin(adminmail, _("Print Quota"), adminmessage)
885        return action       
886       
887class PyKotaFilterOrBackend(PyKotaTool) :   
888    """Class for the PyKota filter or backend."""
889    def __init__(self) :
890        """Initialize local datas from current environment."""
891        # We begin with ignoring signals, we may de-ignore them later on.
892        self.gotSigTerm = 0
893        signal.signal(signal.SIGTERM, signal.SIG_IGN)
894        # signal.signal(signal.SIGCHLD, signal.SIG_IGN)
895        signal.signal(signal.SIGPIPE, signal.SIG_IGN)
896       
897        PyKotaTool.__init__(self)
898        (self.printingsystem, \
899         self.printerhostname, \
900         self.printername, \
901         self.username, \
902         self.jobid, \
903         self.inputfile, \
904         self.copies, \
905         self.title, \
906         self.options, \
907         self.originalbackend) = self.extractInfoFromCupsOrLprng()
908        self.username = self.username or 'root' # when printing test page from CUPS web interface, username is empty
909        if self.config.getUserNameToLower() :
910            self.username = self.username.lower()
911        self.preserveinputfile = self.inputfile 
912        try :
913            self.accounter = accounter.openAccounter(self)
914        except (config.PyKotaConfigError, accounter.PyKotaAccounterError), msg :   
915            self.crashed(msg)
916            raise
917        self.exportJobInfo()
918        self.jobdatastream = self.openJobDataStream()
919        os.environ["PYKOTAJOBSIZEBYTES"] = str(self.jobSizeBytes)
920        self.logdebug("Job size is %s bytes" % self.jobSizeBytes)
921        self.logdebug("Capturing SIGTERM events.")
922        signal.signal(signal.SIGTERM, self.sigterm_handler)
923       
924    def openJobDataStream(self) :   
925        """Opens the file which contains the job's datas."""
926        if self.preserveinputfile is None :
927            # Job comes from sys.stdin, but this is not
928            # seekable and complexifies our task, so create
929            # a temporary file and use it instead
930            self.sendBackChannelData("Duplicating data stream from stdin to temporary file")
931            dummy = 0
932            MEGABYTE = 1024*1024
933            self.jobSizeBytes = 0
934            infile = tempfile.TemporaryFile()
935            while 1 :
936                data = sys.stdin.read(MEGABYTE) 
937                if not data :
938                    break
939                self.jobSizeBytes += len(data)   
940                if not (dummy % 10) :
941                    self.sendBackChannelData("%s bytes read..." % self.jobSizeBytes)
942                dummy += 1   
943                infile.write(data)
944            self.sendBackChannelData("%s bytes read total." % self.jobSizeBytes)
945            infile.flush()   
946            infile.seek(0)
947            return infile
948        else :   
949            # real file, just open it
950            self.sendBackChannelData("Opening data stream %s" % self.preserveinputfile)
951            self.jobSizeBytes = os.stat(self.preserveinputfile)[6]
952            return open(self.preserveinputfile, "rb")
953       
954    def closeJobDataStream(self) :   
955        """Closes the file which contains the job's datas."""
956        self.sendBackChannelData("Closing data stream.")
957        try :
958            self.jobdatastream.close()
959        except :   
960            pass
961       
962    def precomputeJobSize(self) :   
963        """Computes the job size with a software method."""
964        self.logdebug("Precomputing job's size with generic PDL analyzer...")
965        self.jobdatastream.seek(0)
966        try :
967            parser = pdlanalyzer.PDLAnalyzer(self.jobdatastream)
968            jobsize = parser.getJobSize()
969        except pdlanalyzer.PDLAnalyzerError, msg :   
970            # Here we just log the failure, but
971            # we finally ignore it and return 0 since this
972            # computation is just an indication of what the
973            # job's size MAY be.
974            self.logger.log_message(_("Unable to precompute the job's size with the generic PDL analyzer : %s") % msg, "warn")
975            return 0
976        else :   
977            if ((self.printingsystem == "CUPS") \
978                and (self.preserveinputfile is not None)) \
979                or (self.printingsystem != "CUPS") :
980                return jobsize * self.copies
981            else :       
982                return jobsize
983           
984    def sigterm_handler(self, signum, frame) :
985        """Sets an attribute whenever SIGTERM is received."""
986        self.gotSigTerm = 1
987        os.environ["PYKOTASTATUS"] = "CANCELLED"
988        self.logger.log_message(_("SIGTERM received, job %s cancelled.") % self.jobid, "info")
989       
990    def exportJobInfo(self) :   
991        """Exports job information to the environment."""
992        os.environ["PYKOTAUSERNAME"] = str(self.username)
993        os.environ["PYKOTAPRINTERNAME"] = str(self.printername)
994        os.environ["PYKOTAJOBID"] = str(self.jobid)
995        os.environ["PYKOTATITLE"] = self.title or ""
996        os.environ["PYKOTAFILENAME"] = self.preserveinputfile or ""
997        os.environ["PYKOTACOPIES"] = str(self.copies)
998        os.environ["PYKOTAOPTIONS"] = self.options or ""
999   
1000    def exportUserInfo(self, userpquota) :
1001        """Exports user information to the environment."""
1002        os.environ["PYKOTALIMITBY"] = str(userpquota.User.LimitBy)
1003        os.environ["PYKOTABALANCE"] = str(userpquota.User.AccountBalance or 0.0)
1004        os.environ["PYKOTALIFETIMEPAID"] = str(userpquota.User.LifeTimePaid or 0.0)
1005        os.environ["PYKOTAPAGECOUNTER"] = str(userpquota.PageCounter or 0)
1006        os.environ["PYKOTALIFEPAGECOUNTER"] = str(userpquota.LifePageCounter or 0)
1007        os.environ["PYKOTASOFTLIMIT"] = str(userpquota.SoftLimit)
1008        os.environ["PYKOTAHARDLIMIT"] = str(userpquota.HardLimit)
1009        os.environ["PYKOTADATELIMIT"] = str(userpquota.DateLimit)
1010       
1011        # not really an user information, but anyway
1012        # exports the list of printers groups the current
1013        # printer is a member of
1014        os.environ["PYKOTAPGROUPS"] = ",".join([p.Name for p in self.storage.getParentPrinters(userpquota.Printer)])
1015       
1016    def prehook(self, userpquota) :
1017        """Allows plugging of an external hook before the job gets printed."""
1018        prehook = self.config.getPreHook(userpquota.Printer.Name)
1019        if prehook :
1020            self.logdebug("Executing pre-hook [%s]" % prehook)
1021            os.system(prehook)
1022       
1023    def posthook(self, userpquota) :
1024        """Allows plugging of an external hook after the job gets printed and/or denied."""
1025        posthook = self.config.getPostHook(userpquota.Printer.Name)
1026        if posthook :
1027            self.logdebug("Executing post-hook [%s]" % posthook)
1028            os.system(posthook)
1029       
1030    def extractInfoFromCupsOrLprng(self) :   
1031        """Returns a tuple (printingsystem, printerhostname, printername, username, jobid, filename, title, options, backend).
1032       
1033           Returns (None, None, None, None, None, None, None, None, None, None) if no printing system is recognized.
1034        """
1035        # Try to detect CUPS
1036        if os.environ.has_key("CUPS_SERVERROOT") and os.path.isdir(os.environ.get("CUPS_SERVERROOT", "")) :
1037            if len(sys.argv) == 7 :
1038                inputfile = sys.argv[6]
1039            else :   
1040                inputfile = None
1041               
1042            # check that the DEVICE_URI environment variable's value is
1043            # prefixed with "cupspykota:" otherwise don't touch it.
1044            # If this is the case, we have to remove the prefix from
1045            # the environment before launching the real backend in cupspykota
1046            device_uri = os.environ.get("DEVICE_URI", "")
1047            if device_uri.startswith("cupspykota:") :
1048                fulldevice_uri = device_uri[:]
1049                device_uri = fulldevice_uri[len("cupspykota:"):]
1050                if device_uri.startswith("//") :    # lpd (at least)
1051                    device_uri = device_uri[2:]
1052                os.environ["DEVICE_URI"] = device_uri   # TODO : side effect !
1053            # TODO : check this for more complex urls than ipp://myprinter.dot.com:631/printers/lp
1054            try :
1055                (backend, destination) = device_uri.split(":", 1) 
1056            except ValueError :   
1057                raise PyKotaToolError, "Invalid DEVICE_URI : %s\n" % device_uri
1058            while destination.startswith("/") :
1059                destination = destination[1:]
1060            checkauth = destination.split("@", 1)   
1061            if len(checkauth) == 2 :
1062                destination = checkauth[1]
1063            printerhostname = destination.split("/")[0].split(":")[0]
1064            return ("CUPS", \
1065                    printerhostname, \
1066                    os.environ.get("PRINTER"), \
1067                    sys.argv[2].strip(), \
1068                    sys.argv[1].strip(), \
1069                    inputfile, \
1070                    int(sys.argv[4].strip()), \
1071                    sys.argv[3], \
1072                    sys.argv[5], \
1073                    backend)
1074        else :   
1075            # Try to detect LPRng
1076            # TODO : try to extract filename, job's title, and options if available
1077            jseen = Pseen = nseen = rseen = Kseen = None
1078            for arg in sys.argv :
1079                if arg.startswith("-j") :
1080                    jseen = arg[2:].strip()
1081                elif arg.startswith("-n") :     
1082                    nseen = arg[2:].strip()
1083                elif arg.startswith("-P") :   
1084                    Pseen = arg[2:].strip()
1085                elif arg.startswith("-r") :   
1086                    rseen = arg[2:].strip()
1087                elif arg.startswith("-K") or arg.startswith("-#") :   
1088                    Kseen = int(arg[2:].strip())
1089            if Kseen is None :       
1090                Kseen = 1       # we assume the user wants at least one copy...
1091            if (rseen is None) and jseen and Pseen and nseen :   
1092                self.logger.log_message(_("Printer hostname undefined, set to 'localhost'"), "warn")
1093                rseen = "localhost"
1094            if jseen and Pseen and nseen and rseen :       
1095                # job is always in stdin (None)
1096                return ("LPRNG", rseen, Pseen, nseen, jseen, None, Kseen, None, None, None)
1097        self.logger.log_message(_("Printing system unknown, args=%s") % " ".join(sys.argv), "warn")
1098        return (None, None, None, None, None, None, None, None, None, None)   # Unknown printing system
1099       
1100    def getPrinterUserAndUserPQuota(self) :       
1101        """Returns a tuple (policy, printer, user, and user print quota) on this printer.
1102       
1103           "OK" is returned in the policy if both printer, user and user print quota
1104           exist in the Quota Storage.
1105           Otherwise, the policy as defined for this printer in pykota.conf is returned.
1106           
1107           If policy was set to "EXTERNAL" and one of printer, user, or user print quota
1108           doesn't exist in the Quota Storage, then an external command is launched, as
1109           defined in the external policy for this printer in pykota.conf
1110           This external command can do anything, like automatically adding printers
1111           or users, for example, and finally extracting printer, user and user print
1112           quota from the Quota Storage is tried a second time.
1113           
1114           "EXTERNALERROR" is returned in case policy was "EXTERNAL" and an error status
1115           was returned by the external command.
1116        """
1117        for passnumber in range(1, 3) :
1118            printer = self.storage.getPrinter(self.printername)
1119            user = self.storage.getUser(self.username)
1120            userpquota = self.storage.getUserPQuota(user, printer)
1121            if printer.Exists and user.Exists and userpquota.Exists :
1122                policy = "OK"
1123                break
1124            (policy, args) = self.config.getPrinterPolicy(self.printername)
1125            if policy == "EXTERNAL" :   
1126                commandline = self.formatCommandLine(args, user, printer)
1127                if not printer.Exists :
1128                    self.logger.log_message(_("Printer %s not registered in the PyKota system, applying external policy (%s) for printer %s") % (self.printername, commandline, self.printername), "info")
1129                if not user.Exists :
1130                    self.logger.log_message(_("User %s not registered in the PyKota system, applying external policy (%s) for printer %s") % (self.username, commandline, self.printername), "info")
1131                if not userpquota.Exists :
1132                    self.logger.log_message(_("User %s doesn't have quota on printer %s in the PyKota system, applying external policy (%s) for printer %s") % (self.username, self.printername, commandline, self.printername), "info")
1133                if os.system(commandline) :
1134                    self.logger.log_message(_("External policy %s for printer %s produced an error. Job rejected. Please check PyKota's configuration files.") % (commandline, self.printername), "error")
1135                    policy = "EXTERNALERROR"
1136                    break
1137            else :       
1138                if not printer.Exists :
1139                    self.logger.log_message(_("Printer %s not registered in the PyKota system, applying default policy (%s)") % (self.printername, policy), "info")
1140                if not user.Exists :
1141                    self.logger.log_message(_("User %s not registered in the PyKota system, applying default policy (%s) for printer %s") % (self.username, policy, self.printername), "info")
1142                if not userpquota.Exists :
1143                    self.logger.log_message(_("User %s doesn't have quota on printer %s in the PyKota system, applying default policy (%s)") % (self.username, self.printername, policy), "info")
1144                break
1145        if policy == "EXTERNAL" :   
1146            if not printer.Exists :
1147                self.logger.log_message(_("Printer %s still not registered in the PyKota system, job will be rejected") % self.printername, "info")
1148            if not user.Exists :
1149                self.logger.log_message(_("User %s still not registered in the PyKota system, job will be rejected on printer %s") % (self.username, self.printername), "info")
1150            if not userpquota.Exists :
1151                self.logger.log_message(_("User %s still doesn't have quota on printer %s in the PyKota system, job will be rejected") % (self.username, self.printername), "info")
1152        return (policy, printer, user, userpquota)
1153       
1154    def mainWork(self) :   
1155        """Main work is done here."""
1156        (policy, printer, user, userpquota) = self.getPrinterUserAndUserPQuota()
1157        # TODO : check for last user's quota in case pykota filter is used with querying
1158        if policy == "EXTERNALERROR" :
1159            # Policy was 'EXTERNAL' and the external command returned an error code
1160            return self.removeJob()
1161        elif policy == "EXTERNAL" :
1162            # Policy was 'EXTERNAL' and the external command wasn't able
1163            # to add either the printer, user or user print quota
1164            return self.removeJob()
1165        elif policy == "DENY" :   
1166            # Either printer, user or user print quota doesn't exist,
1167            # and the job should be rejected.
1168            return self.removeJob()
1169        else :
1170            if policy not in ("OK", "ALLOW") :
1171                self.logger.log_message(_("Invalid policy %s for printer %s") % (policy, self.printername))
1172                return self.removeJob()
1173            else :
1174                return self.doWork(policy, printer, user, userpquota)
Note: See TracBrowser for help on using the browser.