root / pykota / trunk / bin / pksetup @ 3428

Revision 3413, 19.4 kB (checked in by jerome, 16 years ago)

Removed unnecessary spaces at EOL.

  • 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, 2008 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.51",
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                                       "make pcl",
218                                       "make pcl_install",
219                                     ],
220                      },
221                   ]
222
223    def __init__(self) :
224        """Initializes instance specific datas."""
225        self.launched = []
226
227    def yesno(self, message) :
228        """Asks the end user some question and returns the answer."""
229        try :
230            return raw_input("\n%s ? " % message).strip().upper()[0] == 'Y'
231        except IndexError :
232            return False
233
234    def confirmCommand(self, message, command, record=True) :
235        """Asks for confirmation before a command is launched, and launches it if needed."""
236        if self.yesno("The following command will be launched %(message)s :\n%(command)s\nDo you agree" % locals()) :
237            os.system(command)
238            if record :
239                self.launched.append(command)
240            return True
241        else :
242            return False
243
244    def confirmPipe(self, message, command) :
245        """Asks for confirmation before a command is launched in a pipe, launches it if needed, and returns the result."""
246        if self.yesno("The following command will be launched %(message)s :\n%(command)s\nDo you agree" % locals()) :
247            pipeprocess = os.popen(command, "r")
248            result = pipeprocess.read()
249            pipeprocess.close()
250            return result
251        else :
252            return False
253
254    def listPrinters(self) :
255        """Returns a list of tuples (queuename, deviceuri) for all existing print queues."""
256        result = os.popen("lpstat -v", "r")
257        lines = result.readlines()
258        result.close()
259        printers = []
260        for line in lines :
261            (begin, end) = line.split(':', 1)
262            deviceuri = end.strip()
263            queuename = begin.split()[-1]
264            printers.append((queuename, deviceuri))
265        return printers
266
267    def downloadOtherPackages(self) :
268        """Downloads and install additional packages from http://www.pykota.com or other websites"""
269        olddirectory = os.getcwd()
270        directory = tempfile.mkdtemp()
271        print "\nDownloading additional software not available as packages in %(directory)s" % locals()
272        os.chdir(directory)
273        for package in self.otherpackages :
274            name = package["name"]
275            version = package["version"]
276            url = package["url"] % locals()
277            commands = " && ".join(package["commands"]) % locals()
278            if url.startswith("svn://") :
279                download = 'svn export "%(url)s" %(name)s' % locals()
280            else :
281                download = 'wget --user-agent=pksetup "%(url)s"' % locals()
282            if self.confirmCommand("to download %(name)s" % locals(), download) :
283                self.confirmCommand("to install %(name)s" % locals(), commands)
284        self.confirmCommand("to remove the temporary directory %(directory)s" % locals(),
285                            "rm -fr %(directory)s" % locals(),
286                            record=False)
287        os.chdir(olddirectory)
288
289    def waitPrintersOnline(self) :
290        """Asks the admin to switch all printers ON."""
291        while not self.yesno("First you MUST switch ALL your printers ON. Are ALL your printers ON") :
292            pass
293
294    def setupDatabase(self) :
295        """Creates the database."""
296        pykotadirectory = self.pykotadirectory
297        self.confirmCommand("to create PyKota's database in PostgreSQL", 'su - postgres -c "psql -f %(pykotadirectory)s/postgresql/pykota-postgresql.sql template1"' % locals())
298
299    def configurePostgreSQL(self) :
300        """Configures PostgreSQL for PyKota to work."""
301        pgconffiles = self.confirmPipe("to find PostgreSQL's configuration files", "find /etc -name postgresql.conf 2>/dev/null")
302        if pgconffiles is not False :
303            pgconffiles = [part.strip() for part in pgconffiles.split()]
304            pgconfdirs = [os.path.split(pgconffile)[0] for pgconffile in pgconffiles]
305            for i in range(len(pgconfdirs)) :
306                pgconfdir = pgconfdirs[i]
307                pgconffile = pgconffiles[i]
308                if (len(pgconfdirs) == 1) or self.yesno("Do PostgreSQL configuration files reside in %(pgconfdir)s" % locals()) :
309                    answer = self.confirmPipe("to see if PostgreSQL accepts TCP/IP connections", 'egrep \\"^tcpip_socket|^listen_addresses\\" %(pgconffile)s' % locals())
310                    conflines = pghbaconf.split("\n")
311                    if answer is not False :
312                        answer = answer.strip().lower()
313                        tcpip = answer.endswith("true")
314                        if tcpip is False :
315                            tcpip = answer.startswith("listen_addresses")
316                    else :
317                        tcpip = False
318                    if tcpip :
319                        conflines.insert(2, "host\tpykota\tpykotaadmin,pykotauser\t127.0.0.1\t255.255.255.255\tmd5")
320                    else :
321                        conflines.insert(1, "local\tpykota\tpykotaadmin,pykotauser\t\tmd5")
322                    conf = "\n".join(conflines)
323                    port = 5432
324                    if tcpip :
325                        answer = self.confirmPipe("to see on which TCP port PostgreSQL accepts connections", "grep ^port %(pgconffile)s" % locals())
326                        if answer is not False :
327                            try :
328                                port = int([p.strip() for p in answer.strip().split("=")][1])
329                            except (ValueError, IndexError, TypeError) :
330                                pass
331                    self.confirmCommand("to configure PostgreSQL correctly for PyKota", 'echo "%(conf)s" >%(pgconfdir)s/pg_hba.conf' % locals())
332                    self.confirmCommand("to make PostgreSQL take the changes into account", self.pgrestart)
333                    return (tcpip, port)
334        return (None, None)
335
336    def genConfig(self, adminname, adminemail, dnsdomain, smtpserver, home, tcpip, port) :
337        """Generates minimal configuration files for PyKota."""
338        if tcpip :
339            storageserver = "localhost:%i" % port
340        else :
341            storageserver = ""
342        conf = pykotaconf % locals()
343        self.confirmCommand("to generate PyKota's main configuration file", 'echo "%(conf)s" >%(home)s/pykota.conf' % locals())
344        conf = pykotadminconf % locals()
345        self.confirmCommand("to generate PyKota's administrators configuration file", 'echo "%(conf)s" >%(home)s/pykotadmin.conf' % locals())
346        self.confirmCommand("to change permissions on PyKota's administrators configuration file", "chmod 640 %(home)s/pykotadmin.conf" % locals())
347        self.confirmCommand("to change permissions on PyKota's main configuration file", "chmod 644 %(home)s/pykota.conf" % locals())
348        self.confirmCommand("to change ownership of PyKota's configuration files", "chown pykota.pykota %(home)s/pykota.conf %(home)s/pykotadmin.conf" % locals())
349        answer = self.confirmPipe("to automatically detect the best settings for your printers", "pkturnkey --doconf 2>/dev/null")
350        if answer is not False :
351            lines = answer.split("\n")
352            begin = end = None
353            for i in range(len(lines)) :
354                line = lines[i]
355                if line.strip().startswith("--- CUT ---") :
356                    if begin is None :
357                        begin = i
358                    else :
359                        end = i
360
361            if (begin is not None) and (end is not None) :
362                suffix = "\n".join(lines[begin+1:end])
363                self.confirmCommand("to improve PyKota's configuration wrt your existing printers", 'echo "%(suffix)s" >>%(home)s/pykota.conf' % locals())
364
365    def addPyKotaUser(self) :
366        """Adds a system user named pykota, returns its home directory or None"""
367        try :
368            user = pwd.getpwnam("pykota")
369        except KeyError :
370            if self.confirmCommand("to create a system user named 'pykota'", self.adduser) :
371                try :
372                    return pwd.getpwnam("pykota")[5]
373                except KeyError :
374                    return None
375            else :
376                return None
377        else :
378            return user[5]
379
380    def setupBackend(self) :
381        """Installs the cupspykota backend."""
382        backend = os.path.join(self.backendsdirectory, "cupspykota")
383        if not os.path.exists(backend) :
384            realbackend = os.path.join(self.pykotadirectory, "cupspykota")
385            self.confirmCommand("to make PyKota known to CUPS", "ln -s %(realbackend)s %(backend)s" % locals())
386            self.confirmCommand("to restart CUPS for the changes to take effect", self.cupsrestart)
387
388    def managePrinters(self, printers) :
389        """For each printer, asks if it should be managed with PyKota or not."""
390        for (queuename, deviceuri) in printers :
391            command = 'pkprinters --add --cups --description "Printer created with pksetup" "%(queuename)s"' % locals()
392            self.confirmCommand("to import the %(queuename)s print queue into PyKota's database and reroute it through PyKota" % locals(), command)
393
394    def installPyKotaFiles(self) :
395        """Installs PyKota files through Python's Distutils mechanism."""
396        pksetupdir = os.path.split(os.path.abspath(sys.argv[0]))[0]
397        pykotadir = os.path.abspath(os.path.join(pksetupdir, ".."))
398        setuppy = os.path.join(pykotadir, "setup.py")
399        if os.path.exists(setuppy) :
400            self.confirmCommand("to install PyKota files on your system", "python %(setuppy)s install" % locals())
401
402    def setup(self) :
403        """Installation procedure."""
404        self.installPyKotaFiles()
405        self.waitPrintersOnline()
406        adminname = raw_input("What is the name of the print administrator ? ").strip()
407        adminemail = raw_input("What is the email address of the print administrator ? ").strip()
408        dnsdomain = raw_input("What is your DNS domain name ? ").strip()
409        smtpserver = raw_input("What is the hostname or IP address of your SMTP server ? ").strip()
410        homedirectory = self.addPyKotaUser()
411        if homedirectory is None :
412            logerr("Installation can't proceed. You MUST create a system user named 'pykota'.\n")
413        else :
414            self.upgradeSystem()
415            self.setupPackages()
416            self.downloadOtherPackages()
417            (tcpip, port) = self.configurePostgreSQL()
418            self.genConfig(adminname, adminemail, dnsdomain, smtpserver, homedirectory, tcpip, port)
419            self.setupDatabase()
420            self.setupBackend()
421            self.managePrinters(self.listPrinters())
422            print nowready
423            print "The script %s can be used to reinstall in unattended mode.\n" % self.genInstaller()
424
425    def genInstaller(self) :
426        """Generates an installer script."""
427        scriptname = "/tmp/pykota-installer.sh"
428        commands = [ "#! /bin/sh",
429                     "#",
430                     "# PyKota installer script.",
431                     "#",
432                     "# This script was automatically generated.",
433                     "#",
434                   ] + self.launched
435        script = open(scriptname, "w")
436        script.write("\n".join(commands))
437        script.close()
438        os.chmod(scriptname, \
439                 stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH)
440        return scriptname
441
442
443class Debian(PyKotaSetup) :
444    """Class for Debian installer."""
445    def setupPackages(self) :
446        """Installs missing Debian packages."""
447        self.confirmCommand("to install missing dependencies", "apt-get install %s" % " ".join(self.packages))
448
449    def upgradeSystem(self) :
450        """Upgrades the Debian setup."""
451        if self.confirmCommand("to grab an up-to-date list of available packages", "apt-get update") :
452            self.confirmCommand("to put your system up-to-date", "apt-get -y dist-upgrade")
453
454class Ubuntu(Debian) :
455    """Class for Ubuntu installer."""
456    pass
457
458if __name__ == "__main__" :
459    retcode = 0
460    if (len(sys.argv) != 2) or (sys.argv[1] == "-h") or (sys.argv[1] == "--help") :
461        print "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."
462    elif (sys.argv[1] == "-v") or (sys.argv[1] == "--version") :
463        print "0.1" # pksetup's own version number
464    else :
465        classname = sys.argv[1].strip().title()
466        try :
467            installer = globals()[classname]()
468        except KeyError :
469            logerr("There's currently no support for the %s distribution, sorry.\n" % sys.argv[1])
470            retcode = -1
471        else :
472            try :
473                retcode = installer.setup()
474            except KeyboardInterrupt :
475                logerr("\n\n\nWARNING : Setup was aborted at user's request !\n\n")
476                retcode = -1
477    sys.exit(retcode)
Note: See TracBrowser for help on using the browser.