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

Revision 1365, 37.3 kB (checked in by jalet, 20 years ago)

Should fix the remaining bugs wrt printers groups and users groups.

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