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

Revision 1694, 52.0 kB (checked in by jalet, 20 years ago)

Now exports PYKOTAPRINTERHOSTNAME

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