root / pykota / trunk / bin / pksetup @ 3505

Revision 3505, 19.8 kB (checked in by jerome, 15 years ago)

Added missing double quotes. Fixes #46.

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