#! /usr/bin/env python
# -*- coding: ISO-8859-15 -*-

# PyKota Printers Manager
#
# PyKota - Print Quotas for CUPS and LPRng
#
# (c) 2003, 2004, 2005, 2006, 2007 Jerome Alet <alet@librelogiciel.com>
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# $Id: pkprinters 3394 2008-07-10 19:47:46Z jerome $
#
#

import os
import sys
import pwd

from pykota.tool import Percent, PyKotaTool, PyKotaCommandLineError, crashed, N_
from pykota.storage import StoragePrinter

from pkipplib import pkipplib

__doc__ = N_("""pkprinters v%(__version__)s (c) %(__years__)s %(__author__)s

A Printers Manager for PyKota.

command line usage :

  pkprinters [options] printer1 printer2 printer3 ... printerN

options :

  -v | --version       Prints pkprinters's version number then exits.
  -h | --help          Prints this message then exits.
  
  -a | --add           Adds printers if they don't exist on the Quota 
                       Storage Server. If they exist, they are modified
                       unless -s|--skipexisting is also used.
                       
  -d | --delete        Deletes printers from the quota storage.
  
  -D | --description d Adds a textual description to printers.

  -C | --cups          Also modifies the DeviceURI in CUPS' printers.conf

  -c | --charge p[,j]  Sets the price per page and per job to charge.
                       Job price is optional.
                       If both are to be set, separate them with a comma.
                       Floating point and negative values are allowed.
  
  -g | --groups pg1[,pg2...] Adds or Remove the printer(s) to the printer 
                       groups pg1, pg2, etc... which must already exist.
                       A printer group is just like a normal printer,
                       only that it is usually unknown from the printing
                       system. Create printer groups exactly the same
                       way that you create printers, then add other 
                       printers to them with this option.
                       Accounting is done on a printer and on all
                       the printer groups it belongs to, quota checking
                       is done on a printer and on all the printer groups
                       it belongs to.
                       If the --remove option below is not used, the 
                       default action is to add printers to the specified
                       printer groups.
                       
  -l | --list          List informations about the printer(s) and the
                       printers groups it is a member of.
                       
  -r | --remove        In combination with the --groups option above,                       
                       remove printers from the specified printers groups.
                       
  -s | --skipexisting  In combination with the --add option above, tells
                       pkprinters to not modify existing printers.
                       
  -m | --maxjobsize s  Sets the maximum job size allowed on the printer
                       to s pages.
                       
  -p | --passthrough   Activate passthrough mode for the printer. In this
                       mode, users are allowed to print without any impact
                       on their quota or account balance.
                       
  -n | --nopassthrough Deactivate passthrough mode for the printer.
                       Without -p or -n, printers are created in 
                       normal mode, i.e. no passthrough.
  
  printer1 through printerN can contain wildcards if the --add option 
  is not set.
  
examples :                              

  $ pkprinters --add -D "HP Printer" --charge 0.05,0.1 hp2100 hp2200 hp8000
  
  Will create three printers named hp2100, hp2200 and hp8000.
  Their price per page will be set at 0.05 unit, and their price
  per job will be set at 0.1 unit. Units are in your own currency,
  or whatever you want them to mean.
  All of their descriptions will be set to the string "HP Printer".
  If any of these printers already exists, it will also be modified 
  unless the -s|--skipexisting command line option is also used.
            
  $ pkprinters --delete "*"
  
  This will completely delete all printers and associated quota information,
  as well as their job history. USE WITH CARE !
  
  $ pkprinters --groups Laser,HP "hp*"
  
  This will put all printers which name matches "hp*" into printers groups 
  Laser and HP, which MUST already exist.
  
  $ pkprinters --groups LexMark --remove hp2200
  
  This will remove the hp2200 printer from the LexMark printer group.
""")
        
