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

Revision 1247, 33.8 kB (checked in by jalet, 20 years ago)

This time it should be better...

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