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
Line 
1#! /usr/bin/env python
2# -*- coding: utf-8 -*-
3#
4# PyKota : Print Quotas for CUPS
5#
6# (c) 2003-2009 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# The charset to use when parsing the configuration files
68config_charset : UTF-8
69
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
104# Use usernames as-is or convert them to lowercase or uppercase ?
105usernamecase : native
106
107# Winbind separator character, uncomment if needed
108# winbind_separator : /
109
110# Should we forbid unknown users from printing ?
111reject_unknown : No
112
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
124# Who will receive warning messages ?
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
138# Warning messages to use
139poorwarn : Your Print Quota account balance is low.
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
144 time, but you must contact your administrator to purchase
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
152# Number of banners allowed to be printed by users
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.
171policy : external(pkusers --add --skipexisting --limitby noquota --description \"Added automatically\" \$PYKOTAUSERNAME && edpykota --add --skipexisting --printer \$PYKOTAPRINTERNAME \$PYKOTAUSERNAME)
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
183    packages = [ "wget",
184                 "bzip2",
185                 "subversion",
186                 "postgresql",
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",
200                 "python-pam",
201                 "pkpgcounter" ]
202
203    otherpackages = [
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                      },
212                      { "name" : "ghostpdl",
213                        "version" : "1.54",
214                        "url" : "http://mirror.cs.wisc.edu/pub/mirrors/ghost/GPL/%(name)s/%(name)s-%(version)s.tar.bz2",
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",
221                                       "make pcl",
222                                       "make install",
223                                     ],
224                      },
225                   ]
226
227    def __init__(self) :
228        """Initializes instance specific datas."""
229        self.launched = []
230
231    def yesno(self, message) :
232        """Asks the end user some question and returns the answer."""
233        try :
234            return raw_input("\n%s ? " % message).strip().upper()[0] == 'Y'
235        except IndexError :
236            return False
237
238    def confirmCommand(self, message, command, record=True) :
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
247
248    def confirmPipe(self, message, command) :
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
257
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))
269        return printers
270
271    def downloadOtherPackages(self) :
272        """Downloads and install additional packages from http://www.pykota.com or other websites"""
273        olddirectory = os.getcwd()
274        directory = tempfile.mkdtemp()
275        sys.stdout.write("\nDownloading additional software not available as packages in %(directory)s\n" % locals())
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()
284            else :
285                download = 'wget --user-agent=pksetup "%(url)s"' % locals()
286            if self.confirmCommand("to download %(name)s" % locals(), download) :
287                self.confirmCommand("to install %(name)s" % locals(), commands)
288        self.confirmCommand("to remove the temporary directory %(directory)s" % locals(),
289                            "rm -fr %(directory)s" % locals(),
290                            record=False)
291        os.chdir(olddirectory)
292
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
297
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())
302
303    def configurePostgreSQL(self) :
304        """Configures PostgreSQL for PyKota to work."""
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()) :
313                    answer = self.confirmPipe("to see if PostgreSQL accepts TCP/IP connections", 'egrep "\\"^tcpip_socket|^listen_addresses\\"" %(pgconffile)s' % locals())
314                    conflines = pghbaconf.split("\n")
315                    if answer is not False :
316                        answer = answer.strip().lower()
317                        tcpip = answer.endswith("true")
318                        if tcpip is False :
319                            tcpip = answer.startswith("listen_addresses")
320                    else :
321                        tcpip = False
322                    if tcpip :
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)
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
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)
337                    return (tcpip, port)
338        return (None, None)
339
340    def genConfig(self, adminname, adminemail, dnsdomain, smtpserver, home, tcpip, port) :
341        """Generates minimal configuration files for PyKota."""
342        if tcpip :
343            storageserver = "localhost:%i" % port
344        else :
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
362                    else :
363                        end = i
364
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())
368
369    def addPyKotaUser(self) :
370        """Adds a system user named pykota, returns its home directory or None"""
371        try :
372            user = pwd.getpwnam("pykota")
373        except KeyError :
374            if self.confirmCommand("to create a system user named 'pykota'", self.adduser) :
375                try :
376                    return pwd.getpwnam("pykota")[5]
377                except KeyError :
378                    return None
379            else :
380                return None
381        else :
382            return user[5]
383
384    def setupBackend(self) :
385        """Installs the cupspykota backend."""
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)
391
392    def managePrinters(self, printers) :
393        """For each printer, asks if it should be managed with PyKota or not."""
394        for (queuename, deviceuri) in printers :
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)
397
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")
403        if os.path.exists(setuppy) :
404            self.confirmCommand("to install PyKota files on your system", "python %(setuppy)s install" % locals())
405
406    def setup(self) :
407        """Installation procedure."""
408        self.installPyKotaFiles()
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 :
416            sys.stderr.write("Installation can't proceed. You MUST create a system user named 'pykota'.\n")
417        else :
418            self.upgradeSystem()
419            self.setupPackages()
420            self.downloadOtherPackages()
421            (tcpip, port) = self.configurePostgreSQL()
422            self.genConfig(adminname, adminemail, dnsdomain, smtpserver, homedirectory, tcpip, port)
423            self.setupDatabase()
424            self.setupBackend()
425            self.managePrinters(self.listPrinters())
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())
428
429    def genInstaller(self) :
430        """Generates an installer script."""
431        scriptname = "/tmp/pykota-installer.sh"
432        commands = [ "#! /bin/sh",
433                     "#",
434                     "# PyKota installer script.",
435                     "#",
436                     "# This script was automatically generated.",
437                     "#",
438                   ] + self.launched
439        script = open(scriptname, "w")
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)
444        return scriptname
445
446
447class Debian(PyKotaSetup) :
448    """Class for Debian installer."""
449    def setupPackages(self) :
450        """Installs missing Debian packages."""
451        self.confirmCommand("to install missing dependencies", "apt-get install %s" % " ".join(self.packages))
452
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")
457
458class Ubuntu(Debian) :
459    """Class for Ubuntu installer."""
460    pass
461
462if __name__ == "__main__" :
463    retcode = 0
464    if (len(sys.argv) != 2) or (sys.argv[1] == "-h") or (sys.argv[1] == "--help") :
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")
466    elif (sys.argv[1] == "-v") or (sys.argv[1] == "--version") :
467        sys.stdout.write("0.1\n") # pksetup's own version number
468    else :
469        classname = sys.argv[1].strip().title()
470        try :
471            installer = globals()[classname]()
472        except KeyError :
473            sys.stderr.write("There's currently no support for the %s distribution, sorry.\n" % sys.argv[1])
474            retcode = -1
475        else :
476            try :
477                retcode = installer.setup()
478            except KeyboardInterrupt :
479                sys.stderr.write("\n\n\nWARNING : Setup was aborted at user's request !\n\n")
480                retcode = -1
481    sys.exit(retcode)
Note: See TracBrowser for help on using the browser.