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

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

Now catches all smtplib exceptions when there's a problem sending messages

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