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

Revision 1245, 33.6 kB (checked in by jalet, 20 years ago)

Forgot to remove some code

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