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

Revision 929, 20.7 kB (checked in by jalet, 21 years ago)

repykota now reports account balances too.

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