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

Revision 1068, 20.9 kB (checked in by jalet, 21 years ago)

Lots of small fixes with the help of PyChecker?

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
Line 
1#! /usr/bin/env python
2
3# PyKota - Print Quotas for CUPS and LPRng
4#
5# (c) 2003 Jerome Alet <alet@librelogiciel.com>
6# This program is free software; you can redistribute it and/or modify
7# it under the terms of the GNU General Public License as published by
8# the Free Software Foundation; either version 2 of the License, or
9# (at your option) any later version.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with this program; if not, write to the Free Software
18# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
19#
20# $Id$
21#
22# $Log$
23# Revision 1.44  2003/07/07 11:49:24  jalet
24# Lots of small fixes with the help of PyChecker
25#
26# Revision 1.43  2003/07/04 09:06:32  jalet
27# Small bug fix wrt undefined "LimitBy" field.
28#
29# Revision 1.42  2003/06/30 12:46:15  jalet
30# Extracted reporting code.
31#
32# Revision 1.41  2003/06/25 14:10:01  jalet
33# Hey, it may work (edpykota --reset excepted) !
34#
35# Revision 1.40  2003/06/10 16:37:54  jalet
36# Deletion of the second user which is not needed anymore.
37# Added a debug configuration field in /etc/pykota.conf
38# All queries can now be sent to the logger in debug mode, this will
39# greatly help improve performance when time for this will come.
40#
41# Revision 1.39  2003/04/29 18:37:54  jalet
42# Pluggable accounting methods (actually doesn't support external scripts)
43#
44# Revision 1.38  2003/04/24 11:53:48  jalet
45# Default policy for unknown users/groups is to DENY printing instead
46# of the previous default to ALLOW printing. This is to solve an accuracy
47# problem. If you set the policy to ALLOW, jobs printed by in nexistant user
48# (from PyKota's POV) will be charged to the next user who prints on the
49# same printer.
50#
51# Revision 1.37  2003/04/24 08:08:27  jalet
52# Debug message forgotten
53#
54# Revision 1.36  2003/04/24 07:59:40  jalet
55# LPRng support now works !
56#
57# Revision 1.35  2003/04/23 22:13:57  jalet
58# Preliminary support for LPRng added BUT STILL UNTESTED.
59#
60# Revision 1.34  2003/04/17 09:26:21  jalet
61# repykota now reports account balances too.
62#
63# Revision 1.33  2003/04/16 12:35:49  jalet
64# Groups quota work now !
65#
66# Revision 1.32  2003/04/16 08:53:14  jalet
67# Printing can now be limited either by user's account balance or by
68# page quota (the default). Quota report doesn't include account balance
69# yet, though.
70#
71# Revision 1.31  2003/04/15 11:30:57  jalet
72# More work done on money print charging.
73# Minor bugs corrected.
74# All tools now access to the storage as priviledged users, repykota excepted.
75#
76# Revision 1.30  2003/04/10 21:47:20  jalet
77# Job history added. Upgrade script neutralized for now !
78#
79# Revision 1.29  2003/03/29 13:45:27  jalet
80# GPL paragraphs were incorrectly (from memory) copied into the sources.
81# Two README files were added.
82# Upgrade script for PostgreSQL pre 1.01 schema was added.
83#
84# Revision 1.28  2003/03/29 13:08:28  jalet
85# Configuration is now expected to be found in /etc/pykota.conf instead of
86# in /etc/cups/pykota.conf
87# Installation script can move old config files to the new location if needed.
88# Better error handling if configuration file is absent.
89#
90# Revision 1.27  2003/03/15 23:01:28  jalet
91# New mailto option in configuration file added.
92# No time to test this tonight (although it should work).
93#
94# Revision 1.26  2003/03/09 23:58:16  jalet
95# Comment
96#
97# Revision 1.25  2003/03/07 22:56:14  jalet
98# 0.99 is out with some bug fixes.
99#
100# Revision 1.24  2003/02/27 23:48:41  jalet
101# Correctly maps PyKota's log levels to syslog log levels
102#
103# Revision 1.23  2003/02/27 22:55:20  jalet
104# WARN log priority doesn't exist.
105#
106# Revision 1.22  2003/02/27 09:09:20  jalet
107# Added a method to match strings against wildcard patterns
108#
109# Revision 1.21  2003/02/17 23:01:56  jalet
110# Typos
111#
112# Revision 1.20  2003/02/17 22:55:01  jalet
113# More options can now be set per printer or globally :
114#
115#       admin
116#       adminmail
117#       gracedelay
118#       requester
119#
120# the printer option has priority when both are defined.
121#
122# Revision 1.19  2003/02/10 11:28:45  jalet
123# Localization
124#
125# Revision 1.18  2003/02/10 01:02:17  jalet
126# External requester is about to work, but I must sleep
127#
128# Revision 1.17  2003/02/09 13:05:43  jalet
129# Internationalization continues...
130#
131# Revision 1.16  2003/02/09 12:56:53  jalet
132# Internationalization begins...
133#
134# Revision 1.15  2003/02/08 22:09:52  jalet
135# Name check method moved here
136#
137# Revision 1.14  2003/02/07 10:42:45  jalet
138# Indentation problem
139#
140# Revision 1.13  2003/02/07 08:34:16  jalet
141# Test wrt date limit was wrong
142#
143# Revision 1.12  2003/02/06 23:20:02  jalet
144# warnpykota doesn't need any user/group name argument, mimicing the
145# warnquota disk quota tool.
146#
147# Revision 1.11  2003/02/06 22:54:33  jalet
148# warnpykota should be ok
149#
150# Revision 1.10  2003/02/06 15:03:11  jalet
151# added a method to set the limit date
152#
153# Revision 1.9  2003/02/06 10:39:23  jalet
154# Preliminary edpykota work.
155#
156# Revision 1.8  2003/02/06 09:19:02  jalet
157# More robust behavior (hopefully) when the user or printer is not managed
158# correctly by the Quota System : e.g. cupsFilter added in ppd file, but
159# printer and/or user not 'yet?' in storage.
160#
161# Revision 1.7  2003/02/06 00:00:45  jalet
162# Now includes the printer name in email messages
163#
164# Revision 1.6  2003/02/05 23:55:02  jalet
165# Cleaner email messages
166#
167# Revision 1.5  2003/02/05 23:45:09  jalet
168# Better DateTime manipulation wrt grace delay
169#
170# Revision 1.4  2003/02/05 23:26:22  jalet
171# Incorrect handling of grace delay
172#
173# Revision 1.3  2003/02/05 22:16:20  jalet
174# DEVICE_URI is undefined outside of CUPS, i.e. for normal command line tools
175#
176# Revision 1.2  2003/02/05 22:10:29  jalet
177# Typos
178#
179# Revision 1.1  2003/02/05 21:28:17  jalet
180# Initial import into CVS
181#
182#
183#
184
185import sys
186import fnmatch
187import getopt
188import smtplib
189import gettext
190import locale
191
192from mx import DateTime
193
194from pykota import version, config, storage, logger
195
196class PyKotaToolError(Exception):
197    """An exception for PyKota config related stuff."""
198    def __init__(self, message = ""):
199        self.message = message
200        Exception.__init__(self, message)
201    def __repr__(self):
202        return self.message
203    __str__ = __repr__
204   
205class PyKotaTool :   
206    """Base class for all PyKota command line tools."""
207    def __init__(self, doc="PyKota %s (c) 2003 %s" % (version.__version__, version.__author__)) :
208        """Initializes the command line tool."""
209        # locale stuff
210        try :
211            locale.setlocale(locale.LC_ALL, "")
212            gettext.install("pykota")
213        except (locale.Error, IOError) :
214            gettext.NullTranslations().install()
215   
216        # pykota specific stuff
217        self.documentation = doc
218        self.config = config.PyKotaConfig("/etc")
219        self.logger = logger.openLogger(self)
220        self.storage = storage.openConnection(self)
221        self.smtpserver = self.config.getSMTPServer()
222       
223    def display_version_and_quit(self) :
224        """Displays version number, then exists successfully."""
225        print version.__version__
226        sys.exit(0)
227   
228    def display_usage_and_quit(self) :
229        """Displays command line usage, then exists successfully."""
230        print self.documentation
231        sys.exit(0)
232       
233    def parseCommandline(self, argv, short, long, allownothing=0) :
234        """Parses the command line, controlling options."""
235        # split options in two lists: those which need an argument, those which don't need any
236        withoutarg = []
237        witharg = []
238        lgs = len(short)
239        i = 0
240        while i < lgs :
241            ii = i + 1
242            if (ii < lgs) and (short[ii] == ':') :
243                # needs an argument
244                witharg.append(short[i])
245                ii = ii + 1 # skip the ':'
246            else :
247                # doesn't need an argument
248                withoutarg.append(short[i])
249            i = ii
250               
251        for option in long :
252            if option[-1] == '=' :
253                # needs an argument
254                witharg.append(option[:-1])
255            else :
256                # doesn't need an argument
257                withoutarg.append(option)
258       
259        # we begin with all possible options unset
260        parsed = {}
261        for option in withoutarg + witharg :
262            parsed[option] = None
263       
264        # then we parse the command line
265        args = []       # to not break if something unexpected happened
266        try :
267            options, args = getopt.getopt(argv, short, long)
268            if options :
269                for (o, v) in options :
270                    # we skip the '-' chars
271                    lgo = len(o)
272                    i = 0
273                    while (i < lgo) and (o[i] == '-') :
274                        i = i + 1
275                    o = o[i:]
276                    if o in witharg :
277                        # needs an argument : set it
278                        parsed[o] = v
279                    elif o in withoutarg :
280                        # doesn't need an argument : boolean
281                        parsed[o] = 1
282                    else :
283                        # should never occur
284                        raise PyKotaToolError, "Unexpected problem when parsing command line"
285            elif (not args) and (not allownothing) and sys.stdin.isatty() : # no option and no argument, we display help if we are a tty
286                self.display_usage_and_quit()
287        except getopt.error, msg :
288            sys.stderr.write("%s\n" % msg)
289            sys.stderr.flush()
290            self.display_usage_and_quit()
291        return (parsed, args)
292   
293    def isValidName(self, name) :
294        """Checks if a user or printer name is valid."""
295        # unfortunately Python 2.1 string modules doesn't define ascii_letters...
296        asciiletters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
297        digits = '0123456789'
298        if name[0] in asciiletters :
299            validchars = asciiletters + digits + "-_"
300            for c in name[1:] :
301                if c not in validchars :
302                    return 0
303            return 1       
304        return 0
305       
306    def matchString(self, s, patterns) :
307        """Returns 1 if the string s matches one of the patterns, else 0."""
308        for pattern in patterns :
309            if fnmatch.fnmatchcase(s, pattern) :
310                return 1
311        return 0
312       
313    def sendMessage(self, adminmail, touser, fullmessage) :
314        """Sends an email message containing headers to some user."""
315        if "@" not in touser :
316            touser = "%s@%s" % (touser, self.smtpserver)
317        server = smtplib.SMTP(self.smtpserver)
318        try :
319            server.sendmail(adminmail, [touser], fullmessage)
320        except smtplib.SMTPRecipientsRefused, answer :   
321            for (k, v) in answer.recipients.items() :
322                self.logger.log_message(_("Impossible to send mail to %s, error %s : %s") % (k, v[0], v[1]), "error")
323        server.quit()
324       
325    def sendMessageToUser(self, admin, adminmail, username, subject, message) :
326        """Sends an email message to a user."""
327        message += _("\n\nPlease contact your system administrator :\n\n\t%s - <%s>\n") % (admin, adminmail)
328        self.sendMessage(adminmail, username, "Subject: %s\n\n%s" % (subject, message))
329       
330    def sendMessageToAdmin(self, adminmail, subject, message) :
331        """Sends an email message to the Print Quota administrator."""
332        self.sendMessage(adminmail, adminmail, "Subject: %s\n\n%s" % (subject, message))
333       
334    def checkGroupPQuota(self, grouppquota) :   
335        """Checks the group quota on a printer and deny or accept the job."""
336        group = grouppquota.Group
337        printer = grouppquota.Printer
338        if group.LimitBy and (group.LimitBy.lower() == "balance") : 
339            # TODO : there's no warning (no account balance soft limit)
340            if float(group.AccountBalance) <= 0.0 :
341                action = "DENY"
342            else :   
343                action = "ALLOW"
344        else :
345            if grouppquota.SoftLimit is not None :
346                softlimit = int(grouppquota.SoftLimit)
347                if grouppquota.PageCounter < softlimit :
348                    action = "ALLOW"
349                else :   
350                    if grouppquota.HardLimit is None :
351                        # only a soft limit, this is equivalent to having only a hard limit
352                        action = "DENY"
353                    else :   
354                        hardlimit = int(grouppquota.HardLimit)
355                        if softlimit <= grouppquota.PageCounter < hardlimit :   
356                            now = DateTime.now()
357                            if grouppquota.DateLimit is not None :
358                                datelimit = DateTime.ISO.ParseDateTime(grouppquota.DateLimit)
359                            else :
360                                datelimit = now + self.config.getGraceDelay(printer.Name)
361                                grouppquota.setDateLimit(datelimit)
362                            if now < datelimit :
363                                action = "WARN"
364                            else :   
365                                action = "DENY"
366                        else :         
367                            action = "DENY"
368            else :       
369                if grouppquota.HardLimit is not None :
370                    # no soft limit, only a hard one.
371                    hardlimit = int(grouppquota.HardLimit)
372                    if grouppquota.PageCounter < hardlimit :
373                        action = "ALLOW"
374                    else :     
375                        action = "DENY"
376                else :
377                    # Both are unset, no quota, i.e. accounting only
378                    action = "ALLOW"
379        return action
380   
381    def checkUserPQuota(self, userpquota) :
382        """Checks the user quota on a printer and deny or accept the job."""
383        user = userpquota.User
384        printer = userpquota.Printer
385       
386        # first we check any group the user is a member of
387        for group in self.storage.getUserGroups(user) :
388            grouppquota = self.storage.getGroupPQuota(group, printer)
389            if grouppquota.Exists :
390                action = self.checkGroupPQuota(grouppquota)
391                if action == "DENY" :
392                    return action
393               
394        # then we check the user's own quota
395        policy = self.config.getPrinterPolicy(printer.Name)
396        if user.LimitBy and (user.LimitBy.lower() == "balance") : 
397            if user.AccountBalance is None :
398                if policy == "ALLOW" :
399                    action = "POLICY_ALLOW"
400                else :   
401                    action = "POLICY_DENY"
402                self.logger.log_message(_("Unable to find user %s's account balance, applying default policy (%s) for printer %s") % (user.Name, action, printer.Name))
403            else :   
404                # TODO : there's no warning (no account balance soft limit)
405                if float(user.AccountBalance or 0.0) <= 0.0 :
406                    action = "DENY"
407                else :   
408                    action = "ALLOW"
409        else :
410            if not userpquota.Exists :
411                # Unknown userquota
412                if policy == "ALLOW" :
413                    action = "POLICY_ALLOW"
414                else :   
415                    action = "POLICY_DENY"
416                self.logger.log_message(_("Unable to match user %s on printer %s, applying default policy (%s)") % (user.Name, printer.Name, action))
417            else :   
418                pagecounter = int(userpquota.PageCounter or 0)
419                if userpquota.SoftLimit is not None :
420                    softlimit = int(userpquota.SoftLimit)
421                    if pagecounter < softlimit :
422                        action = "ALLOW"
423                    else :   
424                        if userpquota.HardLimit is None :
425                            # only a soft limit, this is equivalent to having only a hard limit
426                            action = "DENY"
427                        else :   
428                            hardlimit = int(userpquota.HardLimit)
429                            if softlimit <= pagecounter < hardlimit :   
430                                now = DateTime.now()
431                                if userpquota.DateLimit is not None :
432                                    datelimit = DateTime.ISO.ParseDateTime(userpquota.DateLimit)
433                                else :
434                                    datelimit = now + self.config.getGraceDelay(printer.Name)
435                                    userpquota.setDateLimit(datelimit)
436                                if now < datelimit :
437                                    action = "WARN"
438                                else :   
439                                    action = "DENY"
440                            else :         
441                                action = "DENY"
442                else :       
443                    if userpquota.HardLimit is not None :
444                        # no soft limit, only a hard one.
445                        hardlimit = int(userpquota.HardLimit)
446                        if pagecounter < hardlimit :
447                            action = "ALLOW"
448                        else :     
449                            action = "DENY"
450                    else :
451                        # Both are unset, no quota, i.e. accounting only
452                        action = "ALLOW"
453        return action
454   
455    def warnGroupPQuota(self, grouppquota) :
456        """Checks a group quota and send messages if quota is exceeded on current printer."""
457        group = grouppquota.Group
458        printer = grouppquota.Printer
459        admin = self.config.getAdmin(printer.Name)
460        adminmail = self.config.getAdminMail(printer.Name)
461        mailto = self.config.getMailTo(printer.Name)
462        action = self.checkGroupPQuota(grouppquota)
463        if action.startswith("POLICY_") :
464            action = action[7:]
465        if action == "DENY" :
466            adminmessage = _("Print Quota exceeded for group %s on printer %s") % (group.Name, printer.Name)
467            self.logger.log_message(adminmessage)
468            if mailto in [ "BOTH", "ADMIN" ] :
469                self.sendMessageToAdmin(adminmail, _("Print Quota"), adminmessage)
470            for user in self.storage.getGroupMembers(group) :
471                if mailto in [ "BOTH", "USER" ] :
472                    self.sendMessageToUser(admin, adminmail, user.Name, _("Print Quota Exceeded"), _("You are not allowed to print anymore because\nyour group Print Quota is exceeded on printer %s.") % printer.Name)
473        elif action == "WARN" :   
474            adminmessage = _("Print Quota soft limit exceeded for group %s on printer %s") % (group.Name, printer.Name)
475            self.logger.log_message(adminmessage)
476            if mailto in [ "BOTH", "ADMIN" ] :
477                self.sendMessageToAdmin(adminmail, _("Print Quota"), adminmessage)
478            for user in self.storage.getGroupMembers(group) :
479                if mailto in [ "BOTH", "USER" ] :
480                    self.sendMessageToUser(admin, adminmail, user.Name, _("Print Quota Exceeded"), _("You will soon be forbidden to print anymore because\nyour group Print Quota is almost reached on printer %s.") % printer.Name)
481        return action       
482       
483    def warnUserPQuota(self, userpquota) :
484        """Checks a user quota and send him a message if quota is exceeded on current printer."""
485        user = userpquota.User
486        printer = userpquota.Printer
487        admin = self.config.getAdmin(printer.Name)
488        adminmail = self.config.getAdminMail(printer.Name)
489        mailto = self.config.getMailTo(printer.Name)
490        action = self.checkUserPQuota(userpquota)
491        if action.startswith("POLICY_") :
492            action = action[7:]
493        if action == "DENY" :
494            adminmessage = _("Print Quota exceeded for user %s on printer %s") % (user.Name, printer.Name)
495            self.logger.log_message(adminmessage)
496            if mailto in [ "BOTH", "USER" ] :
497                self.sendMessageToUser(admin, adminmail, user.Name, _("Print Quota Exceeded"), _("You are not allowed to print anymore because\nyour Print Quota is exceeded on printer %s.") % printer.Name)
498            if mailto in [ "BOTH", "ADMIN" ] :
499                self.sendMessageToAdmin(adminmail, _("Print Quota"), adminmessage)
500        elif action == "WARN" :   
501            adminmessage = _("Print Quota soft limit exceeded for user %s on printer %s") % (user.Name, printer.Name)
502            self.logger.log_message(adminmessage)
503            if mailto in [ "BOTH", "USER" ] :
504                self.sendMessageToUser(admin, adminmail, user.Name, _("Print Quota Exceeded"), _("You will soon be forbidden to print anymore because\nyour Print Quota is almost reached on printer %s.") % printer.Name)
505            if mailto in [ "BOTH", "ADMIN" ] :
506                self.sendMessageToAdmin(adminmail, _("Print Quota"), adminmessage)
507        return action       
Note: See TracBrowser for help on using the browser.