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

Revision 1271, 35.2 kB (checked in by jalet, 20 years ago)

Major code refactoring, it's way cleaner, and now allows automated addition
of printers on first print.

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