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

Revision 1353, 35.6 kB (checked in by jalet, 20 years ago)

maildomain pykota.conf directive added.
Small improvements on mail headers quality.

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