root / pykota / trunk / bin / pksetup @ 3260

Revision 3260, 19.9 kB (checked in by jerome, 16 years ago)

Changed license to GNU GPL v3 or later.
Changed Python source encoding from ISO-8859-15 to UTF-8 (only ASCII
was used anyway).

  • Property svn:executable set to *
  • Property svn:keywords set to Author Date Id Revision
Line 
1#! /usr/bin/env python
2# -*- coding: UTF-8 -*-
3#
4# PyKota : Print Quotas for CUPS
5#
6# (c) 2003, 2004, 2005, 2006, 2007 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 3 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, see <http://www.gnu.org/licenses/>.
19#
20# $Id$
21#
22#
23
24import sys
25import os
26import stat
27import tempfile
28import pwd
29import grp
30
31nowready = """
32
33
34PyKota is now ready to run !
35
36Before printing, you still have to manually modify CUPS' printers.conf
37to manually prepend cupspykota:// in front of each DeviceURI.
38
39Once this is done, just restart CUPS and all should work fine.
40
41Please report any problem to : alet@librelogiciel.com
42
43Thanks in advance.
44"""
45
46pghbaconf = """local\tall\tpostgres\t\tident sameuser
47local\tall\tall\t\tident sameuser
48host\tall\tall\t127.0.0.1\t255.255.255.255\tident sameuser
49host\tall\tall\t::1\tffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff\treject
50host\tall\tall\t::ffff:127.0.0.1/128\treject
51host\tall\tall\t0.0.0.0\t0.0.0.0\treject"""
52
53pykotadminconf = """[global]
54storageadmin: pykotaadmin
55storageadminpw: readwritepw"""
56
57pykotaconf = """#
58# This is a generated configuration file for PyKota
59#
60# IMPORTANT : many more directives can be used, and some of the directives
61# below accept different and/or more complex parameters. Please read
62# /usr/share/pykota/conf/pykota.conf.sample for more details about the
63# numerous possibilities allowed.
64#
65[global]
66
67# Database settings
68storagebackend : pgstorage
69storageserver : %(storageserver)s
70storagename : pykota
71storageuser : pykotauser
72storageuserpw : readonlypw
73storagecaching : No
74disablehistory : No
75
76# Logging method
77logger : system
78
79# Set debug to Yes during installation and testing
80debug : Yes
81
82# Who should receive automatic bug reports ?
83crashrecipient : pykotacrashed@librelogiciel.com
84
85# Should we keep temporary files on disk ?
86# Set this to yes for debugging software accounting problems
87keepfiles : no
88
89# Logos for banners and CGI scripts
90logourl : http://www.pykota.com/pykota.png
91logolink : http://www.pykota.com/
92
93# SMTP
94smtpserver : %(smtpserver)s
95maildomain : %(dnsdomain)s
96
97# Print Administrator
98admin : %(adminname)s
99adminmail : %(adminemail)s
100
101# Use usernames as-is or convert them to lowercase ?
102utolower : No
103
104# Should we hide some fields in the history (title, filename) ?
105privacy : no
106
107# Should we charge end users when an error occurs ?
108onbackenderror : nocharge
109
110# Default accounting methods :
111preaccounter : software()
112accounter : software()
113onaccountererror : stop
114
115# Who will receive warning messages ?
116# both means admin and user.
117mailto : both
118
119# Grace delay for pages based quotas, works the same
120# as for disk quotas
121gracedelay : 7
122
123# Configurable zero, to give free credits
124balancezero : 0.0
125
126# Warning limit for credit based quotas
127poorman : 1.0
128
129# Warning messages to use
130poorwarn : Your Print Quota account balance is low.
131 Soon you'll not be allowed to print anymore.
132
133softwarn : Your Print Quota Soft Limit is reached.
134 This means that you may still be allowed to print for some
135 time, but you must contact your administrator to purchase
136 more print quota.
137
138hardwarn : Your Print Quota Hard Limit is reached.
139 This means that you are not allowed to print anymore.
140 Please contact your administrator at root@localhost
141 as soon as possible to solve the problem.
142
143# Number of banners allowed to be printed by users
144# who are over quota
145maxdenybanners : 0
146
147# Should we allow users to ever be over quota on their last job ?
148# strict means no.
149enforcement : strict
150
151# Should we trust printers' internal page counter ?
152trustjobsize : yes
153
154# How to handle duplicate jobs
155denyduplicates : no
156duplicatesdelay : 0
157
158# What should we do when an unknown user prints ?
159# The policy below will automatically create a printing account
160# for unknown users, allowing them to print with no limit on the
161# current printer.
162policy : external(pkusers --add --skipexisting --limitby noquota --description \"Added automatically\" \$PYKOTAUSERNAME && edpykota --add --skipexisting --printer \$PYKOTAPRINTERNAME \$PYKOTAUSERNAME)
163
164"""
165
166
167class PyKotaSetup :
168    """Base class for PyKota installers."""
169    backendsdirectory = "/usr/lib/cups/backend" # overload it if needed
170    pykotadirectory = "/usr/share/pykota"       # overload it if needed
171    pgrestart = "/etc/init.d/postgresql* restart" # overload it if needed
172    cupsrestart = "/etc/init.d/cupsys restart"  # overload it if needed
173    adduser = "adduser --system --group --home /etc/pykota --gecos PyKota pykota" # overload it if needed
174    packages = [ "wget", 
175                 "bzip2", 
176                 "subversion", 
177                 "postgresql", 
178                 "postgresql-client",
179                 "cupsys",
180                 "cupsys-client",
181                 "python-dev",
182                 "python-jaxml",
183                 "python-reportlab",
184                 "python-reportlab-accel",
185                 "python-psyco",
186                 "python-pygresql",
187                 "python-psyco",
188                 "python-osd",
189                 "python-egenix-mxdatetime",
190                 "python-imaging",
191                 "python-pysnmp4",
192                 "python-chardet",
193                 "python-pam" ]
194       
195    otherpackages = [ { "name" : "pkpgcounter",
196                        "version" : "3.30",
197                        "url" : "http://www.pykota.com/software/%(name)s/download/tarballs/%(name)s-%(version)s.tar.gz",
198                        "commands" : [ "tar -zxf %(name)s-%(version)s.tar.gz",
199                                       "cd %(name)s-%(version)s",
200                                       "python setup.py install",
201                                     ],
202                      },
203                      { "name" : "pkipplib",
204                        "version" : "0.07",
205                        "url" : "http://www.pykota.com/software/%(name)s/download/tarballs/%(name)s-%(version)s.tar.gz",
206                        "commands" : [ "tar -zxf %(name)s-%(version)s.tar.gz",
207                                       "cd %(name)s-%(version)s",
208                                       "python setup.py install",
209                                     ],
210                      },
211                      { "name" : "ghostpdl",
212                        "version" : "1.51",
213                        "url" : "http://mirror.cs.wisc.edu/pub/mirrors/ghost/GPL/%(name)s/%(name)s-%(version)s.tar.bz2",
214                        "commands" : [ "bunzip2 <%(name)s_%(version)s.tar.bz2 | tar -xf -",
215                                       "cd %(name)s_%(version)s",
216                                       "make pcl",
217                                       "make pcl_install",
218                                     ],
219                      },
220                   ] 
221                   
222    def __init__(self) :               
223        """Initializes instance specific datas."""
224        self.launched = []
225       
226    def yesno(self, message) :       
227        """Asks the end user some question and returns the answer."""
228        try :
229            return raw_input("\n%s ? " % message).strip().upper()[0] == 'Y'
230        except IndexError :   
231            return False
232       
233    def confirmCommand(self, message, command, record=True) :   
234        """Asks for confirmation before a command is launched, and launches it if needed."""
235        if self.yesno("The following command will be launched %(message)s :\n%(command)s\nDo you agree" % locals()) :
236            os.system(command)
237            if record :
238                self.launched.append(command)
239            return True
240        else :
241            return False
242           
243    def confirmPipe(self, message, command) :   
244        """Asks for confirmation before a command is launched in a pipe, launches it if needed, and returns the result."""
245        if self.yesno("The following command will be launched %(message)s :\n%(command)s\nDo you agree" % locals()) :
246            pipeprocess = os.popen(command, "r")
247            result = pipeprocess.read()
248            pipeprocess.close()
249            return result
250        else :
251            return False
252           
253    def listPrinters(self) :
254        """Returns a list of tuples (queuename, deviceuri) for all existing print queues."""
255        result = os.popen("lpstat -v", "r")
256        lines = result.readlines()
257        result.close()
258        printers = []
259        for line in lines :
260            (begin, end) = line.split(':', 1)
261            deviceuri = end.strip()
262            queuename = begin.split()[-1]
263            printers.append((queuename, deviceuri))
264        return printers   
265       
266    def downloadOtherPackages(self) :   
267        """Downloads and install additional packages from http://www.pykota.com or other websites"""
268        olddirectory = os.getcwd()
269        directory = tempfile.mkdtemp()
270        print "\nDownloading additional software not available as packages in %(directory)s" % locals()
271        os.chdir(directory)
272        for package in self.otherpackages :
273            name = package["name"]
274            version = package["version"]
275            url = package["url"] % locals()
276            commands = " && ".join(package["commands"]) % locals()
277            if url.startswith("svn://") :
278                download = 'svn export "%(url)s" %(name)s' % locals()
279            else :   
280                download = 'wget --user-agent=pksetup "%(url)s"' % locals()
281            if self.confirmCommand("to download %(name)s" % locals(), download) :
282                self.confirmCommand("to install %(name)s" % locals(), commands)
283        self.confirmCommand("to remove the temporary directory %(directory)s" % locals(),     
284                            "rm -fr %(directory)s" % locals(),
285                            record=False)
286        os.chdir(olddirectory)   
287       
288    def waitPrintersOnline(self) :
289        """Asks the admin to switch all printers ON."""
290        while not self.yesno("First you MUST switch ALL your printers ON. Are ALL your printers ON") :
291            pass
292           
293    def setupDatabase(self) :
294        """Creates the database."""
295        pykotadirectory = self.pykotadirectory
296        self.confirmCommand("to create PyKota's database in PostgreSQL", 'su - postgres -c "psql -f %(pykotadirectory)s/postgresql/pykota-postgresql.sql template1"' % locals())
297       
298    def configurePostgreSQL(self) :
299        """Configures PostgreSQL for PyKota to work."""
300        pgconffiles = self.confirmPipe("to find PostgreSQL's configuration files", "find /etc -name postgresql.conf 2>/dev/null")
301        if pgconffiles is not False :
302            pgconffiles = [part.strip() for part in pgconffiles.split()]
303            pgconfdirs = [os.path.split(pgconffile)[0] for pgconffile in pgconffiles]
304            for i in range(len(pgconfdirs)) :
305                pgconfdir = pgconfdirs[i]
306                pgconffile = pgconffiles[i]
307                if (len(pgconfdirs) == 1) or self.yesno("Do PostgreSQL configuration files reside in %(pgconfdir)s" % locals()) :
308                    answer = self.confirmPipe("to see if PostgreSQL accepts TCP/IP connections", "grep ^tcpip_socket %(pgconffile)s" % locals())
309                    conflines = pghbaconf.split("\n")
310                    if answer is not False :
311                        tcpip = answer.strip().lower().endswith("true")
312                    else :   
313                        tcpip = False
314                    if tcpip :   
315                        conflines.insert(2, "host\tpykota\tpykotaadmin,pykotauser\t127.0.0.1\t255.255.255.255\tmd5")
316                    else :
317                        conflines.insert(1, "local\tpykota\tpykotaadmin,pykotauser\t\tmd5")
318                    conf = "\n".join(conflines)
319                    port = 5432
320                    if tcpip :
321                        answer = self.confirmPipe("to see on which TCP port PostgreSQL accepts connections", "grep ^port %(pgconffile)s" % locals())
322                        if answer is not False :
323                            try :
324                                port = int([p.strip() for p in answer.strip().split("=")][1])
325                            except (ValueError, IndexError, TypeError) :
326                                pass
327                    self.confirmCommand("to configure PostgreSQL correctly for PyKota", 'echo "%(conf)s" >%(pgconfdir)s/pg_hba.conf' % locals())
328                    self.confirmCommand("to make PostgreSQL take the changes into account", self.pgrestart)
329                    return (tcpip, port)
330        return (None, None)
331       
332    def genConfig(self, adminname, adminemail, dnsdomain, smtpserver, home, tcpip, port) :
333        """Generates minimal configuration files for PyKota."""
334        if tcpip :
335            storageserver = "localhost:%i" % port
336        else :   
337            storageserver = ""
338        conf = pykotaconf % locals()
339        self.confirmCommand("to generate PyKota's main configuration file", 'echo "%(conf)s" >%(home)s/pykota.conf' % locals())
340        conf = pykotadminconf % locals()
341        self.confirmCommand("to generate PyKota's administrators configuration file", 'echo "%(conf)s" >%(home)s/pykotadmin.conf' % locals())
342        self.confirmCommand("to change permissions on PyKota's administrators configuration file", "chmod 640 %(home)s/pykotadmin.conf" % locals())
343        self.confirmCommand("to change permissions on PyKota's main configuration file", "chmod 644 %(home)s/pykota.conf" % locals())
344        self.confirmCommand("to change ownership of PyKota's configuration files", "chown pykota.pykota %(home)s/pykota.conf %(home)s/pykotadmin.conf" % locals())
345        answer = self.confirmPipe("to automatically detect the best settings for your printers", "pkturnkey --doconf 2>/dev/null")
346        if answer is not False :
347            lines = answer.split("\n")
348            begin = end = None
349            for i in range(len(lines)) :
350                line = lines[i]
351                if line.strip().startswith("--- CUT ---") :
352                    if begin is None :
353                        begin = i
354                    else :   
355                        end = i
356                       
357            if (begin is not None) and (end is not None) :
358                suffix = "\n".join(lines[begin+1:end])
359                self.confirmCommand("to improve PyKota's configuration wrt your existing printers", 'echo "%(suffix)s" >>%(home)s/pykota.conf' % locals())
360       
361    def addPyKotaUser(self) :
362        """Adds a system user named pykota, returns its home directory or None"""
363        try :
364            user = pwd.getpwnam("pykota")
365        except KeyError :   
366            if self.confirmCommand("to create a system user named 'pykota'", self.adduser) :
367                try :   
368                    return pwd.getpwnam("pykota")[5]
369                except KeyError :   
370                    return None
371            else :       
372                return None
373        else :   
374            return user[5]
375   
376    def setupBackend(self) :
377        """Installs the cupspykota backend."""
378        backend = os.path.join(self.backendsdirectory, "cupspykota")
379        if not os.path.exists(backend) :
380            realbackend = os.path.join(self.pykotadirectory, "cupspykota")
381            self.confirmCommand("to make PyKota known to CUPS", "ln -s %(realbackend)s %(backend)s" % locals())
382            self.confirmCommand("to restart CUPS for the changes to take effect", self.cupsrestart)
383       
384    def managePrinters(self, printers) :   
385        """For each printer, asks if it should be managed with PyKota or not."""
386        for (queuename, deviceuri) in printers :
387            command = 'pkprinters --add --cups --description "Printer created with pksetup" "%(queuename)s"' % locals()
388            self.confirmCommand("to import the %(queuename)s print queue into PyKota's database and reroute it through PyKota" % locals(), command)
389   
390    def installPyKotaFiles(self) :
391        """Installs PyKota files through Python's Distutils mechanism."""
392        pksetupdir = os.path.split(os.path.abspath(sys.argv[0]))[0]
393        pykotadir = os.path.abspath(os.path.join(pksetupdir, ".."))
394        setuppy = os.path.join(pykotadir, "setup.py")
395        if os.path.exists(setuppy) :
396            self.confirmCommand("to install PyKota files on your system", "python %(setuppy)s install" % locals())
397       
398    def setup(self) :
399        """Installation procedure."""
400        self.installPyKotaFiles()
401        self.waitPrintersOnline()
402        adminname = raw_input("What is the name of the print administrator ? ").strip()
403        adminemail = raw_input("What is the email address of the print administrator ? ").strip()
404        dnsdomain = raw_input("What is your DNS domain name ? ").strip()
405        smtpserver = raw_input("What is the hostname or IP address of your SMTP server ? ").strip()
406        homedirectory = self.addPyKotaUser()
407        if homedirectory is None :
408            sys.stderr.write("Installation can't proceed. You MUST create a system user named 'pykota'.\n")
409        else :   
410            self.upgradeSystem()
411            self.setupPackages()
412            self.downloadOtherPackages()
413            (tcpip, port) = self.configurePostgreSQL()
414            self.genConfig(adminname, adminemail, dnsdomain, smtpserver, homedirectory, tcpip, port)
415            self.setupDatabase()
416            self.setupBackend()
417            self.managePrinters(self.listPrinters())
418            print nowready
419            print "The script %s can be used to reinstall in unattended mode.\n" % self.genInstaller()
420           
421    def genInstaller(self) :       
422        """Generates an installer script."""
423        scriptname = "/tmp/pykota-installer.sh"
424        commands = [ "#! /bin/sh", 
425                     "#",
426                     "# PyKota installer script.", 
427                     "#", 
428                     "# This script was automatically generated.",
429                     "#",
430                   ] + self.launched
431        script = open(scriptname, "w")           
432        script.write("\n".join(commands))
433        script.close()
434        os.chmod(scriptname, \
435                 stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH)
436        return scriptname   
437       
438       
439class Debian(PyKotaSetup) :       
440    """Class for Debian installer."""
441    def setupPackages(self) :   
442        """Installs missing Debian packages."""
443        self.confirmCommand("to install missing dependencies", "apt-get install %s" % " ".join(self.packages))
444           
445    def upgradeSystem(self) :
446        """Upgrades the Debian setup."""
447        if self.confirmCommand("to grab an up-to-date list of available packages", "apt-get update") :
448            self.confirmCommand("to put your system up-to-date", "apt-get -y dist-upgrade")
449   
450class Ubuntu(Debian) :   
451    """Class for Ubuntu installer."""
452    pass
453   
454if __name__ == "__main__" :       
455    retcode = 0
456    if (len(sys.argv) != 2) or (sys.argv[1] == "-h") or (sys.argv[1] == "--help") :
457        print "pksetup v0.1 (c) 2003-2007 Jerome Alet - alet@librelogiciel.com\n\nusage : pksetup distribution\n\ne.g. : pksetup debian\n\nIMPORTANT : only Debian and Ubuntu are currently supported."
458    elif (sys.argv[1] == "-v") or (sys.argv[1] == "--version") :   
459        print "0.1" # pksetup's own version number
460    else :   
461        classname = sys.argv[1].strip().title()
462        try :
463            installer = globals()[classname]()
464        except KeyError :   
465            sys.stderr.write("There's currently no support for the %s distribution, sorry.\n" % sys.argv[1])
466            retcode = -1
467        else :   
468            try :
469                retcode = installer.setup()
470            except KeyboardInterrupt :             
471                sys.stderr.write("\n\n\nWARNING : Setup was aborted at user's request !\n\n")
472                retcode = -1
473    sys.exit(retcode)
Note: See TracBrowser for help on using the browser.