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

Revision 1512, 45.1 kB (checked in by jalet, 20 years ago)

Debug message added

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