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

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

Now catches connection problems to SMTP server

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