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

Revision 1426, 39.7 kB (checked in by jalet, 20 years ago)

uninitialized values caused problems

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