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

Revision 927, 20.5 kB (checked in by jalet, 21 years ago)

Groups quota work now !

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