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

Revision 1041, 20.6 kB (checked in by jalet, 21 years ago)

Hey, it may work (edpykota --reset excepted) !

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