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

Revision 929, 20.7 kB (checked in by jalet, 21 years ago)

repykota now reports account balances too.

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