appy.shared: improved deployment of a Appy app (creation of a Zope instance is no more required; corresponding folders are created in standard unix locations: /etc for the config file, /var/log for logs, /var/lib for the database, /usr/bin for scripts that start and stop the instance). appy.gen: first draft of a migration script that allows to migrate data from Plone-dependent Appy apps (<= 0.7.1) to Ploneless Appy 0.8.0.

This commit is contained in:
Gaetan Delannay 2012-02-02 17:30:54 +01:00
parent 95a899f3de
commit 1275df5753
13 changed files with 351 additions and 161 deletions

View file

@ -80,8 +80,9 @@ class GeneratorScript:
if options.debian: if options.debian:
app = args[0] app = args[0]
appDir = os.path.dirname(app) appDir = os.path.dirname(app)
appName = os.path.basename(app)
# Get the app version from zope/version.txt # Get the app version from zope/version.txt
f = file(os.path.join(app, 'zope', 'version.txt')) f = file(os.path.join(app, 'zope', appName, 'version.txt'))
version = f.read() version = f.read()
f.close() f.close()
version = version[:version.find('build')-1] version = version[:version.find('build')-1]

View file

@ -6,6 +6,7 @@
import os, os.path, sys, shutil, re import os, os.path, sys, shutil, re
from optparse import OptionParser from optparse import OptionParser
from appy.shared.utils import cleanFolder, copyFolder from appy.shared.utils import cleanFolder, copyFolder
from appy.shared.packaging import ooStart, zopeConf
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
class NewError(Exception): pass class NewError(Exception): pass
@ -34,7 +35,7 @@ exec "$ZDCTL" -C "$CONFIG_FILE" "$@"
''' '''
# runzope template file for a pure Zope instance ------------------------------- # runzope template file for a pure Zope instance -------------------------------
runZope = '''#! /bin/sh runZope = '''#!/bin/sh
INSTANCE_HOME="%s" INSTANCE_HOME="%s"
CONFIG_FILE="$INSTANCE_HOME/etc/zope.conf" CONFIG_FILE="$INSTANCE_HOME/etc/zope.conf"
ZOPE_RUN="/usr/lib/zope2.12/bin/runzope" ZOPE_RUN="/usr/lib/zope2.12/bin/runzope"
@ -42,46 +43,6 @@ export INSTANCE_HOME
exec "$ZOPE_RUN" -C "$CONFIG_FILE" "$@" exec "$ZOPE_RUN" -C "$CONFIG_FILE" "$@"
''' '''
# zope.conf template file for a pure Zope instance -----------------------------
zopeConf = '''# Zope configuration.
%%define INSTANCE %s
%%define HTTPPORT 8080
%%define ZOPE_USER zope
instancehome $INSTANCE
effective-user $ZOPE_USER
<eventlog>
level info
<logfile>
path $INSTANCE/log/event.log
level info
</logfile>
</eventlog>
<logger access>
level WARN
<logfile>
path $INSTANCE/log/Z2.log
format %%(message)s
</logfile>
</logger>
<http-server>
address $HTTPPORT
</http-server>
<zodb_db main>
<filestorage>
path $INSTANCE/var/Data.fs
</filestorage>
mount-point /
</zodb_db>
<zodb_db temporary>
<temporarystorage>
name temporary storage for sessioning
</temporarystorage>
mount-point /temp_folder
container-class Products.TemporaryFolder.TemporaryContainer
</zodb_db>
'''
# zopectl template for a Plone (4) Zope instance ------------------------------- # zopectl template for a Plone (4) Zope instance -------------------------------
zopeCtlPlone = '''#!/bin/sh zopeCtlPlone = '''#!/bin/sh
PYTHON="%s" PYTHON="%s"
@ -153,14 +114,14 @@ class ZopeInstanceCreator:
os.chmod('bin/runzope', 0744) # Make it executable by owner. os.chmod('bin/runzope', 0744) # Make it executable by owner.
# Create bin/startoo # Create bin/startoo
f = file('bin/startoo', 'w') f = file('bin/startoo', 'w')
f.write('#!/bin/sh\nsoffice -invisible -headless -nofirststartwizard '\ f.write(ooStart)
'"-accept=socket,host=localhost,port=2002;urp;"&\n')
f.close() f.close()
os.chmod('bin/startoo', 0744) # Make it executable by owner. os.chmod('bin/startoo', 0744) # Make it executable by owner.
# Create etc/zope.conf # Create etc/zope.conf
os.mkdir('etc') os.mkdir('etc')
f = file('etc/zope.conf', 'w') f = file('etc/zope.conf', 'w')
f.write(zopeConf % self.instancePath) f.write(zopeConf % (self.instancePath, '%s/var' % self.instancePath,
'%s/log' % self.instancePath, ''))
f.close() f.close()
# Create other folders # Create other folders
for name in ('Extensions', 'log', 'Products', 'var'): os.mkdir(name) for name in ('Extensions', 'log', 'Products', 'var'): os.mkdir(name)

