def write(self): ''' Write the contents file. ''' dbpath = self.appdb() if not dbpath: OUT.die('No package specified!') self.check_installdir() values = [' '.join(i) for i in list(self.__content.values())] if not self.__p: try: fd = os.open(self.appdb(), os.O_WRONLY | os.O_CREAT, self.__perm(0o600)) os.write(fd, ('\n'.join(values)).encode('utf-8')) os.close(fd) except Exception as e: OUT.warn('Failed to write content file ' + dbpath + '!\n' + 'Error was: ' + str(e)) else: OUT.info('Would have written content file ' + dbpath + '!')
def listunused(self, db): ''' Outputs a list of what has not been installed so far ''' packages = self.list_locations() if not packages: OUT.die('No packages found!') keys = sorted(packages) OUT.debug('Check for unused web applications', 7) for i in keys: db.set_category(packages[i][0]) db.set_package (packages[i][1]) db.set_version (packages[i][2]) if not db.has_installs(): if packages[i][0]: OUT.notice(packages[i][0] + '/' + packages[i][1] + '-' + packages[i][2]) else: OUT.notice(packages[i][1] + '-' + packages[i][2])
def listinstalls(self): ''' Outputs a list of what has been installed so far. ''' loc = self.read_db() if not loc and self.__v: OUT.die('No virtual installs found!') keys = sorted(loc) for j in keys: # The verbose output is meant to be readable for the user if self.__v: OUT.info('Installs for ' + '-'.join(j.split('/')), 4) for i in loc[j]: if self.__v: # The verbose output is meant to be readable for # the user OUT.info(' ' + i[3].strip(), 1) else: # This is a simplified form for the webapp.eclass print(i[3].strip())
def listunused(self, db): ''' Outputs a list of what has not been installed so far ''' packages = self.list_locations() if not packages: OUT.die('No packages found!') keys = sorted(packages) OUT.debug('Check for unused web applications', 7) for i in keys: db.set_category(packages[i][0]) db.set_package(packages[i][1]) db.set_version(packages[i][2]) if not db.has_installs(): if packages[i][0]: OUT.notice(packages[i][0] + '/' + packages[i][1] + '-' + packages[i][2]) else: OUT.notice(packages[i][1] + '-' + packages[i][2])
def get_root(config): '''Returns the $ROOT variable''' if config.config.get('USER', 'package_manager') == "portage": try: import portage except ImportError as e: OUT.die("Portage libraries not found, quitting:\n%s" % e) return portage.settings['ROOT'] elif config.config.get('USER', 'package_manager') == "paludis": cat = config.maybe_get('cat') pn = config.maybe_get('pn') if cat and pn: cmd="cave print-id-environment-variable -b --format '%%v\n' --variable-name ROOT %s/%s" % (cat,pn) fi, fo, fe = os.popen3(cmd) fi.close() result_lines = fo.readlines() fo.close() fe.close() if result_lines[0].strip(): return result_lines[0].strip() else: return '/' else: return '/' else: OUT.die("Unknown package manager: " + pm)
def dirisconfigprotected(self, installdir): ''' Traverses the path of parent directories for the given install dir and checks if any matches the list of config protected files. >>> a = Protection('','horde','3.0.5','portage') Add a virtual config protected directory: >>> a.config_protect += ' /my/strange/htdocs/' >>> a.dirisconfigprotected('/my/strange/htdocs/where/i/installed/x') True >>> a.dirisconfigprotected('/my/strange/htdocs/where/i/installed/x/') True >>> a.config_protect += ' /my/strange/htdocs' >>> a.dirisconfigprotected('/my/strange/htdocs/where/i/installed/x') True >>> a.dirisconfigprotected('/my/strange/htdocs/where/i/installed/x/') True >>> a.config_protect += ' bad_user /my/strange/htdocs' >>> a.dirisconfigprotected('/my/bad_user/htdocs/where/i/installed/x') False >>> a.dirisconfigprotected('/my/strange/htdocs/where/i/installed/x/') True >>> a.dirisconfigprotected('/') False ''' my_master = [] for i in self.config_protect.split(' '): if i[0] == '/': if i[-1] == '/': my_master.append(i[:-1]) else: my_master.append(i) if installdir[0] != '/': OUT.die('BUG! Don\'t call this with a relative path.') if installdir[-1] == '/': my_dir = installdir[:-1] else: my_dir = installdir while my_dir: if my_dir == '.' or my_dir == '/': return False for x in my_master: if my_dir == x: return True my_dir = os.path.dirname(my_dir) # nope, the directory isn't config-protected at this time return False
def __init__(self, directories, permissions, handler, flags, pm): if self.dep and not self.supported(pm): print(self.dep) OUT.die('Your configuration file sets the server type "' + self.name + '"\nbut the corresponding package does not seem to be ' 'installed!\nPlease "emerge ' + self.dep + '" or correct ' 'your settings.') try: self.set_server_user() except KeyError: OUT.die('The user for the server type "' + self.name + '" does not exist!') self.__sourced = directories['source'] self.__destd = directories['destination'] self.__hostroot = directories['hostroot'] self.__vhostroot = directories['vhostroot'] # + server owned if permissions['file']['server-owned'][0] == 0: permissions['file']['server-owned'][0] = self.vhost_server_uid permissions['dir']['server-owned'][0] = self.vhost_server_uid if permissions['file']['server-owned'][1] == 0: permissions['file']['server-owned'][1] = self.vhost_server_gid permissions['dir']['server-owned'][1] = self.vhost_server_gid # and config owned directories have server gid permissions['dir']['config-owned'][1] = self.vhost_server_gid # allows server and config owned permissions['file']['config-server-owned'][ 1] = self.vhost_server_gid permissions['dir']['config-server-owned'][ 1] = self.vhost_server_gid self.__perm = permissions self.__handler = handler self.__flags = flags self.__ws = handler['source'] self.__content = handler['content'] self.__protect = handler['protect'] self.__dotconfig = handler['dotconfig'] self.__ebuild = handler['ebuild'] self.__db = handler['db'] self.__v = flags['verbose'] self.__p = flags['pretend'] wd = WebappRemove(self.__content, self.__v, self.__p) handler['removal'] = wd self.__del = wd # Set by the install function self.__add = None
def want_category(config): '''Check if the package manager requires category info Portage: optional Paludis: mandatory ''' if config.config.get('USER', 'package_manager') == "portage": return elif config.config.get('USER', 'package_manager') == "paludis": if not config.config.has_option('USER', 'cat'): OUT.die("Package name must be in the form CAT/PN") else: OUT.die("Unknown package manager: " + pm)
def create_module(self, package_version, vhost_root, server_files, server_dirs): temp_dir = tempfile.mkdtemp() OUT.info('Creating SELinux modules') cleaned_version = re.match(r'(?P<version>[0-9]*\.[0-9]*(?:\.[0-9]*)?)', package_version).group('version') for policy in self.policy_types: base_dir = os.path.join(temp_dir, policy) os.mkdir(base_dir) with open(os.path.join(base_dir, '{}.te'.format(self.policy_name)), 'w') as te_file: te_file.write('policy_module({},{})\n'.format(self.policy_name, cleaned_version)) te_file.write('require {\n') te_file.write(' type httpd_sys_rw_content_t;\n') te_file.write('}') with open(os.path.join(base_dir, '{}.fc'.format(self.policy_name)), 'w') as fc_file: for files in server_files: fc_file.write('{} gen_context(system_u:object_r:httpd_sys_rw_content_t,s0)\n'.format(SELinux.filename_re_escape(os.path.join(vhost_root, files.rstrip('\n'))))) for dirs in server_dirs: fc_file.write('{}(/.*)? gen_context(system_u:object_r:httpd_sys_rw_content_t,s0)\n'.format(SELinux.filename_re_escape(os.path.join(vhost_root, dirs.rstrip('\n'))))) if subprocess.call(['make', '-s', '-C', base_dir, '-f', os.path.join('/usr/share/selinux', policy, 'include/Makefile'), '{}.pp'.format(self.policy_name)]): if not os.path.isfile(os.path.join('/usr/share/selinux', policy, 'include/Makefile')): OUT.die('Policy {} is not supported, please fix your configuration'.format(policy)) OUT.die('Unable to create {} SELinux module for {} @ {}'.format(policy, self.package_name, self.vhost_hostname)) OUT.info('Installing SELinux modules') try: for policy in self.policy_types: if subprocess.call(['semodule', '-s', policy, '-i', os.path.join(temp_dir, policy, '{}.pp'.format(self.policy_name))]): OUT.die('Unable to install {} SELinux module for {} @ {}'.format(policy, self.package_name, self.vhost_hostname)) except IOError: OUT.die('"semodule" was not found, please check you SELinux installation') shutil.rmtree(temp_dir)
def __init__(self, config_owned, server_owned, server_owned_r, virtual_files = 'virtual', default_dirs = 'default-owned'): ''' Populates the cache with the file types as provided by the ebuild. ''' self.__cache = {} # Validity of entries are checked by the command line parser self.__virtual_files = virtual_files self.__default_dirs = default_dirs # populate cache for i in config_owned: OUT.debug('Adding config-owned file', 8) self.__cache[self.__fix(i)] = 'config-owned' for i in server_owned: if self.__fix(i) in self.__cache.keys(): OUT.debug('Adding config-server-owned file', 8) self.__cache[self.__fix(i)] = 'config-server-owned' else: OUT.debug('Adding server-owned file', 8) self.__cache[self.__fix(i)] = 'server-owned' for i in server_owned_r: if self.__fix(i) in self.__cache.keys(): OUT.die('{} is a the same time recursively server-owned and {}: This case is not supported.'.format(self.__fix(i), self.__cache[self.__fix(i)])) else : OUT.debug('Adding recursively server-owned file', 8) self.__cache[self.__fix(i).strip()] = 'server-owned-dir'
def write(self, category, package, version, host, original_installdir, user_group): ''' Output the .webapp file, that tells us in future what has been installed into this directory. ''' self.__data['WEB_CATEGORY'] = category self.__data['WEB_PN'] = package self.__data['WEB_PVR'] = version self.__data['WEB_INSTALLEDBY'] = pwd.getpwuid(os.getuid())[0] self.__data['WEB_INSTALLEDDATE'] = strftime('%Y-%m-%d %H:%M:%S') self.__data['WEB_INSTALLEDFOR'] = user_group self.__data['WEB_HOSTNAME'] = host self.__data['WEB_INSTALLDIR'] = original_installdir info = ['# ' + self.__file, '# config file for this copy of ' + package + '-' + version, '#', '# automatically created by Gentoo\'s webapp-config', '# do NOT edit this file by hand', '',] for i in self.__tokens: info.append(i + '="' + self.__data[i] + '"') if not self.__p: try: fd = os.open(self.__dot_config(), os.O_WRONLY | os.O_CREAT, self.__perm(0o600)) os.write(fd, ('\n'.join(info)).encode('utf-8')) os.close(fd) except Exception as e: OUT.die('Unable to write to ' + self.__dot_config() + '\nError was: ' + str(e)) else: OUT.info('Would have written the following information into ' + self.__dot_config() + ':\n' + '\n'.join(info))
def show_installed(self): ''' Show which application has been installed in the install location.''' if not self.has_dotconfig(): OUT.die('No ' + self.__file + ' file in ' + self.__instdir + '; unable to continue') self.read() if 'WEB_CATEGORY' in self.__data: OUT.notice(self.__data['WEB_CATEGORY'] + ' ' + self.__data['WEB_PN'] + ' ' + self.__data['WEB_PVR']) else: OUT.notice( self.__data['WEB_PN'] + ' ' + self.__data['WEB_PVR'])
def dirtype(self, directory, parent_type = ''): ''' Inputs: directory - the directory that we need a decision about parent_type - the type of the parent directory returns one of these: server-owned - dir needs to be owned by the webserver user config-owned - dir needs to be owned by the config user config-server-owned - Both the previous cases at the same time server-owned-dir - Directory that contains file/dirs to be owned by the webserver user default-owned - we need a local copy, owned by root NOTE: Use get_filetype(filename) for files NOTE: the user can use --default-dirs on the command-line to change what type default directories are really reported as ''' # remove any whitespace and trailing / directory = self.__fix(directory) # check the cache if directory in self.__cache.keys(): # Check if parent type is recursive if parent_type == 'server-owned-dir': new_type = self.__cache[directory] if new_type == 'config-owned': OUT.die('This version does not support config dirs') if new_type == server-owned: OUT.warn('Configuration error: {} is marked server-owned two times'.format(filename)) return 'server-owned-dir' return self.__cache[directory] # Check if parent type is recursive if parent_type == 'server-owned-dir': return 'server-owned-dir' # unspecified directories are default-owned return self.__default_dirs
def package_installed(full_name, pm): ''' This function identifies installed packages. The Portage part is stolen from gentoolkit. We are not using gentoolkit directly as it doesn't seem to support ${ROOT} ''' if pm == "portage": try: import portage except ImportError as e: OUT.die("Portage libraries not found, quitting:\n%s" % e) try: t = portage.db[portage.root]["vartree"].dbapi.match(full_name) # catch the "ambiguous package" Exception except ValueError as e: if isinstance(e[0], list): t = [] for cp in e[0]: t += portage.db[portage.root]["vartree"].dbapi.match(cp) else: raise ValueError(e) return t elif pm == "paludis": cmd="cave print-best-version '%s'" % (full_name) fi, fo, fe = os.popen3(cmd) fi.close() result_lines = fo.readlines() error_lines = fe.readlines() fo.close() fe.close() if error_lines: for i in error_lines: OUT.warn(i) return ' '.join(result_lines) else: OUT.die("Unknown package manager: " + pm)
def prune_database(self, action): ''' Prunes the installs files to ensure no webapp is incorrectly listed as installed. ''' loc = self.read_db() if not loc and self.__v: OUT.die('No virtual installs found!') files = self.list_locations() keys = sorted(loc) if action != 'clean': OUT.warn( 'This is a list of all outdated entries that would be removed: ' ) for j in keys: for i in loc[j]: appdir = i[3].strip() # We check to see if the webapp is installed. if not os.path.exists(appdir + '/.webapp-' + j): if self.__v: OUT.warn('No .webapp file found in dir: ') OUT.warn(appdir) OUT.warn('Assuming webapp is no longer installed.') OUT.warn('Pruning entry from database.') if action == 'clean': for installs in list(files.keys()): contents = open(installs).readlines() new_entries = '' for entry in contents: # Grab all the other entries but the one that # isn't installed. if not re.search('.* ' + appdir + '\\n', entry): new_entries += entry f = open(installs, 'w') f.write(new_entries) f.close() else: OUT.warn(appdir)
def __init__(self, package_name, vhost_hostname, policy_types = ()): self.package_name = package_name self.vhost_hostname = vhost_hostname self.policy_name = '{}_{}'.format(package_name, vhost_hostname) self.policy_types = policy_types if self.policy_types is (): for filename in MAKE_CONF_FILE: try: with open(filename) as file: for line in file.readlines(): if line.startswith('POLICY_TYPES='): self.policy_types = line[len('POLICY_TYPES='):-1].strip(' "').split() break if self.policy_types is not None: break except IOError: pass if self.policy_types is (): OUT.die('No SELinux policy was found, abording')
def reportpackageavail(self): ''' This is a simple wrapper around packageavail() that outputs user-friendly error messages if an error occurs Cannot test the rest, do not want to die. ''' OUT.info('Do we have ' + self.package_name() + ' available?') available = self.packageavail() if available == 0: OUT.info(' Yes, we do') if available == 1: OUT.die(' Please emerge ' + self.package_name() + ' first.') if available == 3: OUT.die(' ' + self.package_name() + ' is not compatible with ' 'webapp-config.\nIf it should be, report this at ' + wrapper.bugs_link)
def __init__(self, fs_root, root, category = '', package = '', version = '', dbfile = 'installs'): self.__r = fs_root self.root = self.__r + root self.root = re.compile('/+').sub('/', self.root) if not os.path.isdir(self.root): OUT.die('"' + self.root + '" specifies no directory! webapp' '-config needs a valid directory to store/retrieve in' 'formation. Please correct your settings.') self.category = category self.pn = package self.pvr = version self.dbfile = dbfile
def __init__(self, fs_root, root, category='', package='', version='', dbfile='installs'): self.__r = fs_root self.root = self.__r + root self.root = re.compile('/+').sub('/', self.root) if not os.path.isdir(self.root): OUT.die('"' + self.root + '" specifies no directory! webapp' '-config needs a valid directory to store/retrieve in' 'formation. Please correct your settings.') self.category = category self.pn = package self.pvr = version self.dbfile = dbfile
def prune_database(self, action): ''' Prunes the installs files to ensure no webapp is incorrectly listed as installed. ''' loc = self.read_db() if not loc and self.__v: OUT.die('No virtual installs found!') files = self.list_locations() keys = sorted(loc) if action != 'clean': OUT.warn('This is a list of all outdated entries that would be removed: ') for j in keys: for i in loc[j]: appdir = i[3].strip() # We check to see if the webapp is installed. if not os.path.exists(appdir+'/.webapp-'+j): if self.__v: OUT.warn('No .webapp file found in dir: ') OUT.warn(appdir) OUT.warn('Assuming webapp is no longer installed.') OUT.warn('Pruning entry from database.') if action == 'clean': for installs in files.keys(): contents = open(installs).readlines() new_entries = '' for entry in contents: # Grab all the other entries but the one that # isn't installed. if not re.search('.* ' + appdir +'\\n', entry): new_entries += entry f = open(installs, 'w') f.write(new_entries) f.close() else: OUT.warn(appdir)
def config_protect(cat, pn, pvr, pm): '''Return CONFIG_PROTECT (used by protect.py)''' if pm == "portage": try: import portage except ImportError as e: OUT.die("Portage libraries not found, quitting:\n%s" % e) return portage.settings['CONFIG_PROTECT'] elif pm == "paludis": cmd="cave print-id-environment-variable -b --format '%%v\n' --variable-name CONFIG_PROTECT %s/%s" % (cat,pn) fi, fo, fe = os.popen3(cmd) fi.close() result_lines = fo.readlines() fo.close() fe.close() return ' '.join(result_lines).strip() else: OUT.die("Unknown package manager: " + pm)
def write(self): ''' Write the contents file. A short test: >>> import os.path >>> here = os.path.dirname(os.path.realpath(__file__)) >>> a = Contents(here + '/tests/testfiles/contents/', ... package = 'test', version = '1.0', ... pretend = True) >>> a.read() >>> OUT.color_off() >>> a.write() #doctest: +ELLIPSIS * Would have written content file .../tests/testfiles/contents//.webapp-test-1.0! ''' dbpath = self.appdb() if not dbpath: OUT.die('No package specified!') self.check_installdir() values = [' '.join(i) for i in self.__content.values()] if not self.__p: try: fd = os.open(self.appdb(), os.O_WRONLY | os.O_CREAT, self.__perm(0o600)) os.write(fd, ('\n'.join(values)).encode('utf-8')) os.close(fd) except Exception as e: OUT.warn('Failed to write content file ' + dbpath + '!\n' + 'Error was: ' + str(e)) else: OUT.info('Would have written content file ' + dbpath + '!')
def dirisconfigprotected(self, installdir): ''' Traverses the path of parent directories for the given install dir and checks if any matches the list of config protected files. ''' my_master = [] for i in self.config_protect.split(' '): if i[0] == '/': if i[-1] == '/': my_master.append(i[:-1]) else: my_master.append(i) if installdir[0] != '/': OUT.die('BUG! Don\'t call this with a relative path.') if installdir[-1] == '/': my_dir = installdir[:-1] else: my_dir = installdir while my_dir: if my_dir == '.' or my_dir == '/': return False for x in my_master: if my_dir == x: return True my_dir = os.path.dirname(my_dir) # nope, the directory isn't config-protected at this time return False
def add(self, installdir, user, group): ''' Add a record to the list of virtual installs. installdir - the installation directory ''' if not installdir: OUT.die('The installation directory must be specified!') if not str(user): OUT.die('Please specify a valid user!') if not str(group): OUT.die('Please specify a valid group!') OUT.debug('Adding install record', 6) dbpath = self.appdb() if not dbpath: OUT.die('No package specified!') if not self.__p and not os.path.isdir(os.path.dirname(dbpath)): os.makedirs(os.path.dirname(dbpath), self.__dir_perm(0o755)) fd = None if not self.__p: fd = os.open(dbpath, os.O_WRONLY | os.O_APPEND | os.O_CREAT, self.__file_perm(0o600)) entry = str(int(time.time())) + ' ' + str(user) + ' ' + str(group)\ + ' ' + installdir + '\n' OUT.debug('New record', 7) if not self.__p: os.write(fd, (entry).encode('utf-8')) os.close(fd) else: OUT.info('Pretended to append installation ' + installdir) OUT.info('Entry:\n' + entry)
def check_installdir(self): if not os.path.isdir(self.__installdir) and not self.__p: OUT.die('"' + self.__installdir + '" specifies no directory! ' 'webapp-config needs a valid directory to store/retri' 'eve information. Please correct your settings.')
def add(self, dsttype, ctype, destination, path, real_path, relative = True): ''' Add an entry to the contents file. Just like Portage, when we install an app, we create a contents file to say what we installed and when. We use this contents file to help us safely remove & upgrade apps. CONTENTS file format: <what> <rel> <type> <filename> <timestamp> <sum> [<optional>] where <what> is one of dir|sym|file|hardlink <rel> is 1 for relative filenames, 0 for absolute filenames <type> is one of server-owned|default-owned|config-owned|virtual <timestamp> is the timestamp when the file was installed <sum> is the md5sum of the file (this is 0 for directories and symlinks) <filename> is the actual name of the file we have installed <optional> is additional data that depends upon <what> NOTE: Filenames used to be on the end of the line. This made the old bash version more complicated, and prone to failure. So I have moved the filename into the middle of the line. -- Stuart Portage uses absolute names for its files, dirs, and symlinks. We do not. In theory, you can move a directory containing a web-based app, and a) the app itself will not break, and b) webapp-config will still work on that directory for upgrades and cleans. Position-independence *is* a design constraint that all future changes to this script need to honour. Inputs: dsttype - type to add (one of dir|sym|file|hardlink) ctype - internal webapp-config type - (server-owned | config-owned | virtual) destination - install dir (normally $G_INSTALLDIR) path - filename inside 'destination' real_path - for config-protected files realpath =! path (and this is important for md5) relative - 1 for storing a relative filename, 0 otherwise ''' OUT.debug('Adding entry to content dictionary', 6) # Build the full path that we use as index in the contents list while path[0] == '/': path = path[1:] while destination[-1] == '/': destination = destination[:-1] entry = destination + '/' + path # special case - we don't add entries for '.' if os.path.basename(entry) == '.': return if (not self.__p and not os.path.islink(entry) and (not os.path.exists(entry) or not os.access(entry, os.R_OK))): OUT.warn('Cannot access file ' + entry + ' to add it as' ' installation content. This should not happen!') return allowed_types = { 'file' : [ 'file', self.file_md5, self.file_null ], 'hardlink': [ 'file', self.file_md5, self.file_null ], 'dir' : [ 'dir', self.file_zero, self.file_null ], 'sym' : [ 'sym', self.file_zero, self.file_link ], } if not dsttype in list(allowed_types.keys()): OUT.die('Oops, webapp-config bug. "dsttype" is ' + dsttype) # Generate handler for file attributes a = allowed_types[dsttype] # For absolute entries the path must match the entry if not relative: path = entry OUT.debug('Adding entry', 7) # report if pretending if self.__p: OUT.info(' pretending to add: ' + ' '.join([dsttype, str(int(relative)), ctype, '"' + path + '"'])) else: # Only the path is enclosed in quotes, NOT the link targets self.__content[entry] = [ a[0], str(int(relative)), ctype, '"' + path + '"', self.file_time(entry), a[1](real_path), a[2](entry)] if self.__v: msg = path if msg[0] == "/": msg = self.__root + msg msg = self.__re.sub('/', msg) OUT.notice('>>> ' + a[0] + ' ' * (4 - len(a[0])) + ' (' \ + ctype + ') ' + msg)
def add(self, dsttype, ctype, destination, path, real_path, relative = True): ''' Add an entry to the contents file. Just like Portage, when we install an app, we create a contents file to say what we installed and when. We use this contents file to help us safely remove & upgrade apps. CONTENTS file format: <what> <rel> <type> <filename> <timestamp> <sum> [<optional>] where <what> is one of dir|sym|file|hardlink <rel> is 1 for relative filenames, 0 for absolute filenames <type> is one of server-owned|default-owned|config-owned|virtual <timestamp> is the timestamp when the file was installed <sum> is the md5sum of the file (this is 0 for directories and symlinks) <filename> is the actual name of the file we have installed <optional> is additional data that depends upon <what> NOTE: Filenames used to be on the end of the line. This made the old bash version more complicated, and prone to failure. So I have moved the filename into the middle of the line. -- Stuart Portage uses absolute names for its files, dirs, and symlinks. We do not. In theory, you can move a directory containing a web-based app, and a) the app itself will not break, and b) webapp-config will still work on that directory for upgrades and cleans. Position-independence *is* a design constraint that all future changes to this script need to honour. Inputs: dsttype - type to add (one of dir|sym|file|hardlink) ctype - internal webapp-config type - (server-owned | config-owned | virtual) destination - install dir (normally $G_INSTALLDIR) path - filename inside 'destination' real_path - for config-protected files realpath =! path (and this is important for md5) relative - 1 for storing a relative filename, 0 otherwise OUT.color_off() import os.path here = os.path.dirname(os.path.realpath(__file__)) One for pretending: a = Contents(here + '/tests/testfiles/contents/app/', ... package = 'test', version = '1.0', ... pretend = True) And this one is for real: b = Contents(here + '/tests/testfiles/contents/app/', ... package = 'test', version = '1.0') Pretend to add a file: a.add('file', 'config-owned', ... destination = here + '/tests/testfiles/contents/app/', ... path = '/test1', relative = True) * pretending to add: file 1 config-owned "test1" Lets not pretend this time: b.add('file', 'config-owned', ... destination = here + '/tests/testfiles/contents/app/', ... path = '/test1', relative = True) b.entry(here + '/tests/testfiles/contents/app/test1') #doctest: +ELLIPSIS 'file 1 config-owned "test1" ... d8e8fca2dc0f896fd7cb4cb0031ba249 ' Lets produce an error with a file that does not exist: b.add('file', 'config-owned', ... destination = here + '/tests/testfiles/contents/app/', ... path = '/nothere', relative = True) #doctest: +ELLIPSIS * Cannot access file .../tests/testfiles/contents/app/nothere to add it as installation content. This should not happen! Other file types: b.add('hardlink', 'config-owned', ... destination = here + '/tests/testfiles/contents/app/', ... path = '/test2', relative = True) b.entry(here + '/tests/testfiles/contents/app/test2') #doctest: +ELLIPSIS 'file 1 config-owned "test2" ... d8e8fca2dc0f896fd7cb4cb0031ba249 ' b.add('dir', 'default-owned', ... destination = here + '/tests/testfiles/contents/app/', ... path = '/dir1', relative = True) b.entry(here + '/tests/testfiles/contents/app/dir1') #doctest: +ELLIPSIS 'dir 1 default-owned "dir1" ... 0 ' b.add('dir', 'default-owned', destination = here + '/tests/testfiles/contents/app', ... path = '/dir1', ... relative = False) b.entry(here + '/tests/testfiles/contents/app/dir1') #doctest: +ELLIPSIS 'dir 0 default-owned ".../tests/testfiles/contents/app/dir1" ... 0 ' Q: Is the full link to the target what we want? A: Yes, since the link will still be ok even if we move the directory. b.add('sym', 'virtual', ... destination = here + '/tests/testfiles/contents/app/', ... path = '/test3', relative = True) b.entry(here + '/tests/testfiles/contents/app/test3') #doctest: +ELLIPSIS 'sym 1 virtual "test3" ... 0 .../tests/testfiles/contents/app/test1' b.db_print() #doctest: +ELLIPSIS file 1 config-owned "test1" ... d8e8fca2dc0f896fd7cb4cb0031ba249 file 1 config-owned "test2" ... d8e8fca2dc0f896fd7cb4cb0031ba249 sym 1 virtual "test3" ... 0 .../tests/testfiles/contents/app/test1 dir 0 default-owned ".../tests/testfiles/contents/app/dir1" ... 0 ''' OUT.debug('Adding entry to content dictionary', 6) # Build the full path that we use as index in the contents list while path[0] == '/': path = path[1:] while destination[-1] == '/': destination = destination[:-1] entry = destination + '/' + path # special case - we don't add entries for '.' if os.path.basename(entry) == '.': return if (not self.__p and not os.path.islink(entry) and (not os.path.exists(entry) or not os.access(entry, os.R_OK))): OUT.warn('Cannot access file ' + entry + ' to add it as' ' installation content. This should not happen!') return allowed_types = { 'file' : [ 'file', self.file_md5, self.file_null ], 'hardlink': [ 'file', self.file_md5, self.file_null ], 'dir' : [ 'dir', self.file_zero, self.file_null ], 'sym' : [ 'sym', self.file_zero, self.file_link ], } if not dsttype in allowed_types.keys(): OUT.die('Oops, webapp-config bug. "dsttype" is ' + dsttype) # Generate handler for file attributes a = allowed_types[dsttype] # For absolute entries the path must match the entry if not relative: path = entry OUT.debug('Adding entry', 7) # report if pretending if self.__p: OUT.info(' pretending to add: ' + ' '.join([dsttype, str(int(relative)), ctype, '"' + path + '"'])) else: # Only the path is enclosed in quotes, NOT the link targets self.__content[entry] = [ a[0], str(int(relative)), ctype, '"' + path + '"', self.file_time(entry), a[1](real_path), a[2](entry)] if self.__v: msg = path if msg[0] == "/": msg = self.__root + msg msg = self.__re.sub('/', msg) OUT.notice('>>> ' + a[0] + ' ' * (4 - len(a[0])) + ' (' \ + ctype + ') ' + msg)
def __init__(self, directories, permissions, handler, flags, pm): if self.dep and not self.supported(pm): print(self.dep) OUT.die('Your configuration file sets the server type "' + self.name + '"\nbut the corresponding package does not seem to be ' 'installed!\nPlease "emerge ' + self.dep + '" or correct ' 'your settings.') try: self.set_server_user() except KeyError: OUT.die('The user for the server type "' + self.name + '" does not exist!') self.__sourced = directories['source'] self.__destd = directories['destination'] self.__hostroot = directories['hostroot'] self.__vhostroot = directories['vhostroot'] # + server owned if permissions['file']['server-owned'][0] == 0: permissions['file']['server-owned'][0] = self.vhost_server_uid permissions['dir']['server-owned'][0] = self.vhost_server_uid if permissions['file']['server-owned'][1] == 0: permissions['file']['server-owned'][1] = self.vhost_server_gid permissions['dir']['server-owned'][1] = self.vhost_server_gid # and config owned directories have server gid permissions['dir']['config-owned'][1] = self.vhost_server_gid # allows server and config owned permissions['file']['config-server-owned'][1] = self.vhost_server_gid permissions['dir']['config-server-owned'][1] = self.vhost_server_gid self.__perm = permissions self.__handler = handler self.__flags = flags self.__ws = handler['source'] self.__content = handler['content'] self.__protect = handler['protect'] self.__dotconfig = handler['dotconfig'] self.__ebuild = handler['ebuild'] self.__db = handler['db'] self.__v = flags['verbose'] self.__p = flags['pretend'] wd = WebappRemove(self.__content, self.__v, self.__p) handler['removal'] = wd self.__del = wd # Set by the install function self.__add = None
def add(self, dsttype, ctype, destination, path, real_path, relative=True): ''' Add an entry to the contents file. Just like Portage, when we install an app, we create a contents file to say what we installed and when. We use this contents file to help us safely remove & upgrade apps. CONTENTS file format: <what> <rel> <type> <filename> <timestamp> <sum> [<optional>] where <what> is one of dir|sym|file|hardlink <rel> is 1 for relative filenames, 0 for absolute filenames <type> is one of server-owned|default-owned|config-owned|virtual <timestamp> is the timestamp when the file was installed <sum> is the md5sum of the file (this is 0 for directories and symlinks) <filename> is the actual name of the file we have installed <optional> is additional data that depends upon <what> NOTE: Filenames used to be on the end of the line. This made the old bash version more complicated, and prone to failure. So I have moved the filename into the middle of the line. -- Stuart Portage uses absolute names for its files, dirs, and symlinks. We do not. In theory, you can move a directory containing a web-based app, and a) the app itself will not break, and b) webapp-config will still work on that directory for upgrades and cleans. Position-independence *is* a design constraint that all future changes to this script need to honour. Inputs: dsttype - type to add (one of dir|sym|file|hardlink) ctype - internal webapp-config type - (server-owned | config-owned | virtual) destination - install dir (normally $G_INSTALLDIR) path - filename inside 'destination' real_path - for config-protected files realpath =! path (and this is important for md5) relative - 1 for storing a relative filename, 0 otherwise ''' OUT.debug('Adding entry to content dictionary', 6) # Build the full path that we use as index in the contents list while path[0] == '/': path = path[1:] while destination[-1] == '/': destination = destination[:-1] entry = destination + '/' + path # special case - we don't add entries for '.' if os.path.basename(entry) == '.': return if (not self.__p and not os.path.islink(entry) and (not os.path.exists(entry) or not os.access(entry, os.R_OK))): OUT.warn('Cannot access file ' + entry + ' to add it as' ' installation content. This should not happen!') return allowed_types = { 'file': ['file', self.file_md5, self.file_null], 'hardlink': ['file', self.file_md5, self.file_null], 'dir': ['dir', self.file_zero, self.file_null], 'sym': ['sym', self.file_zero, self.file_link], } if not dsttype in list(allowed_types.keys()): OUT.die('Oops, webapp-config bug. "dsttype" is ' + dsttype) # Generate handler for file attributes a = allowed_types[dsttype] # For absolute entries the path must match the entry if not relative: path = entry OUT.debug('Adding entry', 7) # report if pretending if self.__p: OUT.info(' pretending to add: ' + ' '.join( [dsttype, str(int(relative)), ctype, '"' + path + '"'])) else: # Only the path is enclosed in quotes, NOT the link targets self.__content[entry] = [ a[0], str(int(relative)), ctype, '"' + path + '"', self.file_time(entry), a[1](real_path), a[2](entry) ] if self.__v: msg = path if msg[0] == "/": msg = self.__root + msg msg = self.__re.sub('/', msg) OUT.notice('>>> ' + a[0] + ' ' * (4 - len(a[0])) + ' (' \ + ctype + ') ' + msg)
def remove(self, installdir): ''' Remove a record from the list of virtual installs. installdir - the installation directory ''' if not installdir: OUT.die('The installation directory must be specified!') dbpath = self.appdb() if not dbpath: OUT.die('No package specified!') if not os.access(dbpath, os.R_OK): OUT.warn('Unable to read the install database ' + dbpath) return # Read db file fdb = open(dbpath) entries = fdb.readlines() fdb.close() newentries = [] found = False for i in entries: j = i.strip().split(' ') if j: if len(j) != 4: # Remove invalid entry OUT.warn('Invalid line "' + i.strip() + '" remo' 'ved from the database file!') elif j[3] != installdir: OUT.debug('Keeping entry', 7) # Keep valid entry newentries.append(i.strip()) elif j[3] == installdir: # Remove entry, indicate found found = True if not found: OUT.warn('Installation at "' + installdir + '" could not be ' 'found in the database file. Check the entries in "' + dbpath + '"!') if not self.__p: installs = open(dbpath, 'w') installs.write('\n'.join(newentries) + '\n') installs.close() if not self.has_installs(): os.unlink(dbpath) else: OUT.info('Pretended to remove installation ' + installdir) OUT.info('Final DB content:\n' + '\n'.join(newentries) + '\n')
def read(self): ''' Reads the contents database. ''' dbpath = self.appdb() if not dbpath or not os.access(dbpath, os.R_OK): OUT.die('Content file ' + dbpath + ' is missing or not accessibl' 'e!') content = open(dbpath).readlines() for i in content: i = i.strip() rfn = re.compile('"(.*)"') rfs = rfn.search(i) if not rfs: ok = False else: fn = rfs.group(1) i = rfn.sub('', i) line_split = i.split(' ') line_split[3] = fn OUT.debug('Adding content line', 10) ok = True if len(line_split) < 6: ok = False OUT.warn('Content file ' + dbpath + ' has an invalid line' ':\n' + i + '\nNot enough entries.') if ok and not line_split[0] in ['file', 'sym', 'dir']: ok = False OUT.warn('Content file ' + dbpath + ' has an invalid line' ':\n' + i + '\nInvalid file type: ' + line_split[0]) if ok and not line_split[1] in ['0', '1']: ok = False OUT.warn('Content file ' + dbpath + ' has an invalid line' ':\n' + i + '\nInvalid relative flag: ' + line_split[1]) if ok and not line_split[2] in [ 'virtual', 'server-owned', 'config-owned', 'default-owned', 'config-server-owned', # Still need that in case an # application was installed # with w-c-1.11 'root-owned' ]: ok = False OUT.warn('Content file ' + dbpath + ' has an invalid line' ':\n' + i + '\nInvalid owner: ' + line_split[2]) if ok and line_split[0] == 'sym' and len(line_split) == 6: OUT.warn('Content file ' + dbpath + ' has an invalid line' ':\n' + i + '\nMissing link target! ') if len(line_split) == 6: line_split.append('') # I think this could happen if the link target contains # spaces # -- wrobel if len(line_split) > 7: line_split = line_split[0:6] \ + [' '.join(line_split[6:])] if ok: if line_split[1] == '0': self.__content[line_split[3]] = line_split else: self.__content[self.__installdir + '/' + line_split[3]] = line_split else: OUT.warn('Invalid line in content file (' + i + '). Ignor' 'ing!')
def config_libdir(pm): OUT.die("I shouldn't get called at all")
def read(self): ''' Reads the contents database. Some content files have been provided for test purposes: >>> import os.path >>> here = os.path.dirname(os.path.realpath(__file__)) This one should succeed: >>> a = Contents(here + '/tests/testfiles/contents/', ... package = 'test', version = '1.0') >>> a.read() >>> a.db_print() file 1 virtual util/icon_browser.php 1124612216 9ffb2ca9ccd2db656b97cd26a1b06010 file 1 config-owned inc/prefs.php 1124612215 ffae752dba7092cd2d1553d04a0f0045 file 1 virtual lib/prefs.php 1124612215 ffae752dba7092cd2d1553d04a0f0045 file 1 virtual signup.php 1124612220 dc838bc375b3d02dafc414f8e71a2aec file 1 server-owned data.php 1117009618 0 sym 1 virtual test 1124612220 dc838bc375b3d02dafc414f8e71a2aec /I link / to a very / strange location dir 1 default-owned util 1117009618 0 dir 1 config-owned inc 1117009618 0 dir 1 default-owned lib 1117009618 0 dir 0 default-owned /var/www/localhost/cgi-bin 1124577741 0 dir 0 default-owned /var/www/localhost/error 1124577740 0 dir 0 default-owned /var/www/localhost/icons 1124577741 0 >>> a.get_directories() #doctest: +ELLIPSIS ['.../contents//util', '.../contents//inc', '.../contents//lib', '/var/www/localhost/cgi-bin', '/var/www/localhost/error', '/var/www/localhost/icons'] This is a corrupted file that checks all fail safes: >>> OUT.color_off() >>> a = Contents(here + '/tests/testfiles/contents/', ... package = 'test', version = '1.1') >>> a.read() #doctest: +ELLIPSIS * Invalid line in content file (dir 1 default-owned). Ignoring! * Content file .../tests/testfiles/contents//.webapp-test-1.1 has an invalid line: * dir 1 nobody-owned 1117009618 0 * Invalid owner: nobody-owned * Invalid line in content file (dir 1 nobody-owned 1117009618 0). Ignoring! * Content file .../tests/testfiles/contents//.webapp-test-1.1 has an invalid line: * garbage 1 virtual 1124612215 ffae752dba7092cd2d1553d04a0f0045 * Invalid file type: garbage * Invalid line in content file (garbage 1 virtual 1124612215 ffae752dba7092cd2d1553d04a0f0045). Ignoring! * Invalid line in content file (file 1 virtual). Ignoring! * Content file .../tests/testfiles/contents//.webapp-test-1.1 has an invalid line: * file 1 virtual * Not enough entries. * Invalid line in content file (file 1 virtual ). Ignoring! * Content file .../tests/testfiles/contents//.webapp-test-1.1 has an invalid line: * file 31 config-owned 1124612215 ffae752dba7092cd2d1553d04a0f0045 * Invalid relative flag: 31 * Invalid line in content file (file 31 config-owned 1124612215 ffae752dba7092cd2d1553d04a0f0045). Ignoring! ''' dbpath = self.appdb() if not dbpath or not os.access(dbpath, os.R_OK): OUT.die('Content file ' + dbpath + ' is missing or not accessibl' 'e!') content = open(dbpath).readlines() for i in content: i = i.strip() rfn = re.compile('"(.*)"') rfs = rfn.search(i) if not rfs: ok = False else: fn = rfs.group(1) i = rfn.sub('', i) line_split = i.split(' ') line_split[3] = fn OUT.debug('Adding content line', 10) ok = True if len(line_split) < 6: ok = False OUT.warn('Content file ' + dbpath + ' has an invalid line' ':\n' + i + '\nNot enough entries.') if ok and not line_split[0] in ['file', 'sym', 'dir']: ok = False OUT.warn('Content file ' + dbpath + ' has an invalid line' ':\n' + i + '\nInvalid file type: ' + line_split[0]) if ok and not line_split[1] in ['0', '1']: ok = False OUT.warn('Content file ' + dbpath + ' has an invalid line' ':\n' + i + '\nInvalid relative flag: ' + line_split[1]) if ok and not line_split[2] in ['virtual', 'server-owned', 'server-owned-dir', 'config-owned', 'default-owned', 'config-server-owned', # Still need that in case an # application was installed # with w-c-1.11 'root-owned']: ok = False OUT.warn('Content file ' + dbpath + ' has an invalid line' ':\n' + i + '\nInvalid owner: ' + line_split[2]) if ok and line_split[0] == 'sym' and len(line_split) == 6: OUT.warn('Content file ' + dbpath + ' has an invalid line' ':\n' + i + '\nMissing link target! ') if len(line_split) == 6: line_split.append('') # I think this could happen if the link target contains # spaces # -- wrobel if len(line_split) > 7: line_split = line_split[0:6] \ + [' '.join(line_split[6:])] if ok: if line_split[1] == '0': self.__content[line_split[3]] = line_split else: self.__content[self.__installdir + '/' + line_split[3]] = line_split else: OUT.warn('Invalid line in content file (' + i + '). Ignor' 'ing!')
def read(self): ''' Reads the contents database. ''' dbpath = self.appdb() if not dbpath or not os.access(dbpath, os.R_OK): OUT.die('Content file ' + dbpath + ' is missing or not accessibl' 'e!') content = open(dbpath).readlines() for i in content: i = i.strip() rfn = re.compile('"(.*)"') rfs = rfn.search(i) if not rfs: ok = False else: fn = rfs.group(1) i = rfn.sub('', i) line_split = i.split(' ') line_split[3] = fn OUT.debug('Adding content line', 10) ok = True if len(line_split) < 6: ok = False OUT.warn('Content file ' + dbpath + ' has an invalid line' ':\n' + i + '\nNot enough entries.') if ok and not line_split[0] in ['file', 'sym', 'dir']: ok = False OUT.warn('Content file ' + dbpath + ' has an invalid line' ':\n' + i + '\nInvalid file type: ' + line_split[0]) if ok and not line_split[1] in ['0', '1']: ok = False OUT.warn('Content file ' + dbpath + ' has an invalid line' ':\n' + i + '\nInvalid relative flag: ' + line_split[1]) if ok and not line_split[2] in ['virtual', 'server-owned', 'config-owned', 'default-owned', 'config-server-owned', # Still need that in case an # application was installed # with w-c-1.11 'root-owned']: ok = False OUT.warn('Content file ' + dbpath + ' has an invalid line' ':\n' + i + '\nInvalid owner: ' + line_split[2]) if ok and line_split[0] == 'sym' and len(line_split) == 6: OUT.warn('Content file ' + dbpath + ' has an invalid line' ':\n' + i + '\nMissing link target! ') if len(line_split) == 6: line_split.append('') # I think this could happen if the link target contains # spaces # -- wrobel if len(line_split) > 7: line_split = line_split[0:6] \ + [' '.join(line_split[6:])] if ok: if line_split[1] == '0': self.__content[line_split[3]] = line_split else: self.__content[self.__installdir + '/' + line_split[3]] = line_split else: OUT.warn('Invalid line in content file (' + i + '). Ignor' 'ing!')