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

Revision 1439, 42.0 kB (checked in by jalet, 20 years ago)

The list of printers groups the current printer is a member of is
now exported in the PYKOTAPGROUPS environment variable

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