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

Revision 1048, 20.7 kB (checked in by jalet, 21 years ago)

Extracted reporting code.

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