root / pykota / branches / 1.26_fixes / bin / pksetup @ 3494

Revision 3494, 18.9 kB (checked in by jerome, 15 years ago)

Backported the fix to #38.

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