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

Revision 1372, 39.3 kB (checked in by jalet, 20 years ago)

Pre and Post hooks to external commands are available in the cupspykota
backend. Forthe pykota filter they will be implemented real soon now.

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