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
RevLine 
[695]1#! /usr/bin/env python
2
3# PyKota - Print Quotas for CUPS
4#
5# (c) 2003 Jerome Alet <alet@librelogiciel.com>
[873]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.
[695]10#
[873]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.
[695]19#
20# $Id$
21#
22# $Log$
[927]23# Revision 1.33  2003/04/16 12:35:49  jalet
24# Groups quota work now !
25#
[925]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#
[915]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#
[900]36# Revision 1.30  2003/04/10 21:47:20  jalet
37# Job history added. Upgrade script neutralized for now !
38#
[873]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#
[872]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#
[852]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#
[844]54# Revision 1.26  2003/03/09 23:58:16  jalet
55# Comment
56#
[834]57# Revision 1.25  2003/03/07 22:56:14  jalet
58# 0.99 is out with some bug fixes.
59#
[825]60# Revision 1.24  2003/02/27 23:48:41  jalet
61# Correctly maps PyKota's log levels to syslog log levels
62#
[824]63# Revision 1.23  2003/02/27 22:55:20  jalet
64# WARN log priority doesn't exist.
65#
[817]66# Revision 1.22  2003/02/27 09:09:20  jalet
67# Added a method to match strings against wildcard patterns
68#
[804]69# Revision 1.21  2003/02/17 23:01:56  jalet
70# Typos
71#
[802]72# Revision 1.20  2003/02/17 22:55:01  jalet
73# More options can now be set per printer or globally :
74#
[804]75#       admin
76#       adminmail
77#       gracedelay
78#       requester
[802]79#
80# the printer option has priority when both are defined.
81#
[788]82# Revision 1.19  2003/02/10 11:28:45  jalet
83# Localization
84#
[782]85# Revision 1.18  2003/02/10 01:02:17  jalet
86# External requester is about to work, but I must sleep
87#
[773]88# Revision 1.17  2003/02/09 13:05:43  jalet
89# Internationalization continues...
90#
[772]91# Revision 1.16  2003/02/09 12:56:53  jalet
92# Internationalization begins...
93#
[764]94# Revision 1.15  2003/02/08 22:09:52  jalet
95# Name check method moved here
96#
[742]97# Revision 1.14  2003/02/07 10:42:45  jalet
98# Indentation problem
99#
[733]100# Revision 1.13  2003/02/07 08:34:16  jalet
101# Test wrt date limit was wrong
102#
[729]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#
[728]107# Revision 1.11  2003/02/06 22:54:33  jalet
108# warnpykota should be ok
109#
[722]110# Revision 1.10  2003/02/06 15:03:11  jalet
111# added a method to set the limit date
112#
[715]113# Revision 1.9  2003/02/06 10:39:23  jalet
114# Preliminary edpykota work.
115#
[713]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#
[712]121# Revision 1.7  2003/02/06 00:00:45  jalet
122# Now includes the printer name in email messages
123#
[711]124# Revision 1.6  2003/02/05 23:55:02  jalet
125# Cleaner email messages
126#
[709]127# Revision 1.5  2003/02/05 23:45:09  jalet
128# Better DateTime manipulation wrt grace delay
129#
[708]130# Revision 1.4  2003/02/05 23:26:22  jalet
131# Incorrect handling of grace delay
132#
[699]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#
[698]136# Revision 1.2  2003/02/05 22:10:29  jalet
137# Typos
138#
[695]139# Revision 1.1  2003/02/05 21:28:17  jalet
140# Initial import into CVS
141#
142#
143#
144
145import sys
146import os
[817]147import fnmatch
[715]148import getopt
[695]149import smtplib
[772]150import gettext
[782]151import locale
[695]152
[708]153from mx import DateTime
154
[715]155from pykota import version, config, storage, logger
[695]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."""
[915]168    def __init__(self, asadmin=1, doc="PyKota %s (c) 2003 %s" % (version.__version__, version.__author__)) :
[695]169        """Initializes the command line tool."""
[772]170        # locale stuff
[788]171        try :
[782]172            locale.setlocale(locale.LC_ALL, "")
[788]173            gettext.install("pykota")
174        except (locale.Error, IOError) :
175            gettext.NullTranslations().install()
[772]176   
177        # pykota specific stuff
[715]178        self.documentation = doc
[872]179        self.config = config.PyKotaConfig("/etc")
[698]180        self.logger = logger.openLogger(self.config)
[915]181        self.storage = storage.openConnection(self.config, asadmin=asadmin)
[695]182        self.printername = os.environ.get("PRINTER", None)
183        self.smtpserver = self.config.getSMTPServer()
184       
[715]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       
[729]195    def parseCommandline(self, argv, short, long, allownothing=0) :
[715]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"
[729]247            elif (not args) and (not allownothing) and sys.stdin.isatty() : # no option and no argument, we display help if we are a tty
[715]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   
[764]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       
[817]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       
[802]275    def sendMessage(self, adminmail, touser, fullmessage) :
[695]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)
[802]280        server.sendmail(adminmail, [touser], fullmessage)
[695]281        server.quit()
282       
[802]283    def sendMessageToUser(self, admin, adminmail, username, subject, message) :
[695]284        """Sends an email message to a user."""
[802]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))
[695]287       
[802]288    def sendMessageToAdmin(self, adminmail, subject, message) :
[695]289        """Sends an email message to the Print Quota administrator."""
[802]290        self.sendMessage(adminmail, adminmail, "Subject: %s\n\n%s" % (subject, message))
[695]291       
[927]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   
[708]360    def checkUserPQuota(self, username, printername) :
361        """Checks the user quota on a printer and deny or accept the job."""
[927]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
[900]370        printerid = self.storage.getPrinterId(printername)
[925]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))
[713]381            else :   
[925]382                # TODO : there's no warning (no account balance soft limit)
383                if balance <= 0.0 :
384                    action = "DENY"
385                else :   
[713]386                    action = "ALLOW"
[925]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"
[834]394                else :   
[925]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"
[834]405                    else :   
[925]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 :         
[834]422                                action = "DENY"
[925]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 :     
[742]429                            action = "DENY"
[925]430                    else :
431                        # Both are unset, no quota, i.e. accounting only
[834]432                        action = "ALLOW"
433        return action
[708]434   
[927]435    def warnGroupPQuota(self, groupname, printername=None) :
436        """Checks a group quota and send messages if quota is exceeded on current printer."""
[728]437        pname = printername or self.printername
[927]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       
[728]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
[802]466        admin = self.config.getAdmin(pname)
467        adminmail = self.config.getAdminMail(pname)
[852]468        mailto = self.config.getMailTo(pname)
[834]469        action = self.checkUserPQuota(username, pname)
470        if action.startswith("POLICY_") :
471            action = action[7:]
[695]472        if action == "DENY" :
[834]473            adminmessage = _("Print Quota exceeded for user %s on printer %s") % (username, pname)
474            self.logger.log_message(adminmessage)
[852]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)
[695]479        elif action == "WARN" :   
[773]480            adminmessage = _("Print Quota soft limit exceeded for user %s on printer %s") % (username, pname)
[695]481            self.logger.log_message(adminmessage)
[852]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)
[695]486        return action       
487   
Note: See TracBrowser for help on using the browser.