summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'WebappConfig/worker.py')
-rw-r--r--WebappConfig/worker.py563
1 files changed, 563 insertions, 0 deletions
diff --git a/WebappConfig/worker.py b/WebappConfig/worker.py
new file mode 100644
index 0000000..b98a810
--- /dev/null
+++ b/WebappConfig/worker.py
@@ -0,0 +1,563 @@
+#!/usr/bin/python -O
+#
+# /usr/sbin/webapp-config
+# Python script for managing the deployment of web-based
+# applications
+#
+# Originally written for the Gentoo Linux distribution
+#
+# Copyright (c) 1999-2006 Gentoo Foundation
+# Released under v2 of the GNU GPL
+#
+# Author(s) Stuart Herbert <stuart@gentoo.org>
+# Renat Lumpau <rl03@gentoo.org>
+# Gunnar Wrobel <php@gunnarwrobel.de>
+#
+# ========================================================================
+''' This module provides the classes for actually adding or removing
+files of a virtual install location. '''
+
+__version__ = "$Id: worker.py 245 2006-01-13 16:57:29Z wrobel $"
+
+# ========================================================================
+# Dependencies
+# ------------------------------------------------------------------------
+
+import sys, os, os.path, shutil, stat, re
+
+from WebappConfig.debug import OUT
+import WebappConfig.wrapper as wrapper
+
+# ========================================================================
+# Helper functions
+# ------------------------------------------------------------------------
+
+def all(boolean):
+ ''' Replacement for reduce() '''
+ for i in boolean:
+ if not i:
+ return False
+ return True
+
+# ========================================================================
+# Worker class
+# ------------------------------------------------------------------------
+
+class WebappRemove:
+ '''
+ This is the handler for removal of web applications from their virtual
+ install locations.
+
+ For removal of files a content handler is sufficient:
+
+ >>> OUT.color_off()
+ >>> import os.path
+ >>> here = os.path.dirname(os.path.realpath(__file__))
+ >>> from WebappConfig.content import Contents
+ >>> a = Contents(here + '/tests/testfiles/contents/app2',
+ ... package = 'test', version = '1.0', pretend = True)
+ >>> a.read()
+ >>> b = WebappRemove(a, True, True)
+
+ # Pretend to remove files:
+
+ # b.remove_files() #doctest: +ELLIPSIS
+
+ # Deleted the test since this will almost certainly fail because
+ # of the modification time.
+
+ Deleted test for removal of directories. They are always reported as 'not
+ empty' in case I am working in the subversion repository.
+ '''
+
+ def __init__(self,
+ content,
+ verbose,
+ pretend):
+
+ self.__content = content
+ self.__v = verbose
+ self.__p = pretend
+
+ def remove_dirs(self):
+ '''
+ It is time to remove the dirs that we installed originally.
+ '''
+
+ OUT.debug('Trying to remove directories', 6)
+
+ success = [self.remove(i) for i in self.__content.get_directories()]
+
+ # Tell the caller if anything was left behind
+
+ return all(success)
+
+ def remove_files(self):
+ '''
+ It is time to remove the files that we installed originally.
+ '''
+
+ OUT.debug('Trying to remove files', 6)
+
+ success = [self.remove(i) for i in self.__content.get_files()]
+
+ # Tell the caller if anything was left behind
+
+ return all(success)
+
+ def remove(self, entry):
+ '''
+ Decide whether to delete something - and then go ahead and do so
+
+ Just like portage, we only remove files that have not changed
+ from when we installed them. If the timestamp or checksum is
+ different, we leave the file in place.
+
+ Inputs
+
+ entry - file/dir/sym to remove
+ '''
+
+ OUT.debug('Trying to remove file', 6)
+
+ # okay, deal with the file | directory | symlink
+
+ removeable = self.__content.get_canremove(entry)
+
+ if not removeable:
+
+ # Remove directory or file.
+
+ # Report if we are only pretending
+ if self.__p:
+ OUT.info(' pretending to remove: ' + entry)
+
+ # try to remove the entry
+ try:
+ entry_type = self.__content.etype(entry)
+ if self.__content.etype(entry) == 'dir':
+ # its a directory -> rmdir
+ if not self.__p:
+ os.rmdir(entry)
+ else:
+ # its a file -> unlink
+ if not self.__p:
+ os.unlink(entry)
+ except:
+ # Report if there is a problem
+ OUT.notice('!!! '
+ + self.__content.epath(entry))
+ return
+
+ if self.__v and not self.__p:
+ # Report successful deletion
+
+ OUT.notice('<<< ' + entry_type + ' '
+ * (5 - len(entry_type))
+ + self.__content.epath(entry))
+
+ self.__content.delete(entry)
+
+ return True
+
+ else:
+
+ OUT.notice(removeable)
+
+ return False
+
+
+class WebappAdd:
+ '''
+ This is the class that handles the actual transfer of files from
+ the web application source directory to the virtual install location.
+
+ The setup of the class is rather complex since a lot of different
+ handlers are needed for the task.
+
+ >>> OUT.color_off()
+ >>> import os.path
+ >>> here = os.path.dirname(os.path.realpath(__file__))
+
+ The content handler points to the virtual install directory:
+
+ >>> from WebappConfig.content import Contents
+ >>> a = Contents(here + '/tests/testfiles/installtest', pretend = True)
+
+ Removal of files will be necessary while upgrading :
+
+ >>> b = WebappRemove(a, True, True)
+
+ The handler for protected files is simple:
+
+ >>> import WebappConfig.protect
+ >>> c = WebappConfig.protect.Protection()
+
+ And finally a fully initialized source is needed:
+
+ >>> from WebappConfig.db import WebappSource
+ >>> d = WebappSource(here + '/tests/testfiles/share-webapps',
+ ... 'installtest', '1.0')
+ >>> d.read()
+ >>> d.ignore = ['.svn']
+
+ >>> e = WebappAdd('htdocs',
+ ... here + '/tests/testfiles/installtest',
+ ... {'dir' : {
+ ... 'default-owned': ('root', 'root', '0644'),
+ ... },
+ ... 'file' : {
+ ... 'virtual' : ('root', 'root', '0644'),
+ ... 'server-owned' : ('apache', 'apache', '0660'),
+ ... 'config-owned' : ('nobody', 'nobody', '0600'),
+ ... }},
+ ... {'content': a,
+ ... 'removal': b,
+ ... 'protect': c,
+ ... 'source' : d},
+ ... {'relative': 1,
+ ... 'upgrade': False,
+ ... 'pretend': True,
+ ... 'verbose': False,
+ ... 'linktype': 'soft'})
+
+ Installing a standard file:
+
+ >>> e.mkfile('test1')
+ * pretending to add: sym 1 virtual "test1"
+ >>> e.mkfile('test4')
+ * pretending to add: file 1 server-owned "test4"
+
+ This location is already occupied. But since the file is not
+ known, it will be deleted:
+
+ >>> e.mkfile('test2') #doctest: +ELLIPSIS
+ * would have removed ".../tests/testfiles/installtest/test2" since it is in the way for the current install. It should not be present in that location!
+ * pretending to add: sym 1 virtual "test2"
+
+ This location is also occupied but it it is a config protected
+ file so it may not be removed:
+
+ >>> e.mkfile('test3') #doctest:
+ ^o^ hiding test3
+ * pretending to add: file 1 config-owned "test3"
+
+ >>> e.mkdir('dir1')
+ * pretending to add: dir 1 default-owned "dir1"
+
+ >>> e.mkdir('dir2') #doctest: +ELLIPSIS
+ * .../tests/testfiles/installtest/dir2 already exists, but is not a directory - removing
+ * pretending to add: dir 1 default-owned "dir2"
+
+ And finally everything combined:
+
+ >>> e.mkdirs('') #doctest: +ELLIPSIS
+ * Installing from .../tests/testfiles/share-webapps/installtest/1.0/htdocs/
+ * pretending to add: dir 1 default-owned "dir1"
+ * Installing from .../tests/testfiles/share-webapps/installtest/1.0/htdocs/dir1
+ * pretending to add: sym 1 virtual "dir1/webapp_test"
+ * .../tests/testfiles/installtest//dir2 already exists, but is not a directory - removing
+ * pretending to add: dir 1 default-owned "dir2"
+ * Installing from .../tests/testfiles/share-webapps/installtest/1.0/htdocs/dir2
+ * pretending to add: sym 1 virtual "dir2/webapp_test"
+ * pretending to add: sym 1 virtual "test1"
+ * would have removed ".../tests/testfiles/installtest//test2" since it is in the way for the current install. It should not be present in that location!
+ * pretending to add: sym 1 virtual "test2"
+ ^o^ hiding /test3
+ * pretending to add: file 1 config-owned "test3"
+ * pretending to add: file 1 server-owned "test4"
+
+ '''
+
+ def __init__(self,
+ source,
+ destination,
+ permissions,
+ handler,
+ flags):
+
+ self.__sourced = source
+ self.__destd = destination
+ self.__perm = permissions
+ self.__ws = handler['source']
+ self.__content = handler['content']
+ self.__remove = handler['removal']
+ self.__protect = handler['protect']
+ self.__link_type = flags['linktype']
+ self.__relative = flags['relative']
+ self.__u = flags['upgrade']
+ self.__v = flags['verbose']
+ self.__p = flags['pretend']
+
+ self.config_protected_dirs = []
+
+ def mkdirs(self, directory = ''):
+ '''
+ Create a set of directories
+
+ Inputs
+
+ directory - the directory within the source hierarchy
+ '''
+
+ sd = self.__sourced + '/' + directory
+ real_dir = re.compile('/+').sub('/',
+ self.__ws.appdir()
+ + '/' + self.__sourced
+ + '/' + directory)
+
+ OUT.debug('Creating directories', 6)
+
+ if not self.__ws.source_exists(sd):
+
+ OUT.warn(self.__ws.package_name()
+ + ' does not install any files from '
+ + real_dir + '; skipping')
+ return
+
+ OUT.info(' Installing from ' + real_dir)
+
+ for i in self.__ws.get_source_directories(sd):
+
+ OUT.debug('Handling directory', 7)
+
+ # create directory first
+ self.mkdir(directory + '/' + i)
+
+ # then recurse into the directory
+ self.mkdirs(directory + '/' + i)
+
+ for i in self.__ws.get_source_files(sd):
+
+ OUT.debug('Handling file', 7)
+
+ # handle the file
+ self.mkfile(directory + '/' + i)
+
+
+ def mkdir(self, directory):
+ '''
+ Create a directory with the correct ownership and permissions.
+
+ directory - name of the directory
+ '''
+ src_dir = self.__sourced + '/' + directory
+ dst_dir = self.__destd + '/' + directory
+
+ OUT.debug('Creating directory', 6)
+
+ # some special cases
+ #
+ # these should be triggered only if we are trying to install
+ # a webapp into a directory that already has files and dirs
+ # inside it
+
+ if os.path.exists(dst_dir) and not os.path.isdir(dst_dir):
+ # something already exists with the same name
+ #
+ # in theory, this should automatically remove symlinked
+ # directories
+
+ OUT.warn(' ' + dst_dir + ' already exists, but is not a di'
+ 'rectory - removing')
+ if not self.__p:
+ os.unlink(dst_dir)
+
+ dirtype = self.__ws.dirtype(src_dir)
+
+ OUT.debug('Checked directory type', 8)
+
+ (user, group, perm) = self.__perm['dir'][dirtype]
+
+ dsttype = 'dir'
+
+ if not os.path.isdir(dst_dir):
+
+ OUT.debug('Creating directory', 8)
+
+ if not self.__p:
+ os.makedirs(dst_dir, perm(0755))
+
+ os.chown(dst_dir,
+ user,
+ group)
+
+ self.__content.add(dsttype,
+ dirtype,
+ self.__destd,
+ directory,
+ self.__relative)
+
+ def mkfile(self, filename):
+ '''
+ This is what we are all about. No more games - lets take a file
+ from the master image of the web-based app, and make it available
+ inside the install directory.
+
+ filename - name of the file
+
+ '''
+
+ OUT.debug('Creating file', 6)
+
+ dst_name = self.__destd + '/' + filename
+ file_type = self.__ws.filetype(self.__sourced + '/' + filename)
+
+ OUT.debug('File type determined', 7)
+
+ # are we overwriting an existing file?
+
+ OUT.debug('Check for existing file', 7)
+
+ if os.path.exists(dst_name):
+
+ OUT.debug('File in the way!', 7)
+
+ my_canremove = True
+
+ # o-oh - we're going to be overwriting something that already
+ # exists
+
+ # If we are upgrading, check if the file can be removed
+ if self.__u:
+ my_canremove = self.__remove.remove(self.__destd, filename)
+ # Config protected file definitely cannot be removed
+ elif file_type[0:6] == 'config':
+ my_canremove = False
+
+ if not my_canremove:
+ # not able to remove the file
+ # or
+ # file is config-protected
+
+ dst_name = self.__protect.get_protectedname(self.__destd,
+ filename)
+ OUT.notice('^o^ hiding ' + filename)
+ self.config_protected_dirs.append(self.__destd + '/'
+ + os.path.dirname(filename))
+
+ OUT.debug('Hiding config protected file', 7)
+
+ else:
+
+ # it's a file we do not know about - so get rid
+ # of it anyway
+ #
+ # this behaviour here *is* by popular request
+ # personally, I'm not comfortable with it -- Stuart
+
+ if not self.__p:
+ if os.path.isdir(dst_name):
+ os.rmdir(dst_name)
+ else:
+ os.unlink(dst_name)
+ else:
+ OUT.info(' would have removed "' + dst_name + '" s'
+ 'ince it is in the way for the current instal'
+ 'l. It should not be present in that location'
+ '!')
+
+
+ # if we get here, we can get on with the business of making
+ # the file available
+
+ (user, group, perm) = self.__perm['file'][file_type]
+ my_contenttype = ''
+
+ src_name = self.__ws.appdir() + '/' + self.__sourced + '/' + filename
+
+ # Fix the paths
+ src_name = re.compile('/+').sub('/', src_name)
+ dst_name = re.compile('/+').sub('/', dst_name)
+
+ OUT.debug('Creating File', 7)
+
+ # this is our default file type
+ #
+ # we link in (soft and hard links are supported)
+ # if we're allowed to
+ #
+ # some applications (/me points at PHP scripts)
+ # won't run if symlinked in.
+ # so we now support copying files in too
+ #
+ # default behaviour is to hard link (if we can), and
+ # to copy if we cannot
+ #
+ # if the user wants symlinks, then the user has to
+ # use the new '--soft' option
+
+ if file_type == 'virtual' or os.path.islink(src_name):
+
+ if self.__link_type == 'soft':
+ try:
+
+ OUT.debug('Trying to softlink', 8)
+
+ if not self.__p:
+ os.symlink(src_name, dst_name)
+
+ my_contenttype = 'sym'
+
+ except Exception, e:
+
+ if self.__v:
+ OUT.warn('Failed to softlink (' + str(e) + ')')
+
+ elif os.path.islink(src_name):
+ try:
+
+ OUT.debug('Trying to copy symlink', 8)
+
+ if not self.__p:
+ os.symlink(os.readlink(src_name), dst_name)
+
+ my_contenttype = 'sym'
+
+ except Exception, e:
+
+ if self.__v:
+ OUT.warn('Failed copy symlink (' + str(e) + ')')
+
+ else:
+ try:
+
+ OUT.debug('Trying to hardlink', 8)
+
+ if not self.__p:
+ os.link(src_name, dst_name)
+
+ my_contenttype = 'file'
+
+ except Exception, e:
+
+ if self.__v:
+ OUT.warn('Failed to hardlink (' + str(e) + ')')
+
+ if not my_contenttype:
+ if not self.__p:
+ shutil.copy(src_name, dst_name)
+ my_contenttype = 'file'
+
+
+ if not self.__p and not os.path.islink(src_name):
+
+ old_perm = os.stat(src_name)[stat.ST_MODE] & 511
+
+ os.chown(dst_name,
+ user,
+ group)
+
+ os.chmod(dst_name,
+ perm(old_perm))
+
+ self.__content.add(my_contenttype,
+ file_type,
+ self.__destd,
+ filename,
+ self.__relative)
+
+
+if __name__ == '__main__':
+ import doctest, sys
+ doctest.testmod(sys.modules[__name__])