diff options
author | Brian Dolbec <brian.dolbec@gmail.com> | 2011-02-03 00:08:43 -0800 |
---|---|---|
committer | Brian Dolbec <brian.dolbec@gmail.com> | 2011-02-03 00:08:43 -0800 |
commit | c2ea6560f4ca3497eddec9970e76d76d98f4e485 (patch) | |
tree | e21c1780bc081149e36153f34b6cc802ca4f9c48 /overlord | |
parent | use the new dbase list_ids() (diff) | |
download | overlord-c2ea6560f4ca3497eddec9970e76d76d98f4e485.tar.gz overlord-c2ea6560f4ca3497eddec9970e76d76d98f4e485.tar.bz2 overlord-c2ea6560f4ca3497eddec9970e76d76d98f4e485.zip |
Mass namespace rename and update
Diffstat (limited to 'overlord')
33 files changed, 4992 insertions, 0 deletions
diff --git a/overlord/__init__.py b/overlord/__init__.py new file mode 100644 index 0000000..792d600 --- /dev/null +++ b/overlord/__init__.py @@ -0,0 +1 @@ +# diff --git a/overlord/api.py b/overlord/api.py new file mode 100644 index 0000000..936af9b --- /dev/null +++ b/overlord/api.py @@ -0,0 +1,452 @@ +#!python +# -*- coding: utf-8 -*- + +""" + Overlord - A UTILITY TO SELECT AND UPDATE GENTOO OVERLAYS + Distributed under the terms of the GNU General Public License v2 + + Copyright: + (c) 2010 - 2011 Brian Dolbec + Distributed under the terms of the GNU General Public License v2 + + Author(s): + Brian Dolbec <dol-sen@sourceforge.net> + +""" + +from sys import stderr +import os + +from overlord.config import BareConfig +#from overlord.action import Sync + +from overlord.dbbase import UnknownOverlayException +from overlord.db import DB, RemoteDB +#from overlord.utils import path, delete_empty_directory +from overlord.debug import OUT + +# give them some values for now, these are from the packagekit backend +# TODO establish some proper errors for the api. +ERROR_REPO_NOT_FOUND = -1 +ERROR_INTERNAL_ERROR = -2 +UNKNOWN_REPO_ID = "Repo ID '%s' " + \ + "is not listed in the current available overlays list" + +# In order to redirect output you need to get a Message class instance with the +# stderr, stdout, stddebug directed to where you want. +# eg: output = Message('overlord', err=mystderr, dbg=mydebug, out=myoutput) +# there are many more options available, refer to debug.py Message class + + +class OverlordAPI(object): + """High level class to hold and run a overlord instance for use by + API consumer apps, guis, etc. + """ + + def __init__(self, config=None, report_errors=False, output=None): + """ + @param configfile: optional config file to use instead of the default. + can be a BareConfig or ArgsParser config class. + default is BareConfig(output=output) + @param report_errors: optional bool to silence some error reporting to stdout + default is False + @param output: optional Message class instance created with your settings. + default is Message(module='overlord') other params are defaults. + """ + + self.output = output if output else OUT + + self.config = config if config else BareConfig(output=output) + + self.report_errors = report_errors + + # get installed and available dbs + self._installed_db = None + self._installed_ids = None + self._available_db = None + self._available_ids = None + self._error_messages = [] + self.sync_results = [] + + + def is_repo(self, ovl): + """validates that the ovl given is a known repo id + + @param ovl: repo id + @type ovl: str + @rtype boolean + """ + return ovl in self.get_available() + + + def is_installed(self, ovl): + """checks that ovl is a known installed repo id + + @param ovl: repo id + @type ovl: str + @rtype boolean + """ + return ovl in self.get_installed() + + + @staticmethod + def _check_repo_type( repos, caller): + """internal function that validates the repos parameter, + converting a string to a list[string] if it is not already a list. + produces and error message if it is any other type + returns repos as list always""" + if isinstance(repos, basestring): + repos = [repos] + # else assume it is an iterable, if not it will error + return repos + + + def delete_repos(self, repos): + """delete the selected repo from the system + + @type repos: list of strings or string + @param repos: ['repo-id1', ...] or 'repo-id' + @param output: method to handle output if desired + @rtype dict + """ + repos = self._check_repo_type(repos, "delete_repo") + results = [] + for ovl in repos: + if not self.is_installed(ovl): + results.append(True) + break + if not self.is_repo(ovl): + self._error(1, UNKNOWN_REPO_ID %ovl) + results.append(False) + break + try: + self._get_installed_db().delete(self._get_installed_db().select(ovl)) + results.append(True) + except Exception, e: + self._error(ERROR_INTERNAL_ERROR, + "Failed to disable repository '"+ovl+"':\n"+str(e)) + results.append(False) + self.get_installed(reload=True) + if False in results: + return False + return True + + + def add_repos(self, repos): + """installs the seleted repo id + + @type repos: list of strings or string + @param repos: ['repo-id', ...] or 'repo-id' + @param output: method to handle output if desired + @rtype dict + """ + repos = self._check_repo_type(repos, "add_repo") + results = [] + for ovl in repos: + if self.is_installed(ovl): + results.append(True) + break + if not self.is_repo(ovl): + self._error(1, UNKNOWN_REPO_ID %ovl) + results.append(False) + break + try: + self._get_installed_db().add(self._get_remote_db().select(ovl), quiet=True) + results.append(True) + except Exception, e: + self._error(ERROR_INTERNAL_ERROR, + "Failed to enable repository '"+ovl+"' : "+str(e)) + results.append(False) + self.get_installed(reload=True) + if False in results: + return False + return True + + + def get_all_info(self, repos, local=False): + """retrieves the recorded information about the repo(s) + specified by repo-id + + @type repos: list of strings or string + @param repos: ['repo-id1', ...] or 'repo-id' + @rtype list of tuples [(str, bool, bool),...] + @return: dictionary of dictionaries + {'ovl1': + {'name': str, + 'owner_name': str, + 'owner_email': str, + ' homepage': str, + 'description': str, + 'src_uris': list of str ['uri1',...] + 'src_type': str, + 'priority': int, + 'quality': str + 'status':, + 'official': bool, + 'supported': bool, + }, + 'ovl2': {...} + } + """ + + repos = self._check_repo_type(repos, "get_info") + result = {} + + if local: + db = self._get_installed_db() + else: + db = self._get_remote_db() + + for ovl in repos: + if not self.is_repo(ovl): + self._error(1, UNKNOWN_REPO_ID %ovl) + result[ovl] = ('', False, False) + try: + overlay = db.select(ovl) + except UnknownOverlayException, error: + self._error(2, "Error: %s" %str(error)) + result[ovl] = ('', False, False) + else: + result[ovl] = { + 'name': overlay.name, + 'owner_name': overlay.owner_name, + 'owner_email': overlay.owner_email, + 'homepage': overlay.homepage, + 'description': overlay.description, + #'src_uris': [e.src for e in overlay.sources], + 'src_uris': overlay.source_uris(), + 'src_types': overlay.source_types(), + #'src_types': [e.type for e in overlay.sources], + 'priority': overlay.priority, + 'quality': overlay.quality, + 'status': overlay.status, + 'official': overlay.is_official(), + 'supported': overlay.is_supported(), + } + + return result + + + def get_info_str(self, repos, local=True, verbose=False, width=0): + """retrieves the string representation of the recorded information + about the repo(s) specified by ovl + + @type repos: list of strings or string + @param repos: ['repo-id1', ...] or 'repo-id' + @rtype list of tuples [(str, bool, bool),...] + @return: dictionary {'repo-id': (info string, official, supported)} + """ + repos = self._check_repo_type(repos, "get_info") + result = {} + + if local: + db = self._get_installed_db() + else: + db = self._get_remote_db() + + for ovl in repos: + if not self.is_repo(ovl): + self._error(1, UNKNOWN_REPO_ID % ovl) + result[ovl] = ('', False, False) + try: + overlay = db.select(ovl) + #print "overlay = ", ovl + #print overlay + except UnknownOverlayException, error: + #print "ERRORS", str(error) + self._error(2, "Error: %s" %str(error)) + result[ovl] = ('', False, False) + else: + # Is the overlay supported? + if verbose: + info = overlay.__str__() + else: + info = overlay.short_list(width) + official = overlay.is_official() + supported = overlay.is_supported() + result[ovl] = (info, official, supported) + + return result + + def get_info_list(self, local=True, verbose=False, width=0): + """retrieves the string representation of the recorded information + about the repo(s) specified by ovl + + @param local: bool (defaults to True) + @param verbose: bool(defaults to False) + @param width: int (defaults to 0) + @rtype list of tuples [(str, bool, bool),...] + @return: list [(info string, official, supported),...] + """ + + if local: + return self._get_installed_db().list(verbose=verbose, width=width) + else: + return self._get_remote_db().list(verbose=verbose, width=width) + + + def sync(self, repos, output_results=True): + """syncs the specified repo(s) specified by repos + + @type repos: list of strings or string + @param repos: ['repo-id1', ...] or 'repo-id' + @rtype bool or {'repo-id': bool,...} + """ + fatals = [] + warnings = [] + success = [] + repos = self._check_repo_type(repos, "sync") + db = self._get_installed_db() + + for ovl in repos: + try: + odb = db.select(ovl) + except UnknownOverlayException, error: + self._error(1,"Sync(), failed to select %s overlay. Original error was: %s" %(ovl, str(error))) + continue + + try: + ordb = self._get_remote_db().select(ovl) + except UnknownOverlayException: + message = 'Overlay "%s" could not be found in the remote lists.\n' \ + 'Please check if it has been renamed and re-add if necessary.' % ovl + warnings.append((ovl, message)) + else: + current_src = odb.sources[0].src + available_srcs = set(e.src for e in ordb.sources) + if ordb and odb and not current_src in available_srcs: + if len(available_srcs) == 1: + plural = '' + candidates = ' %s' % tuple(available_srcs)[0] + else: + plural = 's' + candidates = '\n'.join((' %d. %s' % (ovl + 1, v)) for ovl, v in enumerate(available_srcs)) + + warnings.append((ovl, + 'The source of the overlay "%(repo_name)s" seems to have changed.\n' + 'You currently sync from\n' + '\n' + ' %(current_src)s\n' + '\n' + 'while the remote lists report\n' + '\n' + '%(candidates)s\n' + '\n' + 'as correct location%(plural)s.\n' + 'Please consider removing and re-adding the overlay.' , { + 'repo_name':ovl, + 'current_src':current_src, + 'candidates':candidates, + 'plural':plural, + })) + + try: + db.sync(ovl, self.config['quiet']) + success.append((ovl,'Successfully synchronized overlay "' + ovl + '".')) + except Exception, error: + fatals.append((ovl, + 'Failed to sync overlay "' + ovl + '".\nError was: ' + + str(error))) + + if output_results: + if success: + self.output.info('\nSucceeded:\n------\n', 3) + for ovl, result in success: + self.output.info(result, 3) + + if warnings: + self.output.warn('\nWarnings:\n------\n', 2) + for ovl, result in warnings: + self.output.warn(result + '\n', 2) + + if fatals: + self.output.error('\nErrors:\n------\n') + for ovl, result in fatals: + self.output.error(result + '\n') + return False + + self.sync_results = (success, warnings, fatals) + + return True + + + def fetch_remote_list(self): + """Fetches the latest remote overlay list""" + try: + self._get_remote_db().cache() + except Exception, error: + self._error(-1,'Failed to fetch overlay list!\n Original Error was: ' + + str(error)) + return False + self.get_available(reload=True) + return True + + + def get_available(self, reload=False): + """returns the list of available overlays""" + if self._available_ids is None or reload: + self._available_ids = self._get_remote_db(reload).list_ids() + return self._available_ids[:] or ['None'] + + + def get_installed(self, reload=False): + """returns the list of installed overlays""" + if self._installed_ids is None or reload: + self._installed_ids = self._get_installed_db(reload).list_ids() + return self._installed_ids[:] + + + def _get_installed_db(self, reload=False): + """returns the list of installed overlays""" + if not self._installed_db or reload: + self._installed_db = DB(self.config) + return self._installed_db + + + def _get_remote_db(self, reload=False): + """returns the list of installed overlays""" + if self._available_db is None or reload: + self._available_db = RemoteDB(self.config) + return self._available_db + + + def reload(self): + """reloads the installed and remote db's to the data on disk""" + result = self.get_available(reload=True) + result = self.get_installed(reload=True) + + + def _error(self, num, message): + """outputs the error to the pre-determined output + defaults to stderr. This method may be removed, is here for now + due to code taken from the packagekit backend. + """ + msg = "Error: %d," % num, message + self._error_messages.append(msg) + if self.report_errors: + print >>stderr, msg + + + def get_errors(self): + """returns any warning or fatal messages that occurred during + an operation and resets it back to None + + @rtype: list + @return: list of error strings + """ + if self._error_messages: + messages = self._error_messages[:] + self._error_messages = [] + return messages + + +def create_fd(): + """creates file descriptor pairs an opens them ready for + use in place of stdin, stdout, stderr. + """ + fd_r, fd_w = os.pipe() + write = os.fdopen(fd_w, 'w') + rread = os.fdopen(fd_r, 'r') + return (read, write, fd_r, fd_w) + + diff --git a/overlord/cli.py b/overlord/cli.py new file mode 100644 index 0000000..501dd65 --- /dev/null +++ b/overlord/cli.py @@ -0,0 +1,312 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +################################################################################# +# overlord ACTIONS +################################################################################# +# File: cli.py +# +# Handles overlord actions via the command line interface. +# +# Copyright: +# (c) 2010 - 2011 +# Gunnar Wrobel +# Brian Dolbec +# Distributed under the terms of the GNU General Public License v2 +# +# Author(s): +# Gunnar Wrobel <wrobel@gentoo.org> +# Brian Dolbec <brian.dolbec@gmail.com +# +''' Provides the command line actions that can be performed by overlord.''' + +__version__ = "$Id: cli.py 2011-01-15 23:52 PST Brian Dolbec$" + + +import os, sys + +from overlord.api import OverlordAPI +from overlord.utils import (decode_selection, encoder, get_encoding, + pad, terminal_width) +from overlord.constants import NOT_OFFICIAL_MSG, NOT_SUPPORTED_MSG + + + +class ListPrinter(object): + def __init__(self, config): + self.config = config + self.output = self.config['output'] + if not self.config['width']: + self.width = terminal_width() + else: + self.width = self.config['width'] + self.srclen = self.width - 43 + self._encoding_ = get_encoding(self.output) + if config['verbose']: + self.my_lister = self.short_list # self.long_list + else: + self.my_lister = self.short_list + + def print_shortdict(self, info, complain): + #print "ListPrinter.print_shortdict()",info, "\n\n" + overlays = sorted(info) + #print "ids =======>", overlays, "\n" + for ovl in overlays: + overlay = info[ovl] + #print "overlay =", overlay + summary, supported, official = overlay + self.print_overlay(summary, supported, official, complain) + + def print_shortlist(self, info, complain): + #print "ListPrinter.print_shortlist()",info + for summary, supported, official in info: + self.print_overlay(summary, supported, official, complain) + + + def print_fulldict(self, info, complain): + ids = sorted(info) + #print "ids =======>", ids, "\n" + for ovl in ids: + overlay = info[ovl] + #print overlay + self.print_overlay(self.my_lister(overlay), + overlay['supported'], + overlay['official'], + complain) + + + def print_overlay(self, summary, supported, official, complain): + # Is the overlay supported? + if supported: + # Is this an official overlay? + if official: + self.output.info(summary, 1) + # Unofficial overlays will only be listed if we are not + # checking or listing verbose + elif complain: + # Give a reason why this is marked yellow if it is a verbose + # listing + if self.config['verbose']: + self.output.warn(NOT_OFFICIAL_MSG, 1) + self.output.warn(summary, 1) + # Unsupported overlays will only be listed if we are not checking + # or listing verbose + elif complain: + # Give a reason why this is marked red if it is a verbose + # listing + if self.config['verbose']: + self.output.error(NOT_SUPPORTED_MSG) + self.output.error(summary) + + + def short_list(self, overlay): + ''' + >>> print short_list(overlay) + wrobel [Subversion] (https://o.g.o/svn/dev/wrobel ) + ''' + name = pad(overlay['name'], 25) + + if len(set(e for e in overlay['src_types'])) == 1: + _type = overlay['src_types'][0] + else: + _type = '%s/..' % overlay['src_type'][0] + mtype = ' [' + pad(_type, 10) + ']' + + source = ', '.join(overlay['src_uris']) + + if len(source) > self.srclen: + source = source.replace("overlays.gentoo.org", "o.g.o") + source = ' (' + pad(source, self.srclen) + ')' + + return encoder(name + mtype + source, self._encoding_) + + +class Main(object): + '''Performs the actions the user selected. + ''' + + def __init__(self, config): + self.config = config + #print "config.keys()", config.keys() + self.output = config['output'] + self.api = OverlordAPI(config, + report_errors=True, + output=config.output) + # Given in order of precedence + self.actions = [('fetch', 'Fetch'), + ('add', 'Add'), + ('sync', 'Sync'), + ('info', 'Info'), + ('sync_all', 'Sync'), + ('delete', 'Delete'), + ('list', 'ListRemote'), + ('list_local', 'ListLocal'),] + + def __call__(self): + # Make fetching the overlay list a default action + if not 'nofetch' in self.config.keys(): + # Actions that implicitely call the fetch operation before + fetch_actions = ['sync', 'sync_all', 'list'] + for i in fetch_actions: + if i in self.config.keys(): + # Implicitely call fetch, break loop + self.Fetch() + break + + result = 0 + + # Set the umask + umask = self.config['umask'] + try: + new_umask = int(umask, 8) + old_umask = os.umask(new_umask) + except Exception, error: + self.output.die('Failed setting to umask "' + umask + + '"!\nError was: ' + str(error)) + + for action in self.actions: + + self.output.debug('Checking for action', 7) + + if action[0] in self.config.keys(): + try: + result += getattr(self, action[1])() + except Exception, error: + self.output.error(self.api.get_errors()) + result = -1 # So it cannot remain 0, i.e. success + break + + # Reset umask + os.umask(old_umask) + + if not result: + sys.exit(0) + else: + sys.exit(1) + + + def Fetch(self): + ''' Fetches the overlay listing. + ''' + self.output.info("Fetching remote list,...", 2) + result = self.api.fetch_remote_list() + if result: + self.output.info('Ok', 2) + else: + errors = self.api.get_errors() + self.output.warn('Download failed.\nError was: ' + + str('\n'.join(errors)), 2) + return result + + + def Add(self): + ''' Adds the selected overlays. + ''' + selection = decode_selection(self.config['add']) + if 'ALL' in selection: + selection = self.api.get_available() + self.output.debug('Adding selected overlays', 6) + result = self.api.add_repos(selection) + if result: + self.output.info('Successfully added overlay(s) '+\ + ', '.join(selection) +'.', 2) + else: + errors = self.api.get_errors() + self.output.warn('Failed to add overlay(s).\nError was: ' + + str('\n'.join(errors)), 2) + return result + + + + def Sync(self): + ''' Syncs the selected overlays. + ''' + # Note api.sync() defaults to printing results + selection = decode_selection(self.config['sync']) + if self.config['sync_all'] or 'ALL' in selection: + selection = self.api.get_installed() + self.output.debug('Updating selected overlays', 6) + return self.api.sync(selection) + + + def Delete(self): + ''' Deletes the selected overlays. + ''' + selection = decode_selection(self.config['delete']) + if 'ALL' in selection: + selection = self.api.get_installed() + self.output.debug('Deleting selected overlays', 6) + result = self.api.delete_repos(selection) + if result: + self.output.info('Successfully deleted overlay(s) ' +\ + ', '.join(selection) + '.', 2) + else: + errors = self.api.get_errors() + self.output.warn('Failed to delete overlay(s).\nError was: ' + + str('\n'.join(errors)), 2) + return result + + + def Info(self): + ''' Print information about the specified overlays. + ''' + selection = decode_selection(self.config['info']) + if 'ALL' in selection: + selection = self.api.get_available() + + list_printer = ListPrinter(self.config) + _complain = self.config['nocheck'] or self.config['verbose'] + + info = self.api.get_info_str(selection, local=False, + verbose=True, width=list_printer.width) + #print "info =", info + list_printer.print_shortdict(info, complain=_complain) + + return info != {} + + + def ListRemote(self): + ''' Lists the available overlays. + ''' + + self.output.debug('Printing remote overlays.', 2) + list_printer = ListPrinter(self.config) + + _complain = self.config['nocheck'] or self.config['verbose'] + info = self.api.get_info_list(local=False, + verbose=self.config['verbose'], width=list_printer.width) + list_printer.print_shortlist(info, complain=_complain) + + return info != {} + + + def ListLocal(self): + ''' Lists the local overlays. + ''' + #print "ListLocal()" + self.output.debug('Printing installed overlays.', 2) + list_printer = ListPrinter(self.config) + + _complain = self.config['nocheck'] or self.config['verbose'] + # + # fast way + info = self.api.get_info_list(verbose=self.config['verbose'], + width=list_printer.width) + list_printer.print_shortlist(info, complain=_complain) + # + # slow way + #info = self.api.get_all_info(self.api.get_installed(), local=True) + #list_printer.print_fulldict(info, complain=_complain) + + return info != {} + + +if __name__ == '__main__': + import doctest + + # Ignore warnings here. We are just testing + from warnings import filterwarnings, resetwarnings + filterwarnings('ignore') + + doctest.testmod(sys.modules[__name__]) + + resetwarnings() diff --git a/overlord/config.py b/overlord/config.py new file mode 100644 index 0000000..4caf83c --- /dev/null +++ b/overlord/config.py @@ -0,0 +1,441 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +################################################################################# +# overlord CONFIGURATION +################################################################################# +# File: config.py +# +# Handles overlord configuration +# +# Copyright: +# (c) 2005 - 2009 Gunnar Wrobel +# (c) 2009 Sebastian Pipping +# Distributed under the terms of the GNU General Public License v2 +# +# Author(s): +# Gunnar Wrobel <wrobel@gentoo.org> +# Sebastian Pipping <sebastian@pipping.org> +# Brian Dolbec <brian.dolbec@gmail.com> +# +'''Defines the configuration options and provides parsing functionality.''' + +__version__ = "$Id: config.py 286 2007-01-09 17:48:23Z wrobel $" + +#=============================================================================== +# +# Dependencies +# +#------------------------------------------------------------------------------- + +import sys, ConfigParser + +from optparse import OptionParser, OptionGroup +from overlord.debug import OUT +from overlord.version import VERSION + +#=============================================================================== +# +# Class Config +# +#------------------------------------------------------------------------------- + +_USAGE = """ + overlord (-a|-d|-s|-i) (OVERLAY|ALL) + overlord -f [-o URL] + overlord (-l|-L|-S)""" + +class BareConfig(object): + '''Handles the configuration only.''' + + def __init__(self, output=None, stdout=None, stdin=None, stderr=None): + ''' + Creates a bare config with defaults and a few output options. + + >>> a = BareConfig() + >>> a['overlays'] + '\\nhttp://www.gentoo.org/proj/en/overlays/repositories.xml' + >>> sorted(a.keys()) + ['bzr_command', 'cache', 'config', 'cvs_command', 'darcs_command', + 'git_command', 'local_list', 'make_conf', 'mercurial_command', + 'nocheck', 'overlays', 'proxy', 'quietness', 'rsync_command', 'storage', + 'svn_command', 'tar_command', 'umask', 'width'] + ''' + self._defaults = {'config' : '/etc/overlord/overlord.cfg', + 'storage' : '/var/lib/layman', + 'cache' : '%(storage)s/cache', + 'local_list': '%(storage)s/overlays.xml', + 'make_conf' : '%(storage)s/make.conf', + 'nocheck' : 'yes', + 'proxy' : '', + 'umask' : '0022', + 'overlays' : + 'http://www.gentoo.org/proj/en/overlays/repositories.xml', + 'bzr_command': '/usr/bin/bzr', + 'cvs_command': '/usr/bin/cvs', + 'darcs_command': '/usr/bin/darcs', + 'git_command': '/usr/bin/git', + 'g-common_command': '/usr/bin/g-common', + 'mercurial_command': '/usr/bin/hg', + 'rsync_command': '/usr/bin/rsync', + 'svn_command': '/usr/bin/svn', + 'tar_command': '/bin/tar' + } + self._options = { + 'stdout': stdout if stdout else sys.stdout, + 'stdin': stdin if stdin else sys.stdin, + 'stderr': stderr if stderr else sys.stderr, + 'output': output if output else OUT, + 'quietness': '4', + 'width': 0, + 'verbose': False, + 'quiet': False, + } + + + def keys(self): + '''Special handler for the configuration keys. + ''' + self._options['output'].debug('Retrieving BareConfig options', 8) + keys = [i for i in self._options] + self._options['output'].debug('Retrieving BareConfig defaults', 8) + keys += [i for i in self._defaults + if not i in keys] + self._options['output'].debug('Retrieving BareConfig done...', 8) + return keys + + + def get_defaults(self): + """returns our defaults dictionary""" + return self._defaults + + + def get_option(self, key): + """returns the current option's value""" + return self.__getitem__(key) + + + def set_option(self, option, value): + """Sets an option to the value """ + self._options[option] = value + + + def __getitem__(self, key): + self._options['output'].debug('Retrieving BareConfig option', 8) + if (key in self._options + and not self._options[key] is None): + return self._options[key] + self._options['output'].debug('Retrieving BareConfig default', 8) + if key in self._defaults: + if '%(storage)s' in self._defaults[key]: + return self._defaults[key] %{'storage': self._defaults['storage']} + return self._defaults[key] + return None + + + + +class ArgsParser(object): + '''Handles the configuration and option parser.''' + + def __init__(self, args=None, output=None, + stdout=None, stdin=None, stderr=None): + ''' + Creates and describes all possible polymeraZe options and creates + a debugging object. + + >>> import os.path + >>> here = os.path.dirname(os.path.realpath(__file__)) + >>> sys.argv.append('--config') + >>> sys.argv.append(here + '/../etc/overlord.cfg') + >>> a = ArgsParser() + >>> a['overlays'] + '\\nhttp://www.gentoo.org/proj/en/overlays/repositories.xml' + >>> sorted(a.keys()) + ['bzr_command', 'cache', 'config', 'cvs_command', 'darcs_command', + 'git_command', 'local_list', 'make_conf', 'mercurial_command', + 'nocheck', 'overlays', 'proxy', 'quietness', 'rsync_command', 'storage', + 'svn_command', 'tar_command', 'umask', 'width'] + ''' + if args == None: + args = sys.argv + + self.stdout = stdout if stdout else sys.stdout + self.stderr = stderr if stderr else sys.stderr + self.stdin = stdin if stdin else sys.stdin + self.output = output if output else OUT + + self.bare_config = BareConfig(self.output, self.stdout, + self.stdin, self.stderr) + self.defaults = self.bare_config.get_defaults() + #print self.defaults + + self.parser = OptionParser( + usage = _USAGE, + version = VERSION) + + #----------------------------------------------------------------- + # Main Options + + group = OptionGroup(self.parser, + '<Actions>') + + group.add_option('-a', + '--add', + action = 'append', + help = 'Add the given overlay from the cached remote li' + 'st to your locally installed overlays.. Specify "ALL" ' + 'to add all overlays from the remote list.') + + group.add_option('-d', + '--delete', + action = 'append', + help = 'Remove the given overlay from your locally inst' + 'alled overlays. Specify "ALL" to remove all overlays') + + group.add_option('-s', + '--sync', + action = 'append', + help = 'Update the specified overlay. Use "ALL" as para' + 'meter to synchronize all overlays') + + group.add_option('-i', + '--info', + action = 'append', + help = 'Display information about the specified overlay' + '.') + + group.add_option('-S', + '--sync-all', + action = 'store_true', + help = 'Update all overlays.') + + group.add_option('-L', + '--list', + action = 'store_true', + help = 'List the contents of the remote list.') + + group.add_option('-l', + '--list-local', + action = 'store_true', + help = 'List the locally installed overlays.') + + group.add_option('-f', + '--fetch', + action = 'store_true', + help = 'Fetch a remote list of overlays. This option is' + ' deprecated. The fetch operation will be performed by ' + 'default when you run sync, sync-all, or list.') + + group.add_option('-n', + '--nofetch', + action = 'store_true', + help = 'Do not fetch a remote list of overlays.') + + group.add_option('-p', + '--priority', + action = 'store', + help = 'Use this with the --add switch to set the prior' + 'ity of the added overlay. This will influence the sort' + 'ing order of the overlays in the PORTDIR_OVERLAY varia' + 'ble.') + + self.parser.add_option_group(group) + + #----------------------------------------------------------------- + # Additional Options + + group = OptionGroup(self.parser, + '<Path options>') + + group.add_option('-c', + '--config', + action = 'store', + help = 'Path to the config file [default: ' \ + + self.defaults['config'] + '].') + + group.add_option('-o', + '--overlays', + action = 'append', + help = 'The list of overlays [default: ' \ + + self.defaults['overlays'] + '].') + + self.parser.add_option_group(group) + + #----------------------------------------------------------------- + # Output Options + + group = OptionGroup(self.parser, + '<Output options>') + + group.add_option('-v', + '--verbose', + action = 'store_true', + help = 'Increase the amount of output and describe the ' + 'overlays.') + + group.add_option('-q', + '--quiet', + action = 'store_true', + help = 'Yield no output. Please be careful with this op' + 'tion: If the processes spawned by overlord when adding o' + 'r synchronizing overlays require any input overlord will' + ' hang without telling you why. This might happen for e' + 'xample if your overlay resides in subversion and the S' + 'SL certificate of the server needs acceptance.') + + group.add_option('-N', + '--nocolor', + action = 'store_true', + help = 'Remove color codes from the overlord output.') + + group.add_option('-Q', + '--quietness', + action = 'store', + type = 'int', + default = '4', + help = 'Set the level of output (0-4). Default: 4. Once' + ' you set this below 2 the same warning as given for --' + 'quiet applies! ') + + group.add_option('-W', + '--width', + action = 'store', + type = 'int', + default = '0', + help = 'Sets the screen width. This setting is usually ' + 'not required as overlord is capable of detecting the ' + 'available number of columns automatically.') + + group.add_option('-k', + '--nocheck', + action = 'store_true', + help = 'Do not check overlay definitions and do not i' + 'ssue a warning if description or contact information' + ' are missing.') + + self.parser.add_option_group(group) + + #----------------------------------------------------------------- + # Debug Options + + self.output.cli_opts(self.parser) + + # Parse the command line first since we need to get the config + # file option. + if len(args) == 1: + print 'Usage:%s' % _USAGE + sys.exit(0) + + (self.options, remain_args) = self.parser.parse_args(args) + # remain_args starts with something like "bin/overlord" ... + if len(remain_args) > 1: + self.parser.error("Unhandled parameters: %s" + % ', '.join(('"%s"' % e) for e in remain_args[1:])) + + # handle debugging + self.output.cli_handle(self.options) + + # add output to the options + self.options.__dict__['output'] = self.output + + # add the std in/out/err to the options + self.options.__dict__['stdout'] = self.stdout + self.options.__dict__['stdin'] = self.stdin + self.options.__dict__['stderr'] = self.stderr + + + if self.options.__dict__['nocolor']: + self.output.color_off() + + # Fetch only an alternate config setting from the options + if not self.options.__dict__['config'] is None: + self.defaults['config'] = self.options.__dict__['config'] + + self.output.debug('Got config file at ' + self.defaults['config'], 8) + + # Now parse the config file + self.config = ConfigParser.ConfigParser(self.defaults) + self.config.add_section('MAIN') + + # handle quietness + if self['quiet']: + self.output.set_info_level(1) + self.output.set_warn_level(1) + self.defaults['quietness'] = 0 + elif 'quietness' in self.keys(): + self.output.set_info_level(int(self['quietness'])) + self.output.set_warn_level(int(self['quietness'])) + + self.output.debug('Reading config file at ' + self.defaults['config'], 8) + + self.config.read(self.defaults['config']) + + def __getitem__(self, key): + + if key == 'overlays': + overlays = '' + if (key in self.options.__dict__.keys() + and not self.options.__dict__[key] is None): + overlays = '\n'.join(self.options.__dict__[key]) + if self.config.has_option('MAIN', 'overlays'): + overlays += '\n' + self.config.get('MAIN', 'overlays') + if overlays: + return overlays + + self.output.debug('Retrieving option', 8) + + if (key in self.options.__dict__.keys() + and not self.options.__dict__[key] is None): + return self.options.__dict__[key] + + self.output.debug('Retrieving option', 8) + + if self.config.has_option('MAIN', key): + if key == 'nocheck': + if self.config.get('MAIN', key).lower() == 'yes': + return True + else: + return False + return self.config.get('MAIN', key) + + self.output.debug('Retrieving option', 8) + + if key in self.defaults.keys(): + return self.defaults[key] + + self.output.debug('Retrieving option', 8) + + return None + + def keys(self): + '''Special handler for the configuration keys.''' + + self.output.debug('Retrieving keys', 8) + + keys = [i for i in self.options.__dict__.keys() + if not self.options.__dict__[i] is None] + + self.output.debug('Retrieving keys', 8) + + keys += [name for name, _ in self.config.items('MAIN') + if not name in keys] + + self.output.debug('Retrieving keys', 8) + + keys += [i for i in self.defaults.keys() + if not i in keys] + + self.output.debug('Retrieving keys', 8) + + return keys + + +#=============================================================================== +# +# Testing +# +#------------------------------------------------------------------------------- + +if __name__ == '__main__': + import doctest + doctest.testmod(sys.modules[__name__]) diff --git a/overlord/constants.py b/overlord/constants.py new file mode 100644 index 0000000..f89f286 --- /dev/null +++ b/overlord/constants.py @@ -0,0 +1,45 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +################################################################################# +# overlord CONSTANTS +################################################################################# +# File: constants.py +# +# Handles overlord actions via the command line interface. +# +# Copyright: +# (c) 2010 - 2011 +# Gunnar Wrobel +# Brian Dolbec +# Distributed under the terms of the GNU General Public License v2 +# +# Author(s): +# Gunnar Wrobel <wrobel@gentoo.org> +# Brian Dolbec <brian.dolbec@gmail.com +# +''' Provides the command line actions that can be performed by overlord.''' + +__version__ = "$Id: constants.py 2011-01-16 23:52 PST Brian Dolbec$" + + + + +################################################################################# +## +## Color codes (taken from portage) +## +################################################################################# + +esc_seq = '\x1b[' + +codes = {} +codes['reset'] = esc_seq + '39;49;00m' +codes['red'] = esc_seq + '31;01m' +codes['green'] = esc_seq + '32;01m' +codes['yellow'] = esc_seq + '33;01m' +codes['turquoise'] = esc_seq + '36;01m' + + +NOT_OFFICIAL_MSG = '*** This is not an official gentoo overlay ***\n' +NOT_SUPPORTED_MSG = '*** You are lacking the necessary tools' +\ + ' to install this overlay ***\n' diff --git a/overlord/db.py b/overlord/db.py new file mode 100644 index 0000000..d54efa5 --- /dev/null +++ b/overlord/db.py @@ -0,0 +1,353 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +################################################################################# +# overlord OVERLAY DB +################################################################################# +# File: db.py +# +# Access to the db of overlays +# +# Copyright: +# (c) 2005 - 2008 Gunnar Wrobel +# Distributed under the terms of the GNU General Public License v2 +# +# Author(s): +# Gunnar Wrobel <wrobel@gentoo.org> +# +'''Handles different storage files.''' + +__version__ = "$Id: db.py 309 2007-04-09 16:23:38Z wrobel $" + +#=============================================================================== +# +# Dependencies +# +#------------------------------------------------------------------------------- + +import os, os.path, urllib2, hashlib + +from overlord.utils import path, delete_empty_directory +from overlord.dbbase import DbBase +from overlord.makeconf import MakeConf + +#from overlord.debug import OUT + +#=============================================================================== +# +# Class DB +# +#------------------------------------------------------------------------------- + +class DB(DbBase): + ''' Handle the list of local overlays.''' + + def __init__(self, config): + + self.config = config + self.output = config['output'] + + self.path = config['local_list'] + + if config['nocheck']: + ignore = 2 + else: + ignore = 1 + + quiet = int(config['quietness']) < 3 + + DbBase.__init__(self, + [config['local_list'], ], + config, + ignore, + quiet) + + self.output.debug('DB handler initiated', 6) + + # overrider + def _broken_catalog_hint(self): + return '' + + def add(self, overlay, quiet = False): + ''' + Add an overlay to the local list of overlays. + + >>> write = os.tmpnam() + >>> write2 = os.tmpnam() + >>> write3 = os.tmpnam() + >>> here = os.path.dirname(os.path.realpath(__file__)) + >>> config = {'local_list' : + ... here + '/tests/testfiles/global-overlays.xml', + ... 'make_conf' : write2, + ... 'nocheck' : True, + ... 'storage' : write3, + ... 'quietness':3} + + >>> here = os.path.dirname(os.path.realpath(__file__)) + >>> a = DB(config) + >>> config['local_list'] = write + >>> b = DB(config) + >>> OUT.color_off() + + >>> m = MakeConf(config, b.overlays) + >>> m.path = write2 + >>> m.write() + + Commented out since it needs network access: + + # >>> b.add(a.select('wrobel-stable')) #doctest: +ELLIPSIS + # * Running command "/usr/bin/rsync -rlptDvz --progress --delete --delete-after --timeout=180 --exclude="distfiles/*" --exclude="local/*" --exclude="packages/*" "rsync://gunnarwrobel.de/wrobel-stable/*" "/tmp/file.../wrobel-stable""... + # >>> c = DbBase([write, ], dict()) + # >>> c.overlays.keys() + # [u'wrobel-stable'] + + # >>> m = MakeConf(config, b.overlays) + # >>> [i.name for i in m.overlays] #doctest: +ELLIPSIS + # [u'wrobel-stable'] + + + # >>> os.unlink(write) + >>> os.unlink(write2) + >>> import shutil + + # >>> shutil.rmtree(write3) + ''' + + if overlay.name not in self.overlays.keys(): + result = overlay.add(self.config['storage'], quiet) + if result == 0: + if 'priority' in self.config.keys(): + overlay.set_priority(self.config['priority']) + self.overlays[overlay.name] = overlay + self.write(self.path) + make_conf = MakeConf(self.config, self.overlays) + make_conf.add(overlay) + else: + mdir = path([self.config['storage'], overlay.name]) + delete_empty_directory(mdir, self.output) + if os.path.exists(mdir): + raise Exception('Adding overlay "%s" failed!' + ' Possible remains of the operation have NOT' + ' been removed and may be left at "%s".' + ' Please remove them manually if required.' \ + % (overlay.name, mdir)) + else: + raise Exception('Adding overlay "%s" failed!' % overlay.name) + else: + raise Exception('Overlay "' + overlay.name + '" already in the loca' + 'l list!') + + def delete(self, overlay): + ''' + Add an overlay to the local list of overlays. + + >>> write = os.tmpnam() + >>> write2 = os.tmpnam() + >>> write3 = os.tmpnam() + >>> here = os.path.dirname(os.path.realpath(__file__)) + >>> config = {'local_list' : + ... here + '/tests/testfiles/global-overlays.xml', + ... 'make_conf' : write2, + ... 'nocheck' : True, + ... 'storage' : write3, + ... 'quietness':3} + + >>> here = os.path.dirname(os.path.realpath(__file__)) + >>> a = DB(config) + >>> config['local_list'] = write + >>> b = DB(config) + >>> .color_off() + + >>> m = MakeConf(config, b.overlays) + >>> m.path = here + '/tests/testfiles/make.conf' + >>> m.read() + + >>> m.path = write2 + >>> m.write() + + # >>> b.add(a.select('wrobel-stable')) #doctest: +ELLIPSIS + # * Running command "/usr/bin/rsync -rlptDvz --progress --delete --delete-after --timeout=180 --exclude="distfiles/*" --exclude="local/*" --exclude="packages/*" "rsync://gunnarwrobel.de/wrobel-stable/*" "/tmp/file.../wrobel-stable""... + # >>> b.add(a.select('wrobel')) #doctest: +ELLIPSIS + # * Running command "/usr/bin/svn co "https://overlays.gentoo.org/svn/dev/wrobel/" "/tmp/file.../wrobel""... + # >>> c = DbBase([write, ], dict()) + # >>> c.overlays.keys() + # [u'wrobel', u'wrobel-stable'] + + # >>> b.delete(b.select('wrobel')) + # >>> c = DbBase([write, ], dict()) + # >>> c.overlays.keys() + # [u'wrobel-stable'] + + # >>> m = MakeConf(config, b.overlays) + # >>> [i.name for i in m.overlays] #doctest: +ELLIPSIS + # [u'wrobel-stable'] + + # >>> os.unlink(write) + >>> os.unlink(write2) + >>> import shutil + + # >>> shutil.rmtree(write3) + ''' + + if overlay.name in self.overlays.keys(): + make_conf = MakeConf(self.config, self.overlays) + overlay.delete(self.config['storage']) + del self.overlays[overlay.name] + self.write(self.path) + make_conf.delete(overlay) + else: + raise Exception('No local overlay named "' + overlay.name + '"!') + + def sync(self, overlay_name, quiet = False): + '''Synchronize the given overlay.''' + + overlay = self.select(overlay_name) + result = overlay.sync(self.config['storage'], quiet) + if result: + raise Exception('Syncing overlay "' + overlay_name + + '" returned status ' + str(result) + '!') + +#=============================================================================== +# +# Class RemoteDB +# +#------------------------------------------------------------------------------- + +class RemoteDB(DbBase): + '''Handles fetching the remote overlay list.''' + + def __init__(self, config, ignore_init_read_errors=False): + + self.config = config + self.output = config['output'] + + self.proxies = {} + + if config['proxy']: + self.proxies['http'] = config['proxy'] + elif os.getenv('http_proxy'): + self.proxies['http'] = os.getenv('http_proxy') + + if self.proxies: + proxy_handler = urllib2.ProxyHandler(self.proxies) + opener = urllib2.build_opener(proxy_handler) + urllib2.install_opener(opener) + + self.urls = [i.strip() for i in config['overlays'].split('\n') if i] + + paths = [self.path(i) for i in self.urls] + + if config['nocheck']: + ignore = 2 + else: + ignore = 0 + + quiet = int(config['quietness']) < 3 + + DbBase.__init__(self, paths, config, ignore, quiet, ignore_init_read_errors) + + # overrider + def _broken_catalog_hint(self): + return 'Try running "sudo overlord -f" to re-fetch that file' + + def cache(self): + ''' + Copy the remote overlay list to the local cache. + + >>> here = os.path.dirname(os.path.realpath(__file__)) + >>> cache = os.tmpnam() + >>> config = {'overlays' : + ... 'file://' + here + '/tests/testfiles/global-overlays.xml', + ... 'cache' : cache, + ... 'nocheck' : True, + ... 'proxy' : None, + ... 'quietness':3} + >>> a = RemoteDB(config) + >>> a.cache() + >>> b = open(a.path(config['overlays'])) + >>> b.readlines()[24] + ' A collection of ebuilds from Gunnar Wrobel [wrobel@gentoo.org].\\n' + + >>> b.close() + >>> os.unlink(a.path(config['overlays'])) + + >>> a.overlays.keys() + [u'wrobel', u'wrobel-stable'] + ''' + for url in self.urls: + + mpath = self.path(url) + + # Check for sufficient privileges + if os.path.exists(mpath) and not os.access(mpath, os.W_OK): + self.output.warn('You do not have permission to update the cache (%s).' % mpath) + import getpass + if getpass.getuser() != 'root': + self.output.warn('Hint: You are not root.\n') + continue + + try: + + # Fetch the remote list + olist = urllib2.urlopen(url).read() + + # Create our storage directory if it is missing + if not os.path.exists(os.path.dirname(mpath)): + try: + os.makedirs(os.path.dirname(mpath)) + except OSError, error: + raise OSError('Failed to create overlord storage direct' + + 'ory ' + os.path.dirname(mpath) + '\n' + + 'Error was:' + str(error)) + + # Before we overwrite the old cache, check that the downloaded + # file is intact and can be parsed + try: + self.read(olist, origin=url) + except Exception, error: + raise IOError('Failed to parse the overlays list fetched fr' + 'om ' + url + '\nThis means that the download' + 'ed file is somehow corrupt or there was a pr' + 'oblem with the webserver. Check the content ' + 'of the file. Error was:\n' + str(error)) + + # Ok, now we can overwrite the old cache + try: + out_file = open(mpath, 'w') + out_file.write(olist) + out_file.close() + + except Exception, error: + raise IOError('Failed to temporarily cache overlays list in' + ' ' + mpath + '\nError was:\n' + str(error)) + + + except IOError, error: + self.output.warn('Failed to update the overlay list from: ' + + url + '\nError was:\n' + str(error)) + + def path(self, url): + '''Return a unique file name for the url.''' + + base = self.config['cache'] + + self.output.debug('Generating cache path.', 6) + + return base + '_' + hashlib.md5(url).hexdigest() + '.xml' + + +#=============================================================================== +# +# Testing +# +#------------------------------------------------------------------------------- + +if __name__ == '__main__': + import doctest, sys + + # Ignore warnings here. We are just testing + from warnings import filterwarnings, resetwarnings + filterwarnings('ignore') + + doctest.testmod(sys.modules[__name__]) + + resetwarnings() diff --git a/overlord/dbbase.py b/overlord/dbbase.py new file mode 100644 index 0000000..7821aea --- /dev/null +++ b/overlord/dbbase.py @@ -0,0 +1,276 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +################################################################################# +# overlord OVERLAY HANDLER +################################################################################# +# File: overlay.py +# +# Access to an xml list of overlays +# +# Copyright: +# (c) 2005 - 2009 Gunnar Wrobel +# (c) 2009 Sebastian Pipping +# (c) 2009 Christian Groschupp +# Distributed under the terms of the GNU General Public License v2 +# +# Author(s): +# Gunnar Wrobel <wrobel@gentoo.org> +# Sebastian Pipping <sebastian@pipping.org> +# Christian Groschupp <christian@groschupp.org> +# +'''Main handler for overlays.''' + +__version__ = "$Id: overlay.py 273 2006-12-30 15:54:50Z wrobel $" + +#=============================================================================== +# +# Dependencies +# +#------------------------------------------------------------------------------- + +import sys, os, os.path +import xml +import xml.etree.ElementTree as ET # Python 2.5 + +#from overlord.debug import OUT +from overlord.utils import indent +from overlord.overlays.overlay import Overlay + +#=============================================================================== +# +# Class UnknownOverlayException +# +#------------------------------------------------------------------------------- + +class UnknownOverlayException(Exception): + def __init__(self, repo_name): + message = 'Overlay "%s" does not exist.' % repo_name + super(UnknownOverlayException, self).__init__(message) + +#=============================================================================== +# +# Class BrokenOverlayCatalog +# +#------------------------------------------------------------------------------- + +class BrokenOverlayCatalog(ValueError): + def __init__(self, origin, expat_error, hint=None): + if hint == None: + hint = '' + else: + hint = '\nHint: %s' % hint + + super(BrokenOverlayCatalog, self).__init__( + 'XML parsing failed for "%(origin)s" (line %(line)d, column %(column)d)%(hint)s' % \ + {'line':expat_error.lineno, 'column':expat_error.offset + 1, 'origin':origin, 'hint':hint}) + + +#=============================================================================== +# +# Class DbBase +# +#------------------------------------------------------------------------------- + +class DbBase: + ''' Handle a list of overlays.''' + + def __init__(self, paths, config, ignore = 0, quiet = False, ignore_init_read_errors=False): + + self.config = config + self.quiet = quiet + self.paths = paths + self.ignore = ignore + self.output = config['output'] + + self.overlays = {} + + self.output.debug('Initializing overlay list handler', 8) + + for path in self.paths: + if not os.path.exists(path): + continue + + try: + self.read_file(path) + except Exception, error: + if not ignore_init_read_errors: raise error + + def __eq__(self, other): + for key in set(self.overlays.keys() + other.overlays.keys()): + if self.overlays[key] != other.overlays[key]: + return False + return True + + def __ne__(self, other): + return not self.__eq__(other) + + def read_file(self, path): + '''Read the overlay definition file.''' + + try: + document = open(path, 'r').read() + + except Exception, error: + raise IOError('Failed to read the overlay list at ("' + + path + '")!\nError was:\n' + str(error)) + + self.read(document, origin=path) + + def _broken_catalog_hint(self): + this_function_name = sys._getframe().f_code.co_name + raise NotImplementedError('Method "%s.%s" not implemented' % \ + (self.__class__.__name__, this_function_name)) + + def read(self, text, origin): + ''' + Read an xml list of overlays (adding to and potentially overwriting existing entries) + + >>> here = os.path.dirname(os.path.realpath(__file__)) + >>> config = {'svn_command': '/usr/bin/svn', 'rsync_command':'/usr/bin/rsync'} + >>> a = DbBase([here + '/tests/testfiles/global-overlays.xml', ], config) + >>> a.overlays.keys() + [u'wrobel', u'wrobel-stable'] + + >>> list(a.overlays['wrobel-stable'].source_uris()) + [u'rsync://gunnarwrobel.de/wrobel-stable'] + ''' + try: + document = ET.fromstring(text) + except xml.parsers.expat.ExpatError, error: + raise BrokenOverlayCatalog(origin, error, self._broken_catalog_hint()) + + overlays = document.findall('overlay') + \ + document.findall('repo') + + for overlay in overlays: + self.output.debug('Parsing overlay entry', 8) + try: + ovl = Overlay(overlay, self.config, self.ignore, self.quiet) + except Exception, error: + self.output.warn(str(error), 3) + else: + self.overlays[ovl.name] = ovl + + + def write(self, path): + ''' + Write the list of overlays to a file. + + >>> write = os.tmpnam() + >>> here = os.path.dirname(os.path.realpath(__file__)) + >>> config = {'svn_command': '/usr/bin/svn', 'rsync_command':'/usr/bin/rsync'} + >>> a = DbBase([here + '/tests/testfiles/global-overlays.xml', ], config) + >>> b = DbBase([write,], dict()) + >>> b.overlays['wrobel-stable'] = a.overlays['wrobel-stable'] + >>> b.write(write) + >>> c = DbBase([write,], dict()) + >>> c.overlays.keys() + [u'wrobel-stable'] + + >>> os.unlink(write) + ''' + + tree = ET.Element('repositories', version="1.0") + tree[:] = [e.to_xml() for e in self.overlays.values()] + indent(tree) + tree = ET.ElementTree(tree) + try: + f = open(path, 'w') + f.write("""\ +<?xml version="1.0" encoding="UTF-8"?> +""") + tree.write(f, encoding='utf-8') + f.close() + except Exception, error: + raise Exception('Failed to write to local overlays file: ' + + path + '\nError was:\n' + str(error)) + + def select(self, overlay): + ''' + Select an overlay from the list. + + >>> here = os.path.dirname(os.path.realpath(__file__)) + >>> config = {'svn_command': '/usr/bin/svn', 'rsync_command':'/usr/bin/rsync'} + >>> a = DbBase([here + '/tests/testfiles/global-overlays.xml', ], config) + >>> list(a.select('wrobel-stable').source_uris()) + [u'rsync://gunnarwrobel.de/wrobel-stable'] + ''' + if not overlay in self.overlays.keys(): + raise UnknownOverlayException(overlay) + return self.overlays[overlay] + + def list(self, repos=None, verbose = False, width = 0): + ''' + List all overlays. + + >>> here = os.path.dirname(os.path.realpath(__file__)) + >>> config = {'svn_command': '/usr/bin/svn', 'rsync_command':'/usr/bin/rsync'} + >>> a = DbBase([here + '/tests/testfiles/global-overlays.xml', ], config) + >>> for i in a.list(True): + ... print i[0] + wrobel + ~~~~~~ + Source : https://overlays.gentoo.org/svn/dev/wrobel + Contact : nobody@gentoo.org + Type : Subversion; Priority: 10 + Quality : experimental + <BLANKLINE> + Description: + Test + <BLANKLINE> + wrobel-stable + ~~~~~~~~~~~~~ + Source : rsync://gunnarwrobel.de/wrobel-stable + Contact : nobody@gentoo.org + Type : Rsync; Priority: 50 + Quality : experimental + <BLANKLINE> + Description: + A collection of ebuilds from Gunnar Wrobel [wrobel@gentoo.org]. + <BLANKLINE> + + >>> for i in a.list(False, 80): + ... print i[0] + wrobel [Subversion] (https://o.g.o/svn/dev/wrobel ) + wrobel-stable [Rsync ] (rsync://gunnarwrobel.de/wrobel-stable) + ''' + result = [] + + selection = [overlay for (a, overlay) in self.overlays.items()] + if repos: + selection = [overlay for overlay in selection if overlay.name in repos] + + for overlay in selection: + if verbose: + result.append((str(overlay), overlay.is_supported(), + overlay.is_official())) + else: + result.append((overlay.short_list(width), overlay.is_supported(), + overlay.is_official())) + + result = sorted(result) + + return result + + def list_ids(self): + """returns a list of the overlay names + """ + return sorted(self.overlays) + + +#=============================================================================== +# +# Testing +# +#------------------------------------------------------------------------------- + +if __name__ == '__main__': + import doctest + + # Ignore warnings here. We are just testing + from warnings import filterwarnings, resetwarnings + filterwarnings('ignore') + + doctest.testmod(sys.modules[__name__]) + + resetwarnings() diff --git a/overlord/debug.py b/overlord/debug.py new file mode 100644 index 0000000..eca6266 --- /dev/null +++ b/overlord/debug.py @@ -0,0 +1,515 @@ +# -*- coding: utf-8 -*- +################################################################################# +# overlord - DEBUGGING FUNCTIONS +################################################################################# +# debug.py -- Utility function for debugging +# Copyright 2005 - 2008 Gunnar Wrobel +# Distributed under the terms of the GNU General Public License v2 + +__version__ = "$Id: debug.py 153 2006-06-05 06:03:16Z wrobel $" + +################################################################################# +## +## Dependancies +## +################################################################################# + +import sys, inspect, types + +from optparse import OptionGroup + +from overlord.constants import codes + + +################################################################################# +## +## Message Class +## +################################################################################# + +class Message: + #FIXME: Think about some simple doctests before you modify this class the + # next time. + + def __init__(self, module = '', + out = sys.stdout, + err = sys.stderr, + dbg = sys.stderr, + debugging_level = 4, + debugging_verbosity = 2, + info_level = 4, + warn_level = 4, + col = True, + mth = None, + obj = None, + var = None): + + if mth == None: mth = ['*'] + if obj == None: obj = ['*'] + if var == None: var = ['*'] + + # A description of the module that is being debugged + self.debug_env = module + + # Where should the debugging output go? This can also be a file + self.debug_out = dbg + + # Where should the error output go? This can also be a file + self.error_out = err + + # Where should the normal output go? This can also be a file + self.std_out = out + + # The higher the level the more information you will get + self.warn_lev = warn_level + + # The higher the level the more information you will get + self.info_lev = info_level + + # The highest level of debugging messages acceptable for output + # The higher the level the more output you will get + self.debug_lev = debugging_level + + # The debugging output can range from very verbose (3) to + # very compressed (1) + self.debug_vrb = debugging_verbosity + + # Which methods should actually be debugged? + # Use '*' to indicate 'All methods' + self.debug_mth = mth + + # Which objects should actually be debugged? + # Use '*' to indicate 'All objects' + self.debug_obj = obj + + # Which variables should actually be debugged? + # Use '*' to indicate 'All variables' + self.debug_var = var + + # Exclude class variables by default + self.show_class_variables = False + + # Should the output be colored? + self.use_color = col + + self.has_error = False + + + ############################################################################ + # Add command line options + + def cli_opts(self, parser): + + #print "Parsing debug opts" + + group = OptionGroup(parser, + '<Debugging options>', + 'Control the debugging features of ' + + self.debug_env) + + group.add_option('--debug', + action = 'store_true', + help = 'Activates debugging features.') + + group.add_option('--debug-level', + action = 'store', + type = 'int', + help = 'A value between 0 and 10. 0 means no debugging ' + 'messages will be selected, 10 selects all debugging me' + 'ssages. Default is "4".') + + group.add_option('--debug-verbose', + action = 'store', + type = 'int', + help = 'A value between 1 and 3. Lower values yield les' + 's verbose debugging output. Default is "2".') + + group.add_option('--debug-methods', + action = 'store', + help = 'Limits the methods that will return debugging o' + 'utput. The function name is sufficient and there is no' + 'difference between class methods or general functions.' + ' Several methods can be specified by seperating them w' + ' with a comma. Default is "*" which specifies all meth' + 'ods.') + + group.add_option('--debug-classes', + action = 'store', + help = 'Limits the classes that will return debugging o' + 'utput. Specify only the class name not including the m' + 'odules in which the class is defined (e.g. MyModule.ma' + 'in.Main should only be represented by "Main"). Several' + 'classes can be specified by seperating them with a com' + 'ma. Default is "*" which specifies all classes.') + + group.add_option('--debug-variables', + action = 'store', + help = 'Limits the variables that will return debugging' + ' output. Several variables can be specified by seperat' + 'ing them with a comma. Default is "*" which specifies ' + 'all variables.') + + group.add_option('--debug-class-vars', + action = 'store_true', + help = 'In default mode the debugging code will only re' + 'turn information on the local variable which does not ' + 'include the class variables. Use this switch to add al' + 'l values that are provided by "self".') + + group.add_option('--debug-nocolor', + action = 'store_true', + help = 'Deactivates colors in the debugging output.') + + parser.add_option_group(group) + + + ############################################################################# + # Handle command line options + + def cli_handle(self, options): + + if (options.__dict__.has_key('debug') + and options.__dict__['debug']): + self.debug_on() + else: + self.debug_off() + return + + if (options.__dict__.has_key('debug_class_vars') + and options.__dict__['debug_class_vars']): + self.class_variables_on() + else: + self.class_variables_off() + + if (options.__dict__.has_key('debug_nocolor') + and options.__dict__['debug_nocolor']): + self.color_off() + else: + self.color_on() + + if (options.__dict__.has_key('debug_level') and + options.__dict__['debug_level']): + dbglvl = int(options.__dict__['debug_level']) + if dbglvl < 0: + dbglvl = 0 + if dbglvl > 10: + dbglvl = 10 + self.set_debug_level(dbglvl) + + if (options.__dict__.has_key('debug_verbose') and + options.__dict__['debug_verbose']): + dbgvrb = int(options.__dict__['debug_verbose']) + if dbgvrb < 1: + dbgvrb = 1 + if dbgvrb > 3: + dbgvrb = 3 + self.set_debug_verbosity(dbgvrb) + + for i in [('debug_methods', self.set_debug_methods), + ('debug_classes', self.set_debug_classes), + ('debug_variables', self.set_debug_variables),]: + + if (options.__dict__.has_key(i[0]) and + options.__dict__[i[0]]): + i[1](options.__dict__[i[0]]) + + + ############################################################################# + ## Helper Functions + + def set_module(self, module): + + self.debug_env = module + + def set_debug_methods(self, methods): + + methods = methods.split(',') + + if methods: + self.debug_mth = methods + + def set_debug_classes(self, classes): + + classes = classes.split(',') + + if classes: + self.debug_obj = classes + + def set_debug_variables(self, variables): + + variables = variables.split(',') + + if variables: + self.debug_var = variables + + def maybe_color (self, col, text): + if self.use_color: + return codes[col] + text + codes['reset'] + return text + + def set_info_level(self, info_level = 4): + self.info_lev = info_level + + def info_off(self): + self.set_info_level(0) + + def info_on(self, info_level = 4): + self.set_info_level(info_level) + + def set_warn_level(self, warn_level = 4): + self.warn_lev = warn_level + + def warn_off(self): + self.set_warn_level(0) + + def warn_on(self, warn_level = 4): + self.set_warn_level(warn_level) + + def set_debug_level(self, debugging_level = 4): + self.debug_lev = debugging_level + + def set_debug_verbosity(self, debugging_verbosity = 2): + self.debug_vrb = debugging_verbosity + + def debug_off(self): + self.set_debug_level(0) + + def debug_on(self): + self.set_debug_level() + + def color_off(self): + self.use_color = False + + def color_on(self): + self.use_color = True + + def class_variables_off(self): + self.show_class_variables = False + + def class_variables_on(self): + self.show_class_variables = True + + ############################################################################# + ## Output Functions + + def notice (self, note): + print >> self.std_out, note + + def info (self, info, level = 4): + + #print "info =", info + + if type(info) not in types.StringTypes: + info = str(info) + + if level > self.info_lev: + return + + for i in info.split('\n'): + print >> self.std_out, self.maybe_color('green', '* ') + i + + def status (self, message, status, info = 'ignored'): + + if type(message) not in types.StringTypes: + message = str(message) + + lines = message.split('\n') + + if not lines: + return + + for i in lines[0:-1]: + print >> self.std_out, self.maybe_color('green', '* ') + i + + i = lines[-1] + + if len(i) > 58: + i = i[0:57] + + if status == 1: + result = '[' + self.maybe_color('green', 'ok') + ']' + elif status == 0: + result = '[' + self.maybe_color('red', 'failed') + ']' + else: + result = '[' + self.maybe_color('yellow', info) + ']' + + print >> self.std_out, self.maybe_color('green', '* ') + i + ' ' + '.' * (58 - len(i)) \ + + ' ' + result + + def warn (self, warn, level = 4): + + #print "DEBUG.warn()" + + if type(warn) not in types.StringTypes: + warn = str(warn) + + if level > self.warn_lev: + return + + for i in warn.split('\n'): + print >> self.std_out, self.maybe_color('yellow', '* ') + i + + def error (self, error): + + if type(error) not in types.StringTypes: + error = str(error) + + for i in error.split('\n'): + # NOTE: Forced flushing ensures that stdout and stderr + # stay in nice order. This is a workaround for calls like + # "overlord -L |& less". + sys.stdout.flush() + print >> self.error_out, self.maybe_color('red', '* ') + i + self.error_out.flush() + self.has_error = True + + def die (self, error): + + if type(error) not in types.StringTypes: + error = str(error) + + for i in error.split('\n'): + self.error(self.maybe_color('red', 'Fatal error: ') + i) + self.error(self.maybe_color('red', 'Fatal error(s) - aborting')) + sys.exit(1) + + def debug (self, message, level = 4): + ''' + This is a generic debugging method. + ''' + ## Check the debug level first. This is the most inexpensive check. + if level > self.debug_lev: + return + + ## Maybe this should be debugged. So get the stack first. + stack = inspect.stack() + + ## This can probably never happen but does not harm to check + ## that there is actually something calling this function + if len(stack) < 2: + return + + ## Get the stack length to determine indentation of the debugging output + stacklength = len(stack) + ls = ' ' * stacklength + + ## Get the information about the caller + caller = stack[1] + + ## The function name of the calling frame is the fourth item in the list + callermethod = caller[3] + + ## Is this actually one of the methods that should be debugged? + if not '*' in self.debug_mth and not callermethod in self.debug_mth: + return + + ## Still looks like this should be debugged. So retrieve the dictionary + ## of local variables from the caller + callerlocals = inspect.getargvalues(caller[0])[3] + + ## Is the caller an obejct? If so he provides 'self' + if 'self' in callerlocals.keys(): + callerobject = callerlocals['self'] + del callerlocals['self'] + if self.show_class_variables: + cv = inspect.getmembers(callerobject, + lambda x: not inspect.ismethod(x)) + callerlocals.sync(cv) + else: + callerobject = None + + # Remove variables not requested + if not '*' in self.debug_var: + callerlocals = dict([i for i in callerlocals.items() + if i[0] in self.debug_var]) + + ## Is the object among the list of objects to debug? + if (not '*' in self.debug_obj and + not str(callerobject.__class__.__name__) in self.debug_obj): + return + + if type(message) not in types.StringTypes: + message = str(message) + + def breaklines(x): + ''' + Helper function to keep width of the debugging output. + + This may look ugly for arrays but it is acceptable and not + breaking the line would break the output format + ''' + ## Get the number of lines we need (rounded down) + lines = len(x) // 60 + if lines > 0: + for j in range(lines): + ## Print line with continuation marker + print >> self.debug_out, ls + '// ' + x[0:60] + ' \\' + ## Remove printed characters from output + x = x[60:] + ## Print final line + print >> self.debug_out, ls + '// ' + x + + if self.debug_vrb == 1: + # Top line indicates class and method + c = '' + if callerobject: + c += 'Class: ' + str(callerobject.__class__.__name__) + ' | ' + if callermethod: + c += 'Method: ' + str(callermethod) + print >> self.debug_out, '// ' + c + # Selected variables follow + if callerlocals: + for i,j in callerlocals.items(): + print >> self.debug_out, '// ' \ + + self.maybe_color('turquoise', str(i)) + ':' + str(j) + # Finally the message + print >> self.debug_out, self.maybe_color('yellow', message) + return + + if self.debug_vrb == 3: + print >> self.debug_out, ls + '/////////////////////////////////' + \ + '////////////////////////////////' + + # General information about what is being debugged + #(module name or similar) + print >> self.debug_out, ls + '// ' + self.debug_env + print >> self.debug_out, ls + '//-----------------------------------' + \ + '----------------------------' + + ## If the caller is a class print the name here + if callerobject: + print >> self.debug_out, ls + \ + '// Object Class: ' + str(callerobject.__class__.__name__) + + ## If the method has been extracted print it here + if callermethod: + print >> self.debug_out, ls + '// ' \ + + self.maybe_color('green', 'Method: ') + str(callermethod) + if self.debug_vrb == 3: + print >> self.debug_out, ls + '//---------------------------' + \ + '------------------------------------' + + ## Print the information on all available local variables + if callerlocals: + if self.debug_vrb == 3: + print >> self.debug_out, ls + '//' + print >> self.debug_out, ls + '// VALUES ' + for i,j in callerlocals.items(): + print >> self.debug_out, ls + '// ------------------> ' \ + + self.maybe_color('turquoise', str(i)) + ':' + breaklines(str(j)) + if self.debug_vrb == 3: + print >> self.debug_out, ls + '//------------------------------'\ + '---------------------------------' + + # Finally print the message + breaklines(self.maybe_color('yellow', message)) + + if self.debug_vrb == 3: + print >> self.debug_out, ls + '//-------------------------------' + \ + '--------------------------------' + print >> self.debug_out, ls + '/////////////////////////////////' + \ + '////////////////////////////////' + +## gloabal message handler +OUT = Message('overlord') diff --git a/overlord/makeconf.py b/overlord/makeconf.py new file mode 100644 index 0000000..4906d0a --- /dev/null +++ b/overlord/makeconf.py @@ -0,0 +1,269 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +################################################################################# +# MAKE-DOT-CONF HANDLING +################################################################################# +# File: makeconf.py +# +# Handles modifications to /etc/make.conf +# +# Copyright: +# (c) 2005 - 2009 Gunnar Wrobel +# Distributed under the terms of the GNU General Public License v2 +# +# Author(s): +# Gunnar Wrobel <wrobel@gentoo.org> +# + +import os +import codecs +import re + +from overlord.utils import path + +#=============================================================================== +# +# Helper class MakeConf +# +#------------------------------------------------------------------------------- + +class MakeConf: + ''' + Handles modifications to /etc/make.conf + + Check that an add/remove cycle does not modify the make.conf: + + >>> import hashlib + >>> write = os.tmpnam() + >>> here = os.path.dirname(os.path.realpath(__file__)) + >>> config = {'local_list' : + ... here + '/tests/testfiles/global-overlays.xml', + ... 'make_conf' : here + '/tests/testfiles/make.conf', + ... 'nocheck' : True, + ... 'storage' : '/var/lib/layman', + ... 'quietness':3} + >>> b = DB(config) + >>> a = MakeConf(config, b.overlays) + >>> o_md5 = str(hashlib.md5(open(here + '/tests/testfiles/make.conf').read()).hexdigest()) + >>> a.path = write + >>> a.add(b.overlays['wrobel-stable']) + >>> [i.name for i in a.overlays] + [u'wrobel-stable', u'wrobel-stable'] + >>> a.add(b.overlays['wrobel']) + >>> [i.name for i in a.overlays] + [u'wrobel', u'wrobel-stable', u'wrobel-stable'] + >>> a.delete(b.overlays['wrobel-stable']) + >>> [i.name for i in a.overlays] + [u'wrobel'] + >>> a.add(b.overlays['wrobel-stable']) + >>> [i.name for i in a.overlays] + [u'wrobel', u'wrobel-stable'] + >>> a.delete(b.overlays['wrobel']) + >>> n_md5 = str(hashlib.md5(open(write).read()).hexdigest()) + >>> o_md5 == n_md5 + True + >>> os.unlink(write) + ''' + + my_re = re.compile('PORTDIR_OVERLAY\s*=\s*"([^"]*)"') + + def __init__(self, config, overlays): + + self.path = config['make_conf'] + self.storage = config['storage'] + self.data = '' + self.db = overlays + self.overlays = [] + self.extra = [] + + self.read() + + def add(self, overlay): + ''' + Add an overlay to make.conf. + + >>> write = os.tmpnam() + >>> here = os.path.dirname(os.path.realpath(__file__)) + >>> config = {'local_list' : + ... here + '/tests/testfiles/global-overlays.xml', + ... 'make_conf' : here + '/tests/testfiles/make.conf', + ... 'nocheck' : True, + ... 'storage' : '/var/lib/layman', + ... 'quietness':3} + >>> c = DB(config) + >>> a = MakeConf(config, c.overlays) + >>> a.path = write + >>> a.add(c.select('wrobel')) + >>> config['make_conf'] = write + >>> b = MakeConf(config, c.overlays) + >>> [i.name for i in b.overlays] + [u'wrobel', u'wrobel-stable'] + >>> b.extra + [u'/usr/local/portage/ebuilds/testing', u'/usr/local/portage/ebuilds/stable', u'/usr/local/portage/kolab2', u'/usr/local/portage/gentoo-webapps-overlay/experimental', u'/usr/local/portage/gentoo-webapps-overlay/production-ready'] + + >>> os.unlink(write) + ''' + self.overlays.append(overlay) + self.write() + + def delete(self, overlay): + ''' + Delete an overlay from make.conf. + + >>> write = os.tmpnam() + >>> here = os.path.dirname(os.path.realpath(__file__)) + >>> config = {'local_list' : + ... here + '/tests/testfiles/global-overlays.xml', + ... 'make_conf' : here + '/tests/testfiles/make.conf', + ... 'nocheck' : True, + ... 'storage' : '/var/lib/layman', + ... 'quietness':3} + >>> c = DB(config) + >>> a = MakeConf(config, c.overlays) + >>> a.path = write + >>> a.delete(c.select('wrobel-stable')) + >>> config['make_conf'] = write + >>> b = MakeConf(config, c.overlays) + >>> [i.name for i in b.overlays] + [] + >>> b.extra + [u'/usr/local/portage/ebuilds/testing', u'/usr/local/portage/ebuilds/stable', u'/usr/local/portage/kolab2', u'/usr/local/portage/gentoo-webapps-overlay/experimental', u'/usr/local/portage/gentoo-webapps-overlay/production-ready'] + + >>> os.unlink(write) + ''' + self.overlays = [i + for i in self.overlays + if i.name != overlay.name] + self.write() + + def read(self): + ''' + Read the list of registered overlays from /etc/make.conf. + + >>> here = os.path.dirname(os.path.realpath(__file__)) + >>> config = {'local_list' : + ... here + '/tests/testfiles/global-overlays.xml', + ... 'make_conf' : here + '/tests/testfiles/make.conf', + ... 'nocheck' : True, + ... 'storage' : '/var/lib/layman', + ... 'quietness':3} + >>> c = DB(config) + >>> a = MakeConf(config, c.overlays) + >>> [i.name for i in a.overlays] + [u'wrobel-stable'] + >>> a.extra + [u'/usr/local/portage/ebuilds/testing', u'/usr/local/portage/ebuilds/stable', u'/usr/local/portage/kolab2', u'/usr/local/portage/gentoo-webapps-overlay/experimental', u'/usr/local/portage/gentoo-webapps-overlay/production-ready'] + ''' + if os.path.isfile(self.path): + self.content() + + overlays = self.my_re.search(self.data) + + if not overlays: + raise Exception('Did not find a PORTDIR_OVERLAY entry in file ' + + self.path +'! Did you specify the correct file?') + + overlays = [i.strip() + for i in overlays.group(1).split('\n') + if i.strip()] + + for i in overlays: + if i[:len(self.storage)] == self.storage: + oname = os.path.basename(i) + if oname in self.db.keys(): + self.overlays.append(self.db[oname]) + else: + # These are additional overlays that we dont know + # anything about. The user probably added them manually + self.extra.append(i) + else: + # These are additional overlays that we dont know anything + # about. The user probably added them manually + self.extra.append(i) + + + else: + self.overlays = [] + self.data = 'PORTDIR_OVERLAY="\n"\n' + + self.extra = [i for i in self.extra + if (i != '$PORTDIR_OVERLAY' + and i != '${PORTDIR_OVERLAY}')] + + def write(self): + ''' + Write the list of registered overlays to /etc/make.conf. + + >>> write = os.tmpnam() + >>> here = os.path.dirname(os.path.realpath(__file__)) + >>> config = {'local_list' : + ... here + '/tests/testfiles/global-overlays.xml', + ... 'make_conf' : here + '/tests/testfiles/make.conf', + ... 'nocheck' : True, + ... 'storage' : '/var/lib/layman', + ... 'quietness':3} + >>> c = DB(config) + >>> a = MakeConf(config, c.overlays) + >>> a.path = write + >>> a.write() + >>> config['make_conf'] = write + >>> b = MakeConf(config, c.overlays) + >>> [i.name for i in b.overlays] + [u'wrobel-stable'] + >>> b.extra + [u'/usr/local/portage/ebuilds/testing', u'/usr/local/portage/ebuilds/stable', u'/usr/local/portage/kolab2', u'/usr/local/portage/gentoo-webapps-overlay/experimental', u'/usr/local/portage/gentoo-webapps-overlay/production-ready'] + + >>> os.unlink(write) + ''' + def prio_sort(a, b): + '''Sort by priority.''' + if a.priority < b.priority: + return -1 + elif a.priority > b.priority: + return 1 + return 0 + + self.overlays.sort(prio_sort) + + paths = [] + for i in self.overlays: + paths.append(path((self.storage, i.name, ))) + + overlays = 'PORTDIR_OVERLAY="\n' + overlays += '\n'.join(paths) + '\n' + overlays += '$PORTDIR_OVERLAY\n' + overlays += '\n'.join(self.extra) + overlays += '"' + + content = self.my_re.sub(overlays, self.data) + + if not self.my_re.search(content): + raise Exception('Ups, failed to set a proper PORTDIR_OVERLAY entry ' + 'in file ' + self.path +'! Did not overwrite the fi' + 'le.') + + try: + make_conf = codecs.open(self.path, 'w', 'utf-8') + + make_conf.write(content) + + make_conf.close() + + except Exception, error: + raise Exception('Failed to read "' + self.path + '".\nError was:\n' + + str(error)) + + def content(self): + ''' + Returns the content of the /etc/make.conf file. + ''' + try: + make_conf = codecs.open(self.path, 'r', 'utf-8') + + self.data = make_conf.read() + + make_conf.close() + + except Exception, error: + raise Exception('Failed to read "' + self.path + '".\nError was:\n' + + str(error)) diff --git a/overlord/overlays/__init__.py b/overlord/overlays/__init__.py new file mode 100644 index 0000000..792d600 --- /dev/null +++ b/overlord/overlays/__init__.py @@ -0,0 +1 @@ +# diff --git a/overlord/overlays/bzr.py b/overlord/overlays/bzr.py new file mode 100644 index 0000000..0b06f94 --- /dev/null +++ b/overlord/overlays/bzr.py @@ -0,0 +1,70 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +################################################################################# +# overlord BZR OVERLAY HANDLER +################################################################################# +# File: bzr.py +# +# Handles bzr overlays +# +# Copyright: +# (c) 2005 - 2008 Adrian Perez, Gunnar Wrobel +# Distributed under the terms of the GNU General Public License v2 +# +# Author(s): +# Adrian Perez <moebius@connectical.net> +# Gunnar Wrobel <wrobel@gentoo.org> +# +'''Should work with any version of Bzr equal to or better than 0.7 -- + caution: tested only with 0.8 and 0.8.2...''' + +__version__ = "$Id: bzr.py 236 2006-09-05 20:39:37Z wrobel $" + +#=============================================================================== +# +# Dependencies +# +#------------------------------------------------------------------------------- + +from overlord.utils import path +from overlord.overlays.source import OverlaySource, require_supported + +#=============================================================================== +# +# Class BzrOverlay +# +#------------------------------------------------------------------------------- + +class BzrOverlay(OverlaySource): + ''' Handles bzr overlays.''' + + type = 'Bzr' + type_key = 'bzr' + + def __init__(self, parent, xml, config, _location, ignore = 0, quiet = False): + + super(BzrOverlay, self).__init__(parent, xml, config, _location, ignore, quiet) + + def add(self, base, quiet = False): + '''Add overlay.''' + + self.supported() + + # bzr get SOURCE TARGET + args = ['get', self.src + '/', path([base, self.parent.name])] + return self.run_command(*args) + + def sync(self, base, quiet = False): + '''Sync overlay.''' + + self.supported() + + # bzr pull --overwrite SOURCE + args = ['pull', '--overwrite', self.src] + return self.run_command(*args, cwd=path([base, self.parent.name])) + + def supported(self): + '''Overlay type supported?''' + + return require_supported([(self.command(), 'bzr', + 'dev-vcs/bzr'),]) diff --git a/overlord/overlays/cvs.py b/overlord/overlays/cvs.py new file mode 100644 index 0000000..d40d38e --- /dev/null +++ b/overlord/overlays/cvs.py @@ -0,0 +1,105 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +################################################################################# +# overlord CVS OVERLAY HANDLER +################################################################################# +# File: cvs.py +# +# Handles cvs overlays +# +# Copyright: +# (c) 2005 - 2008 Gunnar Wrobel +# Distributed under the terms of the GNU General Public License v2 +# +# Author(s): +# Gunnar Wrobel <wrobel@gentoo.org> +# +''' Cvs overlay support.''' + +__version__ = "$Id$" + +#=============================================================================== +# +# Dependencies +# +#------------------------------------------------------------------------------- + +import xml.etree.ElementTree as ET # Python 2.5 + +from overlord.utils import path, ensure_unicode +from overlord.overlays.source import OverlaySource, require_supported + +#=============================================================================== +# +# Class CvsOverlay +# +#------------------------------------------------------------------------------- + +class CvsOverlay(OverlaySource): + ''' Handles cvs overlays.''' + + type = 'cvs' + type_key = 'cvs' + + def __init__(self, parent, xml, config, _location, ignore = 0, quiet = False): + + super(CvsOverlay, self).__init__(parent, xml, config, _location, ignore, quiet) + + _subpath = xml.find('subpath') + if _subpath != None: + self.subpath = ensure_unicode(_subpath.text.strip()) + elif 'subpath' in xml.attrib: + self.subpath = ensure_unicode(xml.attrib['subpath']) + else: + self.subpath = '' + + def __eq__(self, other): + res = super(CvsOverlay, self).__eq__(other) \ + and self.subpath == other.subpath + return res + + def __ne__(self, other): + return not self.__eq__(other) + + # overrider + def to_xml_hook(self, repo_elem): + if self.subpath: + _subpath = ET.Element('subpath') + _subpath.text = self.subpath + repo_elem.append(_subpath) + del _subpath + + def add(self, base, quiet = False): + '''Add overlay.''' + + self.supported() + + # cvs [-q] co -d SOURCE SCOPE + args = [] + if quiet: + args.append('-q') + args.append('co') + args.append('-d') + args.append(self.parent.name) + args.append(self.subpath) + + return self.run_command(*args, cwd=base, env=dict(CVSROOT=self.src)) + + def sync(self, base, quiet = False): + '''Sync overlay.''' + + self.supported() + + # cvs [-q] update -d + args = [] + if quiet: + args.append('-q') + args.append('update') + args.append('-d') + return self.run_command(*args, cwd=path([base, self.parent.name])) + + def supported(self): + '''Overlay type supported?''' + + return require_supported([(self.command(), 'cvs', + 'dev-vcs/cvs'),]) diff --git a/overlord/overlays/darcs.py b/overlord/overlays/darcs.py new file mode 100644 index 0000000..e253861 --- /dev/null +++ b/overlord/overlays/darcs.py @@ -0,0 +1,69 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +################################################################################# +# overlord DARCS OVERLAY HANDLER +################################################################################# +# File: darcs.py +# +# Handles darcs overlays +# +# Copyright: +# (c) 2005 - 2008 Gunnar Wrobel, Andres Loeh +# Distributed under the terms of the GNU General Public License v2 +# +# Author(s): +# Gunnar Wrobel <wrobel@gentoo.org> +# Andres Loeh <kosmikus@gentoo.org> +# +''' Darcs overlay support.''' + +__version__ = "$Id: darcs.py 236 2006-09-05 20:39:37Z wrobel $" + +#=============================================================================== +# +# Dependencies +# +#------------------------------------------------------------------------------- + +from overlord.utils import path +from overlord.overlays.source import OverlaySource, require_supported + +#=============================================================================== +# +# Class BzrOverlay +# +#------------------------------------------------------------------------------- + +class DarcsOverlay(OverlaySource): + ''' Handles darcs overlays.''' + + type = 'Darcs' + type_key = 'darcs' + + def __init__(self, parent, xml, config, _location, ignore = 0, quiet = False): + + super(DarcsOverlay, self).__init__(parent, xml, config, _location, ignore, quiet) + + def add(self, base, quiet = False): + '''Add overlay.''' + + self.supported() + + # darcs get --partial SOURCE TARGET + args = ['get', '--partial', self.src + '/', path([base, self.parent.name])] + return self.run_command(*args) + + def sync(self, base, quiet = False): + '''Sync overlay.''' + + self.supported() + + # darcs pull --all SOURCE + args = ['pull', '--all', self.src] + return self.run_command(*args, cwd=path([base, self.parent.name])) + + def supported(self): + '''Overlay type supported?''' + + return require_supported([(self.command(), 'darcs', + 'dev-vcs/darcs'),]) diff --git a/overlord/overlays/g_common.py b/overlord/overlays/g_common.py new file mode 100644 index 0000000..20f2eae --- /dev/null +++ b/overlord/overlays/g_common.py @@ -0,0 +1,75 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +################################################################################# +# overlord G-COMMON OVERLAY HANDLER +################################################################################# +# File: g_common.py +# +# Handles g-common-style repositories +# +# Copyright: +# (c) 2010 Gentoo Foundation +# Distributed under the terms of the GNU General Public License v2 +# +# Author(s): +# Auke Booij <auke@tulcod.com> +# +''' G-common repository support.''' + +#=============================================================================== +# +# Dependencies +# +#------------------------------------------------------------------------------- + +import os +from overlord.utils import path +from overlord.overlays.source import OverlaySource, require_supported + +#=============================================================================== +# +# Class GCommonOverlay +# +#------------------------------------------------------------------------------- + +class GCommonOverlay(OverlaySource): + ''' Handles g-common-style repositories.''' + + type = 'g-common' + type_key = 'g-common' + + def __init__(self, parent, xml, config, _location, ignore = 0, quiet = False): + super(GCommonOverlay, self).__init__(parent, xml, config, _location, ignore, quiet) + #split source into driver and remote uri. + self.driver=self.src[:self.src.find(' ')] + self.remote_uri=self.src[self.src.find(' ')+1:] + + def add(self, base, quiet = False): + '''Add overlay.''' + + self.supported() + + os.makedirs(os.path.join(base,self.parent.name)) + return self.sync(base, quiet) + + def sync(self, base, quiet = False): + '''Sync overlay.''' + + self.supported() + + args = [os.path.join(base,self.parent.name), 'sync', self.driver, self.remote_uri] + returncode=self.run_command(*args,cwd=path([base,self.parent.name])) + if returncode: return returncode + args = [os.path.join(base,self.parent.name), 'generate-tree'] + return self.run_command(*args,cwd=path([base,self.parent.name])) + + def supported(self): + '''Overlay type supported?''' + + return require_supported( + [(self.command(), + 'g-common', + 'app-portage/g-common'), + ('/usr/share/g-common/drivers/'+self.driver+'.cfg', + 'g-common for '+self.driver, + 'app-portage/g-'+self.driver),]) diff --git a/overlord/overlays/git.py b/overlord/overlays/git.py new file mode 100644 index 0000000..6aef61b --- /dev/null +++ b/overlord/overlays/git.py @@ -0,0 +1,79 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +################################################################################# +# overlord GIT OVERLAY HANDLER +################################################################################# +# File: git.py +# +# Handles git overlays +# +# Copyright: +# (c) 2005 - 2008 Gunnar Wrobel, Stefan Schweizer +# Distributed under the terms of the GNU General Public License v2 +# +# Author(s): +# Gunnar Wrobel <wrobel@gentoo.org> +# Stefan Schweizer <genstef@gentoo.org> +''' Git overlay support.''' + +__version__ = "$Id: git.py 146 2006-05-27 09:52:36Z wrobel $" + +#=============================================================================== +# +# Dependencies +# +#------------------------------------------------------------------------------- + +from overlord.utils import path +from overlord.overlays.source import OverlaySource, require_supported + +#=============================================================================== +# +# Class GitOverlay +# +#------------------------------------------------------------------------------- + +class GitOverlay(OverlaySource): + ''' Handles git overlays.''' + + type = 'Git' + type_key = 'git' + + def __init__(self, parent, xml, config, _location, ignore = 0, quiet = False): + + super(GitOverlay, self).__init__(parent, xml, config, _location, ignore, quiet) + + def add(self, base, quiet = False): + '''Add overlay.''' + + self.supported() + + def fix_git_source(source): + # http:// should get trailing slash, other protocols shouldn't + if source.split(':')[0] == 'http': + return source + '/' + return source + + # git clone [-q] SOURCE TARGET + args = ['clone'] + if quiet: + args.append('-q') + args.append(fix_git_source(self.src)) + args.append(path([base, self.parent.name])) + return self.run_command(*args) + + def sync(self, base, quiet = False): + '''Sync overlay.''' + + self.supported() + + args = ['pull'] + if quiet: + args.append('-q') + return self.run_command(*args, cwd=path([base, self.parent.name])) + + def supported(self): + '''Overlay type supported?''' + + return require_supported([(self.command(), 'git', + 'dev-vcs/git'),]) diff --git a/overlord/overlays/mercurial.py b/overlord/overlays/mercurial.py new file mode 100644 index 0000000..f4a7c2c --- /dev/null +++ b/overlord/overlays/mercurial.py @@ -0,0 +1,69 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +################################################################################# +# overlord MERCURIAL OVERLAY HANDLER +################################################################################# +# File: darcs.py +# +# Handles darcs overlays +# +# Copyright: +# (c) 2005 - 2008 Gunnar Wrobel, Andres Loeh +# Distributed under the terms of the GNU General Public License v2 +# +# Author(s): +# Gunnar Wrobel <wrobel@gentoo.org> +# Andres Loeh <kosmikus@gentoo.org> +# +''' Mercurial overlay support.''' + +__version__ = "$Id: mercurial.py 236 2006-09-05 20:39:37Z wrobel $" + +#=============================================================================== +# +# Dependencies +# +#------------------------------------------------------------------------------- + +from overlord.utils import path +from overlord.overlays.source import OverlaySource, require_supported + +#=============================================================================== +# +# Class MercurialOverlay +# +#------------------------------------------------------------------------------- + +class MercurialOverlay(OverlaySource): + ''' Handles mercurial overlays.''' + + type = 'Mercurial' + type_key = 'mercurial' + + def __init__(self, parent, xml, config, _location, ignore = 0, quiet = False): + + super(MercurialOverlay, self).__init__(parent, xml, config, _location, ignore, quiet) + + def add(self, base, quiet = False): + '''Add overlay.''' + + self.supported() + + # hg clone SOURCE TARGET + args = ['clone', self.src + '/', path([base, self.parent.name])] + return self.run_command(*args) + + def sync(self, base, quiet = False): + '''Sync overlay.''' + + self.supported() + + # hg pull -u SOURCE + args = ['pull', '-u', self.src] + return self.run_command(*args, cwd=path([base, self.parent.name])) + + def supported(self): + '''Overlay type supported?''' + + return require_supported([(self.command(), 'mercurial', + 'dev-vcs/mercurial'),]) diff --git a/overlord/overlays/overlay.py b/overlord/overlays/overlay.py new file mode 100644 index 0000000..4a3cae7 --- /dev/null +++ b/overlord/overlays/overlay.py @@ -0,0 +1,419 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +################################################################################# +# overlord OVERLAY BASE CLASS +################################################################################# +# File: overlay.py +# +# Base class for the different overlay types. +# +# Copyright: +# (c) 2005 - 2009 Gunnar Wrobel +# (c) 2009 Sebastian Pipping +# (c) 2009 Christian Groschupp +# Distributed under the terms of the GNU General Public License v2 +# +# Author(s): +# Gunnar Wrobel <wrobel@gentoo.org> +# Sebastian Pipping <sebastian@pipping.org> +# Christian Groschupp <christian@groschupp.org> +# +''' Basic overlay class.''' + +__version__ = "$Id: overlay.py 273 2006-12-30 15:54:50Z wrobel $" + +#=============================================================================== +# +# Dependencies +# +#------------------------------------------------------------------------------- + +import sys, re, os, os.path +import codecs +import locale +import xml.etree.ElementTree as ET # Python 2.5 + +from overlord.utils import (pad, terminal_width, get_encoding, encoder, + ensure_unicode) +#from overlord.debug import OUT + +from overlord.overlays.bzr import BzrOverlay +from overlord.overlays.darcs import DarcsOverlay +from overlord.overlays.git import GitOverlay +from overlord.overlays.g_common import GCommonOverlay +from overlord.overlays.mercurial import MercurialOverlay +from overlord.overlays.cvs import CvsOverlay +from overlord.overlays.svn import SvnOverlay +from overlord.overlays.rsync import RsyncOverlay +from overlord.overlays.tar import TarOverlay + +#=============================================================================== +# +# Constants +# +#------------------------------------------------------------------------------- + +OVERLAY_TYPES = dict((e.type_key, e) for e in ( + GitOverlay, + GCommonOverlay, + CvsOverlay, + SvnOverlay, + RsyncOverlay, + TarOverlay, + BzrOverlay, + MercurialOverlay, + DarcsOverlay +)) + +QUALITY_LEVELS = 'core|stable|testing|experimental|graveyard'.split('|') + +WHITESPACE_REGEX = re.compile('\s+') + +#=============================================================================== +# +# Class Overlay +# +#------------------------------------------------------------------------------- + +class Overlay(object): + ''' Derive the real implementations from this.''' + + def __init__(self, xml, config, ignore = 0, quiet = False): + ''' + >>> here = os.path.dirname(os.path.realpath(__file__)) + >>> import xml.etree.ElementTree as ET # Python 2.5 + >>> document = ET.parse(here + '/../tests/testfiles/global-overlays.xml') + >>> overlays = document.findall('overlay') + document.findall('repo') + >>> a = Overlay(overlays[0], dict()) + >>> a.name + u'wrobel' + >>> a.is_official() + True + >>> list(a.source_uris()) + [u'https://overlays.gentoo.org/svn/dev/wrobel'] + >>> a.owner_email + u'nobody@gentoo.org' + >>> a.description + u'Test' + >>> a.priority + 10 + >>> b = Overlay(overlays[1], dict()) + >>> b.is_official() + False + ''' + + self.output = config['output'] + self._encoding_ = get_encoding(self.output) + + def strip_text(node): + res = node.text + if res is None: + return '' + return res.strip() + + _name = xml.find('name') + if _name != None: + self.name = ensure_unicode(strip_text(_name)) + elif 'name' in xml.attrib: + self.name = ensure_unicode(xml.attrib['name']) + else: + raise Exception('Overlay is missing a "name" entry!') + + _sources = xml.findall('source') + if _sources: + _sources = [e for e in _sources if 'type' in e.attrib] + elif ('src' in xml.attrib) and ('type' in xml.attrib): + s = ET.Element('source', type=xml.attrib['type']) + s.text = xml.attrib['src'] + _sources = [s] + del s + + if not _sources: + raise Exception('Overlay "' + self.name + '" is missing a "source" entry!') + + + def create_overlay_source(source_elem): + _type = source_elem.attrib['type'] + try: + _class = OVERLAY_TYPES[_type] + except KeyError: + raise Exception('Unknown overlay type "%s"!' % _type) + _location = ensure_unicode(strip_text(source_elem)) + return _class(self, xml, config, _location, ignore, quiet) + + self.sources = [create_overlay_source(e) for e in _sources] + + + _owner = xml.find('owner') + if _owner == None: + _email = None + else: + _email = _owner.find('email') + if _owner != None and _email != None: + self.owner_email = ensure_unicode(strip_text(_email)) + _name = _owner.find('name') + if _name != None: + self.owner_name = ensure_unicode(strip_text(_name)) + else: + self.owner_name = None + elif 'contact' in xml.attrib: + self.owner_email = ensure_unicode(xml.attrib['contact']) + self.owner_name = None + else: + self.owner_email = '' + self.owner_name = None + if not ignore: + raise Exception('Overlay "' + self.name + '" is missing a ' + '"owner.email" entry!') + elif ignore == 1: + self.output.warn('Overlay "' + self.name + '" is missing a ' + '"owner.email" entry!', 4) + + + _desc = xml.find('description') + if _desc != None: + d = WHITESPACE_REGEX.sub(' ', strip_text(_desc)) + self.description = ensure_unicode(d) + del d + else: + self.description = '' + if not ignore: + raise Exception('Overlay "' + self.name + '" is missing a ' + '"description" entry!') + elif ignore == 1: + self.output.warn('Overlay "' + self.name + '" is missing a ' + '"description" entry!', 4) + + if 'status' in xml.attrib: + self.status = ensure_unicode(xml.attrib['status']) + else: + self.status = None + + self.quality = u'experimental' + if 'quality' in xml.attrib: + if xml.attrib['quality'] in set(QUALITY_LEVELS): + self.quality = ensure_unicode(xml.attrib['quality']) + + if 'priority' in xml.attrib: + self.priority = int(xml.attrib['priority']) + else: + self.priority = 50 + + h = xml.find('homepage') + l = xml.find('link') + if h != None: + self.homepage = ensure_unicode(strip_text(h)) + elif l != None: + self.homepage = ensure_unicode(strip_text(l)) + else: + self.homepage = None + + self.feeds = [ensure_unicode(strip_text(e)) for e in xml.findall('feed')] + + + def __eq__(self, other): + for i in ('description', 'homepage', 'name', 'owner_email', + 'owner_name', 'priority', 'status'): + if getattr(self, i) != getattr(other, i): + return False + for i in self.sources + other.sources: + if not i in self.sources: + return False + if not i in other.sources: + return False + return True + + def __ne__(self, other): + return not self.__eq__(other) + + def set_priority(self, priority): + '''Set the priority of this overlay.''' + + self.priority = int(priority) + + def to_xml(self): + '''Convert to xml.''' + + repo = ET.Element('repo') + if self.status != None: + repo.attrib['status'] = self.status + repo.attrib['quality'] = self.quality + repo.attrib['priority'] = str(self.priority) + name = ET.Element('name') + name.text = self.name + repo.append(name) + desc = ET.Element('description') + desc.text = self.description + repo.append(desc) + if self.homepage != None: + homepage = ET.Element('homepage') + homepage.text = self.homepage + repo.append(homepage) + owner = ET.Element('owner') + repo.append(owner) + owner_email = ET.Element('email') + owner_email.text = self.owner_email + owner.append(owner_email) + if self.owner_name != None: + owner_name = ET.Element('name') + owner_name.text = self.owner_name + owner.append(owner_name) + for i in self.sources: + source = ET.Element('source', type=i.__class__.type_key) + source.text = i.src + repo.append(source) + del source + for i in self.sources: + # NOTE: Two loops on purpose so the + # hooks are called with all sources in + i.to_xml_hook(repo) + for i in self.feeds: + feed = ET.Element('feed') + feed.text = i + repo.append(feed) + del feed + return repo + + def add(self, base, quiet = False): + res = 1 + for s in self.sources: + try: + res = s.add(base, quiet) + if res == 0: + # Worked, throw other sources away + self.sources = [s] + break + except Exception, error: + self.output.warn(str(error), 4) + return res + + def sync(self, base, quiet = False): + assert len(self.sources) == 1 + return self.sources[0].sync(base, quiet) + + def delete(self, base): + assert len(self.sources) == 1 + return self.sources[0].delete(base) + + def __str__(self): + ''' + >>> here = os.path.dirname(os.path.realpath(__file__)) + >>> import xml.etree.ElementTree as ET # Python 2.5 + >>> document = ET.parse(here + '/../tests/testfiles/global-overlays.xml') + >>> overlays = document.findall('overlay') + document.findall('repo') + >>> a = Overlay(overlays[0], dict()) + >>> print str(a) + wrobel + ~~~~~~ + Source : https://overlays.gentoo.org/svn/dev/wrobel + Contact : nobody@gentoo.org + Type : Subversion; Priority: 10 + Quality : experimental + <BLANKLINE> + Description: + Test + <BLANKLINE> + ''' + + result = u'' + + result += self.name + u'\n' + (len(self.name) * u'~') + + if len(self.sources) == 1: + result += u'\nSource : ' + self.sources[0].src + else: + result += u'\nSources:' + for i, v in enumerate(self.sources): + result += '\n %d. %s' % (i + 1, v.src) + result += '\n' + + if self.owner_name != None: + result += u'\nContact : %s <%s>' % (self.owner_name, self.owner_email) + else: + result += u'\nContact : ' + self.owner_email + if len(self.sources) == 1: + result += u'\nType : ' + self.sources[0].type + else: + result += u'\nType : ' + '/'.join(sorted(set(e.type for e in self.sources))) + result += u'; Priority: ' + str(self.priority) + u'\n' + result += u'Quality : ' + self.quality + u'\n' + + + description = self.description + description = re.compile(u' +').sub(u' ', description) + description = re.compile(u'\n ').sub(u'\n', description) + result += u'\nDescription:' + result += u'\n '.join((u'\n' + description).split(u'\n')) + result += u'\n' + + if self.homepage != None: + link = self.homepage + link = re.compile(u' +').sub(u' ', link) + link = re.compile(u'\n ').sub(u'\n', link) + result += u'\nLink:' + result += u'\n '.join((u'\n' + link).split(u'\n')) + result += u'\n' + + if self.feeds: + result += u'\n%s:' % ((len(self.feeds) == 1) and "Feed" or "Feeds") + for i in self.feeds: + result += u'\n %s' % i + result += u'\n' + + return encoder(result, self._encoding_) + + def short_list(self, width = 0): + ''' + >>> here = os.path.dirname(os.path.realpath(__file__)) + >>> import xml.etree.ElementTree as ET # Python 2.5 + >>> document = ET.parse(here + '/../tests/testfiles/global-overlays.xml') + >>> overlays = document.findall('repo') + document.findall('overlay') + >>> a = Overlay(overlays[0], dict()) + >>> print a.short_list(80) + wrobel [Subversion] (https://o.g.o/svn/dev/wrobel ) + ''' + + name = pad(self.name, 25) + + if len(set(e.type for e in self.sources)) == 1: + _type = self.sources[0].type + else: + _type = '%s/..' % self.sources[0].type + + mtype = ' [' + pad(_type, 10) + ']' + if not width: + width = terminal_width() + srclen = width - 43 + source = ', '.join(self.source_uris()) + if len(source) > srclen: + source = source.replace("overlays.gentoo.org", "o.g.o") + source = ' (' + pad(source, srclen) + ')' + + return encoder(name + mtype + source, self._encoding_) + + def is_official(self): + '''Is the overlay official?''' + + return self.status == 'official' + + def is_supported(self): + return any(e.is_supported() for e in self.sources) + + def source_uris(self): + for i in self.sources: + yield i.src + + def source_types(self): + for i in self.sources: + yield i.type + + +#================================================================================ +# +# Testing +# +#-------------------------------------------------------------------------------- + +if __name__ == '__main__': + import doctest + doctest.testmod(sys.modules[__name__]) diff --git a/overlord/overlays/rsync.py b/overlord/overlays/rsync.py new file mode 100644 index 0000000..d5dbd48 --- /dev/null +++ b/overlord/overlays/rsync.py @@ -0,0 +1,75 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +################################################################################# +# overlord RSYNC OVERLAY HANDLER +################################################################################# +# File: rsync.py +# +# Handles rsync overlays +# +# Copyright: +# (c) 2005 - 2008 Gunnar Wrobel +# Distributed under the terms of the GNU General Public License v2 +# +# Author(s): +# Gunnar Wrobel <wrobel@gentoo.org> +# +''' Rsync overlay support.''' + +__version__ = "$Id: rsync.py 236 2006-09-05 20:39:37Z wrobel $" + +#=============================================================================== +# +# Dependencies +# +#------------------------------------------------------------------------------- + +from overlord.utils import path +from overlord.overlays.source import OverlaySource, require_supported + +#=============================================================================== +# +# Class RsyncOverlay +# +#------------------------------------------------------------------------------- + +class RsyncOverlay(OverlaySource): + ''' Handles rsync overlays.''' + + type = 'Rsync' + type_key = 'rsync' + + + def __init__(self, parent, xml, config, _location, ignore = 0, quiet = False): + + super(RsyncOverlay, self).__init__(parent, xml, config, _location, ignore, quiet) + + def add(self, base, quiet = False): + '''Add overlay.''' + + self.supported() + + super(RsyncOverlay, self).add(base) + + return self.sync(base) + + def sync(self, base, quiet = False): + '''Sync overlay.''' + + self.supported() + + # rsync OPTIONS [-q] SOURCE TARGET + args = ['-rlptDvz', '--progress', '--delete', '--delete-after', '--timeout=180', + '--exclude=distfiles/*', '--exclude=local/*', '--exclude=packages/*'] + if quiet: + args.append('-q') + args.append(self.src + '/') + args.append(path([base, self.parent.name])) + + return self.run_command(*args) + + def supported(self): + '''Overlay type supported?''' + + return require_supported([(self.command(), 'rsync', + 'net-misc/rsync'),]) diff --git a/overlord/overlays/source.py b/overlord/overlays/source.py new file mode 100644 index 0000000..c645fa8 --- /dev/null +++ b/overlord/overlays/source.py @@ -0,0 +1,161 @@ +# -*- coding: utf-8 -*- +################################################################################# +# overlord OVERLAY SOURCE BASE CLASS +################################################################################# +# File: source.py +# +# Base class for the different overlay types. +# +# Copyright: +# (c) 2010 Sebastian Pipping +# Distributed under the terms of the GNU General Public License v2 +# +# Author(s): +# Sebastian Pipping <sebastian@pipping.org> + +import os +import copy +import sys +import shutil +import subprocess +#from overlord.debug import OUT +from overlord.utils import path + + +def _resolve_command(command): + if os.path.isabs(command): + if not os.path.exists(command): + raise Exception('Program "%s" not found' % command) + return ('File', command) + else: + kind = 'Command' + env_path = os.environ['PATH'] + for d in env_path.split(os.pathsep): + f = os.path.join(d, command) + if os.path.exists(f): + return ('Command', f) + raise Exception('Cound not resolve command "%s" based on PATH "%s"' % (command, env_path)) + + +def require_supported(binaries): + for command, mtype, package in binaries: + found = False + kind, path = _resolve_command(command) + if not path: + raise Exception(kind + ' ' + command + ' seems to be missing!' + ' Overlay type "' + mtype + '" not support' + 'ed. Did you emerge ' + package + '?') + return True + + +class OverlaySource(object): + + type_key = None + + def __init__(self, parent, xml, config, _location, ignore = 0, quiet = False): + self.parent = parent + self.src = _location + self.config = config + self.ignore = ignore + self.quiet = quiet + self.output = config['output'] + + def __eq__(self, other): + return self.src == other.src + + def __ne__(self, other): + return not self.__eq__(other) + + def add(self, base, quiet = False): + '''Add the overlay.''' + + mdir = path([base, self.parent.name]) + + if os.path.exists(mdir): + raise Exception('Directory ' + mdir + ' already exists. Will not ov' + 'erwrite its contents!') + + os.makedirs(mdir) + + def sync(self, base, quiet = False): + '''Sync the overlay.''' + pass + + def delete(self, base): + '''Delete the overlay.''' + mdir = path([base, self.parent.name]) + + if not os.path.exists(mdir): + self.output.warn('Directory ' + mdir + ' did not exist, no files deleted.') + return + + self.output.info('Deleting directory "%s"' % mdir, 2) + shutil.rmtree(mdir) + + def supported(self): + '''Is the overlay type supported?''' + return True + + def is_supported(self): + '''Is the overlay type supported?''' + + try: + self.supported() + return True + except: + return False + + def command(self): + return self.config['%s_command' % self.__class__.type_key] + + def run_command(self, *args, **kwargs): + file_to_run = _resolve_command(self.command())[1] + args = (file_to_run, ) + args + assert('pwd' not in kwargs) # Bug detector + + cwd = kwargs.get('cwd', None) + env = None + env_updates = None + if 'env' in kwargs: + # Build actual env from surrounding plus updates + env_updates = kwargs['env'] + env = copy.copy(os.environ) + env.update(env_updates) + + command_repr = ' '.join(args) + if env_updates: + command_repr = '%s %s' % (' '.join('%s=%s' % (k, v) for (k, v) in sorted(env_updates.items())), command_repr) + if cwd: + command_repr = '( cd %s && %s )' % (cwd, command_repr) + + self.output.info('Running... # %s' % command_repr, 2) + + if self.quiet: + input_source = subprocess.PIPE + output_target = open('/dev/null', 'w') + else: + # Re-use parent file descriptors + input_source = self.config['stdin'] + output_target = self.config['stdout'] + + proc = subprocess.Popen(args, + stdin=input_source, + stdout=output_target, + stderr=self.config['stderr'], + cwd=cwd, + env=env) + + if self.quiet: + # Make child non-interactive + proc.stdin.close() + + result = proc.wait() + + if self.quiet: + output_target.close() + + return result + + + def to_xml_hook(self, repo_elem): + pass diff --git a/overlord/overlays/svn.py b/overlord/overlays/svn.py new file mode 100644 index 0000000..1570450 --- /dev/null +++ b/overlord/overlays/svn.py @@ -0,0 +1,86 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +################################################################################# +# overlord SVN OVERLAY HANDLER +################################################################################# +# File: svn.py +# +# Handles subversion overlays +# +# Copyright: +# (c) 2005 - 2008 Gunnar Wrobel +# Distributed under the terms of the GNU General Public License v2 +# +# Author(s): +# Gunnar Wrobel <wrobel@gentoo.org> +# +''' Subversion overlay support.''' + +__version__ = "$Id: svn.py 236 2006-09-05 20:39:37Z wrobel $" + +#=============================================================================== +# +# Dependencies +# +#------------------------------------------------------------------------------- + +from overlord.utils import path +from overlord.overlays.source import OverlaySource, require_supported + +#=============================================================================== +# +# Class SvnOverlay +# +#------------------------------------------------------------------------------- + +class SvnOverlay(OverlaySource): + ''' Handles subversion overlays.''' + + type = 'Subversion' + type_key = 'svn' + + def __init__(self, parent, xml, config, _location, ignore = 0, quiet = False): + + super(SvnOverlay, self).__init__(parent, xml, config, _location, ignore, quiet) + + def add(self, base, quiet = False): + '''Add overlay.''' + + self.supported() + + super(SvnOverlay, self).add(base) + + args = ['co'] + if quiet: + args.append('-q') + args.append(self.src + '/@') + args.append(path([base, self.parent.name])) + + return self.run_command(*args) + + def sync(self, base, quiet = False): + '''Sync overlay.''' + + self.supported() + + def checkout_location(): + # Append '@' iff needed + # Keeps users of SVN <1.6.5 happy in more cases (bug #313303) + repo_part = self.parent.name + if self.parent.name.find('@') != -1: + repo_part = repo_part + '@' + return path([base, repo_part]) + + # svn up [-q] TARGET + args = ['up'] + if quiet: + args.append('-q') + args.append(checkout_location()) + + return self.run_command(*args) + + def supported(self): + '''Overlay type supported?''' + + return require_supported([(self.command(), 'svn', + 'dev-vcs/subversion'),]) diff --git a/overlord/overlays/tar.py b/overlord/overlays/tar.py new file mode 100644 index 0000000..425fe26 --- /dev/null +++ b/overlord/overlays/tar.py @@ -0,0 +1,214 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +################################################################################# +# overlord TAR OVERLAY HANDLER +################################################################################# +# File: tar.py +# +# Handles tar overlays +# +# Copyright: +# (c) 2005 - 2008 Gunnar Wrobel +# Distributed under the terms of the GNU General Public License v2 +# +# Author(s): +# Gunnar Wrobel <wrobel@gentoo.org> +# +''' Tar overlay support.''' + +__version__ = "$Id: tar.py 310 2007-04-09 16:30:40Z wrobel $" + +#=============================================================================== +# +# Dependencies +# +#------------------------------------------------------------------------------- + +import os, os.path, sys, urllib2, shutil, tempfile +import xml.etree.ElementTree as ET # Python 2.5 + +from overlord.utils import path, ensure_unicode +#from overlord.debug import OUT +from overlord.overlays.source import OverlaySource, require_supported + +#=============================================================================== +# +# Class TarOverlay +# +#------------------------------------------------------------------------------- + +class TarOverlay(OverlaySource): + ''' Handles tar overlays. + + >>> from overlord.debug import OUT + >>> import xml.etree.ElementTree as ET # Python 2.5 + >>> repo = ET.Element('repo') + >>> repo_name = ET.Element('name') + >>> repo_name.text = 'dummy' + >>> desc = ET.Element('description') + >>> desc.text = 'Dummy description' + >>> owner = ET.Element('owner') + >>> owner_email = ET.Element('email') + >>> owner_email.text = 'dummy@example.org' + >>> owner[:] = [owner_email] + >>> source = ET.Element('source', type='tar') + >>> here = os.path.dirname(os.path.realpath(__file__)) + >>> source.text = 'file://' + here + '/../tests/testfiles/overlord-test.tar.bz2' + >>> subpath = ET.Element('subpath') + >>> subpath.text = 'overlord-test' + >>> repo[:] = [repo_name, desc, owner, source, subpath] + >>> config = {'tar_command':'/bin/tar'} + >>> testdir = os.tmpnam() + >>> os.mkdir(testdir) + >>> from overlord.overlays.overlay import Overlay + >>> a = Overlay(repo, config, quiet=False) + >>> OUT.color_off() + >>> a.add(testdir) #doctest: +ELLIPSIS + * Running... # /bin/tar -v -x -f... + >>> sorted(os.listdir(testdir + '/dummy')) + ['app-admin', 'app-portage'] + >>> shutil.rmtree(testdir) + ''' + + type = 'Tar' + type_key = 'tar' + + def __init__(self, parent, xml, config, _location, ignore = 0, quiet = False): + + super(TarOverlay, self).__init__(parent, xml, config, _location, ignore, quiet) + + _subpath = xml.find('subpath') + if _subpath != None: + self.subpath = ensure_unicode(_subpath.text.strip()) + elif 'subpath' in xml.attrib: + self.subpath = ensure_unicode(xml.attrib['subpath']) + else: + self.subpath = '' + + self.output = config['output'] + + def __eq__(self, other): + res = super(TarOverlay, self).__eq__(other) \ + and self.subpath == other.subpath + return res + + def __ne__(self, other): + return not self.__eq__(other) + + # overrider + def to_xml_hook(self, repo_elem): + if self.subpath: + _subpath = ET.Element('subpath') + _subpath.text = self.subpath + repo_elem.append(_subpath) + del _subpath + + def _extract(self, base, tar_url, dest_dir): + ext = '.tar.noidea' + for i in [('tar.%s' % e) for e in ('bz2', 'gz', 'lzma', 'xz', 'Z')] \ + + ['tgz', 'tbz', 'taz', 'tlz', 'txz']: + candidate_ext = '.%s' % i + if self.src.endswith(candidate_ext): + ext = candidate_ext + break + + try: + tar = urllib2.urlopen(tar_url).read() + except Exception, error: + raise Exception('Failed to fetch the tar package from: ' + + self.src + '\nError was:' + str(error)) + + pkg = path([base, self.parent.name + ext]) + + try: + out_file = open(pkg, 'w') + out_file.write(tar) + out_file.close() + except Exception, error: + raise Exception('Failed to store tar package in ' + + pkg + '\nError was:' + str(error)) + + # tar -v -x -f SOURCE -C TARGET + args = ['-v', '-x', '-f', pkg, '-C', dest_dir] + result = self.run_command(*args) + + os.unlink(pkg) + return result + + def _add_unchecked(self, base, quiet): + def try_to_wipe(folder): + if not os.path.exists(folder): + return + + try: + self.output.info('Deleting directory "%s"' % folder, 2) + shutil.rmtree(folder) + except Exception, error: + raise Exception('Failed to remove unnecessary tar structure "' + + folder + '"\nError was:' + str(error)) + + final_path = path([base, self.parent.name]) + temp_path = tempfile.mkdtemp(dir=base) + try: + result = self._extract(base=base, tar_url=self.src, dest_dir=temp_path) + except Exception, error: + try_to_wipe(temp_path) + raise error + + if result == 0: + if self.subpath: + source = temp_path + '/' + self.subpath + else: + source = temp_path + + if os.path.exists(source): + if os.path.exists(final_path): + self.delete(base) + + try: + os.rename(source, final_path) + except Exception, error: + raise Exception('Failed to rename tar subdirectory ' + + source + ' to ' + final_path + '\nError was:' + + str(error)) + os.chmod(final_path, 0755) + else: + raise Exception('Given subpath "' + source + '" does not exist ' + ' in the tar package!') + + try_to_wipe(temp_path) + return result + + def add(self, base, quiet = False): + '''Add overlay.''' + + self.supported() + + final_path = path([base, self.parent.name]) + + if os.path.exists(final_path): + raise Exception('Directory ' + final_path + ' already exists. Will not ov' + 'erwrite its contents!') + + return self._add_unchecked(base, quiet) + + def sync(self, base, quiet = False): + '''Sync overlay.''' + self.supported() + self._add_unchecked(base, quiet) + + def supported(self): + '''Overlay type supported?''' + + return require_supported([(self.command(), 'tar', 'app-arch/tar'), ]) + +if __name__ == '__main__': + import doctest + + # Ignore warnings here. We are just testing + from warnings import filterwarnings, resetwarnings + filterwarnings('ignore') + + doctest.testmod(sys.modules[__name__]) + + resetwarnings() diff --git a/overlord/tests/dtest.py b/overlord/tests/dtest.py new file mode 100644 index 0000000..d380b7f --- /dev/null +++ b/overlord/tests/dtest.py @@ -0,0 +1,90 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +################################################################################# +# overlord DOCTEST AGGREGATOR +################################################################################# +# File: dtest.py +# +# Combines the doctests that are available for the different modules +# +# Copyright: +# (c) 2005 - 2008 Gunnar Wrobel +# Distributed under the terms of the GNU General Public License v2 +# +# Author(s): +# Gunnar Wrobel <wrobel@gentoo.org> +# +'''Aggregates doctests from all modules that provide such tests.''' + +__version__ = '$Id: dtest.py 237 2006-09-05 21:18:54Z wrobel $' + +#=============================================================================== +# +# Dependencies +# +#------------------------------------------------------------------------------- + +import unittest, doctest + +# On module creation: + +# 1.) Check header section (copyright notice) +# 2.) Add module doc string +# 3.) Add version string +# 4.) Add testing handler at bottom of module +# 5.) Add module into tests/dtest.py. Check that tests run through +# 6.) Run pylint over the code. Fix any reasonable complaints. +# 7.) Whitespace clean the buffer. +# 8.) Add svn:keywords "Id" to file. + +# On module change: + +# 1.) Check header section (copyright notice) +# 5.) Check that tests run through +# 6.) Run pylint over the code. Fix any reasonable complaints. +# 7.) Whitespace clean the buffer. + +# clean modules : CT +# not yet clean : UT +# clean but no testing : CN +# unclean but no testing: UN + +import overlord.action #CT +import overlord.config #CT +import overlord.db #CT +import overlord.dbbase #CT +import overlord.utils #CT +import overlord.overlays.overlay #CT +import overlord.overlays.tar #CT + +#=============================================================================== +# +# Test Suite +# +#------------------------------------------------------------------------------- + +def test_suite(): + return unittest.TestSuite(( + doctest.DocTestSuite(overlord.action), + doctest.DocTestSuite(overlord.config), + doctest.DocTestSuite(overlord.db), + doctest.DocTestSuite(overlord.dbbase), + doctest.DocTestSuite(overlord.utils), + doctest.DocTestSuite(overlord.overlays.overlay), + doctest.DocTestSuite(overlord.overlays.tar), + )) + +#=============================================================================== +# +# Run Testing +# +#------------------------------------------------------------------------------- + +if __name__ == '__main__': + # Ignore warnings here. We are just testing + from warnings import filterwarnings, resetwarnings + filterwarnings('ignore') + + unittest.main(defaultTest='test_suite') + + resetwarnings() diff --git a/overlord/tests/external.py b/overlord/tests/external.py new file mode 100644 index 0000000..aa43a3f --- /dev/null +++ b/overlord/tests/external.py @@ -0,0 +1,136 @@ +# -*- coding: utf-8 -*- +################################################################################# +# EXTENRAL overlord TESTS +################################################################################# +# File: external.py +# +# Runs external (non-doctest) test cases. +# +# Copyright: +# (c) 2009 Sebastian Pipping +# Distributed under the terms of the GNU General Public License v2 +# +# Author(s): +# Sebastian Pipping <sebastian@pipping.org> +# +'''Runs external (non-doctest) test cases.''' + +import unittest +import os +import tempfile +import shutil +import urllib +from overlord.dbbase import DbBase +from warnings import filterwarnings, resetwarnings + +HERE = os.path.dirname(os.path.realpath(__file__)) + + +class Unicode(unittest.TestCase): + def _overlays_bug(self, number): + config = {} + filename = os.path.join(HERE, 'testfiles', 'overlays_bug_%d.xml' % number) + o = DbBase([filename], config) + for verbose in (True, False): + for t in o.list(verbose=verbose): + print t[0] + print + + def test_184449(self): + self._overlays_bug(184449) + + def test_286290(self): + self._overlays_bug(286290) + + +class FormatSubpathCategory(unittest.TestCase): + def _run(self, number): + config = {} + filename1 = os.path.join(HERE, 'testfiles', + 'subpath-%d.xml' % number) + + # Read, write, re-read, compare + os1 = DbBase([filename1], config) + filename2 = os.tmpnam() + os1.write(filename2) + os2 = DbBase([filename2], config) + os.unlink(filename2) + self.assertTrue(os1 == os2) + + # Pass original overlays + return os1 + + def test(self): + os1 = self._run(1) + os2 = self._run(2) + + # Same content from old/overlord-global.txt + # and new/repositories.xml format? + self.assertTrue(os1 == os2) + + +# http://bugs.gentoo.org/show_bug.cgi?id=304547 +class TarAddRemoveSync(unittest.TestCase): + def test(self): + repo_name = 'tar-test-overlay' + tar_source_path = os.path.join(HERE, 'testfiles', 'overlord-test.tar.bz2') + + # Duplicate test tarball (so we have it deletable for later) + (_, temp_tarball_path) = tempfile.mkstemp() + shutil.copyfile(tar_source_path, temp_tarball_path) + + # Write overlay collection XML + xml_text = """\ +<?xml version="1.0" encoding="UTF-8"?> +<repositories xmlns="" version="1.0"> + <repo quality="experimental" status="unofficial"> + <name>%(repo_name)s</name> + <description>XXXXXXXXXXX</description> + <owner> + <email>foo@exmaple.org</email> + </owner> + <source type="tar">file://%(temp_tarball_url)s</source> + </repo> +</repositories> +""" % { 'temp_tarball_url':urllib.pathname2url(temp_tarball_path), + 'repo_name':repo_name} + (fd, temp_collection_path) = tempfile.mkstemp() + f = os.fdopen(fd, 'w') + f.write(xml_text) + f.close() + + # Make playground directory + temp_dir_path = tempfile.mkdtemp() + + # Make DB from it + config = {'tar_command':'/bin/tar'} + db = DbBase([temp_collection_path], config) + + specific_overlay_path = os.path.join(temp_dir_path, repo_name) + o = db.select('tar-test-overlay') + + # Actual testcase + o.add(temp_dir_path) + self.assertTrue(os.path.exists(specific_overlay_path)) + # (1/2) Sync with source available + o.sync(temp_dir_path) + self.assertTrue(os.path.exists(specific_overlay_path)) + os.unlink(temp_tarball_path) + try: + # (2/2) Sync with source _not_ available + o.sync(temp_dir_path) + except: + pass + self.assertTrue(os.path.exists(specific_overlay_path)) + o.delete(temp_dir_path) + self.assertFalse(os.path.exists(specific_overlay_path)) + + # Cleanup + os.unlink(temp_collection_path) + os.rmdir(temp_dir_path) + + +if __name__ == '__main__': + filterwarnings('ignore') + unittest.main() + resetwarnings() diff --git a/overlord/tests/pylintrc b/overlord/tests/pylintrc new file mode 100644 index 0000000..b05498a --- /dev/null +++ b/overlord/tests/pylintrc @@ -0,0 +1,19 @@ +[MESSAGES CONTROL] + +# Disable all messages in the listed categories (IRCWEF). +disable-msg-cat=IRC + +# Disable the message(s) with the given id(s). +# :W0613: *Unused argument %r* +# :W0702: *No exception type(s) specified* +# :W0703: *Catch "Exception"* +disable-msg=W0613,W0702,W0703 + + +[REPORTS] + +# Include message's id in output +include-ids=yes + +# Tells whether to display a full report or only the messages +reports=no diff --git a/overlord/tests/testfiles/global-overlays.xml b/overlord/tests/testfiles/global-overlays.xml new file mode 100644 index 0000000..d770692 --- /dev/null +++ b/overlord/tests/testfiles/global-overlays.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" ?> +<layman> + + <overlay + type = "svn" + src = "https://overlays.gentoo.org/svn/dev/wrobel" + contact = "nobody@gentoo.org" + name = "wrobel" + status = "official" + priority = "10"> + + <description> + Test + </description> + + </overlay> + + <overlay + type = "rsync" + src = "rsync://gunnarwrobel.de/wrobel-stable" + contact = "nobody@gentoo.org" + name = "wrobel-stable"> + + <description> + A collection of ebuilds from Gunnar Wrobel [wrobel@gentoo.org]. + </description> + + </overlay> + +</layman> diff --git a/overlord/tests/testfiles/layman-test.tar.bz2 b/overlord/tests/testfiles/layman-test.tar.bz2 Binary files differnew file mode 100644 index 0000000..85ee7fd --- /dev/null +++ b/overlord/tests/testfiles/layman-test.tar.bz2 diff --git a/overlord/tests/testfiles/make.conf b/overlord/tests/testfiles/make.conf new file mode 100644 index 0000000..9a8aac6 --- /dev/null +++ b/overlord/tests/testfiles/make.conf @@ -0,0 +1,345 @@ +# Copyright 1999-2004 Gentoo Foundation +# Distributed under the terms of the GNU General Public License v2 +# $Header: /var/cvsroot/gentoo-src/portage/cnf/make.conf.x86,v 1.5.2.5 2005/04/13 15:28:38 jstubbs Exp $ +# Contains local system settings for Portage system + +# Please review 'man make.conf' for more information. + +# Build-time functionality +# ======================== +# +# The USE variable is used to enable optional build-time functionality. For +# example, quite a few packages have optional X, gtk or GNOME functionality +# that can only be enabled or disabled at compile-time. Gentoo Linux has a +# very extensive set of USE variables described in our USE variable HOWTO at +# http://www.gentoo.org/doc/en/handbook/handbook-x86.xml?part=2&chap=1 +# +# The available list of use flags with descriptions is in your portage tree. +# Use 'less' to view them: --> less /usr/portage/profiles/use.desc <-- +# +# 'ufed' is an ncurses/dialog interface available in portage to make handling +# useflags for you. 'emerge app-portage/ufed' +# +# Example: + +# Use flags will be handled by polymeraZe +USE="-*" + +# Host Setting +# ============ +# +# DO NOT CHANGE THIS SETTING UNLESS YOU ARE USING STAGE1! +# Change this line as appropriate (i686, i586, i486 or i386). +# All modern systems (even Athlons) should use "i686-pc-linux-gnu". +# All K6's are i586. +CHOST="i686-pc-linux-gnu" + +# Host and optimization settings +# ============================== +# +# For optimal performance, enable a CFLAGS setting appropriate for your CPU. +# +# Please note that if you experience strange issues with a package, it may be +# due to gcc's optimizations interacting in a strange way. Please test the +# package (and in some cases the libraries it uses) at default optimizations +# before reporting errors to developers. +# +# -mcpu=<cpu-type> means optimize code for the particular type of CPU without +# breaking compatibility with other CPUs. +# +# -march=<cpu-type> means to take full advantage of the ABI and instructions +# for the particular CPU; this will break compatibility with older CPUs (for +# example, -march=athlon-xp code will not run on a regular Athlon, and +# -march=i686 code will not run on a Pentium Classic. +# +# CPU types supported in gcc-3.2 and higher: athlon-xp, athlon-mp, +# athlon-tbird, athlon, k6, k6-2, k6-3, i386, i486, i586 (Pentium), i686 +# (PentiumPro), pentium, pentium-mmx, pentiumpro, pentium2 (Celeron), +# pentium3, and pentium4. +# +# Note that Gentoo Linux 1.4 and higher include at least gcc-3.2. +# +# CPU types supported in gcc-2.95*: k6, i386, i486, i586 (Pentium), i686 +# (Pentium Pro), pentium, pentiumpro Gentoo Linux 1.2 and below use gcc-2.95* +# +# CRITICAL WARNINGS: ****************************************************** # +# K6 markings are deceptive. Avoid setting -march for them. See Bug #24379. # +# Pentium-M CPU's should not enable sse2 until at least gcc-3.4. Bug 50616. # +# ************************************************************************* # +# +# Decent examples: +# +#CFLAGS="-mcpu=athlon-xp -O3 -pipe" + +CFLAGS="-march=athlon-xp -O3 -pipe" + + +# If you set a CFLAGS above, then this line will set your default C++ flags to +# the same settings. +CXXFLAGS="${CFLAGS}" + +# Advanced Masking +# ================ +# +# Gentoo is using a new masking system to allow for easier stability testing +# on packages. KEYWORDS are used in ebuilds to mask and unmask packages based +# on the platform they are set for. A special form has been added that +# indicates packages and revisions that are expected to work, but have not yet +# been approved for the stable set. '~arch' is a superset of 'arch' which +# includes the unstable, in testing, packages. Users of the 'x86' architecture +# would add '~x86' to ACCEPT_KEYWORDS to enable unstable/testing packages. +# '~ppc', '~sparc' are the unstable KEYWORDS for their respective platforms. +# +# Please note that this is not for development, alpha, beta, nor cvs release +# packages. "Broken" packages will not be added to testing and should not be +# requested to be added. Alternative routes are available to developers +# for experimental packages, and it is at their discretion to use them. +# +# DO NOT PUT ANYTHING BUT YOUR SPECIFIC ~ARCHITECTURE IN THE LIST. +# IF YOU ARE UNSURE OF YOUR ARCH, OR THE IMPLICATIONS, DO NOT MODIFY THIS. +# + +ACCEPT_KEYWORDS="x86" + + +# Portage Directories +# =================== +# +# Each of these settings controls an aspect of portage's storage and file +# system usage. If you change any of these, be sure it is available when +# you try to use portage. *** DO NOT INCLUDE A TRAILING "/" *** +# +# PORTAGE_TMPDIR is the location portage will use for compilations and +# temporary storage of data. This can get VERY large depending upon +# the application being installed. +PORTAGE_TMPDIR=/var/tmp +# +# PORTDIR is the location of the portage tree. This is the repository +# for all profile information as well as all ebuilds. If you change +# this, you must update your /etc/make.profile symlink accordingly. +PORTDIR=/usr/portage +# +# DISTDIR is where all of the source code tarballs will be placed for +# emerges. The source code is maintained here unless you delete +# it. The entire repository of tarballs for gentoo is 9G. This is +# considerably more than any user will ever download. 2-3G is +# a large DISTDIR. +DISTDIR=/usr/distfiles +# +# PKGDIR is the location of binary packages that you can have created +# with '--buildpkg' or '-b' while emerging a package. This can get +# upto several hundred megs, or even a few gigs. +#PKGDIR=${PORTDIR}/packages +# +# PORT_LOGDIR is the location where portage will store all the logs it +# creates from each individual merge. They are stored as NNNN-$PF.log +# in the directory specified. This is disabled until you enable it by +# providing a directory. Permissions will be modified as needed IF the +# directory exists, otherwise logging will be disabled. NNNN is the +# increment at the time the log is created. Logs are thus sequential. +PORT_LOGDIR=/var/log/services/portage.d +# +# PORTDIR_OVERLAY is a directory where local ebuilds may be stored without +# concern that they will be deleted by rsync updates. Default is not +# defined. +PORTDIR_OVERLAY=" +/var/lib/layman/wrobel-stable +$PORTDIR_OVERLAY +/usr/local/portage/ebuilds/testing +/usr/local/portage/ebuilds/stable +/usr/local/portage/kolab2 +/usr/local/portage/gentoo-webapps-overlay/experimental +/usr/local/portage/gentoo-webapps-overlay/production-ready" + +# Fetching files +# ============== +# +# If you need to set a proxy for wget or lukemftp, add the appropriate "export +# ftp_proxy=<proxy>" and "export http_proxy=<proxy>" lines to /etc/profile if +# all users on your system should use them. +# +# Portage uses wget by default. Here are some settings for some alternate +# downloaders -- note that you need to merge these programs first before they +# will be available. +# +# Default fetch command (5 tries, passive ftp for firewall compatibility) +#FETCHCOMMAND="/usr/bin/wget -t 5 --passive-ftp \${URI} -P \${DISTDIR}" +#RESUMECOMMAND="/usr/bin/wget -c -t 5 --passive-ftp \${URI} -P \${DISTDIR}" +# +# Using wget, ratelimiting downloads +#FETCHCOMMAND="/usr/bin/wget -t 5 --passive-ftp --limit-rate=200k \${URI} -P \${DISTDIR}" +#RESUMECOMMAND="/usr/bin/wget -c -t 5 --passive-ftp --limit-rate=200k \${URI} -P \${DISTDIR}" +# +# Lukemftp (BSD ftp): +#FETCHCOMMAND="/usr/bin/lukemftp -s -a -o \${DISTDIR}/\${FILE} \${URI}" +#RESUMECOMMAND="/usr/bin/lukemftp -s -a -R -o \${DISTDIR}/\${FILE} \${URI}" +# + +FETCHCOMMAND="/usr/bin/getdelta.sh \${URI}" + + +# Portage uses GENTOO_MIRRORS to specify mirrors to use for source retrieval. +# The list is a space separated list which is read left to right. If you use +# another mirror we highly recommend leaving the default mirror at the end of +# the list so that portage will fall back to it if the files cannot be found +# on your specified mirror. We _HIGHLY_ recommend that you change this setting +# to a nearby mirror by merging and using the 'mirrorselect' tool. + +GENTOO_MIRRORS="http://pandemonium.tiscali.de/pub/gentoo/ ftp://pandemonium.tiscali.de/pub/gentoo/ ftp://ftp-stud.fht-esslingen.de/pub/Mirrors/gentoo/ http://mir.zyrianes.net/gentoo/ http://ftp.snt.utwente.nl/pub/os/linux/gentoo http://distfiles.gentoo.org http://www.ibiblio.org/pub/Linux/distributions/gentoo" + +# +# Portage uses PORTAGE_BINHOST to specify mirrors for prebuilt-binary packages. +# The list is a single entry specifying the full address of the directory +# serving the tbz2's for your system. Running emerge with either '--getbinpkg' +# or '--getbinpkgonly' will cause portage to retrieve the metadata from all +# packages in the directory specified, and use that data to determine what will +# be downloaded and merged. '-g' or '-gK' are the recommend parameters. Please +# consult the man pages and 'emerge --help' for more information. For FTP, the +# default connection is passive -- If you require an active connection, affix +# an asterisk (*) to the end of the host:port string before the path. +#PORTAGE_BINHOST="http://grp.mirror.site/gentoo/grp/1.4/i686/athlon-xp/" +# This ftp connection is passive ftp. +#PORTAGE_BINHOST="ftp://login:pass@grp.mirror.site/pub/grp/i686/athlon-xp/" +# This ftp connection is active ftp. +#PORTAGE_BINHOST="ftp://login:pass@grp.mirror.site:21*/pub/grp/i686/athlon-xp/" + +# Synchronizing Portage +# ===================== +# +# Each of these settings affects how Gentoo synchronizes your Portage tree. +# Synchronization is handled by rsync and these settings allow some control +# over how it is done. +# +# +# SYNC is the server used by rsync to retrieve a localized rsync mirror +# rotation. This allows you to select servers that are geographically +# close to you, yet still distribute the load over a number of servers. +# Please do not single out specific rsync mirrors. Doing so places undue +# stress on particular mirrors. Instead you may use one of the following +# continent specific rotations: +# +# Default: "rsync://rsync.gentoo.org/gentoo-portage" +# North America: "rsync://rsync.namerica.gentoo.org/gentoo-portage" +# South America: "rsync://rsync.samerica.gentoo.org/gentoo-portage" +# Europe: "rsync://rsync.europe.gentoo.org/gentoo-portage" +# Asia: "rsync://rsync.asia.gentoo.org/gentoo-portage" +# Australia: "rsync://rsync.au.gentoo.org/gentoo-portage" + +SYNC="rsync://rsync.europe.gentoo.org/gentoo-portage" + +# +# RSYNC_RETRIES sets the number of times portage will attempt to retrieve +# a current portage tree before it exits with an error. This allows +# for a more successful retrieval without user intervention most times. +#RSYNC_RETRIES="3" +# +# RSYNC_TIMEOUT sets the length of time rsync will wait before it times out +# on a connection. Most users will benefit from this setting as it will +# reduce the amount of 'dead air' they experience when they run across +# the occasional, unreachable mirror. Dialup users might want to set this +# value up around the 300 second mark. +#RSYNC_TIMEOUT=180 + +# Advanced Features +# ================= +# +# MAKEOPTS provides extra options that may be passed to 'make' when a +# program is compiled. Presently the only use is for specifying +# the number of parallel makes (-j) to perform. The suggested number +# for parallel makes is CPUs+1. +MAKEOPTS="-j2" +# +# PORTAGE_NICENESS provides a default increment to emerge's niceness level. +# Note: This is an increment. Running emerge in a niced environment will +# reduce it further. Default is unset. +PORTAGE_NICENESS=3 +# +# AUTOCLEAN enables portage to automatically clean out older or overlapping +# packages from the system after every successful merge. This is the +# same as running 'emerge -c' after every merge. Set with: "yes" or "no". +# This does not affect the unpacked source. See 'noclean' below. +AUTOCLEAN="yes" +# +# PORTAGE_TMPFS is a location where portage may create temporary files. +# If specified, portage will use this directory whenever possible +# for all rapid operations such as lockfiles and transient data. +# It is _highly_ recommended that this be a tmpfs or ramdisk. Do not +# set this to anything that does not give a significant performance +# enhancement and proper FS compliance for locks and read/write. +# /dev/shm is a glibc mandated tmpfs, and should be a reasonable +# setting for all linux kernel+glibc based systems. +#PORTAGE_TMPFS="/dev/shm" +# +# FEATURES are settings that affect the functionality of portage. Most of +# these settings are for developer use, but some are available to non- +# developers as well. +# +# 'autoaddcvs' causes portage to automatically try to add files to cvs +# that will have to be added later. Done at generation times +# and only has an effect when 'cvs' is also set. +# 'buildpkg' causes binary packages to be created of all packages that +# are being merged. +# 'ccache' enables ccache support via CC. +# 'collision-protect' +# prevents packages from overwriting files that are owned by +# another package or by no package at all. +# 'cvs' causes portage to enable all cvs features (commits, adds), +# and to apply all USE flags in SRC_URI for digests -- for +# developers only. +# 'digest' causes digests to be generated for all packages being merged. +# 'distcc' enables distcc support via CC. +# 'distlocks' enables distfiles locking using fcntl or hardlinks. This +# is enabled by default. Tools exist to help clean the locks +# after crashes: /usr/lib/portage/bin/clean_locks. +# 'fixpackages' allows portage to fix binary packages that are stored in +# PKGDIR. This can consume a lot of time. 'fixpackages' is +# also a script that can be run at any given time to force +# the same actions. +# 'gpg' enables basic verification of Manifest files using gpg. +# This features is UNDER DEVELOPMENT and reacts to features +# of strict and severe. Heavy use of gpg sigs is coming. +# 'keeptemp' prevents the clean phase from deleting the temp files ($T) +# from a merge. +# 'keepwork' prevents the clean phase from deleting the WORKDIR. +# 'maketest' causes ebuilds to perform testing phases if they are capable +# of it. Some packages support this automaticaly via makefiles. +# 'noauto' causes ebuild to perform only the action requested and +# not any other required actions like clean or unpack -- for +# debugging purposes only. +# 'noclean' prevents portage from removing the source and temporary files +# after a merge -- for debugging purposes only. +# 'nostrip' prevents the stripping of binaries. +# 'notitles' disables xterm titlebar updates (which contain status info). +# 'sandbox' enables sandboxing when running emerge and ebuild. +# 'strict' causes portage to react strongly to conditions that are +# potentially dangerous, like missing/incorrect Manifest files. +# 'userpriv' allows portage to drop root privileges while it is compiling, +# as a security measure. As a side effect this can remove +# sandbox access violations for users. +# 'usersandbox' enables sandboxing while portage is running under userpriv. +#FEATURES="sandbox buildpkg ccache distcc userpriv usersandbox notitles noclean noauto cvs keeptemp keepwork autoaddcvs" +FEATURES="sandbox ccache userprivs distlocks cvs" +# +# CCACHE_SIZE sets the space use limitations for ccache. The default size is +# 2G, and will be set if not defined otherwise and ccache is in features. +# Portage will set the default ccache dir if it is not present in the +# user's environment, for userpriv it sets: ${PORTAGE_TMPDIR}/ccache +# (/var/tmp/ccache), and for regular use the default is /root/.ccache. +# Sizes are specified with 'G' 'M' or 'K'. +# '2G' for 2 gigabytes, '2048M' for 2048 megabytes (same as 2G). +CCACHE_SIZE="1G" +# +# DISTCC_DIR sets the temporary space used by distcc. +#DISTCC_DIR="${PORTAGE_TMPDIR}/.distcc" +# +# RSYNC_EXCLUDEFROM is a file that portage will pass to rsync when it updates +# the portage tree. Specific chunks of the tree may be excluded from +# consideration. This may cause dependency failures if you are not careful. +# The file format is one pattern per line, blanks and ';' or '#' lines are +# comments. See 'man rsync' for more details on the exclude-from format. +#RSYNC_EXCLUDEFROM=/etc/portage/rsync_excludes +EBEEP_IGNORE=yes + +CONFIG_PROTECT_MASK="/usr/X11R6/lib/X11" + + diff --git a/overlord/tests/testfiles/overlays_bug_184449.xml b/overlord/tests/testfiles/overlays_bug_184449.xml new file mode 100644 index 0000000..c8dff2d --- /dev/null +++ b/overlord/tests/testfiles/overlays_bug_184449.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" ?> +<layman> + + <overlay + type = "svn" + src = "https://overlays.gentoo.org/svn/dev/wrobel" + contact = "nobody@gentoo.org" + name = "wrÖbel" + status = "official" + priority = "10"> + + <description> + Test ä + </description> + + </overlay> + + +</layman> diff --git a/overlord/tests/testfiles/overlays_bug_286290.xml b/overlord/tests/testfiles/overlays_bug_286290.xml new file mode 100644 index 0000000..1d4bd1b --- /dev/null +++ b/overlord/tests/testfiles/overlays_bug_286290.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> +<layman> + <overlay contact="media-video@gentoo.org" + name="multimedia" + src="git://gitorious.org/gentoo-multimedia/gentoo-multimedia.git" + status="official" + type="git"> + <link>http://gitorious.org/gentoo-multimedia</link> + <description>Repository for development of (mostly bleeding-edge) + multimedia packages for Gentoo Linux. This is the official overlay + for Gentoo’s media herds.</description> + </overlay> +</layman> diff --git a/overlord/tests/testfiles/subpath-1.xml b/overlord/tests/testfiles/subpath-1.xml new file mode 100644 index 0000000..ddb0e3e --- /dev/null +++ b/overlord/tests/testfiles/subpath-1.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<layman> + <overlay + name="b_name" + contact="b_owner@example.org" + type="tar" + src="http://example.org/b.tar.gz" + subpath="b_path"> + <description>b_desc</description> + </overlay> + <overlay + name="c_name" + contact="c_owner@example.org" + type="cvs" + src=":pserver:username@example.org:/usr/local/cvs-repository" + subpath="c_path"> + <description>c_desc</description> + </overlay> +</layman> diff --git a/overlord/tests/testfiles/subpath-2.xml b/overlord/tests/testfiles/subpath-2.xml new file mode 100644 index 0000000..aa11497 --- /dev/null +++ b/overlord/tests/testfiles/subpath-2.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE repositories SYSTEM "/dtd/repositories.dtd"> +<repositories xmlns="" version="1.0"> + <repo> + <name>b_name</name> + <description>b_desc</description> + <owner> + <email>b_owner@example.org</email> + </owner> + <source type="tar">http://example.org/b.tar.gz</source> + <subpath>b_path</subpath> + </repo> + <repo> + <name>c_name</name> + <description>c_desc</description> + <owner> + <email>c_owner@example.org</email> + </owner> + <source type="cvs">:pserver:username@example.org:/usr/local/cvs-repository</source> + <subpath>c_path</subpath> + </repo> +</repositories> diff --git a/overlord/utils.py b/overlord/utils.py new file mode 100644 index 0000000..c5a2019 --- /dev/null +++ b/overlord/utils.py @@ -0,0 +1,186 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +################################################################################# +# POLYMERAZE XML UTILITIES +################################################################################# +# File: xml.py +# +# Utilities to deal with xml nodes. +# +# Copyright: +# (c) 2005 - 2008 Gunnar Wrobel +# (c) 2009 Sebastian Pipping +# (c) 2009 Christian Groschupp +# Distributed under the terms of the GNU General Public License v2 +# +# Author(s): +# Gunnar Wrobel <wrobel@gentoo.org> +# Sebastian Pipping <sebastian@pipping.org> +# Christian Groschupp <christian@groschupp.org> +# + +'''Utility functions to deal with xml nodes. ''' + +__version__ = '$Id: utils.py 236 2006-09-05 20:39:37Z wrobel $' + +#=============================================================================== +# +# Dependencies +# +#------------------------------------------------------------------------------- + +import types, re, os +import sys +import locale +import codecs + +from overlord.debug import OUT + + +#=============================================================================== +# +# Helper functions +# +#------------------------------------------------------------------------------- + +def decode_selection(selection): + '''utility function to decode a list of strings + accoring to the filesystem encoding + ''' + # fix None passed in, return an empty list + selection = selection or [] + enc = sys.getfilesystemencoding() + if enc: + return [i.decode(enc) for i in selection] + return selection + + +def encoder(unicode_text, _encoding_): + return codecs.encode(unicode_text, _encoding_, 'replace') + + +def get_encoding(output): + if hasattr(output, 'encoding') \ + and output.encoding != None: + return output.encoding + else: + return locale.getpreferredencoding() + + +def pad(string, length): + '''Pad a string with spaces.''' + if len(string) <= length: + return string + ' ' * (length - len(string)) + else: + return string[:length - 3] + '...' + + +def terminal_width(): + '''Determine width of terminal window.''' + try: + width = int(os.environ['COLUMNS']) + if width > 0: + return width + except: + pass + try: + import struct, fcntl, termios + query = struct.pack('HHHH', 0, 0, 0, 0) + response = fcntl.ioctl(1, termios.TIOCGWINSZ, query) + width = struct.unpack('HHHH', response)[1] + if width > 0: + return width + except: + pass + return 80 + + +def ensure_unicode(obj, encoding='utf-8'): + if isinstance(obj, basestring): + if not isinstance(obj, unicode): + obj = unicode(obj, encoding) + return obj + +# From <http://effbot.org/zone/element-lib.htm> +# BEGIN +def indent(elem, level=0): + i = "\n" + level*" " + if len(elem): + if not elem.text or not elem.text.strip(): + elem.text = i + " " + if not elem.tail or not elem.tail.strip(): + elem.tail = i + for elem in elem: + indent(elem, level+1) + if not elem.tail or not elem.tail.strip(): + elem.tail = i + else: + if level and (not elem.tail or not elem.tail.strip()): + elem.tail = i +# END + +def path(path_elements): + ''' + Concatenate a path from several elements. + + >>> path([]) + '' + >>> path(['a']) + 'a' + >>> path(['a','b']) + 'a/b' + >>> path(['a/','b']) + 'a/b' + >>> path(['/a/','b']) + '/a/b' + >>> path(['/a/','b/']) + '/a/b' + >>> path(['/a/','b/']) + '/a/b' + >>> path(['/a/','/b/']) + '/a/b' + >>> path(['/a/','/b','c/']) + '/a/b/c' + ''' + pathname = '' + + if type(path_elements) in types.StringTypes: + path_elements = [path_elements] + + # Concatenate elements and seperate with / + for i in path_elements: + pathname += i + '/' + + # Replace multiple consecutive slashes + pathname = re.compile('/+').sub('/', pathname) + + # Remove the final / if there is one + if pathname and pathname[-1] == '/': + pathname = pathname[:-1] + + return pathname + +def delete_empty_directory(mdir, output=OUT): + if os.path.exists(mdir) and not os.listdir(mdir): + # Check for sufficient privileges + if os.access(mdir, os.W_OK): + output.info('Deleting _empty_ directory "%s"' % mdir, 2) + try: + os.rmdir(mdir) + except OSError, error: + output.warn(str(error)) + else: + output.warn('Insufficient permissions to delete _empty_ folder "%s".' % mdir) + import getpass + if getpass.getuser() != 'root': + output.warn('Hint: You are not root.') + +#=============================================================================== +# +# Testing +# +#------------------------------------------------------------------------------- + +if __name__ == '__main__': + import doctest, sys + doctest.testmod(sys.modules[__name__]) diff --git a/overlord/version.py b/overlord/version.py new file mode 100644 index 0000000..abbea1a --- /dev/null +++ b/overlord/version.py @@ -0,0 +1,26 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +################################################################################# +# overlord VERSION +################################################################################# +# File: version.py +# +# Current version number +# +# Copyright: +# (c) 2005 - 2009 Gunnar Wrobel +# (c) 2009 Sebastian Pipping +# Distributed under the terms of the GNU General Public License v2 +# +# Author(s): +# Gunnar Wrobel <wrobel@gentoo.org> +# Sebastian Pipping <sebastian@pipping.org> +# + +__version__ = "$Id: version.py 309 2007-04-09 16:23:38Z wrobel $" + + +VERSION = '1.4.1-API' + +if __name__ == '__main__': + print VERSION |