View file

@ -424,7 +424,7 @@ class Publisher:
f.write(toc) f.write(toc)
f.close() f.close()
privateScripts = ('publish.py', 'zip.py', 'runOpenOffice.sh') privateScripts = ('publish.py', 'zip.py', 'startoo.sh')
def prepareGenFolder(self, minimalist=False): def prepareGenFolder(self, minimalist=False):
'''Creates the basic structure of the temp folder where the appy '''Creates the basic structure of the temp folder where the appy
website will be generated.''' website will be generated.'''

23
bin/zopectl.py Normal file
View file

@ -0,0 +1,23 @@
# ------------------------------------------------------------------------------
import sys, os, os.path
import Zope2.Startup.zopectl as zctl
# ------------------------------------------------------------------------------
class ZopeRunner:
'''This class allows to run a Appy/Zope instance.'''
def run(self):
# Check that an arg has been given (start, stop, fg, run)
if not sys.argv[3].strip():
print 'Argument required.'
sys.exit(-1)
# Identify the name of the application for which Zope must run.
app = os.path.splitext(os.path.basename(sys.argv[2]))[0].lower()
# Launch Zope.
options = zctl.ZopeCtlOptions()
options.realize(None)
options.program = ['/usr/bin/%srun' % app]
c = zctl.ZopeCmd(options)
c.onecmd(" ".join(options.args))
return min(c._exitstatus, 1)
# ------------------------------------------------------------------------------

View file

@ -120,7 +120,8 @@ class Generator:
# Determine application name # Determine application name
self.applicationName = os.path.basename(application) self.applicationName = os.path.basename(application)
# Determine output folder (where to store the generated product) # Determine output folder (where to store the generated product)
self.outputFolder = os.path.join(application, 'zope') self.outputFolder = os.path.join(application, 'zope',
self.applicationName)
self.options = options self.options = options
# Determine templates folder # Determine templates folder
genFolder = os.path.dirname(__file__) genFolder = os.path.dirname(__file__)

View file

