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

Revision 1433, 40.8 kB (checked in by jalet, 20 years ago)

Began work on correct handling of child processes when jobs are cancelled by
the user. Especially important when an external requester is running for a
long time.

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