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

Revision 1443, 41.4 kB (checked in by jalet, 20 years ago)

Exports the PYKOTASTATUS environment variable when SIGTERM is received.

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