@ -8,6 +8,7 @@ import appy.version
import appy.gen as gen import appy.gen as gen
from appy.gen.po import PoParser from appy.gen.po import PoParser
from appy.gen.utils import updateRolesForPermission, createObject from appy.gen.utils import updateRolesForPermission, createObject
from appy.gen.migrator import Migrator
from appy.shared.data import languages from appy.shared.data import languages
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
@ -87,14 +88,16 @@ class ZopeInstaller:
def installUi(self): def installUi(self):
'''Installs the user interface.''' '''Installs the user interface.'''
# Delete the existing folder if it existed.
zopeContent = self.app.objectIds()
if 'ui' in zopeContent: self.app.manage_delObjects(['ui'])
self.app.manage_addFolder('ui')
# Some useful imports # Some useful imports
from OFS.Folder import manage_addFolder
from OFS.Image import manage_addImage, manage_addFile
from Products.PythonScripts.PythonScript import PythonScript from Products.PythonScripts.PythonScript import PythonScript
from Products.PageTemplates.ZopePageTemplate import \ from Products.PageTemplates.ZopePageTemplate import \
manage_addPageTemplate manage_addPageTemplate
# Delete the existing folder if it existed.
zopeContent = self.app.objectIds()
if 'ui' in zopeContent: self.app.manage_delObjects(['ui'])
manage_addFolder(self.app, 'ui')
# Browse the physical folder and re-create it in the Zope folder # Browse the physical folder and re-create it in the Zope folder
j = os.path.join j = os.path.join
ui = j(j(appy.getPath(), 'gen'), 'ui') ui = j(j(appy.getPath(), 'gen'), 'ui')
@ -106,13 +109,13 @@ class ZopeInstaller:
for name in folderName.strip(os.sep).split(os.sep): for name in folderName.strip(os.sep).split(os.sep):
zopeFolder = zopeFolder._getOb(name) zopeFolder = zopeFolder._getOb(name)
# Create sub-folders at this level # Create sub-folders at this level
for name in dirs: zopeFolder.manage_addFolder(name) for name in dirs: manage_addFolder(zopeFolder, name)
# Create files at this level # Create files at this level
for name in files: for name in files:
baseName, ext = os.path.splitext(name) baseName, ext = os.path.splitext(name)
f = file(j(root, name)) f = file(j(root, name))
if ext in gen.File.imageExts: if ext in gen.File.imageExts:
zopeFolder.manage_addImage(name, f) manage_addImage(zopeFolder, name, f)
elif ext == '.pt': elif ext == '.pt':
manage_addPageTemplate(zopeFolder, baseName, '', f.read()) manage_addPageTemplate(zopeFolder, baseName, '', f.read())
elif ext == '.py': elif ext == '.py':
@ -120,7 +123,7 @@ class ZopeInstaller:
zopeFolder._setObject(baseName, obj) zopeFolder._setObject(baseName, obj)
zopeFolder._getOb(baseName).write(f.read()) zopeFolder._getOb(baseName).write(f.read())
else: else:
zopeFolder.manage_addFile(name, f) manage_addFile(zopeFolder, name, f)
f.close() f.close()
# Update the home page # Update the home page
if 'index_html' in zopeContent: if 'index_html' in zopeContent:
@ -199,9 +202,10 @@ class ZopeInstaller:
'''Creates the tool and the root data folder if they do not exist.''' '''Creates the tool and the root data folder if they do not exist.'''
# Create or update the base folder for storing data # Create or update the base folder for storing data
zopeContent = self.app.objectIds() zopeContent = self.app.objectIds()
from OFS.Folder import manage_addFolder
if 'data' not in zopeContent: if 'data' not in zopeContent:
self.app.manage_addFolder('data') manage_addFolder(self.app, 'data')
data = self.app.data data = self.app.data
# Manager has been granted Add permissions for all root classes. # Manager has been granted Add permissions for all root classes.
# This may not be desired, so remove this. # This may not be desired, so remove this.
@ -240,7 +244,13 @@ class ZopeInstaller:
appyTool.log('Appy version is "%s".' % appy.version.short) appyTool.log('Appy version is "%s".' % appy.version.short)
# Create the admin user if no user exists. # Create the admin user if no user exists.
if not self.app.acl_users.getUsers(): try:
users = self.app.acl_users.getUsers()
except:
# When Plone has installed PAS in acl_users this may fail. Plone
# may still be in the way for migration purposes.
users = ('admin') # We suppose there is at least a user.
if not users:
self.app.acl_users._doAddUser('admin', 'admin', ['Manager'], ()) self.app.acl_users._doAddUser('admin', 'admin', ['Manager'], ())
appyTool.log('Admin user "admin" created.') appyTool.log('Admin user "admin" created.')
@ -386,7 +396,7 @@ class ZopeInstaller:
from OFS.Application import install_product from OFS.Application import install_product
import Products import Products
install_product(self.app, Products.__path__[1], 'ZCTextIndex', [], {}) install_product(self.app, Products.__path__[1], 'ZCTextIndex', [], {})
def install(self): def install(self):
self.logger.info('is being installed...') self.logger.info('is being installed...')
self.installDependencies() self.installDependencies()
@ -399,6 +409,10 @@ class ZopeInstaller:
self.installCatalog() self.installCatalog()
self.installTool() self.installTool()
self.installUi() self.installUi()
# Perform migrations if required
Migrator(self).run()
# Update Appy version in the database
self.app.config.appy().appyVersion = appy.version.short
# Empty the fake REQUEST object, only used at Zope startup. # Empty the fake REQUEST object, only used at Zope startup.
del self.app.config.getProductConfig().fakeRequest.wrappers del self.app.config.getProductConfig().fakeRequest.wrappers
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------

View file

