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

Revision 1229, 33.3 kB (checked in by jalet, 20 years ago)

Some code refactoring work. New code is not used at this time.

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