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

Revision 1246, 33.7 kB (checked in by jalet, 20 years ago)

I'm completely stupid !!! Better to not talk while coding !

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