# ------------------------------------------------------------------------------ import os, os.path, subprocess, md5, shutil, random from appy.shared.utils import getOsTempFolder, FolderDeleter, cleanFolder # ------------------------------------------------------------------------------ debianInfo = '''Package: python-appy%s Version: %s Architecture: all Maintainer: Gaetan Delannay Installed-Size: %d Depends: python (>= %s)%s Section: python Priority: optional Homepage: http://appyframework.org Description: Appy builds simple but complex web Python apps. ''' appCtl = '''#! /usr/lib/zope2.12/bin/python import sys from appy.bin.zopectl import ZopeRunner args = ' '.join(sys.argv[1:]) sys.argv = [sys.argv[0], '-C', '/etc/%s.conf', args] ZopeRunner().run() ''' appRun = '''#! /bin/sh exec "/usr/lib/zope2.12/bin/runzope" -C "/etc/%s.conf" "$@" ''' ooStart = 'soffice -invisible -headless -nofirststartwizard ' \ '"-accept=socket,host=localhost,port=2002;urp;"' ooStartSh = '#! /bin/sh\n%s\n' % ooStart zopeConf = '''# Zope configuration. %%define INSTANCE %s %%define DATA %s %%define LOG %s %%define HTTPPORT %s %%define ZOPE_USER zope instancehome $INSTANCE effective-user $ZOPE_USER %s level info path $LOG/event.log level info level WARN path $LOG/Z2.log format %%(message)s address $HTTPPORT path $DATA/Data.fs mount-point / name temporary storage for sessioning mount-point /temp_folder container-class Products.TemporaryFolder.TemporaryContainer ''' # initScript below will be used to define the scripts that will run the # app-powered Zope instance and OpenOffice in server mode at boot time. initScript = '''#! /bin/sh ### BEGIN INIT INFO # Provides: %s # Required-Start: $syslog $remote_fs # Required-Stop: $syslog $remote_fs # Should-Start: $remote_fs # Should-Stop: $remote_fs # Default-Start: 2 3 4 5 # Default-Stop: 0 1 6 # Short-Description: Start %s # Description: Start the Zope and Appy-based %s application. ### END INIT INFO case "$1" in start) %s ;; restart|reload|force-reload) %s ;; stop) %s ;; *) echo "Usage: $0 start|restart|stop" >&2 exit 3 ;; esac exit 0 ''' class Debianizer: '''This class allows to produce a Debian package from a Python (Appy) package.''' def __init__(self, app, out, appVersion='0.1.0', pythonVersions=('2.6',), zopePort=8080, depends=('zope2.12', 'openoffice.org', 'imagemagick'), sign=True): # app is the path to the Python package to Debianize. self.app = app self.appName = os.path.basename(app) self.appNameLower = self.appName.lower() # Must we sign the Debian package? If yes, we make the assumption that # the currently logged user has a public/private key pair in ~/.gnupg, # generated with command "gpg --gen-key". self.sign = sign # out is the folder where the Debian package will be generated. self.out = out # What is the version number for this app ? self.appVersion = appVersion # On which Python versions will the Debian package depend? self.pythonVersions = pythonVersions # Port for Zope self.zopePort = zopePort # Debian package dependencies self.depends = depends # Zope 2.12 requires Python 2.6 if 'zope2.12' in depends: self.pythonVersions = ('2.6',) def run(self): '''Generates the Debian package.''' curdir = os.getcwd() j = os.path.join tempFolder = getOsTempFolder() # Create, in the temp folder, the required sub-structure for the Debian # package. debFolder = j(tempFolder, 'debian') if os.path.exists(debFolder): FolderDeleter.delete(debFolder) # Copy the Python package into it srcFolder = j(debFolder, 'usr', 'lib') for version in self.pythonVersions: libFolder = j(srcFolder, 'python%s' % version) os.makedirs(libFolder) destFolder = j(libFolder, self.appName) shutil.copytree(self.app, destFolder) # Clean dest folder (.svn/.bzr files) cleanFolder(destFolder, folders=('.svn', '.bzr')) # When packaging Appy itself, everything is in /usr/lib/pythonX. When # packaging an Appy app, we will generate more files for creating a # running instance. if self.appName != 'appy': # Create the folders that will collectively represent the deployed # Zope instance. binFolder = j(debFolder, 'usr', 'bin') os.makedirs(binFolder) # ctl name = '%s/%sctl' % (binFolder, self.appNameLower) f = file(name, 'w') f.write(appCtl % self.appNameLower) os.chmod(name, 0744) # Make it executable by owner. f.close() # run name = '%s/%srun' % (binFolder, self.appNameLower) f = file(name, 'w') f.write(appRun % self.appNameLower) os.chmod(name, 0744) # Make it executable by owner. f.close() # startoo name = '%s/startoo' % binFolder f = file(name, 'w') f.write(ooStartSh) f.close() os.chmod(name, 0744) # Make it executable by owner. # /var/lib/ (will store Data.fs, lock files, etc) varLibFolder = j(debFolder, 'var', 'lib', self.appNameLower) os.makedirs(varLibFolder) f = file('%s/README' % varLibFolder, 'w') f.write('This folder stores the %s database.\n' % self.appName) f.close() # /var/log/ (will store event.log and Z2.log) varLogFolder = j(debFolder, 'var', 'log', self.appNameLower) os.makedirs(varLogFolder) f = file('%s/README' % varLogFolder, 'w') f.write('This folder stores the log files for %s.\n' % self.appName) f.close() # /etc/.conf (Zope configuration file) etcFolder = j(debFolder, 'etc') os.makedirs(etcFolder) name = '%s/%s.conf' % (etcFolder, self.appNameLower) n = self.appNameLower f = file(name, 'w') productsFolder = '/usr/lib/python%s/%s/zope' % \ (self.pythonVersions[0], self.appName) f.write(zopeConf % ('/var/lib/%s' % n, '/var/lib/%s' % n, '/var/log/%s' % n, str(self.zopePort), 'products %s\n' % productsFolder)) f.close() # /etc/init.d/ (start the app at boot time) initdFolder = j(etcFolder, 'init.d') os.makedirs(initdFolder) name = '%s/%s' % (initdFolder, self.appNameLower) f = file(name, 'w') n = self.appNameLower f.write(initScript % (n, n, 'Start the Zope and Appy-based %s ' \ 'application.' % n, '%sctl start' % n, '%sctl restart' % n, '%sctl stop' % n)) f.close() os.chmod(name, 0744) # Make it executable by owner. # /etc/init.d/oo (start OpenOffice at boot time) name = '%s/oo' % initdFolder f = file(name, 'w') f.write(initScript % ('oo', 'oo', 'Start OpenOffice in server mode', ooStart, ooStart, "#Can't stop OO.")) f.close() os.chmod(name, 0744) # Make it executable by owner. # Get the size of the app, in Kb. os.chdir(tempFolder) cmd = subprocess.Popen(['du', '-b', '-s', 'debian'], stdout=subprocess.PIPE) size = int(int(cmd.stdout.read().split()[0])/1024.0) os.chdir(debFolder) # Create data.tar.gz based on it. os.system('tar czvf data.tar.gz *') # Create the control file f = file('control', 'w') nameSuffix = '' dependencies = [] if self.appName != 'appy': nameSuffix = '-%s' % self.appNameLower dependencies.append('python-appy') if self.depends: for d in self.depends: dependencies.append(d) depends = '' if dependencies: depends = ', ' + ', '.join(dependencies) f.write(debianInfo % (nameSuffix, self.appVersion, size, self.pythonVersions[0], depends)) f.close() # Create md5sum file f = file('md5sums', 'w') toWalk = ['usr'] if self.appName != 'appy': toWalk += ['etc', 'var'] for folderToWalk in toWalk: for dir, dirnames, filenames in os.walk(folderToWalk): for name in filenames: m = md5.new() pathName = j(dir, name) currentFile = file(pathName, 'rb') while True: data = currentFile.read(8096) if not data: break m.update(data) currentFile.close() # Add the md5 sum to the file f.write('%s %s\n' % (m.hexdigest(), pathName)) f.close() # Create postinst, a script that will: # - bytecompile Python files after the Debian install # - change ownership of some files if required # - [in the case of an app-package] call update-rc.d for starting it at # boot time. f = file('postinst', 'w') content = '#!/bin/sh\nset -e\n' for version in self.pythonVersions: bin = '/usr/bin/python%s' % version lib = '/usr/lib/python%s' % version cmds = ' %s -m compileall -q %s/%s 2> /dev/null\n' % (bin, lib, self.appName) content += 'if [ -e %s ]\nthen\n%sfi\n' % (bin, cmds) if self.appName != 'appy': # Allow user "zope", that runs the Zope instance, to write the # database and log files. content += 'chown -R zope:root /var/lib/%s\n' % self.appNameLower content += 'chown -R zope:root /var/log/%s\n' % self.appNameLower # Call update-rc.d for starting the app at boot time content += 'update-rc.d %s defaults\n' % self.appNameLower content += 'update-rc.d oo defaults\n' # (re-)start the app content += '%sctl restart\n' % self.appNameLower # (re-)start oo content += 'startoo\n' f.write(content) f.close() # Create prerm, a script that will remove all pyc files before removing # the Debian package. f = file('prerm', 'w') content = '#!/bin/sh\nset -e\n' for version in self.pythonVersions: content += 'find /usr/lib/python%s/%s -name "*.pyc" -delete\n' % \ (version, self.appName) f.write(content) f.close() # Create control.tar.gz os.system('tar czvf control.tar.gz ./control ./md5sums ./postinst ' \ './prerm') # Create debian-binary f = file('debian-binary', 'w') f.write('2.0\n') f.close() # Create the signature if required if self.sign: # Create the concatenated version of all files within the deb os.system('cat debian-binary control.tar.gz data.tar.gz > ' \ '/tmp/combined-contents') os.system('gpg -abs -o _gpgorigin /tmp/combined-contents') signFile = '_gpgorigin ' os.remove('/tmp/combined-contents') # Export the public key and name it according to its ID as found by # analyzing the result of command "gpg --fingerprint". cmd = subprocess.Popen(['gpg', '--fingerprint'], stdout=subprocess.PIPE) fingerprint = cmd.stdout.read().split('\n') id = 'pubkey' for line in fingerprint: if '=' not in line: continue id = line.split('=')[1].strip() id = ''.join(id.split()[-4:]) break os.system('gpg --export -a > %s/%s.asc' % (self.out, id)) else: signFile = '' # Create the .deb package debName = 'python-appy%s-%s.deb' % (nameSuffix, self.appVersion) os.system('ar -r %s %sdebian-binary control.tar.gz data.tar.gz' % \ (debName, signFile)) # Move it to self.out os.rename(j(debFolder, debName), j(self.out, debName)) # Clean temp files FolderDeleter.delete(debFolder) os.chdir(curdir) # ------------------------------------------------------------------------------ definitionJson = '''{ "name": "%s", "description": "%s, a Appy-based application", "packages": [{"name": "python-appy" }, {"name": "python-appy-%s" }], "files": [ { "group": "root", "mode": "644", "name": "%s.conf", "owner": "root", "path": "/etc/%s.conf", "template": "%s" }], "handlers": [ { "on": ["_install"] }, { "on": ["_uninstall" ] }, { "on": ["%s_http_port"], "do": [ { "action": "update", "resource": "file://%s.conf" }, { "action": "restart", "resource": "service://%s" } ]} ], "services": [ { "name": "%s", "enabled": "true", "running": "false" }, { "name": "oo", "enabled": "true", "running": "false" }] } ''' definitionJsonConf = '''{ "name": "%s.conf", "uuid": "%s", "parameters": [ { "key": "%s_http_port", "name": "%s HTTP port", "description": "%s HTTP port for the Zope process", "value": "8080"} ] } ''' class Cortexer: '''This class allows to produce a Cortex application definition for a Debianized Python/Appy application.''' def __init__(self, app, pythonVersions=('2.6',)): self.appName = os.path.basename(app) self.pythonVersions = pythonVersions appFolder = os.path.dirname(app) # Prepare the output folder (remove any existing one) cortexFolder = os.path.join(appFolder, 'cortex.admin') if os.path.exists(cortexFolder): FolderDeleter.delete(cortexFolder) allFolders = os.path.join(cortexFolder, 'applications', self.appName) os.makedirs(allFolders) self.out = allFolders uuidChars= ['0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'] def generateUid(self): '''Generates a 32-chars-wide UID for identifying the configuration file in the Cortex DB.''' res = '' for i in range(32): res += self.uuidChars[random.randint(0,15)] return res def run(self): # Create the root app description file "definition.json". uid = self.generateUid() name = os.path.join(self.out, 'definition.json') f = file(name, 'w') n = self.appName nl = self.appName.lower() f.write(definitionJson % (n, n, nl, nl, nl, uid, nl, nl, nl, nl)) f.close() # Create the folder corresponding to the config file, and its content. confFolder = os.path.join(self.out, '%s.conf' % nl) os.mkdir(confFolder) # Create the definition file for this config file, that will contain # the names of Cortex-managed variables within the configuration file. name = os.path.join(confFolder, 'definition.json') f = file(name, 'w') f.write(definitionJsonConf % (nl, uid, nl, n, n)) f.close() # Create the Zope config file, with Cortex-like variables within in. name = os.path.join(confFolder, '%s.conf' % nl) f = file(name, 'w') productsFolder='/usr/lib/python%s/%s/zope' % (self.pythonVersions[0],n) f.write(zopeConf % ('/var/lib/%s' % n, '/var/lib/%s' % n, '/var/log/%s' % n, '${%s_http_port}' % nl, 'products %s\n' % productsFolder)) f.close() # ------------------------------------------------------------------------------