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

Revision 1021, 21.4 kB (checked in by jalet, 21 years ago)

Deletion of the second user which is not needed anymore.
Added a debug configuration field in /etc/pykota.conf
All queries can now be sent to the logger in debug mode, this will
greatly help improve performance when time for this will come.

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