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

Revision 1077, 21.1 kB (checked in by jalet, 21 years ago)

Configurable warning messages.
Poor man's treshold value added.

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