Show
Ignore:
Timestamp:
08/28/05 12:45:51 (19 years ago)
Author:
jerome
Message:

The cupspykota backend was rewritten from scratch. MacOSX servers should
work just fine now.
Severity : High. Testers MORE than welcome :-)

Files:
1 modified

Legend:

Unmodified
Added
Removed
  • pykota/trunk/pykota/tool.py

    r2395 r2409  
    634634        return action         
    635635         
    636 class PyKotaFilterOrBackend(PyKotaTool) :     
    637     """Class for the PyKota filter or backend.""" 
    638     def __init__(self) : 
    639         """Initialize local datas from current environment.""" 
    640         # We begin with ignoring signals, we may de-ignore them later on. 
    641         self.gotSigTerm = 0 
    642         signal.signal(signal.SIGTERM, signal.SIG_IGN) 
    643         # signal.signal(signal.SIGCHLD, signal.SIG_IGN) 
    644         signal.signal(signal.SIGPIPE, signal.SIG_IGN) 
    645          
    646         PyKotaTool.__init__(self) 
    647         (self.printingsystem, \ 
    648          self.printerhostname, \ 
    649          self.printername, \ 
    650          self.username, \ 
    651          self.jobid, \ 
    652          self.inputfile, \ 
    653          self.copies, \ 
    654          self.title, \ 
    655          self.options, \ 
    656          self.originalbackend) = self.extractInfoFromCupsOrLprng() 
    657           
    658     def deferredInit(self) : 
    659         """Deferred initialization.""" 
    660         PyKotaTool.deferredInit(self) 
    661          
    662         arguments = " ".join(['"%s"' % arg for arg in sys.argv]) 
    663         self.logdebug(_("Printing system %s, args=%s") % (str(self.printingsystem), arguments)) 
    664          
    665         self.username = self.username or pwd.getpwuid(os.geteuid())[0] # use CUPS' user when printing test page from CUPS web interface, otherwise username is empty 
    666          
    667         if self.printingsystem == "CUPS" : 
    668             self.extractDatasFromCups() 
    669          
    670         (newusername, newbillingcode, newaction) = self.overwriteJobTicket() 
    671         if newusername : 
    672             self.printInfo(_("Job ticket overwritten : new username = [%s]") % newusername) 
    673             self.username = newusername 
    674         if newbillingcode : 
    675             self.printInfo(_("Job ticket overwritten : new billing code = [%s]") % newbillingcode) 
    676             self.overwrittenBillingCode = newbillingcode 
    677         else :     
    678             self.overwrittenBillingCode = None 
    679         if newaction :     
    680             self.printInfo(_("Job ticket overwritten : job will be denied (but a bit later).")) 
    681             self.mustDeny = 1 
    682         else :     
    683             self.mustDeny = 0 
    684          
    685         # do we want to strip out the Samba/Winbind domain name ? 
    686         separator = self.config.getWinbindSeparator() 
    687         if separator is not None : 
    688             self.username = self.username.split(separator)[-1] 
    689              
    690         # do we want to lowercase usernames ?     
    691         if self.config.getUserNameToLower() : 
    692             self.username = self.username.lower() 
    693              
    694         # do we want to strip some prefix off of titles ?     
    695         stripprefix = self.config.getStripTitle(self.printername) 
    696         if stripprefix : 
    697             if fnmatch.fnmatch(self.title[:len(stripprefix)], stripprefix) : 
    698                 self.logdebug("Prefix [%s] removed from job's title [%s]." % (stripprefix, self.title)) 
    699                 self.title = self.title[len(stripprefix):] 
    700              
    701         self.preserveinputfile = self.inputfile  
    702         try : 
    703             self.accounter = accounter.openAccounter(self) 
    704         except (config.PyKotaConfigError, accounter.PyKotaAccounterError), msg :     
    705             self.crashed(msg) 
    706             raise 
    707         self.exportJobInfo() 
    708         self.jobdatastream = self.openJobDataStream() 
    709         self.checksum = self.computeChecksum() 
    710         self.softwareJobSize = self.precomputeJobSize() 
    711         os.environ["PYKOTAPRECOMPUTEDJOBSIZE"] = str(self.softwareJobSize) 
    712         os.environ["PYKOTAJOBSIZEBYTES"] = str(self.jobSizeBytes) 
    713         self.logdebug("Job size is %s bytes on %s pages." % (self.jobSizeBytes, self.softwareJobSize)) 
    714         self.logdebug("Capturing SIGTERM events.") 
    715         signal.signal(signal.SIGTERM, self.sigterm_handler) 
    716          
    717     def overwriteJobTicket(self) :     
    718         """Should we overwrite the job's ticket (username and billingcode) ?""" 
    719         jobticketcommand = self.config.getOverwriteJobTicket(self.printername) 
    720         if jobticketcommand is not None : 
    721             username = billingcode = action = None 
    722             self.logdebug("Launching subprocess [%s] to overwrite the job's ticket." % jobticketcommand) 
    723             inputfile = os.popen(jobticketcommand, "r") 
    724             for line in inputfile.xreadlines() : 
    725                 line = line.strip() 
    726                 if line == "DENY" : 
    727                     self.logdebug("Seen DENY command.") 
    728                     action = "DENY" 
    729                 elif line.startswith("USERNAME=") :     
    730                     username = line.split("=", 1)[1].strip() 
    731                     self.logdebug("Seen new username [%s]" % username) 
    732                     action = None 
    733                 elif line.startswith("BILLINGCODE=") :     
    734                     billingcode = line.split("=", 1)[1].strip() 
    735                     self.logdebug("Seen new billing code [%s]" % billingcode) 
    736                     action = None 
    737             inputfile.close()     
    738             return (username, billingcode, action) 
    739         else : 
    740             return (None, None, None) 
    741          
    742     def sendBackChannelData(self, message, level="info") :     
    743         """Sends an informational message to CUPS via back channel stream (stderr).""" 
    744         sys.stderr.write("%s: PyKota (PID %s) : %s\n" % (level.upper(), os.getpid(), message.strip())) 
    745         sys.stderr.flush() 
    746          
    747     def computeChecksum(self) :     
    748         """Computes the MD5 checksum of the job's datas, to be able to detect and forbid duplicate jobs.""" 
    749         self.logdebug("Computing MD5 checksum for job %s" % self.jobid) 
    750         MEGABYTE = 1024*1024 
    751         checksum = md5.new() 
    752         while 1 : 
    753             data = self.jobdatastream.read(MEGABYTE)  
    754             if not data : 
    755                 break 
    756             checksum.update(data)     
    757         self.jobdatastream.seek(0) 
    758         digest = checksum.hexdigest() 
    759         self.logdebug("MD5 checksum for job %s is %s" % (self.jobid, digest)) 
    760         os.environ["PYKOTAMD5SUM"] = digest 
    761         return digest 
    762          
    763     def openJobDataStream(self) :     
    764         """Opens the file which contains the job's datas.""" 
    765         if self.preserveinputfile is None : 
    766             # Job comes from sys.stdin, but this is not 
    767             # seekable and complexifies our task, so create 
    768             # a temporary file and use it instead 
    769             self.logdebug("Duplicating data stream from stdin to temporary file") 
    770             dummy = 0 
    771             MEGABYTE = 1024*1024 
    772             self.jobSizeBytes = 0 
    773             infile = tempfile.TemporaryFile() 
    774             while 1 : 
    775                 data = sys.stdin.read(MEGABYTE)  
    776                 if not data : 
    777                     break 
    778                 self.jobSizeBytes += len(data)     
    779                 if not (dummy % 10) : 
    780                     self.logdebug("%s bytes read..." % self.jobSizeBytes) 
    781                 dummy += 1     
    782                 infile.write(data) 
    783             self.logdebug("%s bytes read total." % self.jobSizeBytes) 
    784             infile.flush()     
    785             infile.seek(0) 
    786             return infile 
    787         else :     
    788             # real file, just open it 
    789             self.regainPriv() 
    790             self.logdebug("Opening data stream %s" % self.preserveinputfile) 
    791             self.jobSizeBytes = os.stat(self.preserveinputfile)[6] 
    792             infile = open(self.preserveinputfile, "rb") 
    793             self.dropPriv() 
    794             return infile 
    795          
    796     def closeJobDataStream(self) :     
    797         """Closes the file which contains the job's datas.""" 
    798         self.logdebug("Closing data stream.") 
    799         try : 
    800             self.jobdatastream.close() 
    801         except :     
    802             pass 
    803          
    804     def precomputeJobSize(self) :     
    805         """Computes the job size with a software method.""" 
    806         self.logdebug("Precomputing job's size with generic PDL analyzer...") 
    807         self.jobdatastream.seek(0) 
    808         try : 
    809             parser = analyzer.PDLAnalyzer(self.jobdatastream) 
    810             jobsize = parser.getJobSize() 
    811         except pdlparser.PDLParserError, msg :     
    812             # Here we just log the failure, but 
    813             # we finally ignore it and return 0 since this 
    814             # computation is just an indication of what the 
    815             # job's size MAY be. 
    816             self.printInfo(_("Unable to precompute the job's size with the generic PDL analyzer : %s") % msg, "warn") 
    817             return 0 
    818         else :     
    819             if ((self.printingsystem == "CUPS") \ 
    820                 and (self.preserveinputfile is not None)) \ 
    821                 or (self.printingsystem != "CUPS") : 
    822                 return jobsize * self.copies 
    823             else :         
    824                 return jobsize 
    825              
    826     def sigterm_handler(self, signum, frame) : 
    827         """Sets an attribute whenever SIGTERM is received.""" 
    828         self.gotSigTerm = 1 
    829         os.environ["PYKOTASTATUS"] = "CANCELLED" 
    830         self.printInfo(_("SIGTERM received, job %s cancelled.") % self.jobid) 
    831          
    832     def exportJobInfo(self) :     
    833         """Exports job information to the environment.""" 
    834         os.environ["PYKOTAUSERNAME"] = str(self.username) 
    835         os.environ["PYKOTAPRINTERNAME"] = str(self.printername) 
    836         os.environ["PYKOTAJOBID"] = str(self.jobid) 
    837         os.environ["PYKOTATITLE"] = self.title or "" 
    838         os.environ["PYKOTAFILENAME"] = self.preserveinputfile or "" 
    839         os.environ["PYKOTACOPIES"] = str(self.copies) 
    840         os.environ["PYKOTAOPTIONS"] = self.options or "" 
    841         os.environ["PYKOTAPRINTERHOSTNAME"] = self.printerhostname or "localhost" 
    842      
    843     def exportUserInfo(self, userpquota) : 
    844         """Exports user information to the environment.""" 
    845         os.environ["PYKOTAOVERCHARGE"] = str(userpquota.User.OverCharge) 
    846         os.environ["PYKOTALIMITBY"] = str(userpquota.User.LimitBy) 
    847         os.environ["PYKOTABALANCE"] = str(userpquota.User.AccountBalance or 0.0) 
    848         os.environ["PYKOTALIFETIMEPAID"] = str(userpquota.User.LifeTimePaid or 0.0) 
    849         os.environ["PYKOTAPAGECOUNTER"] = str(userpquota.PageCounter or 0) 
    850         os.environ["PYKOTALIFEPAGECOUNTER"] = str(userpquota.LifePageCounter or 0) 
    851         os.environ["PYKOTASOFTLIMIT"] = str(userpquota.SoftLimit) 
    852         os.environ["PYKOTAHARDLIMIT"] = str(userpquota.HardLimit) 
    853         os.environ["PYKOTADATELIMIT"] = str(userpquota.DateLimit) 
    854         os.environ["PYKOTAWARNCOUNT"] = str(userpquota.WarnCount) 
    855          
    856         # not really an user information, but anyway 
    857         # exports the list of printers groups the current 
    858         # printer is a member of 
    859         os.environ["PYKOTAPGROUPS"] = ",".join([p.Name for p in self.storage.getParentPrinters(userpquota.Printer)]) 
    860          
    861     def prehook(self, userpquota) : 
    862         """Allows plugging of an external hook before the job gets printed.""" 
    863         prehook = self.config.getPreHook(userpquota.Printer.Name) 
    864         if prehook : 
    865             self.logdebug("Executing pre-hook [%s]" % prehook) 
    866             os.system(prehook) 
    867          
    868     def posthook(self, userpquota) : 
    869         """Allows plugging of an external hook after the job gets printed and/or denied.""" 
    870         posthook = self.config.getPostHook(userpquota.Printer.Name) 
    871         if posthook : 
    872             self.logdebug("Executing post-hook [%s]" % posthook) 
    873             os.system(posthook) 
    874              
    875     def printInfo(self, message, level="info") :         
    876         """Sends a message to standard error.""" 
    877         self.logger.log_message("%s" % message, level) 
    878          
    879     def printMoreInfo(self, user, printer, message, level="info") :             
    880         """Prefixes the information printed with 'user@printer(jobid) =>'.""" 
    881         self.printInfo("%s@%s(%s) => %s" % (getattr(user, "Name", None), getattr(printer, "Name", None), self.jobid, message), level) 
    882          
    883     def extractInfoFromCupsOrLprng(self) :     
    884         """Returns a tuple (printingsystem, printerhostname, printername, username, jobid, filename, title, options, backend). 
    885          
    886            Returns (None, None, None, None, None, None, None, None, None, None) if no printing system is recognized. 
    887         """ 
    888         # Try to detect CUPS 
    889         if os.environ.has_key("CUPS_SERVERROOT") and os.path.isdir(os.environ.get("CUPS_SERVERROOT", "")) : 
    890             if len(sys.argv) == 7 : 
    891                 inputfile = sys.argv[6] 
    892             else :     
    893                 inputfile = None 
    894                  
    895             # check that the DEVICE_URI environment variable's value is  
    896             # prefixed with "cupspykota:" otherwise don't touch it. 
    897             # If this is the case, we have to remove the prefix from  
    898             # the environment before launching the real backend in cupspykota 
    899             device_uri = os.environ.get("DEVICE_URI", "") 
    900             if device_uri.startswith("cupspykota:") : 
    901                 fulldevice_uri = device_uri[:] 
    902                 device_uri = fulldevice_uri[len("cupspykota:"):] 
    903                 if device_uri.startswith("//") :    # lpd (at least) 
    904                     device_uri = device_uri[2:] 
    905                 os.environ["DEVICE_URI"] = device_uri   # TODO : side effect ! 
    906             # TODO : check this for more complex urls than ipp://myprinter.dot.com:631/printers/lp 
    907             try : 
    908                 (backend, destination) = device_uri.split(":", 1)  
    909             except ValueError :     
    910                 raise PyKotaToolError, "Invalid DEVICE_URI : %s\n" % device_uri 
    911             while destination.startswith("/") : 
    912                 destination = destination[1:] 
    913             checkauth = destination.split("@", 1)     
    914             if len(checkauth) == 2 : 
    915                 destination = checkauth[1] 
    916             printerhostname = destination.split("/")[0].split(":")[0] 
    917             return ("CUPS", \ 
    918                     printerhostname, \ 
    919                     os.environ.get("PRINTER"), \ 
    920                     sys.argv[2].strip(), \ 
    921                     sys.argv[1].strip(), \ 
    922                     inputfile, \ 
    923                     int(sys.argv[4].strip()), \ 
    924                     sys.argv[3], \ 
    925                     sys.argv[5], \ 
    926                     backend) 
    927         else :     
    928             # Try to detect LPRng 
    929             # TODO : try to extract filename, options if available 
    930             jseen = Jseen = Pseen = nseen = rseen = Kseen = None 
    931             for arg in sys.argv : 
    932                 if arg.startswith("-j") : 
    933                     jseen = arg[2:].strip() 
    934                 elif arg.startswith("-n") :      
    935                     nseen = arg[2:].strip() 
    936                 elif arg.startswith("-P") :     
    937                     Pseen = arg[2:].strip() 
    938                 elif arg.startswith("-r") :     
    939                     rseen = arg[2:].strip() 
    940                 elif arg.startswith("-J") :     
    941                     Jseen = arg[2:].strip() 
    942                 elif arg.startswith("-K") or arg.startswith("-#") :     
    943                     Kseen = int(arg[2:].strip()) 
    944             if Kseen is None :         
    945                 Kseen = 1       # we assume the user wants at least one copy... 
    946             if (rseen is None) and jseen and Pseen and nseen :     
    947                 lparg = [arg for arg in "".join(os.environ.get("PRINTCAP_ENTRY", "").split()).split(":") if arg.startswith("rm=") or arg.startswith("lp=")] 
    948                 try : 
    949                     rseen = lparg[0].split("=")[-1].split("@")[-1].split("%")[0] 
    950                 except :     
    951                     # Not found 
    952                     self.printInfo(_("Printer hostname undefined, set to 'localhost'"), "warn") 
    953                     rseen = "localhost" 
    954                  
    955             spooldir = os.environ.get("SPOOL_DIR", ".")     
    956             df_name = os.environ.get("DATAFILES") 
    957             if not df_name : 
    958                 try :  
    959                     df_name = [line[10:] for line in os.environ.get("HF", "").split() if line.startswith("datafiles=")][0] 
    960                 except IndexError :     
    961                     try :     
    962                         df_name = [line[8:] for line in os.environ.get("HF", "").split() if line.startswith("df_name=")][0] 
    963                     except IndexError : 
    964                         try : 
    965                             cftransfername = [line[15:] for line in os.environ.get("HF", "").split() if line.startswith("cftransfername=")][0] 
    966                         except IndexError :     
    967                             try : 
    968                                 df_name = [line[1:] for line in os.environ.get("CONTROL", "").split() if line.startswith("fdf") or line.startswith("Udf")][0] 
    969                             except IndexError :     
    970                                 raise PyKotaToolError, "Unable to find the file which holds the job's datas. Please file a bug report for PyKota." 
    971                             else :     
    972                                 inputfile = os.path.join(spooldir, df_name) # no need to strip() 
    973                         else :     
    974                             inputfile = os.path.join(spooldir, "d" + cftransfername[1:]) # no need to strip() 
    975                     else :     
    976                         inputfile = os.path.join(spooldir, df_name) # no need to strip() 
    977                 else :     
    978                     inputfile = os.path.join(spooldir, df_name) # no need to strip() 
    979             else :     
    980                 inputfile = os.path.join(spooldir, df_name.strip()) 
    981                  
    982             if jseen and Pseen and nseen and rseen :         
    983                 options = os.environ.get("HF", "") or os.environ.get("CONTROL", "") 
    984                 return ("LPRNG", rseen, Pseen, nseen, jseen, inputfile, Kseen, Jseen, options, None) 
    985         self.printInfo(_("Printing system unknown, args=%s") % " ".join(sys.argv), "warn") 
    986         return (None, None, None, None, None, None, None, None, None, None)   # Unknown printing system 
    987          
    988     def getPrinterUserAndUserPQuota(self) :         
    989         """Returns a tuple (policy, printer, user, and user print quota) on this printer. 
    990          
    991            "OK" is returned in the policy if both printer, user and user print quota 
    992            exist in the Quota Storage. 
    993            Otherwise, the policy as defined for this printer in pykota.conf is returned. 
    994             
    995            If policy was set to "EXTERNAL" and one of printer, user, or user print quota 
    996            doesn't exist in the Quota Storage, then an external command is launched, as 
    997            defined in the external policy for this printer in pykota.conf 
    998            This external command can do anything, like automatically adding printers 
    999            or users, for example, and finally extracting printer, user and user print 
    1000            quota from the Quota Storage is tried a second time. 
    1001             
    1002            "EXTERNALERROR" is returned in case policy was "EXTERNAL" and an error status 
    1003            was returned by the external command. 
    1004         """ 
    1005         for passnumber in range(1, 3) : 
    1006             printer = self.storage.getPrinter(self.printername) 
    1007             user = self.storage.getUser(self.username) 
    1008             userpquota = self.storage.getUserPQuota(user, printer) 
    1009             if printer.Exists and user.Exists and userpquota.Exists : 
    1010                 policy = "OK" 
    1011                 break 
    1012             (policy, args) = self.config.getPrinterPolicy(self.printername) 
    1013             if policy == "EXTERNAL" :     
    1014                 commandline = self.formatCommandLine(args, user, printer) 
    1015                 if not printer.Exists : 
    1016                     self.printInfo(_("Printer %s not registered in the PyKota system, applying external policy (%s) for printer %s") % (self.printername, commandline, self.printername)) 
    1017                 if not user.Exists : 
    1018                     self.printInfo(_("User %s not registered in the PyKota system, applying external policy (%s) for printer %s") % (self.username, commandline, self.printername)) 
    1019                 if not userpquota.Exists : 
    1020                     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)) 
    1021                 if os.system(commandline) : 
    1022                     self.printInfo(_("External policy %s for printer %s produced an error. Job rejected. Please check PyKota's configuration files.") % (commandline, self.printername), "error") 
    1023                     policy = "EXTERNALERROR" 
    1024                     break 
    1025             else :         
    1026                 if not printer.Exists : 
    1027                     self.printInfo(_("Printer %s not registered in the PyKota system, applying default policy (%s)") % (self.printername, policy)) 
    1028                 if not user.Exists : 
    1029                     self.printInfo(_("User %s not registered in the PyKota system, applying default policy (%s) for printer %s") % (self.username, policy, self.printername)) 
    1030                 if not userpquota.Exists : 
    1031                     self.printInfo(_("User %s doesn't have quota on printer %s in the PyKota system, applying default policy (%s)") % (self.username, self.printername, policy)) 
    1032                 break 
    1033         if policy == "EXTERNAL" :     
    1034             if not printer.Exists : 
    1035                 self.printInfo(_("Printer %s still not registered in the PyKota system, job will be rejected") % self.printername) 
    1036             if not user.Exists : 
    1037                 self.printInfo(_("User %s still not registered in the PyKota system, job will be rejected on printer %s") % (self.username, self.printername)) 
    1038             if not userpquota.Exists : 
    1039                 self.printInfo(_("User %s still doesn't have quota on printer %s in the PyKota system, job will be rejected") % (self.username, self.printername)) 
    1040         return (policy, printer, user, userpquota) 
    1041          
    1042     def mainWork(self) :     
    1043         """Main work is done here.""" 
    1044         (policy, printer, user, userpquota) = self.getPrinterUserAndUserPQuota() 
    1045         # TODO : check for last user's quota in case pykota filter is used with querying 
    1046         if policy == "EXTERNALERROR" : 
    1047             # Policy was 'EXTERNAL' and the external command returned an error code 
    1048             return self.removeJob() 
    1049         elif policy == "EXTERNAL" : 
    1050             # Policy was 'EXTERNAL' and the external command wasn't able 
    1051             # to add either the printer, user or user print quota 
    1052             return self.removeJob() 
    1053         elif policy == "DENY" :     
    1054             # Either printer, user or user print quota doesn't exist, 
    1055             # and the job should be rejected. 
    1056             return self.removeJob() 
    1057         else : 
    1058             if policy not in ("OK", "ALLOW") : 
    1059                 self.printInfo(_("Invalid policy %s for printer %s") % (policy, self.printername)) 
    1060                 return self.removeJob() 
    1061             else : 
    1062                 return self.doWork(policy, printer, user, userpquota)