@ -7,56 +7,125 @@ class Migrator:
installation, we've detected a new Appy version.''' installation, we've detected a new Appy version.'''
def __init__(self, installer): def __init__(self, installer):
self.installer = installer self.installer = installer
self.logger = installer.logger
self.app = installer.app
def migrateTo_0_7_1(self): bypassRoles = ('Authenticated', 'Member')
'''Appy 0.7.1 has its own management of Ref fields. So we must bypassGroups = ('Administrators', 'Reviewers')
update data structures that store Ref info on instances.''' def migrateUsers(self, ploneSite):
ins = self.installer '''Migrate users from Plone's acl_users to Zope acl_users with
ins.info('Migrating to Appy 0.7.1...') corresponding Appy objects.'''
allClassNames = [ins.tool.__class__.__name__] + ins.config.allClassNames # First of all, remove the Plone-patched root acl_users by a standard
for className in allClassNames: # (hum, Appy-patched) Zope UserFolder.
i = -1 tool = self.app.config.appy()
updated = 0 from AccessControl.User import manage_addUserFolder
ins.info('Analysing class "%s"...' % className) self.app.manage_delObjects(ids=['acl_users'])
refFields = None manage_addUserFolder(self.app)
for obj in ins.tool.executeQuery(className,\ # Put an admin user into it
noSecurity=True)['objects']: newUsersDb = self.app.acl_users
i += 1 newUsersDb._doAddUser('admin', 'admin', ['Manager'], ())
if i == 0: # Copy users from Plone acl_users to Zope acl_users
# Get the Ref fields for objects of this class for user in ploneSite.acl_users.getUsers():
refFields = [f for f in obj.getAllAppyTypes() \ id = user.getId()
if (f.type == 'Ref') and not f.isBack] userRoles = user.getRoles()
if refFields: for br in self.bypassRoles:
refNames = ', '.join([rf.name for rf in refFields]) if br in userRoles: userRoles.remove(br)
ins.info(' Ref fields found: %s' % refNames) userInfo = ploneSite.portal_membership.getMemberById(id)
userName = userInfo.getProperty('fullname') or id
userEmail = userInfo.getProperty('email') or ''
appyUser = tool.create('users', login=id,
password1='fake', password2='fake', roles=userRoles,
name=userName, firstName=' ', email=userEmail)
appyUser.title = appyUser.title.strip()
# Set the correct password
password = ploneSite.acl_users.source_users._user_passwords[id]
newUsersDb.data[id].__ = password
# Manage groups. Exclude not-used default Plone groups.
for groupId in user.getGroups():
if groupId in self.bypassGroups: continue
if tool.count('Group', login=groupId):
# The Appy group already exists, get it
appyGroup = tool.search('Group', login=groupId)[0]
else:
# Create the group. Todo: get Plone group roles and title
appyGroup = tool.create('groups', login=groupId,
title=groupId)
appyGroup.addUser(appyUser)
def reindexObject(self, obj):
obj.reindex()
i = 1
for subObj in obj.objectValues():
i += self.reindexObject(subObj)
return i # The number of reindexed (sub-)object(s)
def migrateTo_0_8_0(self):
'''Migrates a Plone-based (<= 0.7.1) Appy app to a Ploneless (0.8.0)
Appy app.'''
self.logger.info('Migrating to Appy 0.8.0...')
# Find the Plone site. It must be at the root of the Zope tree.
ploneSite = None
for obj in self.app.objectValues():
if obj.__class__.__name__ == 'PloneSite':
ploneSite = obj
break
# As a preamble: delete translation objects from self.app.config: they
# will be copied from the old tool.
self.app.config.manage_delObjects(ids=self.app.config.objectIds())
# Migrate data objects:
# - from oldDataFolder to self.app.data
# - from oldTool to self.app.config (excepted translation
# objects that were re-created from i18n files).
appName = self.app.config.getAppName()
for oldFolderName in (appName, 'portal_%s' % appName.lower()):
oldFolder = getattr(ploneSite, oldFolderName)
objectIds = [id for id in oldFolder.objectIds()]
cutted = oldFolder.manage_cutObjects(ids=objectIds)
if oldFolderName == appName:
destFolder = self.app.data
else:
destFolder = self.app.config
destFolder.manage_pasteObjects(cutted)
i = 0
for obj in destFolder.objectValues():
i += self.reindexObject(obj)
self.logger.info('%d objects imported into %s.' % \
(i, destFolder.getId()))
if oldFolderName != appName:
# Re-link objects copied into the self.app.config with the Tool
# through Ref fields.
tool = self.app.config.appy()
pList = tool.o.getProductConfig().PersistentList
for field in tool.fields:
if field.type != 'Ref': continue
n = field.name
if n in ('users', 'groups'): continue
uids = getattr(oldFolder, n)
if uids:
# Update the forward reference
setattr(tool.o, n, pList(uids))
# Update the back reference
for obj in getattr(tool, n):
backList = getattr(obj.o, field.back.name)
backList.remove(oldFolder._at_uid)
backList.append(tool.uid)
self.logger.info('config.%s: linked %d object(s)' % \
(n, len(uids)))
else: else:
ins.info(' No Ref field found.') self.logger.info('config.%s: no object to link.' % n)
break self.migrateUsers(ploneSite)
isUpdated = False self.logger.info('Migration done.')
for field in refFields:
# Attr for storing UIDs of referred objects has moved
# from _appy_[fieldName] to [fieldName].
refs = getattr(obj, '_appy_%s' % field.name)
if refs:
isUpdated = True
setattr(obj, field.name, refs)
exec 'del obj._appy_%s' % field.name
# Set the back references
for refObject in field.getValue(obj):
refObject.link(field.back.name, obj, back=True)
if isUpdated: updated += 1
if updated:
ins.info(' %d/%d object(s) updated.' % (updated, i+1))
def run(self): def run(self):
i = self.installer if self.app.acl_users.__class__.__name__ == 'UserFolder':
installedVersion = i.appyTool.appyVersion return # Already Ploneless
startTime = time.time() tool = self.app.config.appy()
migrationRequired = False appyVersion = tool.appyVersion
if not installedVersion or (installedVersion <= '0.7.0'): if not appyVersion or (appyVersion < '0.8.0'):
migrationRequired = True # Migration is required.
self.migrateTo_0_7_1() startTime = time.time()
stopTime = time.time() self.migrateTo_0_8_0()
if migrationRequired: stopTime = time.time()
i.info('Migration done in %d minute(s).'% ((stopTime-startTime)/60)) elapsed = (stopTime-startTime) / 60.0
self.logger.info('Migration done in %d minute(s).' % elapsed)
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------

