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

Revision 925, 15.2 kB (checked in by jalet, 21 years ago)

Printing can now be limited either by user's account balance or by
page quota (the default). Quota report doesn't include account balance
yet, though.

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