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

Revision 1850, 56.9 kB (checked in by jalet, 20 years ago)

Should fix the printer's hostname or IP address detection code.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
Line 
1# PyKota
2# -*- coding: ISO-8859-15 -*-
3
4# PyKota - Print Quotas for CUPS and LPRng
5#
6# (c) 2003-2004 Jerome Alet <alet@librelogiciel.com>
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.
11#
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
19# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
20#
21# $Id$
22#
23# $Log$
24# Revision 1.133  2004/10/19 22:24:00  jalet
25# Should fix the printer's hostname or IP address detection code.
26#
27# Revision 1.132  2004/10/19 21:45:25  jalet
28# Now correctly logs command line arguments
29#
30# Revision 1.131  2004/10/19 21:37:57  jalet
31# Fixes recently introduced bug
32#
33# Revision 1.130  2004/10/19 15:21:48  jalet
34# Fixed incorrect setting of the user's locale
35#
36# Revision 1.129  2004/10/13 20:51:27  jalet
37# Made debugging levels be the same in cupspykota and lprngpykota.
38# Now outputs more information in informational messages : user, printer, jobid
39#
40# Revision 1.128  2004/10/11 22:53:06  jalet
41# Postponed string interpolation to help message's output method
42#
43# Revision 1.127  2004/10/11 12:48:38  jalet
44# Adds fake translation marker
45#
46# Revision 1.126  2004/10/05 09:41:13  jalet
47# Small fix for errors caused by unknown locale
48#
49# Revision 1.125  2004/10/04 11:18:10  jalet
50# Now exports the MD5 sum of the job's datas as an hexadecimal digest
51#
52# Revision 1.124  2004/10/02 05:48:56  jalet
53# Should now correctly deal with charsets both when storing into databases and when
54# retrieving datas. Works with both PostgreSQL and LDAP.
55#
56# Revision 1.123  2004/09/29 20:20:52  jalet
57# Added the winbind_separator directive to pykota.conf to allow the admin to
58# strip out the Samba/Winbind domain name when users print.
59#
60# Revision 1.122  2004/09/28 21:38:56  jalet
61# Now computes the job's datas MD5 checksum to later forbid duplicate print jobs.
62# The checksum is not yet saved into the database.
63#
64# Revision 1.121  2004/09/15 18:47:58  jalet
65# Re-Extends the list of invalid characters in names to prevent
66# people from adding user "*" for example, or to prevent
67# print administrators to hijack the system by putting dangerous
68# datas into the database which would cause commands later run by root
69# to compromise the system.
70#
71# Revision 1.120  2004/09/02 13:26:29  jalet
72# Small fix for old versions of LPRng
73#
74# Revision 1.119  2004/09/02 13:09:58  jalet
75# Now exports PYKOTAPRINTERHOSTNAME
76#
77# Revision 1.118  2004/08/06 20:46:45  jalet
78# Finished group quota fix for balance when no user in group has a balance
79#
80# Revision 1.117  2004/08/06 13:45:51  jalet
81# Fixed french translation problem.
82# Fixed problem with group quotas and strict enforcement.
83#
84# Revision 1.116  2004/07/24 20:20:29  jalet
85# Unitialized variable
86#
87# Revision 1.115  2004/07/21 09:35:48  jalet
88# Software accounting seems to be OK with LPRng support now
89#
90# Revision 1.114  2004/07/20 22:19:45  jalet
91# Sanitized a bit + use of gettext
92#
93# Revision 1.113  2004/07/17 20:37:27  jalet
94# Missing file... Am I really stupid ?
95#
96# Revision 1.112  2004/07/16 12:22:47  jalet
97# LPRng support early version
98#
99# Revision 1.111  2004/07/06 18:09:42  jalet
100# Reduced the set of invalid characters in names
101#
102# Revision 1.110  2004/07/01 19:56:42  jalet
103# Better dispatching of error messages
104#
105# Revision 1.109  2004/07/01 17:45:49  jalet
106# Added code to handle the description field for printers
107#
108# Revision 1.108  2004/06/24 23:09:30  jalet
109# Also prints read size on last block
110#
111# Revision 1.107  2004/06/23 13:03:28  jalet
112# Catches accounter configuration errors earlier
113#
114# Revision 1.106  2004/06/22 09:31:18  jalet
115# Always send some debug info to CUPS' back channel stream (stderr) as
116# informationnal messages.
117#
118# Revision 1.105  2004/06/21 08:17:38  jalet
119# Added version number in subject message for directive crashrecipient.
120#
121# Revision 1.104  2004/06/18 13:34:49  jalet
122# Now all tracebacks include PyKota's version number
123#
124# Revision 1.103  2004/06/18 13:17:26  jalet
125# Now includes PyKota's version number in messages sent by the crashrecipient
126# directive.
127#
128# Revision 1.102  2004/06/17 13:26:51  jalet
129# Better exception handling code
130#
131# Revision 1.101  2004/06/16 20:56:34  jalet
132# Smarter initialisation code
133#
134# Revision 1.100  2004/06/11 08:16:03  jalet
135# More exceptions catched in case of very early failure.
136#
137# Revision 1.99  2004/06/11 07:07:38  jalet
138# Now detects and logs configuration syntax errors instead of failing without
139# any notice message.
140#
141# Revision 1.98  2004/06/08 19:27:12  jalet
142# Doesn't ignore SIGCHLD anymore
143#
144# Revision 1.97  2004/06/07 22:45:35  jalet
145# Now accepts a job when enforcement is STRICT and predicted account balance
146# is equal to 0.0 : since the job hasn't been printed yet, only its printing
147# will really render balance equal to 0.0, so we should be allowed to print.
148#
149# Revision 1.96  2004/06/05 22:18:04  jalet
150# Now catches some exceptions earlier.
151# storage.py and ldapstorage.py : removed old comments
152#
153# Revision 1.95  2004/06/03 21:50:34  jalet
154# Improved error logging.
155# crashrecipient directive added.
156# Now exports the job's size in bytes too.
157#
158# Revision 1.94  2004/06/03 08:51:03  jalet
159# logs job's size in bytes now
160#
161# Revision 1.93  2004/06/02 21:51:02  jalet
162# Moved the sigterm capturing elsewhere
163#
164# Revision 1.92  2004/06/02 13:21:38  jalet
165# Debug message added
166#
167# Revision 1.91  2004/05/25 05:17:52  jalet
168# Now precomputes the job's size only if current printer's enforcement
169# is "STRICT"
170#
171# Revision 1.90  2004/05/24 22:45:49  jalet
172# New 'enforcement' directive added
173# Polling loop improvements
174#
175# Revision 1.89  2004/05/21 22:02:52  jalet
176# Preliminary work on pre-accounting
177#
178# Revision 1.88  2004/05/18 14:49:20  jalet
179# Big code changes to completely remove the need for "requester" directives,
180# jsut use "hardware(... your previous requester directive's content ...)"
181#
182# Revision 1.87  2004/05/17 19:14:59  jalet
183# Now catches SIGPIPE and SIGCHLD
184#
185# Revision 1.86  2004/05/13 13:59:28  jalet
186# Code simplifications
187#
188# Revision 1.85  2004/05/11 08:26:27  jalet
189# Now catches connection problems to SMTP server
190#
191# Revision 1.84  2004/04/21 08:36:32  jalet
192# Exports the PYKOTASTATUS environment variable when SIGTERM is received.
193#
194# Revision 1.83  2004/04/16 17:03:49  jalet
195# The list of printers groups the current printer is a member of is
196# now exported in the PYKOTAPGROUPS environment variable
197#
198# Revision 1.82  2004/04/13 09:38:03  jalet
199# More work on correct child processes handling
200#
201# Revision 1.81  2004/04/09 22:24:47  jalet
202# Began work on correct handling of child processes when jobs are cancelled by
203# the user. Especially important when an external requester is running for a
204# long time.
205#
206# Revision 1.80  2004/04/06 12:00:21  jalet
207# uninitialized values caused problems
208#
209# Revision 1.79  2004/03/28 21:01:29  jalet
210# PYKOTALIMITBY environment variable is now exported too
211#
212# Revision 1.78  2004/03/08 20:13:25  jalet
213# Allow names to begin with a digit
214#
215# Revision 1.77  2004/03/03 13:10:35  jalet
216# Now catches all smtplib exceptions when there's a problem sending messages
217#
218# Revision 1.76  2004/03/01 14:34:15  jalet
219# PYKOTAPHASE wasn't set at the right time at the end of data transmission
220# to underlying layer (real backend)
221#
222# Revision 1.75  2004/03/01 11:23:25  jalet
223# Pre and Post hooks to external commands are available in the cupspykota
224# backend. Forthe pykota filter they will be implemented real soon now.
225#
226# Revision 1.74  2004/02/26 14:18:07  jalet
227# Should fix the remaining bugs wrt printers groups and users groups.
228#
229# Revision 1.73  2004/02/19 14:20:21  jalet
230# maildomain pykota.conf directive added.
231# Small improvements on mail headers quality.
232#
233# Revision 1.72  2004/01/14 15:51:19  jalet
234# Docstring added.
235#
236# Revision 1.71  2004/01/11 23:22:42  jalet
237# Major code refactoring, it's way cleaner, and now allows automated addition
238# of printers on first print.
239#
240# Revision 1.70  2004/01/08 14:10:32  jalet
241# Copyright year changed.
242#
243# Revision 1.69  2004/01/05 16:02:18  jalet
244# Dots in user, groups and printer names should be allowed.
245#
246# Revision 1.68  2004/01/02 17:38:40  jalet
247# This time it should be better...
248#
249# Revision 1.67  2004/01/02 17:37:09  jalet
250# I'm completely stupid !!! Better to not talk while coding !
251#
252# Revision 1.66  2004/01/02 17:31:26  jalet
253# Forgot to remove some code
254#
255# Revision 1.65  2003/12/06 08:14:38  jalet
256# Added support for CUPS device uris which contain authentication information.
257#
258# Revision 1.64  2003/11/29 22:03:17  jalet
259# Some code refactoring work. New code is not used at this time.
260#
261# Revision 1.63  2003/11/29 20:06:20  jalet
262# Added 'utolower' configuration option to convert all usernames to
263# lowercase when printing. All database accesses are still and will
264# remain case sensitive though.
265#
266# Revision 1.62  2003/11/25 22:37:22  jalet
267# Small code move
268#
269# Revision 1.61  2003/11/25 22:03:28  jalet
270# No more message on stderr when the translation is not available.
271#
272# Revision 1.60  2003/11/25 21:54:05  jalet
273# updated FAQ
274#
275# Revision 1.59  2003/11/25 13:33:43  jalet
276# Puts 'root' instead of '' when printing from CUPS web interface (which
277# gives an empty username)
278#
279# Revision 1.58  2003/11/21 14:28:45  jalet
280# More complete job history.
281#
282# Revision 1.57  2003/11/19 23:19:38  jalet
283# Code refactoring work.
284# Explicit redirection to /dev/null has to be set in external policy now, just
285# like in external mailto.
286#
287# Revision 1.56  2003/11/19 07:40:20  jalet
288# Missing import statement.
289# Better documentation for mailto: external(...)
290#
291# Revision 1.55  2003/11/18 23:43:12  jalet
292# Mailto can be any external command now, as usual.
293#
294# Revision 1.54  2003/10/24 21:52:46  jalet
295# Now can force language when coming from CGI script.
296#
297# Revision 1.53  2003/10/08 21:41:38  jalet
298# External policies for printers works !
299# We can now auto-add users on first print, and do other useful things if needed.
300#
301# Revision 1.52  2003/10/07 09:07:28  jalet
302# Character encoding added to please latest version of Python
303#
304# Revision 1.51  2003/10/06 14:21:41  jalet
305# Test reversed to not retrieve group members when no messages for them.
306#
307# Revision 1.50  2003/10/02 20:23:18  jalet
308# Storage caching mechanism added.
309#
310# Revision 1.49  2003/07/29 20:55:17  jalet
311# 1.14 is out !
312#
313# Revision 1.48  2003/07/21 23:01:56  jalet
314# Modified some messages aout soft limit
315#
316# Revision 1.47  2003/07/16 21:53:08  jalet
317# Really big modifications wrt new configuration file's location and content.
318#
319# Revision 1.46  2003/07/09 20:17:07  jalet
320# Email field added to PostgreSQL schema
321#
322# Revision 1.45  2003/07/08 19:43:51  jalet
323# Configurable warning messages.
324# Poor man's treshold value added.
325#
326# Revision 1.44  2003/07/07 11:49:24  jalet
327# Lots of small fixes with the help of PyChecker
328#
329# Revision 1.43  2003/07/04 09:06:32  jalet
330# Small bug fix wrt undefined "LimitBy" field.
331#
332# Revision 1.42  2003/06/30 12:46:15  jalet
333# Extracted reporting code.
334#
335# Revision 1.41  2003/06/25 14:10:01  jalet
336# Hey, it may work (edpykota --reset excepted) !
337#
338# Revision 1.40  2003/06/10 16:37:54  jalet
339# Deletion of the second user which is not needed anymore.
340# Added a debug configuration field in /etc/pykota.conf
341# All queries can now be sent to the logger in debug mode, this will
342# greatly help improve performance when time for this will come.
343#
344# Revision 1.39  2003/04/29 18:37:54  jalet
345# Pluggable accounting methods (actually doesn't support external scripts)
346#
347# Revision 1.38  2003/04/24 11:53:48  jalet
348# Default policy for unknown users/groups is to DENY printing instead
349# of the previous default to ALLOW printing. This is to solve an accuracy
350# problem. If you set the policy to ALLOW, jobs printed by in nexistant user
351# (from PyKota's POV) will be charged to the next user who prints on the
352# same printer.
353#
354# Revision 1.37  2003/04/24 08:08:27  jalet
355# Debug message forgotten
356#
357# Revision 1.36  2003/04/24 07:59:40  jalet
358# LPRng support now works !
359#
360# Revision 1.35  2003/04/23 22:13:57  jalet
361# Preliminary support for LPRng added BUT STILL UNTESTED.
362#
363# Revision 1.34  2003/04/17 09:26:21  jalet
364# repykota now reports account balances too.
365#
366# Revision 1.33  2003/04/16 12:35:49  jalet
367# Groups quota work now !
368#
369# Revision 1.32  2003/04/16 08:53:14  jalet
370# Printing can now be limited either by user's account balance or by
371# page quota (the default). Quota report doesn't include account balance
372# yet, though.
373#
374# Revision 1.31  2003/04/15 11:30:57  jalet
375# More work done on money print charging.
376# Minor bugs corrected.
377# All tools now access to the storage as priviledged users, repykota excepted.
378#
379# Revision 1.30  2003/04/10 21:47:20  jalet
380# Job history added. Upgrade script neutralized for now !
381#
382# Revision 1.29  2003/03/29 13:45:27  jalet
383# GPL paragraphs were incorrectly (from memory) copied into the sources.
384# Two README files were added.
385# Upgrade script for PostgreSQL pre 1.01 schema was added.
386#
387# Revision 1.28  2003/03/29 13:08:28  jalet
388# Configuration is now expected to be found in /etc/pykota.conf instead of
389# in /etc/cups/pykota.conf
390# Installation script can move old config files to the new location if needed.
391# Better error handling if configuration file is absent.
392#
393# Revision 1.27  2003/03/15 23:01:28  jalet
394# New mailto option in configuration file added.
395# No time to test this tonight (although it should work).
396#
397# Revision 1.26  2003/03/09 23:58:16  jalet
398# Comment
399#
400# Revision 1.25  2003/03/07 22:56:14  jalet
401# 0.99 is out with some bug fixes.
402#
403# Revision 1.24  2003/02/27 23:48:41  jalet
404# Correctly maps PyKota's log levels to syslog log levels
405#
406# Revision 1.23  2003/02/27 22:55:20  jalet
407# WARN log priority doesn't exist.
408#
409# Revision 1.22  2003/02/27 09:09:20  jalet
410# Added a method to match strings against wildcard patterns
411#
412# Revision 1.21  2003/02/17 23:01:56  jalet
413# Typos
414#
415# Revision 1.20  2003/02/17 22:55:01  jalet
416# More options can now be set per printer or globally :
417#
418#       admin
419#       adminmail
420#       gracedelay
421#       requester
422#
423# the printer option has priority when both are defined.
424#
425# Revision 1.19  2003/02/10 11:28:45  jalet
426# Localization
427#
428# Revision 1.18  2003/02/10 01:02:17  jalet
429# External requester is about to work, but I must sleep
430#
431# Revision 1.17  2003/02/09 13:05:43  jalet
432# Internationalization continues...
433#
434# Revision 1.16  2003/02/09 12:56:53  jalet
435# Internationalization begins...
436#
437# Revision 1.15  2003/02/08 22:09:52  jalet
438# Name check method moved here
439#
440# Revision 1.14  2003/02/07 10:42:45  jalet
441# Indentation problem
442#
443# Revision 1.13  2003/02/07 08:34:16  jalet
444# Test wrt date limit was wrong
445#
446# Revision 1.12  2003/02/06 23:20:02  jalet
447# warnpykota doesn't need any user/group name argument, mimicing the
448# warnquota disk quota tool.
449#
450# Revision 1.11  2003/02/06 22:54:33  jalet
451# warnpykota should be ok
452#
453# Revision 1.10  2003/02/06 15:03:11  jalet
454# added a method to set the limit date
455#
456# Revision 1.9  2003/02/06 10:39:23  jalet
457# Preliminary edpykota work.
458#
459# Revision 1.8  2003/02/06 09:19:02  jalet
460# More robust behavior (hopefully) when the user or printer is not managed
461# correctly by the Quota System : e.g. cupsFilter added in ppd file, but
462# printer and/or user not 'yet?' in storage.
463#
464# Revision 1.7  2003/02/06 00:00:45  jalet
465# Now includes the printer name in email messages
466#
467# Revision 1.6  2003/02/05 23:55:02  jalet
468# Cleaner email messages
469#
470# Revision 1.5  2003/02/05 23:45:09  jalet
471# Better DateTime manipulation wrt grace delay
472#
473# Revision 1.4  2003/02/05 23:26:22  jalet
474# Incorrect handling of grace delay
475#
476# Revision 1.3  2003/02/05 22:16:20  jalet
477# DEVICE_URI is undefined outside of CUPS, i.e. for normal command line tools
478#
479# Revision 1.2  2003/02/05 22:10:29  jalet
480# Typos
481#
482# Revision 1.1  2003/02/05 21:28:17  jalet
483# Initial import into CVS
484#
485#
486#
487
488import sys
489import os
490import fnmatch
491import getopt
492import smtplib
493import gettext
494import locale
495import signal
496import socket
497import tempfile
498import md5
499import ConfigParser
500
501from mx import DateTime
502
503from pykota import version, config, storage, logger, accounter, pdlanalyzer
504
505def N_(message) :
506    """Fake translation marker for translatable strings extraction."""
507    return message
508
509class PyKotaToolError(Exception):
510    """An exception for PyKota config related stuff."""
511    def __init__(self, message = ""):
512        self.message = message
513        Exception.__init__(self, message)
514    def __repr__(self):
515        return self.message
516    __str__ = __repr__
517   
518def crashed(message) :   
519    """Minimal crash method."""
520    import traceback
521    lines = []
522    for line in traceback.format_exception(*sys.exc_info()) :
523        lines.extend([l for l in line.split("\n") if l])
524    msg = "ERROR: ".join(["%s\n" % l for l in (["ERROR: PyKota v%s" % version.__version__, message] + lines)])
525    sys.stderr.write(msg)
526    sys.stderr.flush()
527    return msg
528
529class PyKotaTool :   
530    """Base class for all PyKota command line tools."""
531    def __init__(self, lang="", charset=None, doc="PyKota %s (c) 2003-2004 %s" % (version.__version__, version.__author__)) :
532        """Initializes the command line tool."""
533        # locale stuff
534        try :
535            locale.setlocale(locale.LC_ALL, lang)
536            gettext.install("pykota")
537        except (locale.Error, IOError) :
538            gettext.NullTranslations().install()
539           
540        # We can force the charset.   
541        # The CHARSET environment variable is set by CUPS when printing.
542        # Else we use the current locale's one.
543        # If nothing is set, we use ISO-8859-15 widely used in western Europe.
544        localecharset = locale.getlocale()[1]
545        try :
546            localecharset = localecharset or locale.getdefaultlocale()[1]
547        except ValueError :   
548            pass        # Unknown locale, strange...
549        self.charset = charset or os.environ.get("CHARSET") or localecharset or "ISO-8859-15"
550   
551        # pykota specific stuff
552        self.documentation = doc
553        try :
554            self.config = config.PyKotaConfig("/etc/pykota")
555        except ConfigParser.ParsingError, msg :   
556            sys.stderr.write("ERROR: Problem encountered while parsing configuration file : %s\n" % msg)
557            sys.stderr.flush()
558            sys.exit(-1)
559           
560        try :
561            self.debug = self.config.getDebug()
562            self.smtpserver = self.config.getSMTPServer()
563            self.maildomain = self.config.getMailDomain()
564            self.logger = logger.openLogger(self.config.getLoggingBackend())
565            self.storage = storage.openConnection(self)
566        except (config.PyKotaConfigError, logger.PyKotaLoggingError, storage.PyKotaStorageError), msg :
567            self.crashed(msg)
568            raise
569        self.softwareJobSize = 0
570        self.softwareJobPrice = 0.0
571        self.logdebug("Charset in use : %s" % self.charset)
572       
573    def getCharset(self) :   
574        """Returns the charset in use."""
575        return self.charset
576       
577    def logdebug(self, message) :   
578        """Logs something to debug output if debug is enabled."""
579        if self.debug :
580            self.logger.log_message(message, "debug")
581           
582    def printInfo(self, message, level="info") :       
583        """Sends a message to standard error."""
584        sys.stderr.write("%s: %s\n" % (level.upper(), message))
585        sys.stderr.flush()
586       
587    def clean(self) :   
588        """Ensures that the database is closed."""
589        try :
590            self.storage.close()
591        except (TypeError, NameError, AttributeError) :   
592            pass
593           
594    def display_version_and_quit(self) :
595        """Displays version number, then exists successfully."""
596        self.clean()
597        print version.__version__
598        sys.exit(0)
599   
600    def display_usage_and_quit(self) :
601        """Displays command line usage, then exists successfully."""
602        self.clean()
603        print _(self.documentation) % (version.__version__, version.__author__)
604        sys.exit(0)
605       
606    def crashed(self, message) :   
607        """Outputs a crash message, and optionally sends it to software author."""
608        msg = crashed(message)
609        try :
610            crashrecipient = self.config.getCrashRecipient()
611            if crashrecipient :
612                admin = self.config.getAdminMail("global") # Nice trick, isn't it ?
613                fullmessage = "========== Traceback :\n\n%s\n\n========== sys.argv :\n\n%s\n\n========== Environment :\n\n%s\n" % \
614                                (msg, \
615                                 "\n".join(["    %s" % repr(a) for a in sys.argv]), \
616                                 "\n".join(["    %s=%s" % (k, v) for (k, v) in os.environ.items()]))
617                server = smtplib.SMTP(self.smtpserver)
618                server.sendmail(admin, [admin, crashrecipient], \
619                                       "From: %s\nTo: %s\nCc: %s\nSubject: PyKota v%s crash traceback !\n\n%s" % \
620                                       (admin, crashrecipient, admin, version.__version__, fullmessage))
621                server.quit()
622        except :
623            pass
624       
625    def parseCommandline(self, argv, short, long, allownothing=0) :
626        """Parses the command line, controlling options."""
627        # split options in two lists: those which need an argument, those which don't need any
628        withoutarg = []
629        witharg = []
630        lgs = len(short)
631        i = 0
632        while i < lgs :
633            ii = i + 1
634            if (ii < lgs) and (short[ii] == ':') :
635                # needs an argument
636                witharg.append(short[i])
637                ii = ii + 1 # skip the ':'
638            else :
639                # doesn't need an argument
640                withoutarg.append(short[i])
641            i = ii
642               
643        for option in long :
644            if option[-1] == '=' :
645                # needs an argument
646                witharg.append(option[:-1])
647            else :
648                # doesn't need an argument
649                withoutarg.append(option)
650       
651        # we begin with all possible options unset
652        parsed = {}
653        for option in withoutarg + witharg :
654            parsed[option] = None
655       
656        # then we parse the command line
657        args = []       # to not break if something unexpected happened
658        try :
659            options, args = getopt.getopt(argv, short, long)
660            if options :
661                for (o, v) in options :
662                    # we skip the '-' chars
663                    lgo = len(o)
664                    i = 0
665                    while (i < lgo) and (o[i] == '-') :
666                        i = i + 1
667                    o = o[i:]
668                    if o in witharg :
669                        # needs an argument : set it
670                        parsed[o] = v
671                    elif o in withoutarg :
672                        # doesn't need an argument : boolean
673                        parsed[o] = 1
674                    else :
675                        # should never occur
676                        raise PyKotaToolError, "Unexpected problem when parsing command line"
677            elif (not args) and (not allownothing) and sys.stdin.isatty() : # no option and no argument, we display help if we are a tty
678                self.display_usage_and_quit()
679        except getopt.error, msg :
680            self.printInfo(msg)
681            self.display_usage_and_quit()
682        return (parsed, args)
683   
684    def isValidName(self, name) :
685        """Checks if a user or printer name is valid."""
686        invalidchars = "/@?*,;&|"
687        for c in list(invalidchars) :
688            if c in name :
689                return 0
690        return 1       
691       
692    def matchString(self, s, patterns) :
693        """Returns 1 if the string s matches one of the patterns, else 0."""
694        for pattern in patterns :
695            if fnmatch.fnmatchcase(s, pattern) :
696                return 1
697        return 0
698       
699    def sendMessage(self, adminmail, touser, fullmessage) :
700        """Sends an email message containing headers to some user."""
701        if "@" not in touser :
702            touser = "%s@%s" % (touser, self.maildomain or self.smtpserver)
703        try :   
704            server = smtplib.SMTP(self.smtpserver)
705        except socket.error, msg :   
706            self.printInfo(_("Impossible to connect to SMTP server : %s") % msg, "error")
707        else :
708            try :
709                server.sendmail(adminmail, [touser], "From: %s\nTo: %s\n%s" % (adminmail, touser, fullmessage))
710            except smtplib.SMTPException, answer :   
711                for (k, v) in answer.recipients.items() :
712                    self.printInfo(_("Impossible to send mail to %s, error %s : %s") % (k, v[0], v[1]), "error")
713            server.quit()
714       
715    def sendMessageToUser(self, admin, adminmail, user, subject, message) :
716        """Sends an email message to a user."""
717        message += _("\n\nPlease contact your system administrator :\n\n\t%s - <%s>\n") % (admin, adminmail)
718        self.sendMessage(adminmail, user.Email or user.Name, "Subject: %s\n\n%s" % (subject, message))
719       
720    def sendMessageToAdmin(self, adminmail, subject, message) :
721        """Sends an email message to the Print Quota administrator."""
722        self.sendMessage(adminmail, adminmail, "Subject: %s\n\n%s" % (subject, message))
723       
724    def _checkUserPQuota(self, userpquota) :           
725        """Checks the user quota on a printer and deny or accept the job."""
726        # then we check the user's own quota
727        # if we get there we are sure that policy is not EXTERNAL
728        user = userpquota.User
729        printer = userpquota.Printer
730        enforcement = self.config.getPrinterEnforcement(printer.Name)
731        self.logdebug("Checking user %s's quota on printer %s" % (user.Name, printer.Name))
732        (policy, dummy) = self.config.getPrinterPolicy(userpquota.Printer.Name)
733        if not userpquota.Exists :
734            # Unknown userquota
735            if policy == "ALLOW" :
736                action = "POLICY_ALLOW"
737            else :   
738                action = "POLICY_DENY"
739            self.printInfo(_("Unable to match user %s on printer %s, applying default policy (%s)") % (user.Name, printer.Name, action))
740        else :   
741            pagecounter = int(userpquota.PageCounter or 0)
742            if enforcement == "STRICT" :
743                pagecounter += self.softwareJobSize
744            if userpquota.SoftLimit is not None :
745                softlimit = int(userpquota.SoftLimit)
746                if pagecounter < softlimit :
747                    action = "ALLOW"
748                else :   
749                    if userpquota.HardLimit is None :
750                        # only a soft limit, this is equivalent to having only a hard limit
751                        action = "DENY"
752                    else :   
753                        hardlimit = int(userpquota.HardLimit)
754                        if softlimit <= pagecounter < hardlimit :   
755                            now = DateTime.now()
756                            if userpquota.DateLimit is not None :
757                                datelimit = DateTime.ISO.ParseDateTime(userpquota.DateLimit)
758                            else :
759                                datelimit = now + self.config.getGraceDelay(printer.Name)
760                                userpquota.setDateLimit(datelimit)
761                            if now < datelimit :
762                                action = "WARN"
763                            else :   
764                                action = "DENY"
765                        else :         
766                            action = "DENY"
767            else :       
768                if userpquota.HardLimit is not None :
769                    # no soft limit, only a hard one.
770                    hardlimit = int(userpquota.HardLimit)
771                    if pagecounter < hardlimit :
772                        action = "ALLOW"
773                    else :     
774                        action = "DENY"
775                else :
776                    # Both are unset, no quota, i.e. accounting only
777                    action = "ALLOW"
778        return action
779   
780    def checkGroupPQuota(self, grouppquota) :   
781        """Checks the group quota on a printer and deny or accept the job."""
782        group = grouppquota.Group
783        printer = grouppquota.Printer
784        enforcement = self.config.getPrinterEnforcement(printer.Name)
785        self.logdebug("Checking group %s's quota on printer %s" % (group.Name, printer.Name))
786        if group.LimitBy and (group.LimitBy.lower() == "balance") : 
787            val = group.AccountBalance or 0.0
788            if enforcement == "STRICT" : 
789                val -= self.softwareJobPrice # use precomputed size.
790            if val <= 0.0 :
791                action = "DENY"
792            elif val <= self.config.getPoorMan() :   
793                action = "WARN"
794            else :   
795                action = "ALLOW"
796            if (enforcement == "STRICT") and (val == 0.0) :
797                action = "WARN" # we can still print until account is 0
798        else :
799            val = grouppquota.PageCounter or 0
800            if enforcement == "STRICT" :
801                val += self.softwareJobSize
802            if grouppquota.SoftLimit is not None :
803                softlimit = int(grouppquota.SoftLimit)
804                if val < softlimit :
805                    action = "ALLOW"
806                else :   
807                    if grouppquota.HardLimit is None :
808                        # only a soft limit, this is equivalent to having only a hard limit
809                        action = "DENY"
810                    else :   
811                        hardlimit = int(grouppquota.HardLimit)
812                        if softlimit <= val < hardlimit :   
813                            now = DateTime.now()
814                            if grouppquota.DateLimit is not None :
815                                datelimit = DateTime.ISO.ParseDateTime(grouppquota.DateLimit)
816                            else :
817                                datelimit = now + self.config.getGraceDelay(printer.Name)
818                                grouppquota.setDateLimit(datelimit)
819                            if now < datelimit :
820                                action = "WARN"
821                            else :   
822                                action = "DENY"
823                        else :         
824                            action = "DENY"
825            else :       
826                if grouppquota.HardLimit is not None :
827                    # no soft limit, only a hard one.
828                    hardlimit = int(grouppquota.HardLimit)
829                    if val < hardlimit :
830                        action = "ALLOW"
831                    else :     
832                        action = "DENY"
833                else :
834                    # Both are unset, no quota, i.e. accounting only
835                    action = "ALLOW"
836        return action
837   
838    def checkUserPQuota(self, userpquota) :
839        """Checks the user quota on a printer and all its parents and deny or accept the job."""
840        user = userpquota.User
841        printer = userpquota.Printer
842       
843        # indicates that a warning needs to be sent
844        warned = 0               
845       
846        # first we check any group the user is a member of
847        for group in self.storage.getUserGroups(user) :
848            grouppquota = self.storage.getGroupPQuota(group, printer)
849            # for the printer and all its parents
850            for gpquota in [ grouppquota ] + grouppquota.ParentPrintersGroupPQuota :
851                if gpquota.Exists :
852                    action = self.checkGroupPQuota(gpquota)
853                    if action == "DENY" :
854                        return action
855                    elif action == "WARN" :   
856                        warned = 1
857                       
858        # Then we check the user's account balance
859        # if we get there we are sure that policy is not EXTERNAL
860        (policy, dummy) = self.config.getPrinterPolicy(printer.Name)
861        if user.LimitBy and (user.LimitBy.lower() == "balance") : 
862            self.logdebug("Checking account balance for user %s" % user.Name)
863            if user.AccountBalance is None :
864                if policy == "ALLOW" :
865                    action = "POLICY_ALLOW"
866                else :   
867                    action = "POLICY_DENY"
868                self.printInfo(_("Unable to find user %s's account balance, applying default policy (%s) for printer %s") % (user.Name, action, printer.Name))
869                return action       
870            else :   
871                val = float(user.AccountBalance or 0.0)
872                enforcement = self.config.getPrinterEnforcement(printer.Name)
873                if enforcement == "STRICT" : 
874                    val -= self.softwareJobPrice # use precomputed size.
875                if val <= 0.0 :
876                    action = "DENY"
877                elif val <= self.config.getPoorMan() :   
878                    action = "WARN"
879                else :
880                    action = "ALLOW"
881                if (enforcement == "STRICT") and (val == 0.0) :
882                    action = "WARN" # we can still print until account is 0
883                return action   
884        else :
885            # Then check the user quota on current printer and all its parents.               
886            policyallowed = 0
887            for upquota in [ userpquota ] + userpquota.ParentPrintersUserPQuota :               
888                action = self._checkUserPQuota(upquota)
889                if action in ("DENY", "POLICY_DENY") :
890                    return action
891                elif action == "WARN" :   
892                    warned = 1
893                elif action == "POLICY_ALLOW" :   
894                    policyallowed = 1
895            if warned :       
896                return "WARN"
897            elif policyallowed :   
898                return "POLICY_ALLOW" 
899            else :   
900                return "ALLOW"
901               
902    def externalMailTo(self, cmd, action, user, printer, message) :
903        """Warns the user with an external command."""
904        username = user.Name
905        printername = printer.Name
906        email = user.Email or user.Name
907        if "@" not in email :
908            email = "%s@%s" % (email, self.maildomain or self.smtpserver)
909        os.system(cmd % locals())
910   
911    def formatCommandLine(self, cmd, user, printer) :
912        """Executes an external command."""
913        username = user.Name
914        printername = printer.Name
915        return cmd % locals()
916       
917    def warnGroupPQuota(self, grouppquota) :
918        """Checks a group quota and send messages if quota is exceeded on current printer."""
919        group = grouppquota.Group
920        printer = grouppquota.Printer
921        admin = self.config.getAdmin(printer.Name)
922        adminmail = self.config.getAdminMail(printer.Name)
923        (mailto, arguments) = self.config.getMailTo(printer.Name)
924        action = self.checkGroupPQuota(grouppquota)
925        if action.startswith("POLICY_") :
926            action = action[7:]
927        if action == "DENY" :
928            adminmessage = _("Print Quota exceeded for group %s on printer %s") % (group.Name, printer.Name)
929            self.printInfo(adminmessage)
930            if mailto in [ "BOTH", "ADMIN" ] :
931                self.sendMessageToAdmin(adminmail, _("Print Quota"), adminmessage)
932            if mailto in [ "BOTH", "USER", "EXTERNAL" ] :
933                for user in self.storage.getGroupMembers(group) :
934                    if mailto != "EXTERNAL" :
935                        self.sendMessageToUser(admin, adminmail, user, _("Print Quota Exceeded"), self.config.getHardWarn(printer.Name))
936                    else :   
937                        self.externalMailTo(arguments, action, user, printer, self.config.getHardWarn(printer.Name))
938        elif action == "WARN" :   
939            adminmessage = _("Print Quota low for group %s on printer %s") % (group.Name, printer.Name)
940            self.printInfo(adminmessage)
941            if mailto in [ "BOTH", "ADMIN" ] :
942                self.sendMessageToAdmin(adminmail, _("Print Quota"), adminmessage)
943            if group.LimitBy and (group.LimitBy.lower() == "balance") : 
944                message = self.config.getPoorWarn()
945            else :     
946                message = self.config.getSoftWarn(printer.Name)
947            if mailto in [ "BOTH", "USER", "EXTERNAL" ] :
948                for user in self.storage.getGroupMembers(group) :
949                    if mailto != "EXTERNAL" :
950                        self.sendMessageToUser(admin, adminmail, user, _("Print Quota Exceeded"), message)
951                    else :   
952                        self.externalMailTo(arguments, action, user, printer, message)
953        return action       
954       
955    def warnUserPQuota(self, userpquota) :
956        """Checks a user quota and send him a message if quota is exceeded on current printer."""
957        user = userpquota.User
958        printer = userpquota.Printer
959        admin = self.config.getAdmin(printer.Name)
960        adminmail = self.config.getAdminMail(printer.Name)
961        (mailto, arguments) = self.config.getMailTo(printer.Name)
962        action = self.checkUserPQuota(userpquota)
963        if action.startswith("POLICY_") :
964            action = action[7:]
965        if action == "DENY" :
966            adminmessage = _("Print Quota exceeded for user %s on printer %s") % (user.Name, printer.Name)
967            self.printInfo(adminmessage)
968            if mailto in [ "BOTH", "USER", "EXTERNAL" ] :
969                message = self.config.getHardWarn(printer.Name)
970                if mailto != "EXTERNAL" :
971                    self.sendMessageToUser(admin, adminmail, user, _("Print Quota Exceeded"), message)
972                else :   
973                    self.externalMailTo(arguments, action, user, printer, message)
974            if mailto in [ "BOTH", "ADMIN" ] :
975                self.sendMessageToAdmin(adminmail, _("Print Quota"), adminmessage)
976        elif action == "WARN" :   
977            adminmessage = _("Print Quota low for user %s on printer %s") % (user.Name, printer.Name)
978            self.printInfo(adminmessage)
979            if mailto in [ "BOTH", "USER", "EXTERNAL" ] :
980                if user.LimitBy and (user.LimitBy.lower() == "balance") : 
981                    message = self.config.getPoorWarn()
982                else :     
983                    message = self.config.getSoftWarn(printer.Name)
984                if mailto != "EXTERNAL" :   
985                    self.sendMessageToUser(admin, adminmail, user, _("Print Quota Low"), message)
986                else :   
987                    self.externalMailTo(arguments, action, user, printer, message)
988            if mailto in [ "BOTH", "ADMIN" ] :
989                self.sendMessageToAdmin(adminmail, _("Print Quota"), adminmessage)
990        return action       
991       
992class PyKotaFilterOrBackend(PyKotaTool) :   
993    """Class for the PyKota filter or backend."""
994    def __init__(self) :
995        """Initialize local datas from current environment."""
996        # We begin with ignoring signals, we may de-ignore them later on.
997        self.gotSigTerm = 0
998        signal.signal(signal.SIGTERM, signal.SIG_IGN)
999        # signal.signal(signal.SIGCHLD, signal.SIG_IGN)
1000        signal.signal(signal.SIGPIPE, signal.SIG_IGN)
1001       
1002        PyKotaTool.__init__(self)
1003        (self.printingsystem, \
1004         self.printerhostname, \
1005         self.printername, \
1006         self.username, \
1007         self.jobid, \
1008         self.inputfile, \
1009         self.copies, \
1010         self.title, \
1011         self.options, \
1012         self.originalbackend) = self.extractInfoFromCupsOrLprng()
1013         
1014        arguments = " ".join(['"%s"' % arg for arg in sys.argv])
1015        self.logdebug(_("Printing system %s, args=%s") % (str(self.printingsystem), arguments))
1016       
1017        self.username = self.username or 'root' # when printing test page from CUPS web interface, username is empty
1018       
1019        # do we want to strip out the Samba/Winbind domain name ?
1020        separator = self.config.getWinbindSeparator()
1021        if separator is not None :
1022            self.username = self.username.split(separator)[-1]
1023           
1024        # do we want to lowercase usernames ?   
1025        if self.config.getUserNameToLower() :
1026            self.username = self.username.lower()
1027           
1028        self.preserveinputfile = self.inputfile 
1029        try :
1030            self.accounter = accounter.openAccounter(self)
1031        except (config.PyKotaConfigError, accounter.PyKotaAccounterError), msg :   
1032            self.crashed(msg)
1033            raise
1034        self.exportJobInfo()
1035        self.jobdatastream = self.openJobDataStream()
1036        self.checksum = self.computeChecksum()
1037        os.environ["PYKOTAJOBSIZEBYTES"] = str(self.jobSizeBytes)
1038        self.logdebug("Job size is %s bytes" % self.jobSizeBytes)
1039        self.logdebug("Capturing SIGTERM events.")
1040        signal.signal(signal.SIGTERM, self.sigterm_handler)
1041       
1042    def sendBackChannelData(self, message, level="info") :   
1043        """Sends an informational message to CUPS via back channel stream (stderr)."""
1044        sys.stderr.write("%s: PyKota (PID %s) : %s\n" % (level.upper(), os.getpid(), message.strip()))
1045        sys.stderr.flush()
1046       
1047    def computeChecksum(self) :   
1048        """Computes the MD5 checksum of the job's datas, to be able to detect and forbid duplicate jobs."""
1049        self.logdebug("Computing MD5 checksum for job %s" % self.jobid)
1050        MEGABYTE = 1024*1024
1051        checksum = md5.new()
1052        while 1 :
1053            data = self.jobdatastream.read(MEGABYTE) 
1054            if not data :
1055                break
1056            checksum.update(data)   
1057        self.jobdatastream.seek(0)
1058        digest = checksum.hexdigest()
1059        self.logdebug("MD5 checksum for job %s is %s" % (self.jobid, digest))
1060        os.environ["PYKOTAMD5SUM"] = digest
1061        return digest
1062       
1063    def openJobDataStream(self) :   
1064        """Opens the file which contains the job's datas."""
1065        if self.preserveinputfile is None :
1066            # Job comes from sys.stdin, but this is not
1067            # seekable and complexifies our task, so create
1068            # a temporary file and use it instead
1069            self.logdebug("Duplicating data stream from stdin to temporary file")
1070            dummy = 0
1071            MEGABYTE = 1024*1024
1072            self.jobSizeBytes = 0
1073            infile = tempfile.TemporaryFile()
1074            while 1 :
1075                data = sys.stdin.read(MEGABYTE) 
1076                if not data :
1077                    break
1078                self.jobSizeBytes += len(data)   
1079                if not (dummy % 10) :
1080                    self.logdebug("%s bytes read..." % self.jobSizeBytes)
1081                dummy += 1   
1082                infile.write(data)
1083            self.logdebug("%s bytes read total." % self.jobSizeBytes)
1084            infile.flush()   
1085            infile.seek(0)
1086            return infile
1087        else :   
1088            # real file, just open it
1089            self.logdebug("Opening data stream %s" % self.preserveinputfile)
1090            self.jobSizeBytes = os.stat(self.preserveinputfile)[6]
1091            return open(self.preserveinputfile, "rb")
1092       
1093    def closeJobDataStream(self) :   
1094        """Closes the file which contains the job's datas."""
1095        self.logdebug("Closing data stream.")
1096        try :
1097            self.jobdatastream.close()
1098        except :   
1099            pass
1100       
1101    def precomputeJobSize(self) :   
1102        """Computes the job size with a software method."""
1103        self.logdebug("Precomputing job's size with generic PDL analyzer...")
1104        self.jobdatastream.seek(0)
1105        try :
1106            parser = pdlanalyzer.PDLAnalyzer(self.jobdatastream)
1107            jobsize = parser.getJobSize()
1108        except pdlanalyzer.PDLAnalyzerError, msg :   
1109            # Here we just log the failure, but
1110            # we finally ignore it and return 0 since this
1111            # computation is just an indication of what the
1112            # job's size MAY be.
1113            self.printInfo(_("Unable to precompute the job's size with the generic PDL analyzer : %s") % msg, "warn")
1114            return 0
1115        else :   
1116            if ((self.printingsystem == "CUPS") \
1117                and (self.preserveinputfile is not None)) \
1118                or (self.printingsystem != "CUPS") :
1119                return jobsize * self.copies
1120            else :       
1121                return jobsize
1122           
1123    def sigterm_handler(self, signum, frame) :
1124        """Sets an attribute whenever SIGTERM is received."""
1125        self.gotSigTerm = 1
1126        os.environ["PYKOTASTATUS"] = "CANCELLED"
1127        self.printInfo(_("SIGTERM received, job %s cancelled.") % self.jobid)
1128       
1129    def exportJobInfo(self) :   
1130        """Exports job information to the environment."""
1131        os.environ["PYKOTAUSERNAME"] = str(self.username)
1132        os.environ["PYKOTAPRINTERNAME"] = str(self.printername)
1133        os.environ["PYKOTAJOBID"] = str(self.jobid)
1134        os.environ["PYKOTATITLE"] = self.title or ""
1135        os.environ["PYKOTAFILENAME"] = self.preserveinputfile or ""
1136        os.environ["PYKOTACOPIES"] = str(self.copies)
1137        os.environ["PYKOTAOPTIONS"] = self.options or ""
1138        os.environ["PYKOTAPRINTERHOSTNAME"] = self.printerhostname or "localhost"
1139   
1140    def exportUserInfo(self, userpquota) :
1141        """Exports user information to the environment."""
1142        os.environ["PYKOTALIMITBY"] = str(userpquota.User.LimitBy)
1143        os.environ["PYKOTABALANCE"] = str(userpquota.User.AccountBalance or 0.0)
1144        os.environ["PYKOTALIFETIMEPAID"] = str(userpquota.User.LifeTimePaid or 0.0)
1145        os.environ["PYKOTAPAGECOUNTER"] = str(userpquota.PageCounter or 0)
1146        os.environ["PYKOTALIFEPAGECOUNTER"] = str(userpquota.LifePageCounter or 0)
1147        os.environ["PYKOTASOFTLIMIT"] = str(userpquota.SoftLimit)
1148        os.environ["PYKOTAHARDLIMIT"] = str(userpquota.HardLimit)
1149        os.environ["PYKOTADATELIMIT"] = str(userpquota.DateLimit)
1150       
1151        # not really an user information, but anyway
1152        # exports the list of printers groups the current
1153        # printer is a member of
1154        os.environ["PYKOTAPGROUPS"] = ",".join([p.Name for p in self.storage.getParentPrinters(userpquota.Printer)])
1155       
1156    def prehook(self, userpquota) :
1157        """Allows plugging of an external hook before the job gets printed."""
1158        prehook = self.config.getPreHook(userpquota.Printer.Name)
1159        if prehook :
1160            self.logdebug("Executing pre-hook [%s]" % prehook)
1161            os.system(prehook)
1162       
1163    def posthook(self, userpquota) :
1164        """Allows plugging of an external hook after the job gets printed and/or denied."""
1165        posthook = self.config.getPostHook(userpquota.Printer.Name)
1166        if posthook :
1167            self.logdebug("Executing post-hook [%s]" % posthook)
1168            os.system(posthook)
1169       
1170    def printInfo(self, message, level="info") :       
1171        """Sends a message to standard error."""
1172        self.logger.log_message("%s" % message, level)
1173       
1174    def printMoreInfo(self, user, printer, message, level="info") :           
1175        """Prefixes the information printed with 'user@printer(jobid) =>'."""
1176        self.printInfo("%s@%s(%s) => %s" % (getattr(user, "Name", None), getattr(printer, "Name", None), self.jobid, message), level)
1177       
1178    def extractInfoFromCupsOrLprng(self) :   
1179        """Returns a tuple (printingsystem, printerhostname, printername, username, jobid, filename, title, options, backend).
1180       
1181           Returns (None, None, None, None, None, None, None, None, None, None) if no printing system is recognized.
1182        """
1183        # Try to detect CUPS
1184        if os.environ.has_key("CUPS_SERVERROOT") and os.path.isdir(os.environ.get("CUPS_SERVERROOT", "")) :
1185            if len(sys.argv) == 7 :
1186                inputfile = sys.argv[6]
1187            else :   
1188                inputfile = None
1189               
1190            # check that the DEVICE_URI environment variable's value is
1191            # prefixed with "cupspykota:" otherwise don't touch it.
1192            # If this is the case, we have to remove the prefix from
1193            # the environment before launching the real backend in cupspykota
1194            device_uri = os.environ.get("DEVICE_URI", "")
1195            if device_uri.startswith("cupspykota:") :
1196                fulldevice_uri = device_uri[:]
1197                device_uri = fulldevice_uri[len("cupspykota:"):]
1198                if device_uri.startswith("//") :    # lpd (at least)
1199                    device_uri = device_uri[2:]
1200                os.environ["DEVICE_URI"] = device_uri   # TODO : side effect !
1201            # TODO : check this for more complex urls than ipp://myprinter.dot.com:631/printers/lp
1202            try :
1203                (backend, destination) = device_uri.split(":", 1) 
1204            except ValueError :   
1205                raise PyKotaToolError, "Invalid DEVICE_URI : %s\n" % device_uri
1206            while destination.startswith("/") :
1207                destination = destination[1:]
1208            checkauth = destination.split("@", 1)   
1209            if len(checkauth) == 2 :
1210                destination = checkauth[1]
1211            printerhostname = destination.split("/")[0].split(":")[0]
1212            return ("CUPS", \
1213                    printerhostname, \
1214                    os.environ.get("PRINTER"), \
1215                    sys.argv[2].strip(), \
1216                    sys.argv[1].strip(), \
1217                    inputfile, \
1218                    int(sys.argv[4].strip()), \
1219                    sys.argv[3], \
1220                    sys.argv[5], \
1221                    backend)
1222        else :   
1223            # Try to detect LPRng
1224            # TODO : try to extract filename, options if available
1225            jseen = Jseen = Pseen = nseen = rseen = Kseen = None
1226            for arg in sys.argv :
1227                if arg.startswith("-j") :
1228                    jseen = arg[2:].strip()
1229                elif arg.startswith("-n") :     
1230                    nseen = arg[2:].strip()
1231                elif arg.startswith("-P") :   
1232                    Pseen = arg[2:].strip()
1233                elif arg.startswith("-r") :   
1234                    rseen = arg[2:].strip()
1235                elif arg.startswith("-J") :   
1236                    Jseen = arg[2:].strip()
1237                elif arg.startswith("-K") or arg.startswith("-#") :   
1238                    Kseen = int(arg[2:].strip())
1239            if Kseen is None :       
1240                Kseen = 1       # we assume the user wants at least one copy...
1241            if (rseen is None) and jseen and Pseen and nseen :   
1242                lparg = [arg for arg in "".join(os.environ.get("PRINTCAP_ENTRY", "").split()).split(":") if arg.startswith("rm=") or arg.startswith("lp=")]
1243                try :
1244                    rseen = lparg[0].split("=")[-1].split("@")[-1].split("%")[0]
1245                except :   
1246                    # Not found
1247                    self.printInfo(_("Printer hostname undefined, set to 'localhost'"), "warn")
1248                    rseen = "localhost"
1249               
1250            spooldir = os.environ.get("SPOOL_DIR", ".")   
1251            try :   
1252                df_name = [line[8:] for line in os.environ.get("HF", "").split() if line.startswith("df_name=")][0]
1253            except IndexError :
1254                try :
1255                    cftransfername = [line[15:] for line in os.environ.get("HF", "").split() if line.startswith("cftransfername=")][0]
1256                except IndexError :   
1257                    try :
1258                        df_name = [line[1:] for line in os.environ.get("CONTROL", "").split() if line.startswith("fdf") or line.startswith("Udf")][0]
1259                    except IndexError :   
1260                        inputfile = None
1261                    else :   
1262                        inputfile = os.path.join(spooldir, df_name)
1263                else :   
1264                    inputfile = os.path.join(spooldir, "d" + cftransfername[1:])
1265            else :   
1266                inputfile = os.path.join(spooldir, df_name)
1267               
1268            if jseen and Pseen and nseen and rseen :       
1269                options = os.environ.get("HF", "") or os.environ.get("CONTROL", "")
1270                return ("LPRNG", rseen, Pseen, nseen, jseen, inputfile, Kseen, Jseen, options, None)
1271        self.printInfo(_("Printing system unknown, args=%s") % " ".join(sys.argv), "warn")
1272        return (None, None, None, None, None, None, None, None, None, None)   # Unknown printing system
1273       
1274    def getPrinterUserAndUserPQuota(self) :       
1275        """Returns a tuple (policy, printer, user, and user print quota) on this printer.
1276       
1277           "OK" is returned in the policy if both printer, user and user print quota
1278           exist in the Quota Storage.
1279           Otherwise, the policy as defined for this printer in pykota.conf is returned.
1280           
1281           If policy was set to "EXTERNAL" and one of printer, user, or user print quota
1282           doesn't exist in the Quota Storage, then an external command is launched, as
1283           defined in the external policy for this printer in pykota.conf
1284           This external command can do anything, like automatically adding printers
1285           or users, for example, and finally extracting printer, user and user print
1286           quota from the Quota Storage is tried a second time.
1287           
1288           "EXTERNALERROR" is returned in case policy was "EXTERNAL" and an error status
1289           was returned by the external command.
1290        """
1291        for passnumber in range(1, 3) :
1292            printer = self.storage.getPrinter(self.printername)
1293            user = self.storage.getUser(self.username)
1294            userpquota = self.storage.getUserPQuota(user, printer)
1295            if printer.Exists and user.Exists and userpquota.Exists :
1296                policy = "OK"
1297                break
1298            (policy, args) = self.config.getPrinterPolicy(self.printername)
1299            if policy == "EXTERNAL" :   
1300                commandline = self.formatCommandLine(args, user, printer)
1301                if not printer.Exists :
1302                    self.printInfo(_("Printer %s not registered in the PyKota system, applying external policy (%s) for printer %s") % (self.printername, commandline, self.printername))
1303                if not user.Exists :
1304                    self.printInfo(_("User %s not registered in the PyKota system, applying external policy (%s) for printer %s") % (self.username, commandline, self.printername))
1305                if not userpquota.Exists :
1306                    self.printInfo(_("User %s doesn't have quota on printer %s in the PyKota system, applying external policy (%s) for printer %s") % (self.username, self.printername, commandline, self.printername))
1307                if os.system(commandline) :
1308                    self.printInfo(_("External policy %s for printer %s produced an error. Job rejected. Please check PyKota's configuration files.") % (commandline, self.printername), "error")
1309                    policy = "EXTERNALERROR"
1310                    break
1311            else :       
1312                if not printer.Exists :
1313                    self.printInfo(_("Printer %s not registered in the PyKota system, applying default policy (%s)") % (self.printername, policy))
1314                if not user.Exists :
1315                    self.printInfo(_("User %s not registered in the PyKota system, applying default policy (%s) for printer %s") % (self.username, policy, self.printername))
1316                if not userpquota.Exists :
1317                    self.printInfo(_("User %s doesn't have quota on printer %s in the PyKota system, applying default policy (%s)") % (self.username, self.printername, policy))
1318                break
1319        if policy == "EXTERNAL" :   
1320            if not printer.Exists :
1321                self.printInfo(_("Printer %s still not registered in the PyKota system, job will be rejected") % self.printername)
1322            if not user.Exists :
1323                self.printInfo(_("User %s still not registered in the PyKota system, job will be rejected on printer %s") % (self.username, self.printername))
1324            if not userpquota.Exists :
1325                self.printInfo(_("User %s still doesn't have quota on printer %s in the PyKota system, job will be rejected") % (self.username, self.printername))
1326        return (policy, printer, user, userpquota)
1327       
1328    def mainWork(self) :   
1329        """Main work is done here."""
1330        (policy, printer, user, userpquota) = self.getPrinterUserAndUserPQuota()
1331        # TODO : check for last user's quota in case pykota filter is used with querying
1332        if policy == "EXTERNALERROR" :
1333            # Policy was 'EXTERNAL' and the external command returned an error code
1334            return self.removeJob()
1335        elif policy == "EXTERNAL" :
1336            # Policy was 'EXTERNAL' and the external command wasn't able
1337            # to add either the printer, user or user print quota
1338            return self.removeJob()
1339        elif policy == "DENY" :   
1340            # Either printer, user or user print quota doesn't exist,
1341            # and the job should be rejected.
1342            return self.removeJob()
1343        else :
1344            if policy not in ("OK", "ALLOW") :
1345                self.printInfo(_("Invalid policy %s for printer %s") % (policy, self.printername))
1346                return self.removeJob()
1347            else :
1348                return self.doWork(policy, printer, user, userpquota)
Note: See TracBrowser for help on using the browser.