View file

@ -129,7 +129,7 @@ class ModelClass:
class User(ModelClass): class User(ModelClass):
# In a ModelClass we need to declare attributes in the following list. # In a ModelClass we need to declare attributes in the following list.
_appy_attributes = ['title', 'name', 'firstName', 'login', 'password1', _appy_attributes = ['title', 'name', 'firstName', 'login', 'password1',
'password2', 'roles'] 'password2', 'email', 'roles']
# All methods defined below are fake. Real versions are in the wrapper. # All methods defined below are fake. Real versions are in the wrapper.
title = gen.String(show=False, indexed=True) title = gen.String(show=False, indexed=True)
gm = {'group': 'main', 'multiplicity': (1,1), 'width': 25} gm = {'group': 'main', 'multiplicity': (1,1), 'width': 25}
@ -144,6 +144,7 @@ class User(ModelClass):
password1 = gen.String(format=gen.String.PASSWORD, show=showPassword, password1 = gen.String(format=gen.String.PASSWORD, show=showPassword,
validator=validatePassword, **gm) validator=validatePassword, **gm)
password2 = gen.String(format=gen.String.PASSWORD, show=showPassword, **gm) password2 = gen.String(format=gen.String.PASSWORD, show=showPassword, **gm)
email = gen.String(group='main', width=25)
gm['multiplicity'] = (0, None) gm['multiplicity'] = (0, None)
roles = gen.String(validator=gen.Selection('getGrantableRoles'), roles = gen.String(validator=gen.Selection('getGrantableRoles'),
indexed=True, **gm) indexed=True, **gm)
@ -177,7 +178,7 @@ class Translation(ModelClass):
def show(self, name): pass def show(self, name): pass
# The Tool class --------------------------------------------------------------- # The Tool class ---------------------------------------------------------------
# Here are the prefixes of the fields generated on the Tool. # Prefixes of the fields generated on the Tool.
toolFieldPrefixes = ('defaultValue', 'podTemplate', 'formats', 'resultColumns', toolFieldPrefixes = ('defaultValue', 'podTemplate', 'formats', 'resultColumns',
'enableAdvancedSearch', 'numberOfSearchColumns', 'enableAdvancedSearch', 'numberOfSearchColumns',
'searchFields', 'optionalFields', 'showWorkflow', 'searchFields', 'optionalFields', 'showWorkflow',

View file

@ -100,7 +100,7 @@
</tal:operator> </tal:operator>
<tal:comment replace="nothing">The list of values</tal:comment> <tal:comment replace="nothing">The list of values</tal:comment>
<select tal:attributes="name widgetName; size widget/height" multiple="multiple"> <select tal:attributes="name widgetName; size widget/height" multiple="multiple">
<option tal:repeat="v python:tool.getPossibleValues(name, withTranslations=True, withBlankValue=False, className=contentType)" <option tal:repeat="v python:tool.getPossibleValues(name, withTranslations=True, withBlankValue=False, className=className)"
tal:attributes="value python:v[0]; title python: v[1]" tal:attributes="value python:v[0]; title python: v[1]"
tal:content="python: tool.truncateValue(v[1], widget)"> tal:content="python: tool.truncateValue(v[1], widget)">
</option> </option>

View file

@ -110,6 +110,7 @@ class ZopeUserPatches:
def getRolesInContext(self, object): def getRolesInContext(self, object):
'''Return the list of global and local (to p_object) roles granted to '''Return the list of global and local (to p_object) roles granted to
this user (or to any of its groups).''' this user (or to any of its groups).'''
if isinstance(object, AbstractWrapper): object = object.o
object = getattr(object, 'aq_inner', object) object = getattr(object, 'aq_inner', object)
# Start with user global roles # Start with user global roles
res = self.getRoles() res = self.getRoles()
@ -120,7 +121,8 @@ class ZopeUserPatches:
groups = getattr(self, 'groups', ()) groups = getattr(self, 'groups', ())
for id, roles in localRoles.iteritems(): for id, roles in localRoles.iteritems():
if (id != userId) and (id not in groups): continue if (id != userId) and (id not in groups): continue
for role in roles: res.add(role) for role in roles:
if role not in res: res.append(role)
return res return res
def allowed(self, object, object_roles=None): def allowed(self, object, object_roles=None):

View file

@ -1,6 +1,6 @@
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
import os, os.path, subprocess, md5, shutil import os, os.path, subprocess, md5, shutil
from appy.shared.utils import getOsTempFolder, FolderDeleter from appy.shared.utils import getOsTempFolder, FolderDeleter, cleanFolder
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
debianInfo = '''Package: python-appy%s debianInfo = '''Package: python-appy%s
@ -8,23 +8,77 @@ Version: %s
Architecture: all Architecture: all
Maintainer: Gaetan Delannay <gaetan.delannay@geezteem.com> Maintainer: Gaetan Delannay <gaetan.delannay@geezteem.com>
Installed-Size: %d Installed-Size: %d
Depends: python (>= %s), python (<= %s)%s Depends: python (>= %s)%s
Section: python Section: python
Priority: optional Priority: optional
Homepage: http://appyframework.org Homepage: http://appyframework.org
Description: Appy builds simple but complex web Python apps. 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 = '#!/bin/sh\nsoffice -invisible -headless -nofirststartwizard ' \
'"-accept=socket,host=localhost,port=2002;urp;"&\n'
zopeConf = '''# Zope configuration.
%%define INSTANCE %s
%%define DATA %s
%%define LOG %s
%%define HTTPPORT 8080
%%define ZOPE_USER zope
instancehome $INSTANCE
effective-user $ZOPE_USER
%s
<eventlog>
level info
<logfile>
path $LOG/event.log
level info
</logfile>
</eventlog>
<logger access>
level WARN
<logfile>
path $LOG/Z2.log
format %%(message)s
</logfile>
</logger>
<http-server>
address $HTTPPORT
</http-server>
<zodb_db main>
<filestorage>
path $DATA/Data.fs
</filestorage>
mount-point /
</zodb_db>
<zodb_db temporary>
<temporarystorage>
name temporary storage for sessioning
</temporarystorage>
mount-point /temp_folder
container-class Products.TemporaryFolder.TemporaryContainer
</zodb_db>
'''
class Debianizer: class Debianizer:
'''This class allows to produce a Debian package from a Python (Appy) '''This class allows to produce a Debian package from a Python (Appy)
package.''' package.'''
def __init__(self, app, out, appVersion='0.1.0', def __init__(self, app, out, appVersion='0.1.0',
pythonVersions=('2.6', '2.7'), pythonVersions=('2.6',),
depends=('zope2.12', 'openoffice.org', 'imagemagick')): depends=('zope2.12', 'openoffice.org', 'imagemagick')):
# app is the path to the Python package to Debianize. # app is the path to the Python package to Debianize.
self.app = app self.app = app
self.appName = os.path.basename(app) self.appName = os.path.basename(app)
self.appNameLower = self.appName.lower()
# out is the folder where the Debian package will be generated. # out is the folder where the Debian package will be generated.
self.out = out self.out = out
# What is the version number for this app ? # What is the version number for this app ?
@ -33,6 +87,8 @@ class Debianizer:
self.pythonVersions = pythonVersions self.pythonVersions = pythonVersions
# Debian package dependencies # Debian package dependencies
self.depends = depends self.depends = depends
# Zope 2.12 requires Python 2.6
if 'zope2.12' in depends: self.pythonVersions = ('2.6',)
def run(self): def run(self):
'''Generates the Debian package.''' '''Generates the Debian package.'''
@ -49,19 +105,74 @@ class Debianizer:
for version in self.pythonVersions: for version in self.pythonVersions:
libFolder = j(srcFolder, 'python%s' % version) libFolder = j(srcFolder, 'python%s' % version)
os.makedirs(libFolder) os.makedirs(libFolder)
shutil.copytree(self.app, j(libFolder, self.appName)) destFolder = j(libFolder, self.appName)
# Create data.tar.gz based on it. shutil.copytree(self.app, destFolder)
os.chdir(debFolder) # Clean dest folder (.svn/.bzr files)
os.system('tar czvf data.tar.gz ./usr') 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)
# <app>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()
# <app>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(ooStart)
f.close()
os.chmod(name, 0744) # Make it executable by owner.
# /var/lib/<app> (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/<app> (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/<app>.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,
'products %s\n' % productsFolder))
f.close()
# Get the size of the app, in Kb. # Get the size of the app, in Kb.
cmd = subprocess.Popen(['du', '-b', '-s', 'usr'],stdout=subprocess.PIPE) os.chdir(tempFolder)
cmd = subprocess.Popen(['du', '-b', '-s', 'debian'],
stdout=subprocess.PIPE)
size = int(int(cmd.stdout.read().split()[0])/1024.0) 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 # Create the control file
f = file('control', 'w') f = file('control', 'w')
nameSuffix = '' nameSuffix = ''
dependencies = [] dependencies = []
if self.appName != 'appy': if self.appName != 'appy':
nameSuffix = '-%s' % self.appName.lower() nameSuffix = '-%s' % self.appNameLower
dependencies.append('python-appy') dependencies.append('python-appy')
if self.depends: if self.depends:
for d in self.depends: dependencies.append(d) for d in self.depends: dependencies.append(d)
@ -69,51 +180,48 @@ class Debianizer:
if dependencies: if dependencies:
depends = ', ' + ', '.join(dependencies) depends = ', ' + ', '.join(dependencies)
f.write(debianInfo % (nameSuffix, self.appVersion, size, f.write(debianInfo % (nameSuffix, self.appVersion, size,
self.pythonVersions[0], self.pythonVersions[1], self.pythonVersions[0], depends))
depends))
f.close() f.close()
# Create md5sum file # Create md5sum file
f = file('md5sums', 'w') f = file('md5sums', 'w')
for dir, dirnames, filenames in os.walk('usr'): toWalk = ['usr']
for name in filenames: if self.appName != 'appy':
m = md5.new() toWalk += ['etc', 'var']
pathName = j(dir, name) for folderToWalk in toWalk:
currentFile = file(pathName, 'rb') for dir, dirnames, filenames in os.walk(folderToWalk):
while True: for name in filenames:
data = currentFile.read(8096) m = md5.new()
if not data: pathName = j(dir, name)
break currentFile = file(pathName, 'rb')
m.update(data) while True:
currentFile.close() data = currentFile.read(8096)
# Add the md5 sum to the file if not data:
f.write('%s %s\n' % (m.hexdigest(), pathName)) break
m.update(data)
currentFile.close()
# Add the md5 sum to the file
f.write('%s %s\n' % (m.hexdigest(), pathName))
f.close() f.close()
# Create postinst, a script that will: # Create postinst, a script that will:
# - bytecompile Python files after the Debian install # - bytecompile Python files after the Debian install
# - create a Zope instance (excepted if we are installing Appy itself). # - change ownership of some files if required
f = file('postinst', 'w') f = file('postinst', 'w')
content = '#!/bin/sh\nset -e\n' content = '#!/bin/sh\nset -e\n'
for version in self.pythonVersions: for version in self.pythonVersions:
bin = '/usr/bin/python%s' % version bin = '/usr/bin/python%s' % version
lib = '/usr/lib/python%s' % version lib = '/usr/lib/python%s' % version
cmds = ' %s -m compileall -q %s/%s 2> /dev/null\n' % (bin, lib, cmds = ' %s -m compileall -q %s/%s 2> /dev/null\n' % (bin, lib,
self.appName) self.appName)
if self.appName != 'appy':
inst = '/home/zope/%sInstance' % self.appName
cmds += ' if [ -e %s ]\n then\n' % inst
# If the Zope instance already exists, simply restart it.
cmds += ' %s/bin/zopectl restart\n else\n' % inst
# Else, create a Zope instance in the home of user "zope".
cmds += ' %s %s/appy/bin/new.py zope /usr/lib/zope2.12 ' \
'%s\n' % (bin, lib, inst)
# Within this instance, create a symlink to the Zope product
cmds += ' ln -s %s/%s/zope %s/Products/%s\n' % \
(lib, self.appName, inst, self.appName)
# Launch the instance
cmds += ' %s/bin/zopectl start\n' % inst
# Launch OpenOffice in server mode
cmds += ' %s/bin/startoo\n fi\n' % inst
content += 'if [ -e %s ]\nthen\n%sfi\n' % (bin, cmds) 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
# (re-)start the app
content += '%sctl restart\n' % self.appNameLower
# (re-)start oo
content += 'startoo\n'
f.write(content) f.write(content)
f.close() f.close()
# Create prerm, a script that will remove all pyc files before removing # Create prerm, a script that will remove all pyc files before removing