class PKPrinters(PyKotaTool) :        
    """A class for a printers manager."""
    def modifyPrinter(self, printer, charges, perpage, perjob, description, passthrough, nopassthrough, maxjobsize) :
        if charges :
            printer.setPrices(perpage, perjob)    
        if description is not None :        # NB : "" is allowed !
            printer.setDescription(description)
        if nopassthrough :    
            printer.setPassThrough(False)
        if passthrough :    
            printer.setPassThrough(True)
        if maxjobsize is not None :
            printer.setMaxJobSize(maxjobsize)
            
    def managePrintersGroups(self, pgroups, printer, remove) :        
        """Manage printer group membership."""
        for pgroup in pgroups :
            if remove :
                pgroup.delPrinterFromGroup(printer)
            else :
                pgroup.addPrinterToGroup(printer)    
                
    def getPrinterDeviceURI(self, printername) :            
        """Returns the Device URI attribute for a particular printer."""
        if not printername :
            return ""
        cups = pkipplib.CUPS()
        req = cups.newRequest(pkipplib.IPP_GET_PRINTER_ATTRIBUTES)
        req.operation["printer-uri"] = ("uri", cups.identifierToURI("printers", printername))
        try :
            return cups.doRequest(req).printer["device-uri"][0][1]
        except :    
            return ""
        
    def isPrinterCaptured(self, printername=None, deviceuri=None) :
        """Returns True if the printer is already redirected through PyKota's backend, else False."""
        if (deviceuri or self.getPrinterDeviceURI(printername)).find("cupspykota:") != -1 :
            return True
        else :    
            return False
        
    def reroutePrinterThroughPyKota(self, printer) :    
        """Reroutes a CUPS printer through PyKota."""
        uri = self.getPrinterDeviceURI(printer.Name)
        if uri and (not self.isPrinterCaptured(deviceuri=uri)) :
             newuri = "cupspykota://%s" % uri
             self.regainPriv() # to avoid having to enter password.
             os.system('lpadmin -p "%s" -v "%s"' % (printer.Name, newuri))
             self.logdebug("Printer %s rerouted to %s" % (printer.Name, newuri))
             self.dropPriv()
             
    def deroutePrinterFromPyKota(self, printer) :    
        """Deroutes a PyKota printer through CUPS only."""
        uri = self.getPrinterDeviceURI(printer.Name)
        if uri and self.isPrinterCaptured(deviceuri=uri) :
             newuri = uri.replace("cupspykota:", "")
             if newuri.startswith("//") :
                 newuri = newuri[2:]
             self.regainPriv() # to avoid having to enter password.
             os.system('lpadmin -p "%s" -v "%s"' % (printer.Name, newuri))
             self.logdebug("Printer %s rerouted to %s" % (printer.Name, newuri))
             self.dropPriv()    
                                     
    def main(self, names, options) :
        """Manage printers."""
        if (not self.config.isAdmin) and (not options["list"]) :
            raise PyKotaCommandLineError, "%s : %s" % (pwd.getpwuid(os.geteuid())[0], _("You're not allowed to use this command."))
            
        docups = options["cups"]
        
        if not options["list"] :    
            percent = Percent(self)
            
        if not options["add"] :
            if not options["list"] :
                percent.display("%s..." % _("Extracting datas"))
            if not names :      # NB : can't happen for --delete because it's catched earlier
                names = ["*"]
            printers = self.storage.getMatchingPrinters(",".join(names))
            if not printers :
                if not options["list"] :
                    percent.display("\n")
                raise PyKotaCommandLineError, _("There's no printer matching %s") % " ".join(names)
            if not options["list"] :    
                percent.setSize(len(printers))
                
        if options["list"] :
            for printer in printers :
                parents = ", ".join([p.Name for p in self.storage.getParentPrinters(printer)])
                print "%s [%s] (%s + #*%s)" % \
                      (printer.Name, printer.Description, printer.PricePerJob, \
                       printer.PricePerPage)
                print "    %s" % (_("Passthrough mode : %s") % ((printer.PassThrough and _("ON")) or _("OFF")))
                print "    %s" % (_("Maximum job size : %s") % ((printer.MaxJobSize and (_("%s pages") % printer.MaxJobSize)) or _("Unlimited")))
                print "    %s" % (_("Routed through PyKota : %s") % ((self.isPrinterCaptured(printer.Name) and _("YES")) or _("NO")))
                if parents : 
                    print "    %s %s" % (_("in"), parents)
                print    
        elif options["delete"] :    
            percent.display("\n%s..." % _("Deletion"))
            self.storage.deleteManyPrinters(printers)
            percent.display("\n")
            if docups :
                percent.display("%s...\n" % _("Rerouting printers to CUPS"))
                for printer in printers :
                    self.deroutePrinterFromPyKota(printer)
                    percent.oneMore()
        else :
            if options["groups"] :        
                printersgroups = self.storage.getMatchingPrinters(options["groups"])
                if not printersgroups :
                    raise PyKotaCommandLineError, _("There's no printer matching %s") % " ".join(options["groups"].split(','))
            else :         
                printersgroups = []
                    
            if options["charge"] :
                try :
                    charges = [float(part) for part in options["charge"].split(',', 1)]
                except ValueError :    
                    raise PyKotaCommandLineError, _("Invalid charge amount value %s") % options["charge"]
                else :    
                    if len(charges) > 2 :
                        charges = charges[:2]
                    if len(charges) != 2 :
                        charges = [charges[0], None]
                    (perpage, perjob) = charges
            else :        
                charges = perpage = perjob = None
                    
            if options["maxjobsize"] :        
                try :
                    maxjobsize = int(options["maxjobsize"])
                    if maxjobsize < 0 :
                        raise ValueError
                except ValueError :    
                    raise PyKotaCommandLineError, _("Invalid maximum job size value %s") % options["maxjobsize"]
            else :        
                maxjobsize = None
                    
            description = options["description"]
            if description :
                description = description.strip()
                
            nopassthrough = options["nopassthrough"]    
            passthrough = options["passthrough"]
            remove = options["remove"]
            skipexisting = options["skipexisting"]
            self.storage.beginTransaction()
            try :
                if options["add"] :    
                    percent.display("%s...\n" % _("Creation"))
                    percent.setSize(len(names))
                    for pname in names :
                        if self.isValidName(pname) :
                            printer = StoragePrinter(self.storage, pname)
                            self.modifyPrinter(printer, charges, perpage, perjob, \
                                           description, passthrough, \
                                           nopassthrough, maxjobsize)
                            oldprinter = self.storage.addPrinter(printer)               
                            
                            if docups :
                                 self.reroutePrinterThroughPyKota(printer)
                                     
                            if oldprinter is not None :
                                if skipexisting :
                                    self.logdebug(_("Printer %s already exists, skipping.") % pname)
                                else :    
                                    self.logdebug(_("Printer %s already exists, will be modified.") % pname)
                                    self.modifyPrinter(oldprinter, charges, \
                                               perpage, perjob, description, \
                                               passthrough, nopassthrough, \
                                               maxjobsize)
                                    oldprinter.save()           
                                    self.managePrintersGroups(printersgroups, oldprinter, remove)
                            elif printersgroups :        
                                self.managePrintersGroups(printersgroups, \
                                                          self.storage.getPrinter(pname), \
                                                          remove)
                        else :    
                            raise PyKotaCommandLineError, _("Invalid printer name %s") % pname
                        percent.oneMore()
                else :        
                    percent.display("\n%s...\n" % _("Modification"))
                    for printer in printers :        
                        self.modifyPrinter(printer, charges, perpage, perjob, \
                                           description, passthrough, \
                                           nopassthrough, maxjobsize)
                        printer.save()    
                        self.managePrintersGroups(printersgroups, printer, remove)
                        if docups :
                            self.reroutePrinterThroughPyKota(printer)
                        percent.oneMore()
            except :                    
                self.storage.rollbackTransaction()
                raise
            else :    
                self.storage.commitTransaction()
                
        if not options["list"] :
            percent.done()
                     
