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

Revision 852, 12.8 kB (checked in by jalet, 21 years ago)

New mailto option in configuration file added.
No time to test this tonight (although it should work).

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