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

Revision 915, 14.0 kB (checked in by jalet, 21 years ago)

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