if __name__ == "__main__" : 
    retcode = 0
    try :
        short_options = "hvaCc:D:dg:lrsnpm:"
        long_options = ["help", "version", "add", "cups", "charge=", "description=", \
                        "delete", "groups=", "list", "remove", \
                        "skipexisting", "passthrough", "nopassthrough", \
                        "maxjobsize="]
        
        # Initializes the command line tool
        manager = PKPrinters(doc=__doc__)
        manager.deferredInit()
        
        # parse and checks the command line
        (options, args) = manager.parseCommandline(sys.argv[1:], short_options, long_options)
        
        # sets long options
        options["help"] = options["h"] or options["help"]
        options["version"] = options["v"] or options["version"]
        options["add"] = options["a"] or options["add"]
        options["cups"] = options["C"] or options["cups"]
        options["charge"] = options["c"] or options["charge"]
        options["description"] = options["D"] or options["description"]
        options["delete"] = options["d"] or options["delete"] 
        options["groups"] = options["g"] or options["groups"]
        options["list"] = options["l"] or options["list"]
        options["remove"] = options["r"] or options["remove"]
        options["skipexisting"] = options["s"] or options["skipexisting"]
        options["maxjobsize"] = options["m"] or options["maxjobsize"]
        options["passthrough"] = options["p"] or options["passthrough"]
        options["nopassthrough"] = options["n"] or options["nopassthrough"]
        
        if options["help"] :
            manager.display_usage_and_quit()
        elif options["version"] :
            manager.display_version_and_quit()
        elif (options["delete"] and (options["add"] or options["groups"] or options["charge"] or options["remove"] or options["description"])) \
           or (options["skipexisting"] and not options["add"]) \
           or (options["list"] and (options["add"] or options["delete"] or options["groups"] or options["charge"] or options["remove"] or options["description"])) \
           or (options["passthrough"] and options["nopassthrough"]) \
           or (options["remove"] and options["add"]) :
            raise PyKotaCommandLineError, _("incompatible options, see help.")
        elif options["remove"] and not options["groups"] :
            raise PyKotaCommandLineError, _("You have to pass printer groups names on the command line")
        elif (not args) and (options["add"] or options["delete"]) :
            raise PyKotaCommandLineError, _("You have to pass printer names on the command line")
        else :
            retcode = manager.main(args, options)
    except KeyboardInterrupt :        
        sys.stderr.write("\nInterrupted with Ctrl+C !\n")
        retcode = -3
    except PyKotaCommandLineError, msg :    
        sys.stderr.write("%s : %s\n" % (sys.argv[0], msg))
        retcode = -2
    except SystemExit :        
        pass
    except :
        try :
            manager.crashed("pkprinters failed")
        except :    
            crashed("pkprinters failed")
        retcode = -1

    try :
        manager.storage.close()
    except (TypeError, NameError, AttributeError) :    
        pass
        
    sys.exit(retcode)    
