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

Revision 872, 13.0 kB (checked in by jalet, 21 years ago)

Configuration is now expected to be found in /etc/pykota.conf instead of
in /etc/cups/pykota.conf
Installation script can move old config files to the new location if needed.
Better error handling if configuration file is absent.

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