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

Revision 900, 13.8 kB (checked in by jalet, 21 years ago)

Job history added. Upgrade script neutralized for now !

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