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

Revision 3188, 35.6 kB (checked in by jerome, 17 years ago)

Uses the stderr logging backend at startup, in case of early
failure (like permissions problem on the content of ~pykota/)

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
RevLine 
[1144]1# PyKota
2# -*- coding: ISO-8859-15 -*-
[695]3
[952]4# PyKota - Print Quotas for CUPS and LPRng
[695]5#
[3133]6# (c) 2003, 2004, 2005, 2006, 2007 Jerome Alet <alet@librelogiciel.com>
[873]7# This program is free software; you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by
9# the Free Software Foundation; either version 2 of the License, or
10# (at your option) any later version.
[695]11#
[873]12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with this program; if not, write to the Free Software
[2302]19# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
[695]20#
21# $Id$
22#
[2093]23#
[695]24
[3184]25"""This module defines the base classes for PyKota command line tools."""
26
[695]27import sys
[1193]28import os
[1960]29import pwd
[817]30import fnmatch
[715]31import getopt
[695]32import smtplib
[772]33import gettext
[782]34import locale
[1469]35import socket
[2783]36import time
[2643]37from email.MIMEText import MIMEText
38from email.Header import Header
[3013]39import email.Utils
[695]40
[708]41from mx import DateTime
42
[3006]43try :
44    import chardet
45except ImportError :   
46    def detectCharset(text) :
47        """Fakes a charset detection if the chardet module is not installed."""
48        return "ISO-8859-15"
49else :   
50    def detectCharset(text) :
51        """Uses the chardet module to workaround CUPS lying to us."""
52        return chardet.detect(text)["encoding"]
53
[2830]54from pykota import config, storage, logger
[2344]55from pykota.version import __version__, __author__, __years__, __gplblurb__
[695]56
[1795]57def N_(message) :
58    """Fake translation marker for translatable strings extraction."""
59    return message
60
[695]61class PyKotaToolError(Exception):
[2512]62    """An exception for PyKota related stuff."""
[695]63    def __init__(self, message = ""):
64        self.message = message
65        Exception.__init__(self, message)
66    def __repr__(self):
67        return self.message
68    __str__ = __repr__
69   
[2512]70class PyKotaCommandLineError(PyKotaToolError) :   
71    """An exception for Pykota command line tools."""
72    pass
73   
[2229]74def crashed(message="Bug in PyKota") :   
[1546]75    """Minimal crash method."""
76    import traceback
77    lines = []
78    for line in traceback.format_exception(*sys.exc_info()) :
79        lines.extend([l for l in line.split("\n") if l])
[2344]80    msg = "ERROR: ".join(["%s\n" % l for l in (["ERROR: PyKota v%s" % __version__, message] + lines)])
[1546]81    sys.stderr.write(msg)
82    sys.stderr.flush()
83    return msg
84
[2782]85class Percent :
86    """A class to display progress."""
[2783]87    def __init__(self, app, size=None) :
[2782]88        """Initializes the engine."""
89        self.app = app
[2783]90        self.size = None
91        if size :
92            self.setSize(size)
93        self.previous = None
94        self.before = time.time()
95       
96    def setSize(self, size) :     
97        """Sets the total size."""
98        self.number = 0
[2782]99        self.size = size
[2793]100        if size :
101            self.factor = 100.0 / float(size)
[2782]102       
103    def display(self, msg) :   
104        """Displays the value."""
105        self.app.display(msg)
106       
107    def oneMore(self) :   
108        """Increments internal counter."""
[2783]109        if self.size :
110            self.number += 1
111            percent = "%.02f" % (float(self.number) * self.factor)
112            if percent != self.previous : # optimize for large number of items
113                self.display("\r%s%%" % percent)
114                self.previous = percent
[2782]115           
116    def done(self) :         
117        """Displays the 'done' message."""
[2783]118        after = time.time()
119        if self.size :
[2788]120            speed = self.size / (after - self.before)
121            self.display("\r100.00%%\r        \r%s. %s : %.2f %s.\n" \
[2783]122                     % (_("Done"), _("Average speed"), speed, _("entries per second")))
123        else :             
124            self.display("\r100.00%%\r        \r%s.\n" % _("Done"))
[2782]125       
[1911]126class Tool :
127    """Base class for tools with no database access."""
[2344]128    def __init__(self, lang="", charset=None, doc="PyKota v%(__version__)s (c) %(__years__)s %(__author__)s") :
[695]129        """Initializes the command line tool."""
[3187]130        self.debug = True # in case of early failure
[3188]131        self.logger = logger.openLogger("stderr")
[3187]132       
[2006]133        # did we drop priviledges ?
134        self.privdropped = 0
135       
[772]136        # locale stuff
[788]137        try :
[3055]138            locale.setlocale(locale.LC_ALL, (lang, charset))
[788]139        except (locale.Error, IOError) :
[3055]140            locale.setlocale(locale.LC_ALL, None)
141        (self.language, self.charset) = locale.getlocale()
142        self.language = self.language or "C"
[3061]143        try :
144            self.charset = self.charset or locale.getpreferredencoding()
145        except locale.Error :   
146            self.charset = sys.getfilesystemencoding()
[3055]147       
[3173]148        # Dirty hack : if the charset is ASCII, we can safely use UTF-8 instead
149        # This has the advantage of allowing transparent support for recent
150        # versions of CUPS which (en-)force charset to UTF-8 when printing.
151        # This should be needed only when printing, but is probably (?) safe
152        # to do when using interactive commands.
153        if self.charset.upper() in ('ASCII', 'ANSI_X3.4-1968') :
154            self.charset = "UTF-8"
155       
[3055]156        # translation stuff
[1898]157        try :
[3055]158            try :
159                trans = gettext.translation("pykota", languages=["%s.%s" % (self.language, self.charset)], codeset=self.charset)
160            except TypeError : # Python <2.4
161                trans = gettext.translation("pykota", languages=["%s.%s" % (self.language, self.charset)])
162            trans.install()
[1898]163        except :
[788]164            gettext.NullTranslations().install()
[772]165   
166        # pykota specific stuff
[715]167        self.documentation = doc
[1960]168       
[2210]169    def deferredInit(self) :       
170        """Deferred initialization."""
[3045]171        confdir = os.environ.get("PYKOTA_HOME")
172        environHome = True
173        missingUser = False
174        if confdir is None :
175            environHome = False
176            # check for config files in the 'pykota' user's home directory.
177            try :
178                self.pykotauser = pwd.getpwnam("pykota")
179                confdir = self.pykotauser[5]
180            except KeyError :   
181                self.pykotauser = None
182                confdir = "/etc/pykota"
183                missingUser = True
[1960]184           
[2210]185        self.config = config.PyKotaConfig(confdir)
186        self.debug = self.config.getDebug()
187        self.smtpserver = self.config.getSMTPServer()
188        self.maildomain = self.config.getMailDomain()
189        self.logger = logger.openLogger(self.config.getLoggingBackend())
[1542]190           
[2006]191        # now drop priviledge if possible
192        self.dropPriv()   
193       
[1960]194        # We NEED this here, even when not in an accounting filter/backend   
[1497]195        self.softwareJobSize = 0
[1495]196        self.softwareJobPrice = 0.0
[1960]197       
[3045]198        if environHome :
199            self.printInfo("PYKOTA_HOME environment variable is set. Configuration files were searched in %s" % confdir, "info")
200        else :
201            if missingUser :     
202                self.printInfo("The 'pykota' system account is missing. Configuration files were searched in %s instead." % confdir, "warn")
[1960]203       
[3055]204        self.logdebug("Language in use : %s" % self.language)
[1761]205        self.logdebug("Charset in use : %s" % self.charset)
[3055]206       
[1872]207        arguments = " ".join(['"%s"' % arg for arg in sys.argv])
208        self.logdebug("Command line arguments : %s" % arguments)
[695]209       
[2006]210    def dropPriv(self) :   
211        """Drops priviledges."""
212        uid = os.geteuid()
[2889]213        try :
214            self.originalUserName = pwd.getpwuid(uid)[0]
215        except (KeyError, IndexError), msg :   
216            self.printInfo(_("Strange problem with uid(%s) : %s") % (uid, msg), "warn")
217            self.originalUserName = None
218        else :
219            if uid :
220                self.logdebug(_("Running as user '%s'.") % self.originalUserName)
[2006]221            else :
[2889]222                if self.pykotauser is None :
223                    self.logdebug(_("No user named 'pykota'. Not dropping priviledges."))
[2006]224                else :   
[2889]225                    try :
226                        os.setegid(self.pykotauser[3])
227                        os.seteuid(self.pykotauser[2])
228                    except OSError, msg :   
229                        self.printInfo(_("Impossible to drop priviledges : %s") % msg, "warn")
230                    else :   
231                        self.logdebug(_("Priviledges dropped. Now running as user 'pykota'."))
232                        self.privdropped = 1
[2006]233           
234    def regainPriv(self) :   
235        """Drops priviledges."""
236        if self.privdropped :
237            try :
238                os.seteuid(0)
239                os.setegid(0)
240            except OSError, msg :   
241                self.printInfo(_("Impossible to regain priviledges : %s") % msg, "warn")
242            else :   
[2008]243                self.logdebug(_("Regained priviledges. Now running as root."))
[2006]244                self.privdropped = 0
245       
[2804]246    def UTF8ToUserCharset(self, text) :
247        """Converts from UTF-8 to user's charset."""
[3006]248        if text is None :
249            return None
250        try :
251            return text.decode("UTF-8").encode(self.charset, "replace") 
252        except (UnicodeError, AttributeError) :   
[2804]253            try :
[3006]254                # Maybe already in Unicode ?
255                return text.encode(self.charset, "replace") 
256            except (UnicodeError, AttributeError) :
257                # Try to autodetect the charset
258                return text.decode(detectCharset(text), "replace").encode(self.charset, "replace")
[2804]259       
260    def userCharsetToUTF8(self, text) :
261        """Converts from user's charset to UTF-8."""
[3006]262        if text is None :
263            return None
264        try :
265            # We don't necessarily trust the default charset, because
266            # xprint sends us titles in UTF-8 but CUPS gives us an ISO-8859-1 charset !
267            # So we first try to see if the text is already in UTF-8 or not, and
268            # if it is, we delete characters which can't be converted to the user's charset,
269            # then convert back to UTF-8. PostgreSQL 7.3.x used to reject some unicode characters,
270            # this is fixed by the ugly line below :
271            return text.decode("UTF-8").encode(self.charset, "replace").decode(self.charset).encode("UTF-8", "replace")
272        except (UnicodeError, AttributeError) :
[2804]273            try :
[3006]274                return text.decode(self.charset).encode("UTF-8", "replace") 
275            except (UnicodeError, AttributeError) :   
[2804]276                try :
[3006]277                    # Maybe already in Unicode ?
278                    return text.encode("UTF-8", "replace") 
279                except (UnicodeError, AttributeError) :
280                    # Try to autodetect the charset
281                    return text.decode(detectCharset(text), "replace").encode("UTF-8", "replace")
282        return newtext
[2804]283       
[2657]284    def display(self, message) :
285        """Display a message but only if stdout is a tty."""
286        if sys.stdout.isatty() :
287            sys.stdout.write(message)
288            sys.stdout.flush()
289           
[1130]290    def logdebug(self, message) :   
291        """Logs something to debug output if debug is enabled."""
292        if self.debug :
293            self.logger.log_message(message, "debug")
[1582]294           
[1584]295    def printInfo(self, message, level="info") :       
[1582]296        """Sends a message to standard error."""
[1584]297        sys.stderr.write("%s: %s\n" % (level.upper(), message))
[1582]298        sys.stderr.flush()
[1130]299       
[2210]300    def matchString(self, s, patterns) :
[2650]301        """Returns True if the string s matches one of the patterns, else False."""
302        if not patterns :
303            return True # No pattern, always matches.
304        else :   
305            for pattern in patterns :
306                if fnmatch.fnmatchcase(s, pattern) :
307                    return True
308            return False
[2210]309       
[2762]310    def sanitizeNames(self, options, names) :
311        """Ensures that an user can only see the datas he is allowed to see, by modifying the list of names."""
312        if not self.config.isAdmin :
313            username = pwd.getpwuid(os.geteuid())[0]
314            if not options["list"] :
315                raise PyKotaCommandLineError, "%s : %s" % (username, _("You're not allowed to use this command."))
316            else :
317                if options["groups"] :
318                    user = self.storage.getUser(username)
319                    if user.Exists :
320                        return [ g.Name for g in self.storage.getUserGroups(user) ]
321                return [ username ]
322        elif not names :       
323            return ["*"]
324        else :   
325            return names
326       
[715]327    def display_version_and_quit(self) :
328        """Displays version number, then exists successfully."""
[1923]329        try :
330            self.clean()
331        except AttributeError :   
332            pass
[2344]333        print __version__
[715]334        sys.exit(0)
335   
336    def display_usage_and_quit(self) :
337        """Displays command line usage, then exists successfully."""
[1923]338        try :
339            self.clean()
340        except AttributeError :   
341            pass
[2344]342        print _(self.documentation) % globals()
343        print __gplblurb__
344        print
345        print _("Please report bugs to :"), __author__
[715]346        sys.exit(0)
347       
[2229]348    def crashed(self, message="Bug in PyKota") :   
[1517]349        """Outputs a crash message, and optionally sends it to software author."""
[1546]350        msg = crashed(message)
[2229]351        fullmessage = "========== Traceback :\n\n%s\n\n========== sys.argv :\n\n%s\n\n========== Environment :\n\n%s\n" % \
352                        (msg, \
353                         "\n".join(["    %s" % repr(a) for a in sys.argv]), \
354                         "\n".join(["    %s=%s" % (k, v) for (k, v) in os.environ.items()]))
[1541]355        try :
356            crashrecipient = self.config.getCrashRecipient()
357            if crashrecipient :
[1517]358                admin = self.config.getAdminMail("global") # Nice trick, isn't it ?
359                server = smtplib.SMTP(self.smtpserver)
[2795]360                msg = MIMEText(fullmessage, _charset=self.charset)
[3136]361                msg["Subject"] = Header("PyKota v%s crash traceback !" \
362                                        % __version__, charset=self.charset)
[2795]363                msg["From"] = admin
364                msg["To"] = crashrecipient
365                msg["Cc"] = admin
[3013]366                msg["Date"] = email.Utils.formatdate(localtime=True)
[2795]367                server.sendmail(admin, [admin, crashrecipient], msg.as_string())
[1517]368                server.quit()
[1541]369        except :
370            pass
[2229]371        return fullmessage   
[1517]372       
[729]373    def parseCommandline(self, argv, short, long, allownothing=0) :
[715]374        """Parses the command line, controlling options."""
375        # split options in two lists: those which need an argument, those which don't need any
[2605]376        short = "%sA:" % short
377        long.append("arguments=")
[715]378        withoutarg = []
379        witharg = []
380        lgs = len(short)
381        i = 0
382        while i < lgs :
383            ii = i + 1
384            if (ii < lgs) and (short[ii] == ':') :
385                # needs an argument
386                witharg.append(short[i])
387                ii = ii + 1 # skip the ':'
388            else :
389                # doesn't need an argument
390                withoutarg.append(short[i])
391            i = ii
392               
393        for option in long :
394            if option[-1] == '=' :
395                # needs an argument
396                witharg.append(option[:-1])
397            else :
398                # doesn't need an argument
399                withoutarg.append(option)
400       
401        # then we parse the command line
[2605]402        done = 0
403        while not done :
404            # we begin with all possible options unset
405            parsed = {}
406            for option in withoutarg + witharg :
407                parsed[option] = None
408            args = []       # to not break if something unexpected happened
409            try :
410                options, args = getopt.getopt(argv, short, long)
411                if options :
412                    for (o, v) in options :
413                        # we skip the '-' chars
414                        lgo = len(o)
415                        i = 0
416                        while (i < lgo) and (o[i] == '-') :
417                            i = i + 1
418                        o = o[i:]
419                        if o in witharg :
420                            # needs an argument : set it
421                            parsed[o] = v
422                        elif o in withoutarg :
423                            # doesn't need an argument : boolean
424                            parsed[o] = 1
425                        else :
426                            # should never occur
[2610]427                            raise PyKotaCommandLineError, "Unexpected problem when parsing command line"
[2605]428                elif (not args) and (not allownothing) and sys.stdin.isatty() : # no option and no argument, we display help if we are a tty
429                    self.display_usage_and_quit()
430            except getopt.error, msg :
[2610]431                raise PyKotaCommandLineError, str(msg)
[2605]432            else :   
433                if parsed["arguments"] or parsed["A"] :
434                    # arguments are in a file, we ignore all other arguments
435                    # and reset the list of arguments to the lines read from
436                    # the file.
437                    argsfile = open(parsed["arguments"] or parsed["A"], "r")
438                    argv = [ l.strip() for l in argsfile.readlines() ]
439                    argsfile.close()
440                    for i in range(len(argv)) :
441                        argi = argv[i]
442                        if argi.startswith('"') and argi.endswith('"') :
443                            argv[i] = argi[1:-1]
444                else :   
445                    done = 1
[715]446        return (parsed, args)
447   
[1911]448class PyKotaTool(Tool) :   
449    """Base class for all PyKota command line tools."""
[2344]450    def __init__(self, lang="", charset=None, doc="PyKota v%(__version__)s (c) %(__years__)s %(__author__)s") :
[1911]451        """Initializes the command line tool and opens the database."""
452        Tool.__init__(self, lang, charset, doc)
[2210]453       
454    def deferredInit(self) :   
455        """Deferred initialization."""
456        Tool.deferredInit(self)
457        self.storage = storage.openConnection(self)
458        if self.config.isAdmin : # TODO : We don't know this before, fix this !
459            self.logdebug("Beware : running as a PyKota administrator !")
[2093]460        else :   
[2210]461            self.logdebug("Don't Panic : running as a mere mortal !")
[1911]462       
463    def clean(self) :   
464        """Ensures that the database is closed."""
465        try :
466            self.storage.close()
467        except (TypeError, NameError, AttributeError) :   
468            pass
469           
[764]470    def isValidName(self, name) :
471        """Checks if a user or printer name is valid."""
[1725]472        invalidchars = "/@?*,;&|"
[1637]473        for c in list(invalidchars) :
[1593]474            if c in name :
[1394]475                return 0
476        return 1       
[764]477       
[802]478    def sendMessage(self, adminmail, touser, fullmessage) :
[695]479        """Sends an email message containing headers to some user."""
[1469]480        try :   
481            server = smtplib.SMTP(self.smtpserver)
482        except socket.error, msg :   
[1584]483            self.printInfo(_("Impossible to connect to SMTP server : %s") % msg, "error")
[1469]484        else :
485            try :
[2795]486                server.sendmail(adminmail, [touser], fullmessage)
[1469]487            except smtplib.SMTPException, answer :   
488                for (k, v) in answer.recipients.items() :
[1584]489                    self.printInfo(_("Impossible to send mail to %s, error %s : %s") % (k, v[0], v[1]), "error")
[1469]490            server.quit()
[695]491       
[1079]492    def sendMessageToUser(self, admin, adminmail, user, subject, message) :
[695]493        """Sends an email message to a user."""
[802]494        message += _("\n\nPlease contact your system administrator :\n\n\t%s - <%s>\n") % (admin, adminmail)
[2795]495        usermail = user.Email or user.Name
496        if "@" not in usermail :
497            usermail = "%s@%s" % (usermail, self.maildomain or self.smtpserver or "localhost")
498        msg = MIMEText(message, _charset=self.charset)
[3136]499        msg["Subject"] = Header(subject, charset=self.charset)
[2795]500        msg["From"] = adminmail
501        msg["To"] = usermail
[3013]502        msg["Date"] = email.Utils.formatdate(localtime=True)
[2795]503        self.sendMessage(adminmail, usermail, msg.as_string())
[695]504       
[802]505    def sendMessageToAdmin(self, adminmail, subject, message) :
[695]506        """Sends an email message to the Print Quota administrator."""
[2795]507        if "@" not in adminmail :
508            adminmail = "%s@%s" % (adminmail, self.maildomain or self.smtpserver or "localhost")
509        msg = MIMEText(message, _charset=self.charset)
[3136]510        msg["Subject"] = Header(subject, charset=self.charset)
[2795]511        msg["From"] = adminmail
512        msg["To"] = adminmail
513        self.sendMessage(adminmail, adminmail, msg.as_string())
[695]514       
[1365]515    def _checkUserPQuota(self, userpquota) :           
516        """Checks the user quota on a printer and deny or accept the job."""
517        # then we check the user's own quota
518        # if we get there we are sure that policy is not EXTERNAL
519        user = userpquota.User
520        printer = userpquota.Printer
[1495]521        enforcement = self.config.getPrinterEnforcement(printer.Name)
[1365]522        self.logdebug("Checking user %s's quota on printer %s" % (user.Name, printer.Name))
523        (policy, dummy) = self.config.getPrinterPolicy(userpquota.Printer.Name)
524        if not userpquota.Exists :
525            # Unknown userquota
526            if policy == "ALLOW" :
527                action = "POLICY_ALLOW"
528            else :   
529                action = "POLICY_DENY"
[1584]530            self.printInfo(_("Unable to match user %s on printer %s, applying default policy (%s)") % (user.Name, printer.Name, action))
[1365]531        else :   
532            pagecounter = int(userpquota.PageCounter or 0)
[1495]533            if enforcement == "STRICT" :
534                pagecounter += self.softwareJobSize
[1365]535            if userpquota.SoftLimit is not None :
536                softlimit = int(userpquota.SoftLimit)
537                if pagecounter < softlimit :
538                    action = "ALLOW"
539                else :   
540                    if userpquota.HardLimit is None :
541                        # only a soft limit, this is equivalent to having only a hard limit
542                        action = "DENY"
543                    else :   
544                        hardlimit = int(userpquota.HardLimit)
545                        if softlimit <= pagecounter < hardlimit :   
546                            now = DateTime.now()
547                            if userpquota.DateLimit is not None :
[3050]548                                datelimit = DateTime.ISO.ParseDateTime(str(userpquota.DateLimit)[:19])
[1365]549                            else :
550                                datelimit = now + self.config.getGraceDelay(printer.Name)
551                                userpquota.setDateLimit(datelimit)
552                            if now < datelimit :
553                                action = "WARN"
554                            else :   
555                                action = "DENY"
556                        else :         
557                            action = "DENY"
558            else :       
559                if userpquota.HardLimit is not None :
560                    # no soft limit, only a hard one.
561                    hardlimit = int(userpquota.HardLimit)
562                    if pagecounter < hardlimit :
563                        action = "ALLOW"
564                    else :     
565                        action = "DENY"
566                else :
567                    # Both are unset, no quota, i.e. accounting only
568                    action = "ALLOW"
569        return action
570   
[1041]571    def checkGroupPQuota(self, grouppquota) :   
[927]572        """Checks the group quota on a printer and deny or accept the job."""
[1041]573        group = grouppquota.Group
574        printer = grouppquota.Printer
[1495]575        enforcement = self.config.getPrinterEnforcement(printer.Name)
[1365]576        self.logdebug("Checking group %s's quota on printer %s" % (group.Name, printer.Name))
[1061]577        if group.LimitBy and (group.LimitBy.lower() == "balance") : 
[1666]578            val = group.AccountBalance or 0.0
[1495]579            if enforcement == "STRICT" : 
580                val -= self.softwareJobPrice # use precomputed size.
[2692]581            balancezero = self.config.getBalanceZero()
582            if val <= balancezero :
[1041]583                action = "DENY"
[1495]584            elif val <= self.config.getPoorMan() :   
[1077]585                action = "WARN"
[927]586            else :   
[1041]587                action = "ALLOW"
[2692]588            if (enforcement == "STRICT") and (val == balancezero) :
[1529]589                action = "WARN" # we can still print until account is 0
[927]590        else :
[1665]591            val = grouppquota.PageCounter or 0
[1495]592            if enforcement == "STRICT" :
[2992]593                val += int(self.softwareJobSize) # TODO : this is not a fix, problem is elsewhere in grouppquota.PageCounter
[1041]594            if grouppquota.SoftLimit is not None :
595                softlimit = int(grouppquota.SoftLimit)
[1495]596                if val < softlimit :
[1041]597                    action = "ALLOW"
[927]598                else :   
[1041]599                    if grouppquota.HardLimit is None :
600                        # only a soft limit, this is equivalent to having only a hard limit
601                        action = "DENY"
[927]602                    else :   
[1041]603                        hardlimit = int(grouppquota.HardLimit)
[1495]604                        if softlimit <= val < hardlimit :   
[1041]605                            now = DateTime.now()
606                            if grouppquota.DateLimit is not None :
[3050]607                                datelimit = DateTime.ISO.ParseDateTime(str(grouppquota.DateLimit)[:19])
[1041]608                            else :
609                                datelimit = now + self.config.getGraceDelay(printer.Name)
610                                grouppquota.setDateLimit(datelimit)
611                            if now < datelimit :
612                                action = "WARN"
613                            else :   
[927]614                                action = "DENY"
[1041]615                        else :         
[927]616                            action = "DENY"
[1041]617            else :       
618                if grouppquota.HardLimit is not None :
619                    # no soft limit, only a hard one.
620                    hardlimit = int(grouppquota.HardLimit)
[1495]621                    if val < hardlimit :
[927]622                        action = "ALLOW"
[1041]623                    else :     
624                        action = "DENY"
625                else :
626                    # Both are unset, no quota, i.e. accounting only
627                    action = "ALLOW"
[927]628        return action
629   
[1041]630    def checkUserPQuota(self, userpquota) :
[1365]631        """Checks the user quota on a printer and all its parents and deny or accept the job."""
[1041]632        user = userpquota.User
633        printer = userpquota.Printer
634       
[1365]635        # indicates that a warning needs to be sent
636        warned = 0               
637       
[927]638        # first we check any group the user is a member of
[1041]639        for group in self.storage.getUserGroups(user) :
[2452]640            # No need to check anything if the group is in noquota mode
641            if group.LimitBy != "noquota" :
642                grouppquota = self.storage.getGroupPQuota(group, printer)
643                # for the printer and all its parents
644                for gpquota in [ grouppquota ] + grouppquota.ParentPrintersGroupPQuota :
645                    if gpquota.Exists :
646                        action = self.checkGroupPQuota(gpquota)
647                        if action == "DENY" :
648                            return action
649                        elif action == "WARN" :   
650                            warned = 1
[1365]651                       
652        # Then we check the user's account balance
[1152]653        # if we get there we are sure that policy is not EXTERNAL
654        (policy, dummy) = self.config.getPrinterPolicy(printer.Name)
[1061]655        if user.LimitBy and (user.LimitBy.lower() == "balance") : 
[1365]656            self.logdebug("Checking account balance for user %s" % user.Name)
[1041]657            if user.AccountBalance is None :
[956]658                if policy == "ALLOW" :
[925]659                    action = "POLICY_ALLOW"
660                else :   
661                    action = "POLICY_DENY"
[1584]662                self.printInfo(_("Unable to find user %s's account balance, applying default policy (%s) for printer %s") % (user.Name, action, printer.Name))
[1365]663                return action       
[713]664            else :   
[2054]665                if user.OverCharge == 0.0 :
666                    self.printInfo(_("User %s will not be charged for printing.") % user.Name)
667                    action = "ALLOW"
[1529]668                else :
[2054]669                    val = float(user.AccountBalance or 0.0)
670                    enforcement = self.config.getPrinterEnforcement(printer.Name)
671                    if enforcement == "STRICT" : 
672                        val -= self.softwareJobPrice # use precomputed size.
[2692]673                    balancezero = self.config.getBalanceZero()   
674                    if val <= balancezero :
[2054]675                        action = "DENY"
676                    elif val <= self.config.getPoorMan() :   
677                        action = "WARN"
678                    else :
679                        action = "ALLOW"
[2692]680                    if (enforcement == "STRICT") and (val == balancezero) :
[2054]681                        action = "WARN" # we can still print until account is 0
[1529]682                return action   
[925]683        else :
[1365]684            # Then check the user quota on current printer and all its parents.               
685            policyallowed = 0
686            for upquota in [ userpquota ] + userpquota.ParentPrintersUserPQuota :               
687                action = self._checkUserPQuota(upquota)
688                if action in ("DENY", "POLICY_DENY") :
689                    return action
690                elif action == "WARN" :   
691                    warned = 1
692                elif action == "POLICY_ALLOW" :   
693                    policyallowed = 1
694            if warned :       
695                return "WARN"
696            elif policyallowed :   
697                return "POLICY_ALLOW" 
[925]698            else :   
[1365]699                return "ALLOW"
700               
[1196]701    def externalMailTo(self, cmd, action, user, printer, message) :
[1192]702        """Warns the user with an external command."""
703        username = user.Name
[1196]704        printername = printer.Name
[1192]705        email = user.Email or user.Name
706        if "@" not in email :
[1353]707            email = "%s@%s" % (email, self.maildomain or self.smtpserver)
[1192]708        os.system(cmd % locals())
709   
[1196]710    def formatCommandLine(self, cmd, user, printer) :
711        """Executes an external command."""
712        username = user.Name
713        printername = printer.Name
714        return cmd % locals()
715       
[1041]716    def warnGroupPQuota(self, grouppquota) :
[927]717        """Checks a group quota and send messages if quota is exceeded on current printer."""
[1041]718        group = grouppquota.Group
719        printer = grouppquota.Printer
720        admin = self.config.getAdmin(printer.Name)
721        adminmail = self.config.getAdminMail(printer.Name)
[1192]722        (mailto, arguments) = self.config.getMailTo(printer.Name)
[2547]723        if group.LimitBy in ("noquota", "nochange") :
724            action = "ALLOW"
725        else :   
726            action = self.checkGroupPQuota(grouppquota)
727            if action.startswith("POLICY_") :
728                action = action[7:]
729            if action == "DENY" :
730                adminmessage = _("Print Quota exceeded for group %s on printer %s") % (group.Name, printer.Name)
731                self.printInfo(adminmessage)
732                if mailto in [ "BOTH", "ADMIN" ] :
733                    self.sendMessageToAdmin(adminmail, _("Print Quota"), adminmessage)
734                if mailto in [ "BOTH", "USER", "EXTERNAL" ] :
735                    for user in self.storage.getGroupMembers(group) :
736                        if mailto != "EXTERNAL" :
737                            self.sendMessageToUser(admin, adminmail, user, _("Print Quota Exceeded"), self.config.getHardWarn(printer.Name))
738                        else :   
739                            self.externalMailTo(arguments, action, user, printer, self.config.getHardWarn(printer.Name))
740            elif action == "WARN" :   
741                adminmessage = _("Print Quota low for group %s on printer %s") % (group.Name, printer.Name)
742                self.printInfo(adminmessage)
743                if mailto in [ "BOTH", "ADMIN" ] :
744                    self.sendMessageToAdmin(adminmail, _("Print Quota"), adminmessage)
745                if group.LimitBy and (group.LimitBy.lower() == "balance") : 
746                    message = self.config.getPoorWarn()
747                else :     
748                    message = self.config.getSoftWarn(printer.Name)
749                if mailto in [ "BOTH", "USER", "EXTERNAL" ] :
750                    for user in self.storage.getGroupMembers(group) :
751                        if mailto != "EXTERNAL" :
752                            self.sendMessageToUser(admin, adminmail, user, _("Print Quota Exceeded"), message)
753                        else :   
754                            self.externalMailTo(arguments, action, user, printer, message)
[927]755        return action       
[728]756       
[1041]757    def warnUserPQuota(self, userpquota) :
[728]758        """Checks a user quota and send him a message if quota is exceeded on current printer."""
[1041]759        user = userpquota.User
[1245]760        printer = userpquota.Printer
761        admin = self.config.getAdmin(printer.Name)
762        adminmail = self.config.getAdminMail(printer.Name)
763        (mailto, arguments) = self.config.getMailTo(printer.Name)
[2547]764       
765        if user.LimitBy in ("noquota", "nochange") :
766            action = "ALLOW"
767        elif user.LimitBy == "noprint" :
768            action = "DENY"
[2558]769            message = _("User %s is not allowed to print at this time.") % user.Name
770            self.printInfo(message)
[1245]771            if mailto in [ "BOTH", "USER", "EXTERNAL" ] :
772                if mailto != "EXTERNAL" :
[2547]773                    self.sendMessageToUser(admin, adminmail, user, _("Printing denied."), message)
[1245]774                else :   
775                    self.externalMailTo(arguments, action, user, printer, message)
776            if mailto in [ "BOTH", "ADMIN" ] :
[2547]777                self.sendMessageToAdmin(adminmail, _("Print Quota"), message)
778        else :
779            action = self.checkUserPQuota(userpquota)
780            if action.startswith("POLICY_") :
781                action = action[7:]
782               
783            if action == "DENY" :
784                adminmessage = _("Print Quota exceeded for user %s on printer %s") % (user.Name, printer.Name)
785                self.printInfo(adminmessage)
786                if mailto in [ "BOTH", "USER", "EXTERNAL" ] :
787                    message = self.config.getHardWarn(printer.Name)
788                    if mailto != "EXTERNAL" :
789                        self.sendMessageToUser(admin, adminmail, user, _("Print Quota Exceeded"), message)
790                    else :   
791                        self.externalMailTo(arguments, action, user, printer, message)
792                if mailto in [ "BOTH", "ADMIN" ] :
793                    self.sendMessageToAdmin(adminmail, _("Print Quota"), adminmessage)
794            elif action == "WARN" :   
795                adminmessage = _("Print Quota low for user %s on printer %s") % (user.Name, printer.Name)
796                self.printInfo(adminmessage)
797                if mailto in [ "BOTH", "USER", "EXTERNAL" ] :
798                    if user.LimitBy and (user.LimitBy.lower() == "balance") : 
799                        message = self.config.getPoorWarn()
800                    else :     
801                        message = self.config.getSoftWarn(printer.Name)
802                    if mailto != "EXTERNAL" :   
803                        self.sendMessageToUser(admin, adminmail, user, _("Print Quota Low"), message)
804                    else :   
805                        self.externalMailTo(arguments, action, user, printer, message)
806                if mailto in [ "BOTH", "ADMIN" ] :
807                    self.sendMessageToAdmin(adminmail, _("Print Quota"), adminmessage)
[1245]808        return action       
[1196]809       
Note: See TracBrowser for help on using the browser.