View file

@ -36,18 +36,28 @@ class FolderDeleter:
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
extsToClean = ('.pyc', '.pyo', '.fsz', '.deltafsz', '.dat', '.log') extsToClean = ('.pyc', '.pyo', '.fsz', '.deltafsz', '.dat', '.log')
def cleanFolder(folder, exts=extsToClean, verbose=False): def cleanFolder(folder, exts=extsToClean, folders=(), verbose=False):
'''This function allows to remove, in p_folder and subfolders, any file '''This function allows to remove, in p_folder and subfolders, any file
whose extension is in p_exts.''' whose extension is in p_exts, and any folder whose name is in
p_folders.'''
if verbose: print 'Cleaning folder', folder, '...' if verbose: print 'Cleaning folder', folder, '...'
# Remove files with an extension listed in exts # Remove files with an extension listed in p_exts
for root, dirs, files in os.walk(folder): if exts:
for fileName in files: for root, dirs, files in os.walk(folder):
ext = os.path.splitext(fileName)[1] for fileName in files:
if (ext in exts) or ext.endswith('~'): ext = os.path.splitext(fileName)[1]
fileToRemove = os.path.join(root, fileName) if (ext in exts) or ext.endswith('~'):
if verbose: print 'Removing %s...' % fileToRemove fileToRemove = os.path.join(root, fileName)
os.remove(fileToRemove) if verbose: print 'Removing file %s...' % fileToRemove
os.remove(fileToRemove)
# Remove folders whose names are in p_folders.
if folders:
for root, dirs, files in os.walk(folder):
for folderName in dirs:
if folderName in folders:
toDelete = os.path.join(root, folderName)
if verbose: print 'Removing folder %s...' % toDelete
FolderDeleter.delete(toDelete)
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
def copyFolder(source, dest, cleanDest=False): def copyFolder(source, dest, cleanDest=False):