Francesco Banconi has proposed merging lp:~yellow/launchpad/lxcsetup into lp:launchpad.
Requested reviews: Launchpad code reviewers (launchpad-reviewers) For more details, see: https://code.launchpad.net/~yellow/launchpad/lxcsetup/+merge/89660 = Summary = This branch adds a script that can be used to set up a Launchpad environment inside a LXC, useful for testing Launchpad. == Proposed fix == https://dev.launchpad.net/ParallelTests describes how LXC containers offer a cheap way to run tests in parallel using ephemeral instances, obtaining the required isolation to workaround the existing globals (shared work dirs, hardcoded tcp ports, etc.). The `setuplxc.py` script creates a Launchpad environment from scratch, ready to be used by ephemerals (e.g. in a buildbot slave context). == Pre-implementation notes == During development we realized that the same approach (with few changes) should work to set up a Launchpad development environment. The developer can just: - run the script (passing his current local username as user), e.g.:: ./setuplxc.py -u username -e [email protected] -n 'Firstname Lastname' -c lp-devel -d /home/username/lp-deps /home/username/lp-branches/ - ssh into the container (in the example above: ssh lp-devel) - cd ~/lp-branches/devel - make schema - make run - start hacking == Implementation details == utilities/setuplxc.py: * the script is implemented in Python * requires Python 2.7 * must be run as root * help on required arguments: utilities/setuplxc.py -h utilities/launchpad-database-setup * refactored so that it can be run by root == Tests == The script uses internal helper functions providing doctests. To run them:: python -m doctest -v utilities/setuplxc.py A more extensive and complete testing approach would require to setup a precise KVM. == Demo and Q/A == To demo and Q/A this change, do the following: * Install precise in a virtual machine (e.g. KVM) * Copy the script inside the virtual machine, e.g. for kvm:: kvm -hda precise_HDA.img -boot c -m 2000 -redir tcp:2222::22 & scp -P 2222 /utilities/setuplxc.py root@localhost:/tmp/ * Run the script, e.g.:: ssh -p 2222 root@localhost cd /tmp/ ./setuplxc [arguments] == lint == = Launchpad lint = Checking for conflicts and issues in changed files. Linting changed files: utilities/setuplxc.py -- https://code.launchpad.net/~yellow/launchpad/lxcsetup/+merge/89660 Your team Launchpad code reviewers is requested to review the proposed merge of lp:~yellow/launchpad/lxcsetup into lp:launchpad.
=== modified file 'utilities/launchpad-database-setup' --- utilities/launchpad-database-setup 2011-09-06 01:38:11 +0000 +++ utilities/launchpad-database-setup 2012-01-23 11:06:17 +0000 @@ -18,9 +18,15 @@ # https://dev.launchpad.net/DatabaseSetup which are intended for # initial Launchpad setup on an otherwise unconfigured postgresql instance +if [ $(id -u) = 0 ]; then + SUDO='' +else + SUDO='sudo' +fi + for pgversion in 8.4 8.3 8.2 do - sudo grep -q "^auto" /etc/postgresql/$pgversion/main/start.conf + $SUDO grep -q "^auto" /etc/postgresql/$pgversion/main/start.conf if [ $? -eq 0 ]; then break fi @@ -35,31 +41,31 @@ echo "Using postgres $pgversion" # Make sure that we have the correct version running on port 5432 -sudo grep -q "port.*5432" /etc/postgresql/$pgversion/main/postgresql.conf +$SUDO grep -q "port.*5432" /etc/postgresql/$pgversion/main/postgresql.conf if [ $? -ne 0 ]; then echo "Please check /etc/postgresql/$pgversion/main/postgresql.conf and" echo "ensure postgres is running on port 5432." fi; if [ -e /etc/init.d/postgresql-$pgversion ]; then - sudo /etc/init.d/postgresql-$pgversion stop + $SUDO /etc/init.d/postgresql-$pgversion stop else # This is Maverick. - sudo /etc/init.d/postgresql stop $pgversion + $SUDO /etc/init.d/postgresql stop $pgversion fi echo Purging postgresql data... -sudo pg_dropcluster $pgversion main --stop-server +$SUDO pg_dropcluster $pgversion main --stop-server echo Re-creating postgresql database... # Setting locale to C to make the server run in that locale. -LC_ALL=C sudo pg_createcluster $pgversion main --encoding UNICODE +LC_ALL=C $SUDO pg_createcluster $pgversion main --encoding UNICODE echo Applying postgresql configuration changes... -sudo cp -a /etc/postgresql/$pgversion/main/pg_hba.conf \ +$SUDO cp -a /etc/postgresql/$pgversion/main/pg_hba.conf \ /etc/postgresql/$pgversion/main/pg_hba.conf.old -sudo grep -q Launchpad /etc/postgresql/$pgversion/main/pg_hba.conf || \ -sudo patch /etc/postgresql/$pgversion/main/pg_hba.conf <<'EOF' +$SUDO grep -q Launchpad /etc/postgresql/$pgversion/main/pg_hba.conf || \ +$SUDO patch /etc/postgresql/$pgversion/main/pg_hba.conf <<'EOF' --- pg_hba.conf 2005-11-02 17:33:08.000000000 -0800 +++ /tmp/pg_hba.conf 2005-11-03 07:32:46.932400423 -0800 @@ -58,7 +58,10 @@ @@ -75,13 +81,13 @@ EOF -sudo chown --reference=/etc/postgresql/$pgversion/main/pg_hba.conf.old \ +$SUDO chown --reference=/etc/postgresql/$pgversion/main/pg_hba.conf.old \ /etc/postgresql/$pgversion/main/pg_hba.conf -sudo chmod --reference=/etc/postgresql/$pgversion/main/pg_hba.conf.old \ +$SUDO chmod --reference=/etc/postgresql/$pgversion/main/pg_hba.conf.old \ /etc/postgresql/$pgversion/main/pg_hba.conf -sudo grep -q Launchpad /etc/postgresql/$pgversion/main/postgresql.conf || \ -sudo tee -a /etc/postgresql/$pgversion/main/postgresql.conf <<'EOF' +$SUDO grep -q Launchpad /etc/postgresql/$pgversion/main/postgresql.conf || \ +$SUDO tee -a /etc/postgresql/$pgversion/main/postgresql.conf <<'EOF' ## ## Launchpad configuration @@ -98,25 +104,29 @@ if [ "$pgversion" = 8.2 -o "$pgversion" = 8.3 ] then - sudo grep -q '^[[:space:]]*max_fsm_relations' /etc/postgresql/$pgversion/main/postgresql.conf || \ - sudo tee -a /etc/postgresql/$pgversion/main/postgresql.conf <<'EOF' + $SUDO grep -q '^[[:space:]]*max_fsm_relations' /etc/postgresql/$pgversion/main/postgresql.conf || \ + $SUDO tee -a /etc/postgresql/$pgversion/main/postgresql.conf <<'EOF' max_fsm_relations=2000 EOF fi if [ -e /etc/init.d/postgresql-$pgversion ]; then - sudo /etc/init.d/postgresql-$pgversion start + $SUDO /etc/init.d/postgresql-$pgversion start else # This is Maverick. - sudo /etc/init.d/postgresql start $pgversion + $SUDO /etc/init.d/postgresql start $pgversion fi echo Waiting 10 seconds for postgresql to come up... sleep 10 echo Creating postgresql user $USER -sudo -u postgres /usr/lib/postgresql/$pgversion/bin/createuser -a -d $USER +if [ $(id -u) = 0 ]; then + su postgres -c "/usr/lib/postgresql/$pgversion/bin/createuser -a -d $USER" +else + sudo -u postgres /usr/lib/postgresql/$pgversion/bin/createuser -a -d $USER +fi echo echo Looks like everything went ok. === added file 'utilities/setuplxc.py' --- utilities/setuplxc.py 1970-01-01 00:00:00 +0000 +++ utilities/setuplxc.py 2012-01-23 11:06:17 +0000 @@ -0,0 +1,554 @@ +#!/usr/bin/env python +# Copyright 2012 Canonical Ltd. This software is licensed under the +# GNU Affero General Public License version 3 (see the file LICENSE). + +"""Create an LXC test environment for Launchpad testing.""" + +__metaclass__ = type +__all__ = [ + 'cd', + 'create_lxc', + 'error', + 'file_append', + 'file_insert', + 'get_container_path', + 'get_user_ids', + 'initialize_host', + 'initialize_lxc', + 'Namespace', + 'ssh', + 'stop_lxc', + 'su', + 'user_exists', + ] + +# This script is run as root. +# To run doctests: python -m doctest -v setuplxc.py + +from collections import namedtuple, OrderedDict +from contextlib import contextmanager +import argparse +import os +import pwd +import shutil +import subprocess +import sys +import time + + +DEPENDENCIES_DIR = '~/dependencies' +DHCP_FILE = '/etc/dhcp/dhclient.conf' +HOST_PACKAGES = ['ssh', 'lxc', 'libvirt-bin', 'bzr', 'language-pack-en'] +HOSTS_FILE = '/etc/hosts' +LP_APACHE_MODULES = 'proxy proxy_http rewrite ssl deflate headers' +LP_APACHE_ROOTS = ( + '/var/tmp/bazaar.launchpad.dev/static', + '/var/tmp/archive', + '/var/tmp/ppa', + ) +LP_CHECKOUT = 'devel' +LP_REPOSITORY = 'lp:launchpad' +LP_SOURCE_DEPS = ( + 'http://bazaar.launchpad.net/~launchpad/lp-source-dependencies/trunk') +LXC_CONFIG_TEMPLATE = '/etc/lxc/local.conf' +LXC_GATEWAY = '10.0.3.1' +LXC_GUEST_OS = 'lucid' +LXC_HOSTS_CONTENT = ( + ('127.0.0.88', + 'launchpad.dev answers.launchpad.dev archive.launchpad.dev ' + 'api.launchpad.dev bazaar-internal.launchpad.dev beta.launchpad.dev ' + 'blueprints.launchpad.dev bugs.launchpad.dev code.launchpad.dev ' + 'feeds.launchpad.dev id.launchpad.dev keyserver.launchpad.dev ' + 'lists.launchpad.dev openid.launchpad.dev ' + 'ubuntu-openid.launchpad.dev ppa.launchpad.dev ' + 'private-ppa.launchpad.dev testopenid.dev translations.launchpad.dev ' + 'xmlrpc-private.launchpad.dev xmlrpc.launchpad.dev'), + ('127.0.0.99', 'bazaar.launchpad.dev'), + ) +LXC_NAME = 'lptests' +LXC_OPTIONS = ( + ('lxc.network.type', 'veth'), + ('lxc.network.link', 'lxcbr0'), + ('lxc.network.flags', 'up'), + ) +LXC_PATH = '/var/lib/lxc/' +LXC_REPOS = ( + 'deb http://archive.ubuntu.com/ubuntu ' + 'lucid main universe multiverse', + 'deb http://archive.ubuntu.com/ubuntu ' + 'lucid-updates main universe multiverse', + 'deb http://archive.ubuntu.com/ubuntu ' + 'lucid-security main universe multiverse', + 'deb http://ppa.launchpad.net/launchpad/ppa/ubuntu lucid main', + 'deb http://ppa.launchpad.net/bzr/ppa/ubuntu lucid main', + ) +RESOLV_FILE = '/etc/resolv.conf' + + +Env = namedtuple('Env', 'uid gid home') + + +@contextmanager +def ssh(location, user=None): + """Return a callable that can be used to run shell commands into another + host using ssh. + + The ssh `location` and, optionally, `user` must be given. + If the user is None then the current user is used for the connection. + """ + if user is not None: + location = '%s@%s' % (user, location) + + def _sshcall(cmd): + sshcmd = ( + 'ssh', + '-o', 'StrictHostKeyChecking=no', + '-o', 'UserKnownHostsFile=/dev/null', + location, + '--', cmd, + ) + return subprocess.call(sshcmd) + + yield _sshcall + + +def get_user_ids(user): + """Return the uid and gid of given `user`, e.g.:: + + >>> get_user_ids('root') + (0, 0) + """ + userdata = pwd.getpwnam(user) + return userdata.pw_uid, userdata.pw_gid + + +@contextmanager +def su(user): + """A context manager to temporary run the Python interpreter as a + different user. + """ + uid, gid = get_user_ids(user) + os.setegid(gid) + os.seteuid(uid) + current_home = os.getenv('HOME') + home = os.path.join(os.path.sep, 'home', user) + os.environ['HOME'] = home + yield Env(uid, gid, home) + os.setegid(os.getgid()) + os.seteuid(os.getuid()) + os.environ['HOME'] = current_home + + +@contextmanager +def cd(directory): + """A context manager to temporary change current working dir, e.g.:: + + >>> import os + >>> os.chdir('/tmp') + >>> with cd('/bin'): print os.getcwd() + /bin + >>> os.getcwd() + '/tmp' + """ + cwd = os.getcwd() + os.chdir(directory) + yield + os.chdir(cwd) + + +def get_container_path(lxcname, path='', base_path=LXC_PATH): + """Return the path of LXC container called `lxcname`. + If a `path` is given, return that path inside the container, e.g.:: + + >>> get_container_path('mycontainer') + '/var/lib/lxc/mycontainer/rootfs/' + >>> get_container_path('mycontainer', '/etc/apt/') + '/var/lib/lxc/mycontainer/rootfs/etc/apt/' + >>> get_container_path('mycontainer', 'home') + '/var/lib/lxc/mycontainer/rootfs/home' + """ + return os.path.join(base_path, lxcname, 'rootfs', path.lstrip('/')) + + +def file_insert(filename, line): + """Insert given `line`, if not present, at the beginning of `filename`, + e.g.:: + + >>> import tempfile + >>> f = tempfile.NamedTemporaryFile('w', delete=False) + >>> f.write('line1\\n') + >>> f.close() + >>> file_insert(f.name, 'line0\\n') + >>> open(f.name).read() + 'line0\\nline1\\n' + >>> file_insert(f.name, 'line0\\n') + >>> open(f.name).read() + 'line0\\nline1\\n' + """ + with open(filename, 'r+') as f: + lines = f.readlines() + if lines[0] != line: + lines.insert(0, line) + f.seek(0) + f.writelines(lines) + + +def file_append(filename, content): + """Append given `content`, if not present, at the end of `filename`, + e.g.:: + + >>> import tempfile + >>> f = tempfile.NamedTemporaryFile('w', delete=False) + >>> f.write('line1\\n') + >>> f.close() + >>> file_append(f.name, 'new content\\n') + >>> open(f.name).read() + 'line1\\nnew content\\n' + >>> file_append(f.name, 'content') + >>> open(f.name).read() + 'line1\\nnew content\\n' + """ + with open(filename, 'r+') as f: + current_content = f.read() + if content not in current_content: + f.seek(0) + f.write(current_content + content) + + +def user_exists(username): + """Return True if given `username` exists, e.g.:: + + >>> user_exists('root') + True + >>> user_exists('_this_user_does_not_exist_') + False + """ + try: + pwd.getpwnam(username) + except KeyError: + return False + return True + + +def error(msg): + """Print out the error message and quit the script.""" + print 'ERROR: %s' % msg + sys.exit(1) + + +parser = argparse.ArgumentParser(description=__doc__) +parser.add_argument( + '-u', '--user', required=True, + help='The name of the system user to be created or updated.') +parser.add_argument( + '-e', '--email', required=True, + help='The email of the user, used for bzr whoami.') +parser.add_argument( + '-n', '--name', required=True, + help='The full name of the user, used fo bzr whoami.') +parser.add_argument( + '-l', '--lpuser', + help=('The name of the Launchpad user that will be used to check out ' + 'dependencies. If not provided, the system user name is used.')) +parser.add_argument( + '-v', '--private-key', + help=('The SSH private key for the Launchpad user (without passphrase). ' + 'If the system user already exists with SSH key pair set up, ' + 'this argument can be omitted.')) +parser.add_argument( + '-b', '--public-key', + help=('The SSH public key for the Launchpad user. ' + 'If the system user already exists with SSH key pair set up, ' + 'this argument can be omitted.')) +parser.add_argument( + '-a', '--actions', nargs='+', + choices=('initialize_host', 'create_lxc', 'initialize_lxc', 'stop_lxc'), + help='Only for debugging. Call one or more internal functions.') +parser.add_argument( + '-c', '--lxc-name', default=LXC_NAME, + metavar='LXC_NAME (default=%s)' % LXC_NAME, + help='The LXC container name.') +parser.add_argument( + '-d', '--dependencies-dir', default=DEPENDENCIES_DIR, + metavar='DEPENDENCIES_DIR (default=%s)' % DEPENDENCIES_DIR, + help=('The directory of the Launchpad dependencies to be created. ' + 'The directory must reside under the home directory of the ' + 'given user (see -u argument).')) +parser.add_argument( + 'directory', + help=('The directory of the Launchpad repository to be created. ' + 'The directory must reside under the home directory of the ' + 'given user (see -u argument).')) + + +def initialize_host( + user, fullname, email, lpuser, private_key, public_key, + dependencies_dir, directory): + """Initialize host machine.""" + # Install necessary deb packages. This requires Oneiric or later. + subprocess.call(['apt-get', 'update']) + subprocess.call(['apt-get', '-y', 'install'] + HOST_PACKAGES) + # Create the user (if he does not exist). + if not user_exists(user): + subprocess.call(['useradd', '-m', '-s', '/bin/bash', '-U', user]) + # Generate root ssh keys if they do not exist. + if not os.path.exists('/root/.ssh/id_rsa.pub'): + subprocess.call([ + 'ssh-keygen', '-q', '-t', 'rsa', '-N', '', + '-f', '/root/.ssh/id_rsa']) + with su(user) as env: + # Set up the user's ssh directory. The ssh key must be associated + # with the lpuser's Launchpad account. + ssh_dir = os.path.join(env.home, '.ssh') + if not os.path.exists(ssh_dir): + os.makedirs(ssh_dir) + priv_file = os.path.join(ssh_dir, 'id_rsa') + pub_file = os.path.join(ssh_dir, 'id_rsa.pub') + auth_file = os.path.join(ssh_dir, 'authorized_keys') + known_hosts = os.path.join(ssh_dir, 'known_hosts') + known_host_content = subprocess.check_output([ + 'ssh-keyscan', '-t', 'rsa', 'bazaar.launchpad.net']) + for filename, contents, mode in [ + (priv_file, private_key, 'w'), + (pub_file, public_key, 'w'), + (auth_file, public_key, 'a'), + (known_hosts, known_host_content, 'a'), + ]: + with open(filename, mode) as f: + f.write('%s\n' % contents) + os.chmod(filename, 0644) + os.chmod(priv_file, 0600) + # Set up bzr and Launchpad authentication. + subprocess.call(['bzr', 'whoami', '"%s <%s>"' % (fullname, email)]) + subprocess.call(['bzr', 'lp-login', lpuser]) + # Set up the repository. + if not os.path.exists(directory): + os.makedirs(directory) + subprocess.call(['bzr', 'init-repo', directory]) + checkout_dir = os.path.join(directory, LP_CHECKOUT) + # bzr branch does not work well with seteuid. + os.system( + "su - %s -c 'bzr branch %s %s'" % (user, LP_REPOSITORY, checkout_dir)) + with su(user) as env: + # Set up source dependencies. + os.makedirs('%s/eggs' % dependencies_dir) + os.makedirs('%s/yui' % dependencies_dir) + os.makedirs('%s/sourcecode' % dependencies_dir) + with cd(dependencies_dir): + subprocess.call([ + 'bzr', 'co', '--lightweight', + LP_SOURCE_DEPS, 'download-cache']) + # Update resolv file in order to get the ability to ssh into the LXC + # container using its name. + file_insert(RESOLV_FILE, 'nameserver %s\n' % LXC_GATEWAY) + file_append(DHCP_FILE, 'prepend domain-name-servers %s;\n' % LXC_GATEWAY) + + +def create_lxc(user, lxcname): + """Create the LXC container that will be used for ephemeral instances.""" + # Container configuration template. + content = ''.join('%s=%s\n' % i for i in LXC_OPTIONS) + with open(LXC_CONFIG_TEMPLATE, 'w') as f: + f.write(content) + # Creating container. + exit_code = subprocess.call([ + 'lxc-create', + '-t', 'ubuntu', + '-n', lxcname, + '-f', LXC_CONFIG_TEMPLATE, + '--', + '-r %s -a i386 -b %s' % (LXC_GUEST_OS, user) + ]) + if exit_code: + error('Unable to create the LXC container.') + subprocess.call(['lxc-start', '-n', lxcname, '-d']) + # Set up root ssh key. + user_authorized_keys = os.path.join( + os.path.sep, 'home', user, '.ssh/authorized_keys') + with open(user_authorized_keys, 'a') as f: + f.write(open('/root/.ssh/id_rsa.pub').read()) + dst = get_container_path(lxcname, '/root/.ssh/') + if not os.path.exists(dst): + os.makedirs(dst) + shutil.copy(user_authorized_keys, dst) + # SSH into the container. + with ssh(lxcname, user) as sshcall: + timeout = 60 + while timeout: + if not sshcall('true'): + break + timeout -= 1 + time.sleep(1) + else: + error('Unable to SSH into LXC.') + + +def initialize_lxc(user, dependencies_dir, directory, lxcname): + """Set up the Launchpad development environment inside the LXC container. + """ + with ssh(lxcname) as sshcall: + # APT repository update. + sources = get_container_path(lxcname, '/etc/apt/sources.list') + with open(sources, 'w') as f: + f.write('\n'.join(LXC_REPOS)) + # XXX frankban 2012-01-13 - Bug 892892: upgrading mountall in LXC + # containers currently does not work. + sshcall("echo 'mountall hold' | dpkg --set-selections") + # Upgrading packages. + sshcall( + 'apt-get update && ' + 'DEBIAN_FRONTEND=noninteractive ' + 'apt-get -y --allow-unauthenticated install language-pack-en') + sshcall( + 'DEBIAN_FRONTEND=noninteractive ' + 'apt-get -y --allow-unauthenticated install ' + 'bzr launchpad-developer-dependencies apache2 apache2-mpm-worker') + # User configuration. + sshcall('adduser %s sudo' % user) + pygetgid = 'import pwd; print pwd.getpwnam("%s").pw_gid' % user + gid = "`python -c '%s'`" % pygetgid + sshcall('addgroup --gid %s %s' % (gid, user)) + with ssh(lxcname, user) as sshcall: + # Set up Launchpad dependencies. + checkout_dir = os.path.join(directory, LP_CHECKOUT) + sshcall( + 'cd %s && utilities/update-sourcecode %s/sourcecode' % ( + checkout_dir, dependencies_dir)) + sshcall( + 'cd %s && utilities/link-external-sourcecode %s' % ( + checkout_dir, dependencies_dir)) + # Create Apache document roots, to avoid warnings. + sshcall(' && '.join('mkdir -p %s' % i for i in LP_APACHE_ROOTS)) + with ssh(lxcname) as sshcall: + # Set up Apache modules. + for module in LP_APACHE_MODULES.split(): + sshcall('a2enmod %s' % module) + # Launchpad database setup. + sshcall( + 'cd %s && utilities/launchpad-database-setup %s' % ( + checkout_dir, user)) + with ssh(lxcname, user) as sshcall: + sshcall('cd %s && make' % checkout_dir) + # Set up container hosts file. + lines = ['%s\t%s' % (ip, names) for ip, names in LXC_HOSTS_CONTENT] + lxc_hosts_file = get_container_path(lxcname, HOSTS_FILE) + file_append(lxc_hosts_file, '\n'.join(lines)) + # Make and install launchpad. + with ssh(lxcname) as sshcall: + sshcall('cd %s && make install' % checkout_dir) + + +def stop_lxc(lxcname): + """Stop the lxc instance named `lxcname`.""" + with ssh(lxcname) as sshcall: + sshcall('poweroff') + time.sleep(5) + subprocess.call(['lxc-stop', '-n', lxcname]) + + +def main( + user, fullname, email, lpuser, private_key, public_key, actions, + lxc_name, dependencies_dir, directory): + function_args_map = OrderedDict(( + ('initialize_host', (user, fullname, email, lpuser, private_key, + public_key, dependencies_dir, directory)), + ('create_lxc', (user, lxc_name)), + ('initialize_lxc', (user, dependencies_dir, directory, lxc_name)), + ('stop_lxc', (lxc_name,)), + )) + if actions is None: + actions = function_args_map.keys() + scope = globals() + for action in actions: + scope[action](*function_args_map[action]) + + +class Namespace(object): + """A namespace for argparse. + + Add methods for further arguments validation. + This class implements ssh key validation, e.g.:: + + >>> args = parser.parse_args('-u example_user -e [email protected] ' + ... '-n exampleuser -v PRIVATE -b PUBLIC ' + ... '/home/example_user/launchpad/'.split(), + ... namespace=Namespace()) + >>> args.are_valid() + True + >>> args = parser.parse_args('-u example_user -e [email protected] ' + ... '-n exampleuser -b PUBLIC ' + ... '/home/example_user/launchpad/'.split(), + ... namespace=Namespace()) + >>> args.are_valid() + False + >>> args.error_message # doctest: +ELLIPSIS + 'argument private_key ...' + + and directory validation:: + + >>> args = parser.parse_args('-u example_user -e [email protected] ' + ... '-n exampleuser -v PRIVATE -b PUBLIC ' + ... '/home/'.split(), + ... namespace=Namespace()) + >>> args.are_valid() + False + >>> args.error_message # doctest: +ELLIPSIS + 'argument directory ...' + """ + _errors = None + + def __repr__(self): + return repr(vars(self)) + + @property + def error_message(self): + return '\n'.join(self._errors) + + def _get_ssh_key(self, attr, filename): + value = getattr(self, attr) + if value: + return value.decode('string-escape') + try: + return open(filename).read() + except IOError: + self._errors.append( + 'argument %s is required if the system user ' + 'does not exists with SSH key pair set up.' % attr) + + def _get_directory(self, attr, home_dir): + directory = getattr(self, attr).replace('~', home_dir) + if not directory.startswith(home_dir + os.path.sep): + self._errors.append('argument %s does not reside under the home ' + 'directory of the system user.' % attr) + return directory + + def are_valid(self): + self._errors = [] + home_dir = os.path.join(os.path.sep, 'home', self.user) + if self.lpuser is None: + self.lpuser = self.user + self.private_key = self._get_ssh_key( + 'private_key', os.path.join(home_dir, '.ssh', 'id_rsa')) + self.public_key = self._get_ssh_key( + 'public_key', os.path.join(home_dir, '.ssh', 'id_rsa.pub')) + self.directory = self._get_directory('directory', home_dir) + self.dependencies_dir = self._get_directory( + 'dependencies_dir', home_dir) + return not self._errors + + +if __name__ == '__main__': + args = parser.parse_args(namespace=Namespace()) + if args.are_valid(): + main(args.user, + args.name, + args.email, + args.lpuser, + args.private_key, + args.public_key, + args.actions, + args.lxc_name, + args.dependencies_dir, + args.directory) + else: + parser.error(args.error_message)
_______________________________________________ Mailing list: https://launchpad.net/~launchpad-reviewers Post to : [email protected] Unsubscribe : https://launchpad.net/~launchpad-reviewers More help : https://help.launchpad.net/ListHelp

