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

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

LPRng support now works !

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