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

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

Pluggable accounting methods (actually doesn't support external scripts)

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