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

Revision 1475, 41.7 kB (checked in by jalet, 20 years ago)

Code simplifications

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