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

Revision 1394, 39.5 kB (checked in by jalet, 20 years ago)

Allow names to begin with a digit

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