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

Revision 1374, 39.4 kB (checked in by jalet, 20 years ago)

PYKOTAPHASE wasn't set at the right time at the end of data transmission
to underlying layer (real backend)

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