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

Revision 1695, 52.5 kB (checked in by jalet, 20 years ago)

Small fix for old versions of LPRng

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