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

Revision 1257, 34.0 kB (checked in by jalet, 20 years ago)

Copyright year changed.

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