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

Revision 1248, 33.9 kB (checked in by jalet, 20 years ago)

Dots in user, groups and printer names should be allowed.

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