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

Revision 1240, 34.8 kB (checked in by uid67467, 20 years ago)

Should be ok now.

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