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

Revision 1421, 39.6 kB (checked in by jalet, 20 years ago)

PYKOTALIMITBY environment variable is now exported too

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