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

Revision 1666, 51.9 kB (checked in by jalet, 20 years ago)

Finished group quota fix for balance when no user in group has a balance

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