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

Revision 834, 12.3 kB (checked in by jalet, 21 years ago)

0.99 is out with some bug fixes.

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