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

Revision 955, 22.8 kB (checked in by jalet, 21 years ago)

Debug message forgotten

  • 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 and LPRng
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.37  2003/04/24 08:08:27  jalet
24# Debug message forgotten
25#
26# Revision 1.36  2003/04/24 07:59:40  jalet
27# LPRng support now works !
28#
29# Revision 1.35  2003/04/23 22:13:57  jalet
30# Preliminary support for LPRng added BUT STILL UNTESTED.
31#
32# Revision 1.34  2003/04/17 09:26:21  jalet
33# repykota now reports account balances too.
34#
35# Revision 1.33  2003/04/16 12:35:49  jalet
36# Groups quota work now !
37#
38# Revision 1.32  2003/04/16 08:53:14  jalet
39# Printing can now be limited either by user's account balance or by
40# page quota (the default). Quota report doesn't include account balance
41# yet, though.
42#
43# Revision 1.31  2003/04/15 11:30:57  jalet
44# More work done on money print charging.
45# Minor bugs corrected.
46# All tools now access to the storage as priviledged users, repykota excepted.
47#
48# Revision 1.30  2003/04/10 21:47:20  jalet
49# Job history added. Upgrade script neutralized for now !
50#
51# Revision 1.29  2003/03/29 13:45:27  jalet
52# GPL paragraphs were incorrectly (from memory) copied into the sources.
53# Two README files were added.
54# Upgrade script for PostgreSQL pre 1.01 schema was added.
55#
56# Revision 1.28  2003/03/29 13:08:28  jalet
57# Configuration is now expected to be found in /etc/pykota.conf instead of
58# in /etc/cups/pykota.conf
59# Installation script can move old config files to the new location if needed.
60# Better error handling if configuration file is absent.
61#
62# Revision 1.27  2003/03/15 23:01:28  jalet
63# New mailto option in configuration file added.
64# No time to test this tonight (although it should work).
65#
66# Revision 1.26  2003/03/09 23:58:16  jalet
67# Comment
68#
69# Revision 1.25  2003/03/07 22:56:14  jalet
70# 0.99 is out with some bug fixes.
71#
72# Revision 1.24  2003/02/27 23:48:41  jalet
73# Correctly maps PyKota's log levels to syslog log levels
74#
75# Revision 1.23  2003/02/27 22:55:20  jalet
76# WARN log priority doesn't exist.
77#
78# Revision 1.22  2003/02/27 09:09:20  jalet
79# Added a method to match strings against wildcard patterns
80#
81# Revision 1.21  2003/02/17 23:01:56  jalet
82# Typos
83#
84# Revision 1.20  2003/02/17 22:55:01  jalet
85# More options can now be set per printer or globally :
86#
87#       admin
88#       adminmail
89#       gracedelay
90#       requester
91#
92# the printer option has priority when both are defined.
93#
94# Revision 1.19  2003/02/10 11:28:45  jalet
95# Localization
96#
97# Revision 1.18  2003/02/10 01:02:17  jalet
98# External requester is about to work, but I must sleep
99#
100# Revision 1.17  2003/02/09 13:05:43  jalet
101# Internationalization continues...
102#
103# Revision 1.16  2003/02/09 12:56:53  jalet
104# Internationalization begins...
105#
106# Revision 1.15  2003/02/08 22:09:52  jalet
107# Name check method moved here
108#
109# Revision 1.14  2003/02/07 10:42:45  jalet
110# Indentation problem
111#
112# Revision 1.13  2003/02/07 08:34:16  jalet
113# Test wrt date limit was wrong
114#
115# Revision 1.12  2003/02/06 23:20:02  jalet
116# warnpykota doesn't need any user/group name argument, mimicing the
117# warnquota disk quota tool.
118#
119# Revision 1.11  2003/02/06 22:54:33  jalet
120# warnpykota should be ok
121#
122# Revision 1.10  2003/02/06 15:03:11  jalet
123# added a method to set the limit date
124#
125# Revision 1.9  2003/02/06 10:39:23  jalet
126# Preliminary edpykota work.
127#
128# Revision 1.8  2003/02/06 09:19:02  jalet
129# More robust behavior (hopefully) when the user or printer is not managed
130# correctly by the Quota System : e.g. cupsFilter added in ppd file, but
131# printer and/or user not 'yet?' in storage.
132#
133# Revision 1.7  2003/02/06 00:00:45  jalet
134# Now includes the printer name in email messages
135#
136# Revision 1.6  2003/02/05 23:55:02  jalet
137# Cleaner email messages
138#
139# Revision 1.5  2003/02/05 23:45:09  jalet
140# Better DateTime manipulation wrt grace delay
141#
142# Revision 1.4  2003/02/05 23:26:22  jalet
143# Incorrect handling of grace delay
144#
145# Revision 1.3  2003/02/05 22:16:20  jalet
146# DEVICE_URI is undefined outside of CUPS, i.e. for normal command line tools
147#
148# Revision 1.2  2003/02/05 22:10:29  jalet
149# Typos
150#
151# Revision 1.1  2003/02/05 21:28:17  jalet
152# Initial import into CVS
153#
154#
155#
156
157import sys
158import os
159import fnmatch
160import getopt
161import smtplib
162import gettext
163import locale
164
165from mx import DateTime
166
167from pykota import version, config, storage, logger
168
169class PyKotaToolError(Exception):
170    """An exception for PyKota config related stuff."""
171    def __init__(self, message = ""):
172        self.message = message
173        Exception.__init__(self, message)
174    def __repr__(self):
175        return self.message
176    __str__ = __repr__
177   
178class PyKotaTool :   
179    """Base class for all PyKota command line tools."""
180    def __init__(self, asadmin=1, doc="PyKota %s (c) 2003 %s" % (version.__version__, version.__author__)) :
181        """Initializes the command line tool."""
182        # locale stuff
183        try :
184            locale.setlocale(locale.LC_ALL, "")
185            gettext.install("pykota")
186        except (locale.Error, IOError) :
187            gettext.NullTranslations().install()
188   
189        # pykota specific stuff
190        self.documentation = doc
191        self.config = config.PyKotaConfig("/etc")
192        self.logger = logger.openLogger(self.config)
193        self.storage = storage.openConnection(self.config, asadmin=asadmin)
194        self.smtpserver = self.config.getSMTPServer()
195       
196    def extractInfoFromCupsOrLprng(self) :   
197        """Returns a tuple (printingsystem, printerhostname, printername, username, jobid, filename) depending on the printing system in use (as seen by the print filter).
198       
199           Returns (None, None, None, None, None, None) if no printing system is recognized.
200        """
201        # Try to detect CUPS
202        if os.environ.has_key("CUPS_SERVERROOT") and os.path.isdir(os.environ.get("CUPS_SERVERROOT", "")) :
203            if len(sys.argv) == 7 :
204                inputfile = sys.argv[6]
205            else :   
206                inputfile = None
207               
208            device_uri = os.environ.get("DEVICE_URI", "")
209            # TODO : check this for more complex urls than ipp://myprinter.dot.com:631/printers/lp
210            try :
211                (backend, destination) = device_uri.split(":", 1) 
212            except ValueError :   
213                raise PyKotaToolError, "Invalid DEVICE_URI : %s\n" % device_uri
214            while destination.startswith("/") :
215                destination = destination[1:]
216            printerhostname = destination.split("/")[0].split(":")[0]
217            return ("CUPS", printerhostname, os.environ.get("PRINTER"), sys.argv[2].strip(), sys.argv[1].strip(), inputfile)
218        else :   
219            # Try to detect LPRng
220            jseen = Jseen = Pseen = nseen = rseen = None
221            for arg in sys.argv :
222                if arg.startswith("-j") :
223                    jseen = arg[2:].strip()
224                elif arg.startswith("-J") :   
225                    Jseen = arg[2:].strip()
226                    if Jseen == "(STDIN)" :
227                        Jseen = None
228                elif arg.startswith("-n") :     
229                    nseen = arg[2:].strip()
230                elif arg.startswith("-P") :   
231                    Pseen = arg[2:].strip()
232                elif arg.startswith("-r") :   
233                    rseen = arg[2:].strip()
234            if jseen and Pseen and nseen and rseen :       
235                return ("LPRNG", rseen, Pseen, nseen, jseen, Jseen)
236        return (None, None, None, None, None, None)   # Unknown printing system         
237       
238    def display_version_and_quit(self) :
239        """Displays version number, then exists successfully."""
240        print version.__version__
241        sys.exit(0)
242   
243    def display_usage_and_quit(self) :
244        """Displays command line usage, then exists successfully."""
245        print self.documentation
246        sys.exit(0)
247       
248    def parseCommandline(self, argv, short, long, allownothing=0) :
249        """Parses the command line, controlling options."""
250        # split options in two lists: those which need an argument, those which don't need any
251        withoutarg = []
252        witharg = []
253        lgs = len(short)
254        i = 0
255        while i < lgs :
256            ii = i + 1
257            if (ii < lgs) and (short[ii] == ':') :
258                # needs an argument
259                witharg.append(short[i])
260                ii = ii + 1 # skip the ':'
261            else :
262                # doesn't need an argument
263                withoutarg.append(short[i])
264            i = ii
265               
266        for option in long :
267            if option[-1] == '=' :
268                # needs an argument
269                witharg.append(option[:-1])
270            else :
271                # doesn't need an argument
272                withoutarg.append(option)
273       
274        # we begin with all possible options unset
275        parsed = {}
276        for option in withoutarg + witharg :
277            parsed[option] = None
278       
279        # then we parse the command line
280        args = []       # to not break if something unexpected happened
281        try :
282            options, args = getopt.getopt(argv, short, long)
283            if options :
284                for (o, v) in options :
285                    # we skip the '-' chars
286                    lgo = len(o)
287                    i = 0
288                    while (i < lgo) and (o[i] == '-') :
289                        i = i + 1
290                    o = o[i:]
291                    if o in witharg :
292                        # needs an argument : set it
293                        parsed[o] = v
294                    elif o in withoutarg :
295                        # doesn't need an argument : boolean
296                        parsed[o] = 1
297                    else :
298                        # should never occur
299                        raise PyKotaToolError, "Unexpected problem when parsing command line"
300            elif (not args) and (not allownothing) and sys.stdin.isatty() : # no option and no argument, we display help if we are a tty
301                self.display_usage_and_quit()
302        except getopt.error, msg :
303            sys.stderr.write("%s\n" % msg)
304            sys.stderr.flush()
305            self.display_usage_and_quit()
306        return (parsed, args)
307   
308    def isValidName(self, name) :
309        """Checks if a user or printer name is valid."""
310        # unfortunately Python 2.1 string modules doesn't define ascii_letters...
311        asciiletters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
312        digits = '0123456789'
313        if name[0] in asciiletters :
314            validchars = asciiletters + digits + "-_"
315            for c in name[1:] :
316                if c not in validchars :
317                    return 0
318            return 1       
319        return 0
320       
321    def matchString(self, s, patterns) :
322        """Returns 1 if the string s matches one of the patterns, else 0."""
323        for pattern in patterns :
324            if fnmatch.fnmatchcase(s, pattern) :
325                return 1
326        return 0
327       
328    def sendMessage(self, adminmail, touser, fullmessage) :
329        """Sends an email message containing headers to some user."""
330        if "@" not in touser :
331            touser = "%s@%s" % (touser, self.smtpserver)
332        server = smtplib.SMTP(self.smtpserver)
333        server.sendmail(adminmail, [touser], fullmessage)
334        server.quit()
335       
336    def sendMessageToUser(self, admin, adminmail, username, subject, message) :
337        """Sends an email message to a user."""
338        message += _("\n\nPlease contact your system administrator :\n\n\t%s - <%s>\n") % (admin, adminmail)
339        self.sendMessage(adminmail, username, "Subject: %s\n\n%s" % (subject, message))
340       
341    def sendMessageToAdmin(self, adminmail, subject, message) :
342        """Sends an email message to the Print Quota administrator."""
343        self.sendMessage(adminmail, adminmail, "Subject: %s\n\n%s" % (subject, message))
344       
345    def checkGroupPQuota(self, groupname, printername) :   
346        """Checks the group quota on a printer and deny or accept the job."""
347        printerid = self.storage.getPrinterId(printername)
348        policy = self.config.getPrinterPolicy(printername)
349        groupid = self.storage.getGroupId(groupname)
350        limitby = self.storage.getGroupLimitBy(groupid)
351        if limitby == "balance" : 
352            balance = self.storage.getGroupBalance(groupid)
353            if balance is None :
354                if policy in [None, "ALLOW"] :
355                    action = "POLICY_ALLOW"
356                else :   
357                    action = "POLICY_DENY"
358                self.logger.log_message(_("Unable to find group %s's account balance, applying default policy (%s) for printer %s") % (groupname, action, printername))
359            else :   
360                # TODO : there's no warning (no account balance soft limit)
361                (balance, lifetimepaid) = balance
362                if balance <= 0.0 :
363                    action = "DENY"
364                else :   
365                    action = "ALLOW"
366        else :
367            quota = self.storage.getGroupPQuota(groupid, printerid)
368            if quota is None :
369                # Unknown group or printer or combination
370                if policy in [None, "ALLOW"] :
371                    action = "POLICY_ALLOW"
372                else :   
373                    action = "POLICY_DENY"
374                self.logger.log_message(_("Unable to match group %s on printer %s, applying default policy (%s)") % (groupname, printername, action))
375            else :   
376                pagecounter = quota["pagecounter"]
377                softlimit = quota["softlimit"]
378                hardlimit = quota["hardlimit"]
379                datelimit = quota["datelimit"]
380                if softlimit is not None :
381                    if pagecounter < softlimit :
382                        action = "ALLOW"
383                    else :   
384                        if hardlimit is None :
385                            # only a soft limit, this is equivalent to having only a hard limit
386                            action = "DENY"
387                        else :   
388                            if softlimit <= pagecounter < hardlimit :   
389                                now = DateTime.now()
390                                if datelimit is not None :
391                                    datelimit = DateTime.ISO.ParseDateTime(datelimit)
392                                else :
393                                    datelimit = now + self.config.getGraceDelay(printername)
394                                    self.storage.setGroupDateLimit(groupid, printerid, datelimit)
395                                if now < datelimit :
396                                    action = "WARN"
397                                else :   
398                                    action = "DENY"
399                            else :         
400                                action = "DENY"
401                else :       
402                    if hardlimit is not None :
403                        # no soft limit, only a hard one.
404                        if pagecounter < hardlimit :
405                            action = "ALLOW"
406                        else :     
407                            action = "DENY"
408                    else :
409                        # Both are unset, no quota, i.e. accounting only
410                        action = "ALLOW"
411        return action
412   
413    def checkUserPQuota(self, username, printername) :
414        """Checks the user quota on a printer and deny or accept the job."""
415        # first we check any group the user is a member of
416        userid = self.storage.getUserId(username)
417        for groupname in self.storage.getUserGroupsNames(userid) :
418            action = self.checkGroupPQuota(groupname, printername)
419            if action in ("DENY", "POLICY_DENY") :
420                return action
421               
422        # then we check the user's own quota
423        printerid = self.storage.getPrinterId(printername)
424        policy = self.config.getPrinterPolicy(printername)
425        limitby = self.storage.getUserLimitBy(userid)
426        if limitby == "balance" : 
427            balance = self.storage.getUserBalance(userid)
428            if balance is None :
429                if policy in [None, "ALLOW"] :
430                    action = "POLICY_ALLOW"
431                else :   
432                    action = "POLICY_DENY"
433                self.logger.log_message(_("Unable to find user %s's account balance, applying default policy (%s) for printer %s") % (username, action, printername))
434            else :   
435                # TODO : there's no warning (no account balance soft limit)
436                (balance, lifetimepaid) = balance
437                if balance <= 0.0 :
438                    action = "DENY"
439                else :   
440                    action = "ALLOW"
441        else :
442            quota = self.storage.getUserPQuota(userid, printerid)
443            if quota is None :
444                # Unknown user or printer or combination
445                if policy in [None, "ALLOW"] :
446                    action = "POLICY_ALLOW"
447                else :   
448                    action = "POLICY_DENY"
449                self.logger.log_message(_("Unable to match user %s on printer %s, applying default policy (%s)") % (username, printername, action))
450            else :   
451                pagecounter = quota["pagecounter"]
452                softlimit = quota["softlimit"]
453                hardlimit = quota["hardlimit"]
454                datelimit = quota["datelimit"]
455                if softlimit is not None :
456                    if pagecounter < softlimit :
457                        action = "ALLOW"
458                    else :   
459                        if hardlimit is None :
460                            # only a soft limit, this is equivalent to having only a hard limit
461                            action = "DENY"
462                        else :   
463                            if softlimit <= pagecounter < hardlimit :   
464                                now = DateTime.now()
465                                if datelimit is not None :
466                                    datelimit = DateTime.ISO.ParseDateTime(datelimit)
467                                else :
468                                    datelimit = now + self.config.getGraceDelay(printername)
469                                    self.storage.setUserDateLimit(userid, printerid, datelimit)
470                                if now < datelimit :
471                                    action = "WARN"
472                                else :   
473                                    action = "DENY"
474                            else :         
475                                action = "DENY"
476                else :       
477                    if hardlimit is not None :
478                        # no soft limit, only a hard one.
479                        if pagecounter < hardlimit :
480                            action = "ALLOW"
481                        else :     
482                            action = "DENY"
483                    else :
484                        # Both are unset, no quota, i.e. accounting only
485                        action = "ALLOW"
486        return action
487   
488    def warnGroupPQuota(self, groupname, printername) :
489        """Checks a group quota and send messages if quota is exceeded on current printer."""
490        admin = self.config.getAdmin(printername)
491        adminmail = self.config.getAdminMail(printername)
492        mailto = self.config.getMailTo(printername)
493        action = self.checkGroupPQuota(groupname, printername)
494        groupmembers = self.storage.getGroupMembersNames(groupname)
495        if action.startswith("POLICY_") :
496            action = action[7:]
497        if action == "DENY" :
498            adminmessage = _("Print Quota exceeded for group %s on printer %s") % (groupname, printername)
499            self.logger.log_message(adminmessage)
500            if mailto in [ "BOTH", "ADMIN" ] :
501                self.sendMessageToAdmin(adminmail, _("Print Quota"), adminmessage)
502            for username in groupmembers :
503                if mailto in [ "BOTH", "USER" ] :
504                    self.sendMessageToUser(admin, adminmail, username, _("Print Quota Exceeded"), _("You are not allowed to print anymore because\nyour group Print Quota is exceeded on printer %s.") % printername)
505        elif action == "WARN" :   
506            adminmessage = _("Print Quota soft limit exceeded for group %s on printer %s") % (groupname, printername)
507            self.logger.log_message(adminmessage)
508            if mailto in [ "BOTH", "ADMIN" ] :
509                self.sendMessageToAdmin(adminmail, _("Print Quota"), adminmessage)
510            for username in groupmembers :
511                if mailto in [ "BOTH", "USER" ] :
512                    self.sendMessageToUser(admin, adminmail, username, _("Print Quota Exceeded"), _("You will soon be forbidden to print anymore because\nyour group Print Quota is almost reached on printer %s.") % printername)
513        return action       
514       
515    def warnUserPQuota(self, username, printername) :
516        """Checks a user quota and send him a message if quota is exceeded on current printer."""
517        admin = self.config.getAdmin(printername)
518        adminmail = self.config.getAdminMail(printername)
519        mailto = self.config.getMailTo(printername)
520        action = self.checkUserPQuota(username, printername)
521        if action.startswith("POLICY_") :
522            action = action[7:]
523        if action == "DENY" :
524            adminmessage = _("Print Quota exceeded for user %s on printer %s") % (username, printername)
525            self.logger.log_message(adminmessage)
526            if mailto in [ "BOTH", "USER" ] :
527                self.sendMessageToUser(admin, adminmail, username, _("Print Quota Exceeded"), _("You are not allowed to print anymore because\nyour Print Quota is exceeded on printer %s.") % printername)
528            if mailto in [ "BOTH", "ADMIN" ] :
529                self.sendMessageToAdmin(adminmail, _("Print Quota"), adminmessage)
530        elif action == "WARN" :   
531            adminmessage = _("Print Quota soft limit exceeded for user %s on printer %s") % (username, printername)
532            self.logger.log_message(adminmessage)
533            if mailto in [ "BOTH", "USER" ] :
534                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.") % printername)
535            if mailto in [ "BOTH", "ADMIN" ] :
536                self.sendMessageToAdmin(adminmail, _("Print Quota"), adminmessage)
537        return action       
538   
Note: See TracBrowser for help on using the browser.