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

Revision 1290, 35.4 kB (checked in by jalet, 20 years ago)

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