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

Revision 873, 13.6 kB (checked in by jalet, 21 years ago)

GPL paragraphs were incorrectly (from memory) copied into the sources.
Two README files were added.
Upgrade script for PostgreSQL pre 1.01 schema was added.

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