root / pykota / trunk / bin / lprngpykota @ 2250

Revision 2217, 13.7 kB (checked in by jerome, 20 years ago)

The job-billing info is now saved into the database if present.

  • Property svn:eol-style set to native
  • Property svn:executable set to *
  • Property svn:keywords set to Author Date Id Revision
Line 
1#! /usr/bin/env python
2# -*- coding: ISO-8859-15 -*-
3
4# LPRngPyKota accounting filter
5#
6# PyKota - Print Quotas for CUPS and LPRng
7#
8# (c) 2003, 2004, 2005 Jerome Alet <alet@librelogiciel.com>
9# This program is free software; you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation; either version 2 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program; if not, write to the Free Software
21# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
22#
23# $Id$
24#
25#
26
27import sys
28import os
29
30from pykota.tool import PyKotaFilterOrBackend, PyKotaToolError, crashed
31from pykota.config import PyKotaConfigError
32from pykota.storage import PyKotaStorageError
33from pykota.accounter import PyKotaAccounterError
34   
35# Exit codes
36JSUCC = 0       # filter succeeded
37JFAIL = 1       # filter failed, print server should retry later
38JABORT = 2      # filter failed, print server should suspend the queue
39JREMOVE = 3     # job will be removed from print queue
40JHOLD = 6       # job will be prevented from printing until lpc release
41
42# Environment variables
43# PRINTER = printer name
44# PRINTCAP_ENTRY = complete printcap entry for this printer
45# HF = job hold file contents
46# SPOOL_DIR = spool directory
47
48# HF contains df_name which is DataFile_Name created in SPOOL_DIR
49
50class PyKotaFilter(PyKotaFilterOrBackend) :       
51    """A class for the pykota filter for LPRng."""
52    def acceptJob(self) :       
53        """Returns the appropriate exit code to tell LPRng all is OK."""
54        return JSUCC
55           
56    def removeJob(self) :           
57        """Returns the appropriate exit code to tell LPRng job has to be removed."""   
58        return JREMOVE
59       
60    def getJobOriginatingHostname(self, printername, username, jobid) :
61        """Retrieves the job-originating-hostname if possible."""
62        try :
63            return [line[11:] for line in os.environ.get("HF", "").split() if line.startswith("remotehost=")][0]
64        except IndexError :   
65            try :
66                return [line[1:] for line in os.environ.get("CONTROL", "").split() if line.startswith("H")][0]
67            except IndexError :   
68                return None
69               
70    def firstPass(self, policy, printer, user, userpquota) :           
71        """First pass done here."""
72        # first we have to check if previous job was correctly accounted for
73        self.logdebug("First pass begins : POLICY_%s => %s => %s" % (policy, getattr(printer, "Name", None), getattr(user, "Name", None)))
74        if printer.LastJob.Exists and not printer.LastJob.JobSize :
75            # here we know that previous job wasn't accounted for correctly
76            # we are sure (?) that it was hardware accounting which was used
77            # and that the second pass didn't work or wasn't even launched
78            # we know have to act just as if we were in second pass
79            # for previous user on this printer, then we will continue
80            # with normal processing of current user.
81            self.secondPass(policy, printer, None, None)
82       
83        # export user info with initial values
84        self.exportUserInfo(userpquota)
85       
86        # tries to extract job-originating-hostname
87        clienthost = self.getJobOriginatingHostname(printer.Name, user.Name, self.jobid)
88        self.logdebug("Client Hostname : %s" % (clienthost or "Unknown"))   
89        os.environ["PYKOTAJOBORIGINATINGHOSTNAME"] = str(clienthost or "")
90       
91        # indicates first pass
92        os.environ["PYKOTAPHASE"] = "BEFORE"
93       
94        # precomputes the job's price
95        self.softwareJobPrice = userpquota.computeJobPrice(self.softwareJobSize)
96        os.environ["PYKOTAPRECOMPUTEDJOBPRICE"] = str(self.softwareJobPrice)
97        self.logdebug("Precomputed job's size is %s pages, price is %s units" % (self.softwareJobSize, self.softwareJobPrice))
98       
99        # if no data to pass to real backend, probably a filter
100        # higher in the chain failed because of a misconfiguration.
101        # we deny the job in this case (nothing to print anyway)
102        if not self.jobSizeBytes :
103            self.printMoreInfo(user, printer, _("Job contains no data. Printing is denied."), "warn")
104            action = "DENY"
105        elif self.config.getDenyDuplicates(printer.Name) \
106             and printer.LastJob.Exists \
107             and (printer.LastJob.UserName == user.Name) \
108             and (printer.LastJob.JobMD5Sum == self.checksum) :
109            self.printMoreInfo(user, printer, _("Job is a duplicate. Printing is denied."), "warn")
110            action = "DENY" 
111        else :   
112            # checks the user's quota
113            action = self.warnUserPQuota(userpquota)
114       
115        # exports some new environment variables
116        os.environ["PYKOTAACTION"] = action
117       
118        # launches the pre hook
119        self.prehook(userpquota)
120       
121        self.printMoreInfo(user, printer, _("Job accounting begins."))
122        self.accounter.beginJob(printer)
123       
124        jobsize = None
125        if self.accounter.isSoftware :
126            self.accounter.endJob(printer)
127            jobsize = self.accounter.getJobSize(printer)
128            self.printMoreInfo(user, printer, _("Job accounting ends."))
129           
130        if action == "DENY" :   
131            jobsize = 0
132            self.printMoreInfo(user, printer, _("Job size forced to 0 because printing is denied."))
133           
134        if (self.accounter.isSoftware) or (action == "DENY") :   
135            # update the quota for the current user on this printer
136            self.printMoreInfo(user, printer, _("Job size : %i") % jobsize)
137            self.printInfo(_("Updating user %s's quota on printer %s") % (user.Name, printer.Name))
138            jobprice = userpquota.increasePagesUsage(jobsize)
139           
140            printer.addJobToHistory(self.jobid, user, self.accounter.getLastPageCounter(), \
141                                    action, jobsize, jobprice, self.preserveinputfile, \
142                                    self.title, self.copies, self.options, clienthost, \
143                                    self.jobSizeBytes, self.checksum, None, None) # TODO : pages detail and billing code
144            self.printMoreInfo(user, printer, _("Job added to history."))
145           
146            # exports some new environment variables
147            os.environ["PYKOTAPHASE"] = "AFTER"
148            os.environ["PYKOTAJOBSIZE"] = str(jobsize)
149            os.environ["PYKOTAJOBPRICE"] = str(jobprice)
150           
151            # then re-export user information with new value
152            self.exportUserInfo(userpquota)
153           
154            # Launches the post hook
155            self.posthook(userpquota)
156           
157            # here accounting was completed, either software, or hardware but over quota
158        else :
159            printer.addJobToHistory(self.jobid, user, self.accounter.getLastPageCounter(), \
160                                    action, filename=self.preserveinputfile, title=self.title, \
161                                    copies=self.copies, options=self.options, clienthost=clienthost, \
162                                    jobsizebytes=self.jobSizeBytes, jobmd5sum=self.checksum, jobpages=None, jobbilling=None) # TODO : pages detail and billing code
163            self.logdebug("Job added to history during first pass : Job's size and price are still unknown.")
164           
165        self.logdebug("First pass ends : ACTION_%s => %s => %s" % (action, getattr(printer, "Name", None), getattr(user, "Name", None)))
166        if action == "DENY" :
167            return self.removeJob()
168        else :   
169            return self.acceptJob()
170       
171    def secondPass(self, policy, printer, user, userpquota) :   
172        """Second pass done here."""
173        # Last job for current printer has the same JobId than
174        # the current job, so we know we are in the second pass
175        self.logdebug("Second pass begins : POLICY_%s => %s => %s" % (policy, getattr(printer, "Name", None), getattr(user, "Name", None)))
176        if self.accounter.isSoftware :
177            # Software accounting method was used, and we are
178            # in second pass, so all work is already done,
179            # now we just have to exit successfully
180            self.printInfo(_("Software accounting already done in first pass. Ignoring."))
181        elif printer.LastJob.JobAction == "DENY" :
182            # Hardware accounting method was used, but job
183            # was rejected during first pass, so nothing to do
184            self.printInfo(_("Hardware accounting already done in first pass. Ignoring."))
185        else :   
186            # here if user and userpquota are both None
187            # then it's a special second pass for a job
188            # which should have had one but didn't, so
189            # we need to get the last user, not the current one.
190            if (user is None) and (userpquota is None) :
191                user = printer.LastJob.User
192                userpquota = self.storage.getUserPQuota(user, printer)
193               
194            # exports user info for last user   
195            self.exportUserInfo(userpquota)
196           
197            # indicate phase change
198            os.environ["PYKOTAPHASE"] = "AFTER"
199           
200            # fakes beginning of job with old page counter
201            self.accounter.LastPageCounter = int(printer.LastJob.PrinterPageCounter or 0)
202            self.accounter.fakeBeginJob()
203            self.logdebug("Fakes beginning of job with LastPageCounter: %s" % self.accounter.getLastPageCounter())
204           
205            # stops accounting.
206            self.accounter.endJob(printer)
207            self.logdebug("Job accounting ends.")
208               
209            # retrieve the job size   
210            jobsize = self.accounter.getJobSize(printer)
211            if self.softwareJobSize and (jobsize != self.softwareJobSize) :
212                self.printInfo(_("Beware : computed job size (%s) != precomputed job size (%s)") % (jobsize, self.softwareJobSize), "error")
213                (limit, replacement) = self.config.getTrustJobSize(printer.Name)
214                if limit is None :
215                    self.printInfo(_("The job size will be trusted anyway according to the 'trustjobsize' directive"), "warn")
216                else :
217                    if jobsize <= limit :
218                        self.printInfo(_("The job size will be trusted because it is inferior to the 'trustjobsize' directive's limit %s") % limit, "warn")
219                    else :
220                        self.printInfo(_("The job size will be modified according to the 'trustjobsize' directive : %s") % replacement, "warn")
221                        if replacement == "PRECOMPUTED" :
222                            jobsize = self.softwareJobSize
223                        else :   
224                            jobsize = replacement
225           
226            self.printMoreInfo(user, printer, _("Job size : %i") % jobsize)
227            self.printInfo(_("Updating user %s's quota on printer %s") % (user.Name, printer.Name))
228            jobprice = userpquota.increasePagesUsage(jobsize)
229           
230            self.storage.writeLastJobSize(printer.LastJob, jobsize, jobprice)
231            self.printMoreInfo(user, printer, _("Job size and price now set in history."))
232           
233            # exports some new environment variables
234            os.environ["PYKOTAPHASE"] = "AFTER"
235            os.environ["PYKOTAJOBSIZE"] = str(jobsize)
236            os.environ["PYKOTAJOBPRICE"] = str(jobprice)
237           
238            # then re-export user information with new value
239            self.exportUserInfo(userpquota)
240           
241            # Launches the post hook
242            self.posthook(userpquota)
243           
244            # here hardware accounting was completed.
245        self.logdebug("Second pass ends : POLICY_%s => %s => %s" % (policy, getattr(printer, "Name", None), getattr(user, "Name", None)))
246        return self.acceptJob()
247       
248    def doWork(self, policy, printer, user, userpquota) :   
249        """Most of the work is done here."""
250        # Two different values possible for policy here :
251        # ALLOW means : Either printer, user or user print quota doesn't exist,
252        #               but the job should be allowed anyway.
253        # OK means : Both printer, user and user print quota exist, job should
254        #            be allowed if current user is allowed to print on this printer
255        if policy == "ALLOW" :
256            # nothing to do, just accept the job
257            return self.acceptJob()
258        else :   
259            if printer.LastJob.Exists and (printer.LastJob.JobId == self.jobid) :
260                # here we know we are in second pass.
261                return self.secondPass(policy, printer, user, userpquota)
262            else :   
263                # Last job for current printer has a different JobId than
264                # the current job, so we know we are in the first pass.
265                return self.firstPass(policy, printer, user, userpquota)
266           
267if __name__ == "__main__" :   
268    retcode = JSUCC
269    try :
270        # Initializes the backend
271        kotabackend = PyKotaFilter()   
272        kotabackend.deferredInit()
273        retcode = kotabackend.mainWork()
274        kotabackend.storage.close()
275        kotabackend.closeJobDataStream()   
276    except SystemExit :   
277        retcode = JABORT
278    except :
279        try :
280            kotabackend.crashed("lprngpykota filter failed")
281        except :   
282            crashed("lprngpykota filter failed")
283        retcode = JABORT
284       
285    sys.exit(retcode)   
Note: See TracBrowser for help on using the browser.