root / pykota / trunk / bin / pksetup @ 3402

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

Now uses Debian or Ubuntu's package for pkpgcounter.

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