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

Revision 1483, 42.4 kB (checked in by jalet, 20 years ago)

Big code changes to completely remove the need for "requester" directives,
jsut use "hardware(... your previous requester directive's content ...)"

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