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

Revision 1756, 53.7 kB (checked in by jalet, 20 years ago)

Now computes the job's datas MD5 checksum to later forbid duplicate print jobs.
The checksum is not yet saved into the database.

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