← Back to team overview

yellow team mailing list archive

[Merge] lp:~frankban/lpsetup/split-files into lp:lpsetup

 

Francesco Banconi has proposed merging lp:~frankban/lpsetup/split-files into lp:lpsetup.

Requested reviews:
  Launchpad Yellow Squad (yellow)

For more details, see:
https://code.launchpad.net/~frankban/lpsetup/split-files/+merge/97028

== Changes ==

- Created an lpsetup project (personal, but this should change).
- Splitted the code present in `lpsetup.py` into smaller files.
- Added distribution files.
- The script now uses helpers from `python-shell-toolbox`.
- Fixed some doctests.
- Fixed the way `file_append` and `file_prepend` are used.

This is an incremental change, not tested, but required to start working on
lpsetup as a project and not just a standalone script.
-- 
https://code.launchpad.net/~frankban/lpsetup/split-files/+merge/97028
Your team Launchpad Yellow Squad is requested to review the proposed merge of lp:~frankban/lpsetup/split-files into lp:lpsetup.
=== added file '.bzrignore'
--- .bzrignore	1970-01-01 00:00:00 +0000
+++ .bzrignore	2012-03-12 14:27:19 +0000
@@ -0,0 +1,3 @@
+build
+dist
+MANIFEST

=== added file 'INSTALL'
--- INSTALL	1970-01-01 00:00:00 +0000
+++ INSTALL	2012-03-12 14:27:19 +0000
@@ -0,0 +1,4 @@
+To install lpsetup, run the following command inside this directory:
+
+    python setup.py install
+

=== added file 'MANIFEST.in'
--- MANIFEST.in	1970-01-01 00:00:00 +0000
+++ MANIFEST.in	2012-03-12 14:27:19 +0000
@@ -0,0 +1,5 @@
+include INSTALL
+include LICENSE
+include README.rst
+include MANIFEST.in
+recursive-include lpsetup/tests *

=== added file 'README.rst'
--- README.rst	1970-01-01 00:00:00 +0000
+++ README.rst	2012-03-12 14:27:19 +0000
@@ -0,0 +1,34 @@
+lpsetup
+=======
+
+This project provides the `lp-setup` command, that can be used to create
+and update a Launchpad development or testing environment.
+
+This project also allows user to set up Launchpad inside an LXC container.
+
+
+Requirements
+~~~~~~~~~~~~
+
+lp-setup requires Python >= 2.7 and `python-shell-toolbox`.
+The package `python-argparse` is required to run lp-setup using Python 2.6.
+
+
+Getting started
+~~~~~~~~~~~~~~~
+
+Install the Launchpad environment::
+
+    lp-setup install
+
+Install the Launchpad environment inside an LXC::
+
+    lp-setup lxc-install
+
+Update Launchpad::
+
+    lp-setup update
+
+Help::
+
+    lp-setup help [command]

=== added file 'lp-setup'
--- lp-setup	1970-01-01 00:00:00 +0000
+++ lp-setup	2012-03-12 14:27:19 +0000
@@ -0,0 +1,11 @@
+#!/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).
+
+"""Command line interface entry point for lpsetup."""
+
+from lpsetup import cli
+
+
+if __name__ == '__main__':
+    cli.main()

=== added directory 'lpsetup'
=== removed file 'lpsetup.py'
--- lpsetup.py	2012-03-09 09:57:35 +0000
+++ lpsetup.py	1970-01-01 00:00:00 +0000
@@ -1,1730 +0,0 @@
-#!/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 and update Launchpad development and testing environments."""
-
-__metaclass__ = type
-__all__ = [
-    'ActionsBasedSubCommand',
-    'apt_get_install',
-    'ArgumentParser',
-    'BaseSubCommand',
-    'bzr_whois',
-    'call',
-    'cd',
-    'check_output',
-    'CommandError',
-    'create_lxc',
-    'environ',
-    'file_append',
-    'file_prepend',
-    'generate_ssh_keys',
-    'get_container_path',
-    'get_lxc_gateway',
-    'get_su_command',
-    'get_user_home',
-    'get_user_ids',
-    'initialize',
-    'InstallSubCommand',
-    'join_command',
-    'LaunchpadError',
-    'link_sourcecode_in_branches',
-    'LXCInstallSubCommand',
-    'lxc_in_state',
-    'lxc_running',
-    'lxc_stopped',
-    'make_launchpad',
-    'mkdirs',
-    'setup_apt',
-    'setup_codebase',
-    'setup_external_sourcecode',
-    'setup_launchpad',
-    'setup_launchpad_lxc',
-    'ssh',
-    'SSHError',
-    'start_lxc',
-    'stop_lxc',
-    'su',
-    'this_command',
-    'update_launchpad',
-    'user_exists',
-    'ValidationError',
-    'wait_for_lxc',
-    ]
-
-# To run doctests: python -m doctest -v lpsetup.py
-
-from collections import namedtuple
-from contextlib import contextmanager
-from email.Utils import parseaddr, formataddr
-from functools import partial
-import argparse
-import errno
-import os
-import pipes
-import platform
-import pwd
-import shutil
-import subprocess
-import sys
-import time
-
-
-APT_REPOSITORIES = (
-    'deb http://archive.ubuntu.com/ubuntu {distro} multiverse',
-    'deb http://archive.ubuntu.com/ubuntu {distro}-updates multiverse',
-    'deb http://archive.ubuntu.com/ubuntu {distro}-security multiverse',
-    'ppa:launchpad/ppa',
-    'ppa:bzr/ppa',
-    )
-BASE_PACKAGES = ['ssh', 'bzr', 'language-pack-en']
-CHECKOUT_DIR = '~/launchpad/branches'
-DEPENDENCIES_DIR = '~/launchpad/dependencies'
-DHCP_FILE = '/etc/dhcp/dhclient.conf'
-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'),
-    )
-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/bazaar.launchpad.dev/mirrors',
-    '/var/tmp/archive',
-    '/var/tmp/ppa',
-    )
-LP_BZR_LOCATIONS = (
-    ('submit_branch', '{checkout_dir}'),
-    ('public_branch', 'bzr+ssh://bazaar.launchpad.net/~{lpuser}/launchpad'),
-    ('public_branch:policy', 'appendpath'),
-    ('push_location', 'lp:~{lpuser}/launchpad'),
-    ('push_location:policy', 'appendpath'),
-    ('merge_target', '{checkout_dir}'),
-    ('submit_to', 'merge@xxxxxxxxxxxxxxxxxx'),
-    )
-LP_CHECKOUT = 'devel'
-LP_PACKAGES = [
-    'bzr', 'launchpad-developer-dependencies', 'apache2',
-    'apache2-mpm-worker', 'libapache2-mod-wsgi'
-    ]
-LP_REPOS = (
-    'http://bazaar.launchpad.net/~launchpad-pqm/launchpad/devel',
-    'lp:launchpad',
-    )
-LP_SOURCE_DEPS = (
-    'http://bazaar.launchpad.net/~launchpad/lp-source-dependencies/trunk')
-LXC_CONFIG_TEMPLATE = '/etc/lxc/local.conf'
-LXC_GUEST_ARCH = 'i386'
-LXC_GUEST_CHOICES = ('lucid', 'oneiric', 'precise')
-LXC_GUEST_OS = LXC_GUEST_CHOICES[0]
-LXC_NAME = 'lptests'
-LXC_OPTIONS = """
-lxc.network.type = veth
-lxc.network.link = {interface}
-lxc.network.flags = up
-"""
-LXC_PACKAGES = ['lxc', 'libvirt-bin']
-LXC_PATH = '/var/lib/lxc/'
-RESOLV_FILE = '/etc/resolv.conf'
-
-
-Env = namedtuple('Env', 'uid gid home')
-
-
-class LaunchpadError(Exception):
-    """Base exception for lpsetup."""
-
-
-class CommandError(LaunchpadError):
-    """Errors occurred running shell commands."""
-
-
-class SSHError(LaunchpadError):
-    """Errors occurred during SSH connection."""
-
-
-class ValidationError(LaunchpadError):
-    """Argparse invalid arguments."""
-
-
-def apt_get_install(*args):
-    """Install given packages using apt."""
-    with environ(DEBIAN_FRONTEND='noninteractive'):
-        cmd = ('apt-get', '-y', 'install') + args
-        return call(*cmd)
-
-
-def join_command(args):
-    """Return a valid Unix command line from `args`.
-
-        >>> join_command(['ls', '-l'])
-        'ls -l'
-
-    Arguments containing spaces and empty args are correctly quoted::
-
-        >>> join_command(['command', 'arg1', 'arg containing spaces', ''])
-        "command arg1 'arg containing spaces' ''"
-    """
-    return ' '.join(pipes.quote(arg) for arg in args)
-
-
-def bzr_whois(user):
-    """Return full name and email of bzr `user`.
-
-    Return None if the given `user` does not have a bzr user id.
-    """
-    with su(user):
-        try:
-            whoami = check_output('bzr', 'whoami')
-        except (CommandError, OSError):
-            return None
-    return parseaddr(whoami)
-
-
-@contextmanager
-def cd(directory):
-    """A context manager to temporarily change current working dir, e.g.::
-
-        >>> import os
-        >>> os.chdir('/tmp')
-        >>> with cd('/bin'): print os.getcwd()
-        /bin
-        >>> print os.getcwd()
-        /tmp
-    """
-    cwd = os.getcwd()
-    os.chdir(directory)
-    try:
-        yield
-    finally:
-        os.chdir(cwd)
-
-
-def check_output(*args, **kwargs):
-    r"""Run the command with the given arguments.
-
-    The first argument is the path to the command to run.
-    Subsequent arguments are command-line arguments to be passed.
-
-    Usually the output is captured by a pipe and returned::
-
-        >>> check_output('echo', 'output')
-        'output\n'
-
-    A `CommandError` exception is raised if the return code is not zero::
-
-        >>> check_output('ls', '--not a valid switch', stderr=subprocess.PIPE)
-        Traceback (most recent call last):
-        CommandError: Error running: ls '--not a valid switch'
-
-    None arguments are ignored::
-
-        >>> check_output(None, 'echo', None, 'output')
-        'output\n'
-    """
-    args = [i for i in args if i is not None]
-    process = subprocess.Popen(
-        args, stdout=kwargs.pop('stdout', subprocess.PIPE),
-        close_fds=True, **kwargs)
-    stdout, stderr = process.communicate()
-    retcode = process.poll()
-    if retcode:
-        raise CommandError('Error running: ' + join_command(args))
-    return stdout
-
-
-call = partial(check_output, stdout=None)
-
-
-@contextmanager
-def environ(**kwargs):
-    """A context manager to temporarily change environment variables.
-
-    If an existing environment variable is changed, it is restored during
-    context cleanup::
-
-        >>> import os
-        >>> os.environ['MY_VARIABLE'] = 'foo'
-        >>> with environ(MY_VARIABLE='bar'): print os.getenv('MY_VARIABLE')
-        bar
-        >>> print os.getenv('MY_VARIABLE')
-        foo
-        >>> del os.environ['MY_VARIABLE']
-
-    If we are adding environment variables, they are removed during context
-    cleanup::
-
-        >>> import os
-        >>> with environ(MY_VAR1='foo', MY_VAR2='bar'):
-        ...     print os.getenv('MY_VAR1'), os.getenv('MY_VAR2')
-        foo bar
-        >>> os.getenv('MY_VAR1') == os.getenv('MY_VAR2') == None
-        True
-    """
-    backup = {}
-    for key, value in kwargs.items():
-        backup[key] = os.getenv(key)
-        os.environ[key] = value
-    try:
-        yield
-    finally:
-        for key, value in backup.items():
-            if value is None:
-                del os.environ[key]
-            else:
-                os.environ[key] = value
-
-
-def file_append(filename, line):
-    r"""Append given `line`, if not present, at the end of `filename`.
-
-    Usage example::
-
-        >>> import tempfile
-        >>> f = tempfile.NamedTemporaryFile('w', delete=False)
-        >>> f.write('line1\n')
-        >>> f.close()
-        >>> file_append(f.name, 'new line\n')
-        >>> open(f.name).read()
-        'line1\nnew line\n'
-
-    Nothing happens if the file already contains the given `line`::
-
-        >>> file_append(f.name, 'new line\n')
-        >>> open(f.name).read()
-        'line1\nnew line\n'
-
-    A new line is automatically added before the given `line` if it is not
-    present at the end of current file content::
-
-        >>> import tempfile
-        >>> f = tempfile.NamedTemporaryFile('w', delete=False)
-        >>> f.write('line1')
-        >>> f.close()
-        >>> file_append(f.name, 'new line\n')
-        >>> open(f.name).read()
-        'line1\nnew line\n'
-
-    The file is created if it does not exist::
-
-        >>> import tempfile
-        >>> filename = tempfile.mktemp()
-        >>> file_append(filename, 'line1\n')
-        >>> open(filename).read()
-        'line1\n'
-    """
-    with open(filename, 'a+') as f:
-        content = f.read()
-        if line not in content:
-            if content.endswith('\n') or not content:
-                f.write(line)
-            else:
-                f.write('\n' + line)
-
-
-def file_prepend(filename, line):
-    r"""Insert given `line`, if not present, at the beginning of `filename`.
-
-    Usage example::
-
-        >>> import tempfile
-        >>> f = tempfile.NamedTemporaryFile('w', delete=False)
-        >>> f.write('line1\n')
-        >>> f.close()
-        >>> file_prepend(f.name, 'line0\n')
-        >>> open(f.name).read()
-        'line0\nline1\n'
-
-    If the file starts with the given `line`, nothing happens::
-
-        >>> file_prepend(f.name, 'line0\n')
-        >>> open(f.name).read()
-        'line0\nline1\n'
-
-    If the file contains the given `line`, but not at the beginning,
-    the line is moved on top::
-
-        >>> file_prepend(f.name, 'line1\n')
-        >>> open(f.name).read()
-        'line1\nline0\n'
-    """
-    with open(filename, 'r+') as f:
-        lines = f.readlines()
-        if lines[0] != line:
-            if line in lines:
-                lines.remove(line)
-            lines.insert(0, line)
-            f.seek(0)
-            f.writelines(lines)
-
-
-def generate_ssh_keys(directory, filename='id_rsa'):
-    """Generate ssh key pair, saving them inside the given `directory`.
-
-        >>> generate_ssh_keys('/tmp/')
-        >>> open('/tmp/id_rsa').readlines()[0].strip()
-        '-----BEGIN RSA PRIVATE KEY-----'
-        >>> open('/tmp/id_rsa.pub').read().startswith('ssh-rsa')
-        True
-        >>> os.remove('/tmp/id_rsa')
-        >>> os.remove('/tmp/id_rsa.pub')
-
-    The key filename can be changed using the `filename` keyword argument
-    (default is 'id_rsa')::
-
-        >>> generate_ssh_keys('/tmp/', 'custom_key')
-        >>> os.path.exists('/tmp/custom_key')
-        True
-        >>> os.path.exists('/tmp/id_rsa')
-        False
-
-        >>> os.remove('/tmp/custom_key')
-        >>> os.remove('/tmp/custom_key.pub')
-    """
-    path = os.path.join(directory, filename)
-    return call('ssh-keygen', '-q', '-t', 'rsa', '-N', '', '-f', path)
-
-
-def get_container_path(lxc_name, path='', base_path=LXC_PATH):
-    """Return the path of LXC container called `lxc_name`.
-
-    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, lxc_name, 'rootfs', path.lstrip('/'))
-
-
-def get_lxc_gateway():
-    """Return a tuple of gateway name and address.
-
-    The gateway name and address will change depending on which version
-    of Ubuntu the script is running on.
-    """
-    release_name = platform.linux_distribution()[2]
-    if release_name == 'oneiric':
-        return 'virbr0', '192.168.122.1'
-    else:
-        return 'lxcbr0', '10.0.3.1'
-
-
-def get_su_command(user, args):
-    """Return a command line as a sequence, prepending "su" if necessary.
-
-    This can be used together with `call` or `check_output` when the `su`
-    context manager is not enaugh (e.g. an external program uses uid rather
-    than euid).
-
-        >>> import getpass
-        >>> current_user = getpass.getuser()
-
-    If the su is requested as current user, the arguments are returned as
-    given::
-
-        >>> get_su_command(current_user, ('ls', '-l'))
-        ('ls', '-l')
-
-    Otherwise, "su" is prepended::
-
-        >>> get_su_command('nobody', ('ls', '-l', 'my file'))
-        ('su', 'nobody', '-c', "ls -l 'my file'")
-    """
-    if get_user_ids(user)[0] != os.getuid():
-        args = [i for i in args if i is not None]
-        return ('su', user, '-c', join_command(args))
-    return args
-
-
-def get_user_home(user):
-    """Return the home directory of the given `user`.
-
-        >>> get_user_home('root')
-        '/root'
-
-    If the user does not exist, return a default /home/[username] home::
-
-        >>> get_user_home('_this_user_does_not_exist_')
-        '/home/_this_user_does_not_exist_'
-    """
-    try:
-        return pwd.getpwnam(user).pw_dir
-    except KeyError:
-        return os.path.join(os.path.sep, 'home', user)
-
-
-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
-
-
-def lxc_in_state(state, lxc_name, timeout=30):
-    """Return True if the LXC named `lxc_name` is in state `state`.
-
-    Return False otherwise.
-    """
-    while timeout:
-        try:
-            output = check_output(
-                'lxc-info', '-n', lxc_name, stderr=subprocess.STDOUT)
-        except CommandError:
-            pass
-        else:
-            if state in output:
-                return True
-        timeout -= 1
-        time.sleep(1)
-    return False
-
-
-lxc_running = partial(lxc_in_state, 'RUNNING')
-lxc_stopped = partial(lxc_in_state, 'STOPPED')
-
-
-def mkdirs(*args):
-    """Create leaf directories (given as `args`) and all intermediate ones.
-
-        >>> import tempfile
-        >>> base_dir = tempfile.mktemp(suffix='/')
-        >>> dir1 = tempfile.mktemp(prefix=base_dir)
-        >>> dir2 = tempfile.mktemp(prefix=base_dir)
-        >>> mkdirs(dir1, dir2)
-        >>> os.path.isdir(dir1)
-        True
-        >>> os.path.isdir(dir2)
-        True
-
-    If the leaf directory already exists the function returns without errors::
-
-        >>> mkdirs(dir1)
-
-    An `OSError` is raised if the leaf path exists and it is a file::
-
-        >>> f = tempfile.NamedTemporaryFile(
-        ...     'w', delete=False, prefix=base_dir)
-        >>> f.close()
-        >>> mkdirs(f.name) # doctest: +ELLIPSIS
-        Traceback (most recent call last):
-        OSError: ...
-    """
-    for directory in args:
-        try:
-            os.makedirs(directory)
-        except OSError as err:
-            if err.errno != errno.EEXIST or os.path.isfile(directory):
-                raise
-
-
-def ssh(location, user=None, caller=subprocess.call):
-    """Return a callable that can be used to run ssh shell commands.
-
-    The ssh `location` and, optionally, `user` must be given.
-    If the user is None then the current user is used for the connection.
-
-    The callable internally uses the given `caller`::
-
-        >>> def caller(cmd):
-        ...     print cmd
-        >>> sshcall = ssh('example.com', 'myuser', caller=caller)
-        >>> root_sshcall = ssh('example.com', caller=caller)
-        >>> sshcall('ls -l') # doctest: +ELLIPSIS
-        ('ssh', '-t', ..., 'myuser@xxxxxxxxxxx', '--', 'ls -l')
-        >>> root_sshcall('ls -l') # doctest: +ELLIPSIS
-        ('ssh', '-t', ..., 'example.com', '--', 'ls -l')
-
-    If the ssh command exits with an error code, an `SSHError` is raised::
-
-        >>> ssh('loc', caller=lambda cmd: 1)('ls -l') # doctest: +ELLIPSIS
-        Traceback (most recent call last):
-        SSHError: ...
-
-    If ignore_errors is set to True when executing the command, no error
-    will be raised, even if the command itself returns an error code.
-
-        >>> sshcall = ssh('loc', caller=lambda cmd: 1)
-        >>> sshcall('ls -l', ignore_errors=True)
-    """
-    if user is not None:
-        location = '{user}@{location}'.format(user=user, location=location)
-
-    def _sshcall(cmd, ignore_errors=False):
-        sshcmd = (
-            'ssh',
-            '-t',
-            '-t',  # Yes, this second -t is deliberate. See `man ssh`.
-            '-o', 'StrictHostKeyChecking=no',
-            '-o', 'UserKnownHostsFile=/dev/null',
-            location,
-            '--', cmd,
-            )
-        if caller(sshcmd) and not ignore_errors:
-            raise SSHError('Error running command: ' + ' '.join(sshcmd))
-
-    return _sshcall
-
-
-@contextmanager
-def su(user):
-    """A context manager to temporarily run the script as a different user."""
-    uid, gid = get_user_ids(user)
-    os.setegid(gid)
-    os.seteuid(uid)
-    home = get_user_home(user)
-    with environ(HOME=home):
-        try:
-            yield Env(uid, gid, home)
-        finally:
-            os.setegid(os.getgid())
-            os.seteuid(os.getuid())
-
-
-def this_command(directory, args):
-    """Return a command line to re-launch this script using given `args`.
-
-    The command returned will execute this script from `directory`::
-
-        >>> import os
-        >>> script_name = os.path.basename(__file__)
-
-        >>> cmd = this_command('/home/user/lp/branches', ['install'])
-        >>> cmd == ('cd /home/user/lp/branches && devel/utilities/' +
-        ...         script_name + ' install')
-        True
-
-    Arguments are correctly quoted::
-
-        >>> cmd = this_command('/path', ['-arg', 'quote me'])
-        >>> cmd == ('cd /path && devel/utilities/' +
-        ...         script_name + " -arg 'quote me'")
-        True
-    """
-    script = join_command([
-        os.path.join(LP_CHECKOUT, 'utilities', os.path.basename(__file__)),
-        ] + list(args))
-    return join_command(['cd', directory]) + ' && ' + script
-
-
-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 setup_codebase(user, valid_ssh_keys, checkout_dir, dependencies_dir):
-    """Set up Launchpad repository and source dependencies.
-
-    Return True if new changes are pulled from bzr repository.
-    """
-    # Using real su because bzr uses uid.
-    if os.path.exists(checkout_dir):
-        # Pull the repository.
-        revno_args = ('bzr', 'revno', checkout_dir)
-        revno = check_output(*revno_args)
-        call(*get_su_command(user, ['bzr', 'pull', '-d', checkout_dir]))
-        changed = revno != check_output(*revno_args)
-    else:
-        # Branch the repository.
-        cmd = ('bzr', 'branch',
-               LP_REPOS[1] if valid_ssh_keys else LP_REPOS[0], checkout_dir)
-        call(*get_su_command(user, cmd))
-        changed = True
-    # Check repository integrity.
-    if subprocess.call(['bzr', 'status', '-q', checkout_dir]):
-        raise LaunchpadError(
-            'Repository {0} is corrupted.'.format(checkout_dir))
-    # Set up source dependencies.
-    with su(user):
-        for subdir in ('eggs', 'yui', 'sourcecode'):
-            mkdirs(os.path.join(dependencies_dir, subdir))
-        download_cache = os.path.join(dependencies_dir, 'download-cache')
-        if os.path.exists(download_cache):
-            call('bzr', 'up', download_cache)
-        else:
-            call('bzr', 'co', '--lightweight', LP_SOURCE_DEPS, download_cache)
-    return changed
-
-
-def setup_external_sourcecode(
-    user, valid_ssh_keys, checkout_dir, dependencies_dir):
-    """Update and link external sourcecode."""
-    cmd = (
-        'utilities/update-sourcecode',
-        None if valid_ssh_keys else '--use-http',
-        os.path.join(dependencies_dir, 'sourcecode'),
-        )
-    with cd(checkout_dir):
-        # Using real su because update-sourcecode uses uid.
-        call(*get_su_command(user, cmd))
-        with su(user):
-            call('utilities/link-external-sourcecode', dependencies_dir)
-
-
-def make_launchpad(user, checkout_dir, install=False):
-    """Make and optionally install Launchpad."""
-    # Using real su because mailman make script uses uid.
-    call(*get_su_command(user, ['make', '-C', checkout_dir]))
-    if install:
-        call('make', '-C', checkout_dir, 'install')
-
-
-def initialize(
-    user, full_name, email, lpuser, private_key, public_key, valid_ssh_keys,
-    dependencies_dir, directory):
-    """Initialize host machine."""
-    # Install necessary deb packages.  This requires Oneiric or later.
-    call('apt-get', 'update')
-    apt_get_install(*BASE_PACKAGES)
-    # Create the user (if he does not exist).
-    if not user_exists(user):
-        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'):
-        generate_ssh_keys('/root/.ssh/')
-    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')
-        mkdirs(ssh_dir)
-        # Generate user ssh keys if none are supplied.
-        if not valid_ssh_keys:
-            generate_ssh_keys(ssh_dir)
-            private_key = open(os.path.join(ssh_dir, 'id_rsa')).read()
-            public_key = open(os.path.join(ssh_dir, 'id_rsa.pub')).read()
-        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 = 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(contents + '\n')
-            os.chmod(filename, 0644)
-        os.chmod(priv_file, 0600)
-        # Set up bzr and Launchpad authentication.
-        call('bzr', 'whoami', formataddr([full_name, email]))
-        if valid_ssh_keys:
-            subprocess.call(['bzr', 'lp-login', lpuser])
-        # Set up the repository.
-        mkdirs(directory)
-        call('bzr', 'init-repo', '--2a', directory)
-    # Set up the codebase.
-    checkout_dir = os.path.join(directory, LP_CHECKOUT)
-    setup_codebase(user, valid_ssh_keys, checkout_dir, dependencies_dir)
-    # Set up bzr locations
-    tmpl = ''.join('{0} = {1}\n'.format(k, v) for k, v in LP_BZR_LOCATIONS)
-    locations = tmpl.format(checkout_dir=checkout_dir, lpuser=lpuser)
-    contents = '[{0}]\n'.format(directory) + locations
-    with su(user) as env:
-        bzr_locations = os.path.join(env.home, '.bazaar', 'locations.conf')
-        file_append(bzr_locations, contents)
-
-
-def setup_apt(no_repositories=True):
-    """Setup, update and upgrade deb packages."""
-    if not no_repositories:
-        distro = check_output('lsb_release', '-cs').strip()
-        # APT repository update.
-        for reposirory in APT_REPOSITORIES:
-            assume_yes = None if distro == 'lucid' else '-y'
-            call('add-apt-repository', assume_yes,
-                 reposirory.format(distro=distro))
-        # XXX frankban 2012-01-13 - Bug 892892: upgrading mountall in LXC
-        # containers currently does not work.
-        call("echo 'mountall hold' | dpkg --set-selections", shell=True)
-    call('apt-get', 'update')
-    # Install base and Launchpad deb packages.
-    apt_get_install(*LP_PACKAGES)
-
-
-def setup_launchpad(user, dependencies_dir, directory, valid_ssh_keys):
-    """Set up the Launchpad environment."""
-    # User configuration.
-    subprocess.call(['adduser', user, 'sudo'])
-    gid = pwd.getpwnam(user).pw_gid
-    subprocess.call(['addgroup', '--gid', str(gid), user])
-    # Set up Launchpad dependencies.
-    checkout_dir = os.path.join(directory, LP_CHECKOUT)
-    setup_external_sourcecode(
-        user, valid_ssh_keys, checkout_dir, dependencies_dir)
-    with su(user):
-        # Create Apache document roots, to avoid warnings.
-        mkdirs(*LP_APACHE_ROOTS)
-    # Set up Apache modules.
-    for module in LP_APACHE_MODULES.split():
-        call('a2enmod', module)
-    with cd(checkout_dir):
-        # Launchpad database setup.
-        call('utilities/launchpad-database-setup', user)
-    # Make and install launchpad.
-    make_launchpad(user, checkout_dir, install=True)
-    # Set up container hosts file.
-    lines = ['{0}\t{1}'.format(ip, names) for ip, names in HOSTS_CONTENT]
-    file_append(HOSTS_FILE, '\n'.join(lines))
-
-
-def update_launchpad(user, valid_ssh_keys, dependencies_dir, directory, apt):
-    """Update the Launchpad environment."""
-    if apt:
-        setup_apt(no_repositories=True)
-    checkout_dir = os.path.join(directory, LP_CHECKOUT)
-    # Update the Launchpad codebase.
-    changed = setup_codebase(
-        user, valid_ssh_keys, checkout_dir, dependencies_dir)
-    setup_external_sourcecode(
-        user, valid_ssh_keys, checkout_dir, dependencies_dir)
-    if changed:
-        make_launchpad(user, checkout_dir, install=False)
-
-
-def link_sourcecode_in_branches(user, dependencies_dir, directory):
-    """Link external sourcecode for all branches in the project."""
-    checkout_dir = os.path.join(directory, LP_CHECKOUT)
-    cmd = os.path.join(checkout_dir, 'utilities', 'link-external-sourcecode')
-    with su(user):
-        for dirname in os.listdir(directory):
-            branch = os.path.join(directory, dirname)
-            sourcecode_dir = os.path.join(branch, 'sourcecode')
-            if (branch != checkout_dir and
-                os.path.exists(sourcecode_dir) and
-                os.path.isdir(sourcecode_dir)):
-                call(cmd, '--parent', dependencies_dir, '--target', branch)
-
-
-def create_lxc(user, lxc_name, lxc_arch, lxc_os):
-    """Create the LXC named `lxc_name` sharing `user` home directory.
-
-    The container will be used as development environment or as base template
-    for parallel testing using ephemeral instances.
-    """
-    # Install necessary deb packages.
-    apt_get_install(*LXC_PACKAGES)
-    # XXX 2012-02-02 gmb bug=925024:
-    #     These calls need to be removed once the lxc vs. apparmor bug
-    #     is resolved, since having apparmor enabled for lxc is very
-    #     much a Good Thing.
-    # Disable the apparmor profiles for lxc so that we don't have
-    # problems installing postgres.
-    call('ln', '-s',
-         '/etc/apparmor.d/usr.bin.lxc-start', '/etc/apparmor.d/disable/')
-    call('apparmor_parser', '-R', '/etc/apparmor.d/usr.bin.lxc-start')
-    # Update resolv file in order to get the ability to ssh into the LXC
-    # container using its name.
-    lxc_gateway_name, lxc_gateway_address = get_lxc_gateway()
-    file_prepend(RESOLV_FILE, 'nameserver {0}\n'.format(lxc_gateway_address))
-    file_append(
-        DHCP_FILE,
-        'prepend domain-name-servers {0};\n'.format(lxc_gateway_address))
-    # Container configuration template.
-    content = LXC_OPTIONS.format(interface=lxc_gateway_name)
-    with open(LXC_CONFIG_TEMPLATE, 'w') as f:
-        f.write(content)
-    # Creating container.
-    call(
-        'lxc-create',
-        '-t', 'ubuntu',
-        '-n', lxc_name,
-        '-f', LXC_CONFIG_TEMPLATE,
-        '--',
-        '-r {os} -a {arch} -b {user}'.format(
-            os=lxc_os, arch=lxc_arch, user=user),
-        )
-    # Set up root ssh key.
-    user_authorized_keys = os.path.expanduser(
-        '~' + 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(lxc_name, '/root/.ssh/')
-    mkdirs(dst)
-    shutil.copy(user_authorized_keys, dst)
-
-
-def initialize_lxc(lxc_name, lxc_os):
-    """Initialize LXC container."""
-    base_packages = list(BASE_PACKAGES)
-    if lxc_os == 'lucid':
-        # Install argparse to be able to run this script inside a lucid lxc.
-        base_packages.append('python-argparse')
-    ssh(lxc_name)(
-        'DEBIAN_FRONTEND=noninteractive '
-        'apt-get install -y ' + ' '.join(base_packages))
-
-
-def setup_launchpad_lxc(
-    user, dependencies_dir, directory, valid_ssh_keys, lxc_name):
-    """Set up the Launchpad environment inside an LXC."""
-    # Use ssh to call this script from inside the container.
-    args = [
-        'install', '-u', user, '-a', 'setup_apt', 'setup_launchpad',
-        '-d', dependencies_dir, '-c', directory
-        ]
-    cmd = this_command(directory, args)
-    ssh(lxc_name)(cmd)
-
-
-def start_lxc(lxc_name):
-    """Start the lxc instance named `lxc_name`."""
-    call('lxc-start', '-n', lxc_name, '-d')
-
-
-def stop_lxc(lxc_name):
-    """Stop the lxc instance named `lxc_name`."""
-    ssh(lxc_name)('poweroff')
-    if not lxc_stopped(lxc_name):
-        subprocess.call(['lxc-stop', '-n', lxc_name])
-
-
-def wait_for_lxc(lxc_name, trials=60, sleep_seconds=1):
-    """Try to ssh as `user` into the LXC container named `lxc_name`."""
-    sshcall = ssh(lxc_name)
-    while True:
-        trials -= 1
-        try:
-            sshcall('true')
-        except SSHError:
-            if not trials:
-                raise
-            time.sleep(sleep_seconds)
-        else:
-            break
-
-
-def handle_user(namespace):
-    """Handle user argument.
-
-    This validator populates namespace with `home_dir` name::
-
-        >>> import getpass
-        >>> username = getpass.getuser()
-
-        >>> namespace = argparse.Namespace(user=username)
-
-        >>> handle_user(namespace)
-        >>> namespace.home_dir == '/home/' + username
-        True
-
-    The validation fails if the current user is root and no user is provided::
-
-        >>> namespace = argparse.Namespace(user=None, euid=0)
-        >>> handle_user(namespace) # doctest: +ELLIPSIS
-        Traceback (most recent call last):
-        ValidationError: argument user ...
-    """
-    if namespace.user is None:
-        if not namespace.euid:
-            raise ValidationError('argument user can not be omitted if '
-                                  'the script is run as root.')
-        namespace.user = pwd.getpwuid(namespace.euid).pw_name
-    namespace.home_dir = get_user_home(namespace.user)
-
-
-def handle_lpuser(namespace):
-    """Handle lpuser argument.
-
-    If lpuser is not provided by namespace, the user name is used::
-
-        >>> import getpass
-        >>> username = getpass.getuser()
-
-        >>> namespace = argparse.Namespace(user=username, lpuser=None)
-        >>> handle_lpuser(namespace)
-        >>> namespace.lpuser == username
-        True
-    """
-    if namespace.lpuser is None:
-        namespace.lpuser = namespace.user
-
-
-def handle_userdata(namespace, whois=bzr_whois):
-    """Handle full_name and email arguments.
-
-    If they are not provided, this function tries to obtain them using
-    the given `whois` callable::
-
-        >>> namespace = argparse.Namespace(
-        ...     full_name=None, email=None, user='root')
-        >>> email = 'email@xxxxxxxxxxx'
-        >>> handle_userdata(namespace, lambda user: (user, email))
-        >>> namespace.full_name == namespace.user
-        True
-        >>> namespace.email == email
-        True
-
-    The validation fails if full_name or email are not provided and
-    they can not be obtained using the `whois` callable::
-
-        >>> namespace = argparse.Namespace(
-        ...     full_name=None, email=None, user='root')
-        >>> handle_userdata(namespace, lambda user: None) # doctest: +ELLIPSIS
-        Traceback (most recent call last):
-        ValidationError: arguments full-name ...
-
-        >>> namespace = argparse.Namespace(
-        ...     full_name=None, email=None, user='this_user_does_not_exist')
-        >>> handle_userdata(namespace) # doctest: +ELLIPSIS
-        Traceback (most recent call last):
-        ValidationError: arguments full-name ...
-
-    It does not make sense to provide only one argument::
-
-        >>> namespace = argparse.Namespace(full_name='Foo Bar', email=None)
-        >>> handle_userdata(namespace) # doctest: +ELLIPSIS
-        Traceback (most recent call last):
-        ValidationError: arguments full-name ...
-    """
-    args = (namespace.full_name, namespace.email)
-    if not all(args):
-        if any(args):
-            raise ValidationError(
-                'arguments full-name and email: '
-                'either none or both must be provided.')
-        if user_exists(namespace.user):
-            userdata = whois(namespace.user)
-            if userdata is None:
-                raise ValidationError(
-                    'arguments full-name and email are required: '
-                    'bzr user id not found.')
-            namespace.full_name, namespace.email = userdata
-        else:
-            raise ValidationError(
-                'arguments full-name and email are required: '
-                'system user not found.')
-
-
-def handle_ssh_keys(namespace):
-    r"""Handle private and public ssh keys.
-
-    Keys contained in the namespace are escaped::
-
-        >>> private = r'PRIVATE\nKEY'
-        >>> public = r'PUBLIC\nKEY'
-        >>> namespace = argparse.Namespace(
-        ...     private_key=private, public_key=public)
-        >>> handle_ssh_keys(namespace)
-        >>> namespace.private_key == private.decode('string-escape')
-        True
-        >>> namespace.public_key == public.decode('string-escape')
-        True
-        >>> namespace.valid_ssh_keys
-        True
-
-    Keys are None if they are not provided and can not be found in the
-    current home directory::
-
-        >>> namespace = argparse.Namespace(
-        ...     private_key=None, home_dir='/tmp/__does_not_exists__')
-        >>> handle_ssh_keys(namespace) # doctest: +ELLIPSIS
-        >>> print namespace.private_key
-        None
-        >>> print namespace.public_key
-        None
-        >>> namespace.valid_ssh_keys
-        False
-
-    If only one of private_key and public_key is provided, a
-    ValidationError will be raised.
-
-        >>> namespace = argparse.Namespace(
-        ...     private_key=private, public_key=None,
-        ...     home_dir='/tmp/__does_not_exists__')
-        >>> handle_ssh_keys(namespace) # doctest: +ELLIPSIS
-        Traceback (most recent call last):
-        ValidationError: arguments private-key...
-    """
-    namespace.valid_ssh_keys = True
-    for attr, filename in (
-        ('private_key', 'id_rsa'),
-        ('public_key', 'id_rsa.pub')):
-        value = getattr(namespace, attr, None)
-        if value:
-            setattr(namespace, attr, value.decode('string-escape'))
-        else:
-            path = os.path.join(namespace.home_dir, '.ssh', filename)
-            try:
-                value = open(path).read()
-            except IOError:
-                value = None
-                namespace.valid_ssh_keys = False
-            setattr(namespace, attr, value)
-    if bool(namespace.private_key) != bool(namespace.public_key):
-        raise ValidationError(
-            "arguments private-key and public-key: "
-            "both must be provided or neither must be provided.")
-
-
-def handle_directories(namespace):
-    """Handle checkout and dependencies directories.
-
-    The ~ construction is automatically expanded::
-
-        >>> namespace = argparse.Namespace(
-        ...     directory='~/launchpad', dependencies_dir='~/launchpad/deps',
-        ...     home_dir='/home/foo')
-        >>> handle_directories(namespace)
-        >>> namespace.directory
-        '/home/foo/launchpad'
-        >>> namespace.dependencies_dir
-        '/home/foo/launchpad/deps'
-
-    The validation fails for directories not residing inside the home::
-
-        >>> namespace = argparse.Namespace(
-        ...     directory='/tmp/launchpad',
-        ...     dependencies_dir='~/launchpad/deps',
-        ...     home_dir='/home/foo')
-        >>> handle_directories(namespace) # doctest: +ELLIPSIS
-        Traceback (most recent call last):
-        ValidationError: argument directory ...
-
-    The validation fails if the directory contains spaces::
-
-        >>> namespace = argparse.Namespace(directory='my directory')
-        >>> handle_directories(namespace) # doctest: +ELLIPSIS
-        Traceback (most recent call last):
-        ValidationError: argument directory ...
-    """
-    if ' ' in namespace.directory:
-        raise ValidationError('argument directory can not contain spaces.')
-    for attr in ('directory', 'dependencies_dir'):
-        directory = getattr(
-            namespace, attr).replace('~', namespace.home_dir)
-        if not directory.startswith(namespace.home_dir + os.path.sep):
-            raise ValidationError(
-                'argument {0} does not reside under the home '
-                'directory of the system user.'.format(attr))
-        setattr(namespace, attr, directory)
-
-
-class ArgumentParser(argparse.ArgumentParser):
-    """A customized argument parser for `argparse`."""
-
-    def __init__(self, *args, **kwargs):
-        self.actions = []
-        self.subparsers = None
-        super(ArgumentParser, self).__init__(*args, **kwargs)
-
-    def add_argument(self, *args, **kwargs):
-        """Override to store actions in a "public" instance attribute.
-
-            >>> parser = ArgumentParser()
-            >>> parser.add_argument('arg1')
-            >>> parser.add_argument('arg2')
-            >>> [action.dest for action in parser.actions]
-            ['help', 'arg1', 'arg2']
-        """
-        action = super(ArgumentParser, self).add_argument(*args, **kwargs)
-        self.actions.append(action)
-
-    def register_subcommand(self, name, subcommand_class, handler=None):
-        """Add a subcommand to this parser.
-
-        A sub command is registered giving the sub command `name` and class::
-
-            >>> parser = ArgumentParser()
-
-            >>> class SubCommand(BaseSubCommand):
-            ...     def handle(self, namespace):
-            ...         return self.name
-
-            >>> parser = ArgumentParser()
-            >>> _ = parser.register_subcommand('foo', SubCommand)
-
-        The `main` method of the subcommand class is added to namespace, and
-        can be used to actually handle the sub command execution.
-
-            >>> namespace = parser.parse_args(['foo'])
-            >>> namespace.main(namespace)
-            'foo'
-
-        A `handler` callable can also be provided to handle the subcommand
-        execution::
-
-            >>> handler = lambda namespace: 'custom handler'
-
-            >>> parser = ArgumentParser()
-            >>> _ = parser.register_subcommand(
-            ...         'bar', SubCommand, handler=handler)
-
-            >>> namespace = parser.parse_args(['bar'])
-            >>> namespace.main(namespace)
-            'custom handler'
-        """
-        if self.subparsers is None:
-            self.subparsers = self.add_subparsers(
-                title='subcommands',
-                help='Each subcommand accepts --h or --help to describe it.')
-        subcommand = subcommand_class(name, handler=handler)
-        parser = self.subparsers.add_parser(
-            subcommand.name, help=subcommand.help)
-        subcommand.add_arguments(parser)
-        parser.set_defaults(main=subcommand.main, get_parser=lambda: parser)
-        return subcommand
-
-    def get_args_from_namespace(self, namespace):
-        """Return a list of arguments taking values from `namespace`.
-
-        Having a parser defined as usual::
-
-            >>> parser = ArgumentParser()
-            >>> _ = parser.add_argument('--foo')
-            >>> _ = parser.add_argument('bar')
-            >>> namespace = parser.parse_args('--foo eggs spam'.split())
-
-        It is possible to recreate the argument list taking values from
-        a different namespace::
-
-            >>> namespace.foo = 'changed'
-            >>> parser.get_args_from_namespace(namespace)
-            ['--foo', 'changed', 'spam']
-        """
-        args = []
-        for action in self.actions:
-            dest = action.dest
-            option_strings = action.option_strings
-            value = getattr(namespace, dest, None)
-            if value:
-                if option_strings:
-                    args.append(option_strings[0])
-                if isinstance(value, list):
-                    args.extend(value)
-                elif not isinstance(value, bool):
-                    args.append(value)
-        return args
-
-    def _handle_help(self, namespace):
-        """Help sub command handler."""
-        command = namespace.command
-        help = self.prefix_chars + 'h'
-        args = [help] if command is None else [command, help]
-        self.parse_args(args)
-
-    def _add_help_subcommand(self):
-        """Add an help sub command to this parser."""
-        name = 'help'
-        choices = self.subparsers.choices.keys()
-        if name not in choices:
-            choices.append(name)
-            parser = self.subparsers.add_parser(
-                name, help='More help on a command.')
-            parser.add_argument('command', nargs='?', choices=choices)
-            parser.set_defaults(main=self._handle_help)
-
-    def parse_args(self, *args, **kwargs):
-        """Override to add an help sub command.
-
-        The help sub command is added only if other sub commands exist::
-
-            >>> stderr, sys.stderr = sys.stderr, sys.stdout
-            >>> parser = ArgumentParser()
-            >>> parser.parse_args(['help'])
-            Traceback (most recent call last):
-            SystemExit: 2
-            >>> sys.stderr = stderr
-
-            >>> class SubCommand(BaseSubCommand): pass
-            >>> _ = parser.register_subcommand('command', SubCommand)
-            >>> namespace = parser.parse_args(['help'])
-            >>> namespace.main(namespace)
-            Traceback (most recent call last):
-            SystemExit: 0
-        """
-        if self.subparsers is not None:
-            self._add_help_subcommand()
-        return super(ArgumentParser, self).parse_args(*args, **kwargs)
-
-
-class BaseSubCommand(object):
-    """Base class defining a sub command.
-
-    Objects of this class can be used to easily add sub commands to this
-    script as plugins, providing arguments, validating them, restarting
-    as root if needed.
-
-    Override `add_arguments()` to add arguments, `validators` to add
-    namespace validators, and `handle()` to manage sub command execution::
-
-        >>> def validator(namespace):
-        ...     namespace.bar = True
-
-        >>> class SubCommand(BaseSubCommand):
-        ...     help = 'Sub command example.'
-        ...     validators = (validator,)
-        ...
-        ...     def add_arguments(self, parser):
-        ...         super(SubCommand, self).add_arguments(parser)
-        ...         parser.add_argument('--foo')
-        ...
-        ...     def handle(self, namespace):
-        ...         return namespace
-
-    Register the sub command using `ArgumentParser.register_subcommand`::
-
-        >>> parser = ArgumentParser()
-        >>> sub_command = parser.register_subcommand('spam', SubCommand)
-
-    Now the subcommand has a name::
-
-        >>> sub_command.name
-        'spam'
-
-    The sub command handler can be called using `namespace.main()`::
-
-        >>> namespace = parser.parse_args('spam --foo eggs'.split())
-        >>> namespace = namespace.main(namespace)
-        >>> namespace.foo
-        'eggs'
-        >>> namespace.bar
-        True
-
-    The help attribute of sub command instances is used to generate
-    the command usage message::
-        >>> help = parser.format_help()
-        >>> 'spam' in help
-        True
-        >>> 'Sub command example.' in help
-        True
-    """
-
-    help = ''
-    needs_root = False
-    validators = ()
-
-    def __init__(self, name, handler=None):
-        self.name = name
-        self.handler = handler or self.handle
-
-    def __repr__(self):
-        return '<{klass}: {name}>'.format(
-            klass=self.__class__.__name__, name=self.name)
-
-    def init_namespace(self, namespace):
-        """Add `run_as_root` and `euid` names to the given `namespace`."""
-        euid = os.geteuid()
-        namespace.euid, namespace.run_as_root = euid, not euid
-
-    def get_needs_root(self, namespace):
-        """Return True if root is needed to run this subcommand.
-
-        Subclasses can override this to dynamically change this value
-        based on the given `namespace`.
-        """
-        return self.needs_root
-
-    def get_validators(self, namespace):
-        """Return an iterable of namespace validators for this sub command.
-
-        Subclasses can override this to dynamically change validators
-        based on the given `namespace`.
-        """
-        return self.validators
-
-    def validate(self, parser, namespace):
-        """Validate the current namespace.
-
-        The method `self.get_validators` can contain an iterable of objects
-        that are called once the arguments namespace is fully populated.
-        This allows cleaning and validating arguments that depend on
-        each other, or on the current environment.
-
-        Each validator is a callable object, takes the current namespace
-        and can raise ValidationError if the arguments are not valid::
-
-            >>> import sys
-            >>> stderr, sys.stderr = sys.stderr, sys.stdout
-            >>> def validator(namespace):
-            ...     raise ValidationError('nothing is going on')
-            >>> sub_command = BaseSubCommand('foo')
-            >>> sub_command.validators = [validator]
-            >>> sub_command.validate(ArgumentParser(), argparse.Namespace())
-            Traceback (most recent call last):
-            SystemExit: 2
-            >>> sys.stderr = stderr
-        """
-        for validator in self.get_validators(namespace):
-            try:
-                validator(namespace)
-            except ValidationError as err:
-                parser.error(err)
-
-    def restart_as_root(self, parser, namespace):
-        """Restart this script using *sudo*.
-
-        The arguments are recreated using the given `namespace`.
-        """
-        args = parser.get_args_from_namespace(namespace)
-        return subprocess.call(['sudo', sys.argv[0], self.name] + args)
-
-    def main(self, namespace):
-        """Entry point for subcommand execution.
-
-        This method takes care of:
-
-        - current argparse subparser retrieval
-        - namespace initialization
-        - namespace validation
-        - script restart as root (if this sub command needs to be run as root)
-
-        If everything is ok the sub command handler is called passing
-        the validated namespace.
-        """
-        parser = namespace.get_parser()
-        self.init_namespace(namespace)
-        self.validate(parser, namespace)
-        if self.get_needs_root(namespace) and not namespace.run_as_root:
-            return self.restart_as_root(parser, namespace)
-        return self.handler(namespace)
-
-    def handle(self, namespace):
-        """Default sub command handler.
-
-        Subclasses must either implement this method or provide another
-        callable handler during sub command registration.
-        """
-        raise NotImplementedError
-
-    def add_arguments(self, parser):
-        """Here subclasses can add arguments to the subparser."""
-        pass
-
-
-class ActionsBasedSubCommand(BaseSubCommand):
-    """A sub command that uses "actions" to handle its execution.
-
-    Actions are callables stored in the `actions` attribute, together
-    with the arguments they expect. Those arguments are strings
-    representing attributes of the argparse namespace::
-
-        >>> trace = []
-
-        >>> def action1(foo):
-        ...     trace.append('action1 received ' + foo)
-
-        >>> def action2(foo, bar):
-        ...     trace.append('action2 received {0} and {1}'.format(foo, bar))
-
-        >>> class SubCommand(ActionsBasedSubCommand):
-        ...     actions = (
-        ...         (action1, 'foo'),
-        ...         (action2, 'foo', 'bar'),
-        ...         )
-        ...
-        ...     def add_arguments(self, parser):
-        ...         super(SubCommand, self).add_arguments(parser)
-        ...         parser.add_argument('--foo')
-        ...         parser.add_argument('--bar')
-
-    This class implements an handler method that executes actions in the
-    order they are provided::
-
-        >>> parser = ArgumentParser()
-        >>> _ = parser.register_subcommand('sub', SubCommand)
-        >>> namespace = parser.parse_args('sub --foo eggs --bar spam'.split())
-        >>> namespace.main(namespace)
-        >>> trace
-        ['action1 received eggs', 'action2 received eggs and spam']
-
-    A special argument `-a` or `--actions` is automatically added to the
-    parser. It can be used to execute only one or a subset of actions::
-
-        >>> trace = []
-
-        >>> namespace = parser.parse_args('sub --foo eggs -a action1'.split())
-        >>> namespace.main(namespace)
-        >>> trace
-        ['action1 received eggs']
-
-    A special argument `--skip-actions` is automatically added to the
-    parser. It can be used to skip one or more actions::
-
-        >>> trace = []
-
-        >>> namespace = parser.parse_args(
-        ...     'sub --foo eggs --skip-actions action1'.split())
-        >>> namespace.main(namespace)
-        >>> trace
-        ['action2 received eggs and None']
-
-    The actions execution is stopped if an action raises `LaunchpadError`.
-    In that case, the error is returned by the handler.
-
-        >>> trace = []
-
-        >>> def erroneous_action(foo):
-        ...     raise LaunchpadError('error')
-
-        >>> class SubCommandWithErrors(SubCommand):
-        ...     actions = (
-        ...         (action1, 'foo'),
-        ...         (erroneous_action, 'foo'),
-        ...         (action2, 'foo', 'bar'),
-        ...         )
-
-        >>> parser = ArgumentParser()
-        >>> _ = parser.register_subcommand('sub', SubCommandWithErrors)
-        >>> namespace = parser.parse_args('sub --foo eggs'.split())
-        >>> error = namespace.main(namespace)
-        >>> error.message
-        'error'
-
-    The action `action2` is not executed::
-
-        >>> trace
-        ['action1 received eggs']
-    """
-
-    actions = ()
-
-    def __init__(self, *args, **kwargs):
-        super(ActionsBasedSubCommand, self).__init__(*args, **kwargs)
-        self._action_names = []
-        self._actions = {}
-        for action_args in self.actions:
-            action, args = action_args[0], action_args[1:]
-            action_name = self._get_action_name(action)
-            self._action_names.append(action_name)
-            self._actions[action_name] = (action, args)
-
-    def _get_action_name(self, action):
-        """Return the string representation of an action callable.
-
-        The name is retrieved using attributes lookup for `action_name`
-        and then `__name__`::
-
-            >>> def action1():
-            ...     pass
-            >>> action1.action_name = 'myaction'
-
-            >>> def action2():
-            ...     pass
-
-            >>> sub_command = ActionsBasedSubCommand('foo')
-            >>> sub_command._get_action_name(action1)
-            'myaction'
-            >>> sub_command._get_action_name(action2)
-            'action2'
-        """
-        try:
-            return action.action_name
-        except AttributeError:
-            return action.__name__
-
-    def add_arguments(self, parser):
-        super(ActionsBasedSubCommand, self).add_arguments(parser)
-        parser.add_argument(
-            '-a', '--actions', nargs='+', choices=self._action_names,
-            help='Call one or more internal functions.')
-        parser.add_argument(
-            '--skip-actions', nargs='+', choices=self._action_names,
-            help='Skip one or more internal functions.')
-
-    def handle(self, namespace):
-        skip_actions = namespace.skip_actions or []
-        action_names = filter(
-            lambda action_name: action_name not in skip_actions,
-            namespace.actions or self._action_names)
-        for action_name in action_names:
-            action, arg_names = self._actions[action_name]
-            args = [getattr(namespace, i) for i in arg_names]
-            try:
-                action(*args)
-            except LaunchpadError as err:
-                return err
-
-
-class InstallSubCommand(ActionsBasedSubCommand):
-    """Install the Launchpad environment."""
-
-    actions = (
-        (initialize,
-         'user', 'full_name', 'email', 'lpuser', 'private_key',
-         'public_key', 'valid_ssh_keys', 'dependencies_dir', 'directory'),
-        (setup_apt, 'no_repositories'),
-        (setup_launchpad,
-         'user', 'dependencies_dir', 'directory', 'valid_ssh_keys'),
-        )
-    help = __doc__
-    needs_root = True
-    validators = (
-        handle_user,
-        handle_lpuser,
-        handle_userdata,
-        handle_ssh_keys,
-        handle_directories,
-        )
-
-    def add_arguments(self, parser):
-        super(InstallSubCommand, self).add_arguments(parser)
-        parser.add_argument(
-            '-u', '--user',
-            help='The name of the system user to be created or updated. '
-                 'The current user is used if this script is not run as '
-                 'root and this argument is omitted.')
-        parser.add_argument(
-            '-e', '--email',
-            help='The email of the user, used for bzr whoami. This argument '
-                 'can be omitted if a bzr id exists for current user.')
-        parser.add_argument(
-            '-f', '--full-name',
-            help='The full name of the user, used for bzr whoami. '
-                 'This argument can be omitted if a bzr id exists for '
-                 'current user.')
-        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 this argument is omitted and a keypair is '
-                 'not found in the home directory of the system user a new '
-                 'SSH keypair will be generated and the checkout of the '
-                 'Launchpad code will use HTTP rather than bzr+ssh.')
-        parser.add_argument(
-            '-b', '--public-key',
-            help='The SSH public key for the Launchpad user. '
-                 'If this argument is omitted and a keypair is not found '
-                 'in the home directory of the system user a new SSH '
-                 'keypair will be generated and the checkout of the '
-                 'Launchpad code will use HTTP rather than bzr+ssh.')
-        parser.add_argument(
-            '-d', '--dependencies-dir', default=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). '
-                 '[DEFAULT={0}]'.format(DEPENDENCIES_DIR))
-        parser.add_argument(
-            '-c', '--directory', default=CHECKOUT_DIR,
-            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). '
-                 '[DEFAULT={0}]'.format(CHECKOUT_DIR))
-        parser.add_argument(
-            '-N', '--no-repositories', action='store_true',
-            help='Do not add APT repositories.')
-
-
-class UpdateSubCommand(ActionsBasedSubCommand):
-    """Update the Launchpad environment to latest version."""
-
-    actions = (
-        (update_launchpad,
-         'user', 'valid_ssh_keys', 'dependencies_dir', 'directory', 'apt'),
-        (link_sourcecode_in_branches,
-            'user', 'dependencies_dir', 'directory'),
-        )
-    help = __doc__
-    validators = (
-        handle_user,
-        handle_ssh_keys,
-        handle_directories,
-        )
-
-    def get_needs_root(self, namespace):
-        # Root is needed only if an apt update/upgrade is requested.
-        return namespace.apt
-
-    def add_arguments(self, parser):
-        super(UpdateSubCommand, self).add_arguments(parser)
-        parser.add_argument(
-            '-u', '--user',
-            help='The name of the system user used to update Launchpad. '
-                 'The current user is used if this script is not run as '
-                 'root and this argument is omitted.')
-        parser.add_argument(
-            '-d', '--dependencies-dir', default=DEPENDENCIES_DIR,
-            help='The directory of the Launchpad dependencies to be updated. '
-                 'The directory must reside under the home directory of the '
-                 'given user (see -u argument). '
-                 '[DEFAULT={0}]'.format(DEPENDENCIES_DIR))
-        parser.add_argument(
-            '-c', '--directory', default=CHECKOUT_DIR,
-            help='The directory of the Launchpad repository to be updated. '
-                 'The directory must reside under the home directory of the '
-                 'given user (see -u argument). '
-                 '[DEFAULT={0}]'.format(CHECKOUT_DIR))
-        parser.add_argument(
-            '-D', '--apt', action='store_true',
-            help='Also update deb packages.')
-
-
-class LXCInstallSubCommand(InstallSubCommand):
-    """Install the Launchpad environment inside an LXC."""
-
-    actions = (
-        (initialize,
-         'user', 'full_name', 'email', 'lpuser', 'private_key',
-         'public_key', 'valid_ssh_keys', 'dependencies_dir', 'directory'),
-        (create_lxc,
-         'user', 'lxc_name', 'lxc_arch', 'lxc_os'),
-        (start_lxc, 'lxc_name'),
-        (wait_for_lxc, 'lxc_name'),
-        (initialize_lxc,
-         'lxc_name', 'lxc_os'),
-        (setup_launchpad_lxc,
-         'user', 'dependencies_dir', 'directory', 'valid_ssh_keys',
-         'lxc_name'),
-        (stop_lxc, 'lxc_name'),
-        )
-    help = __doc__
-
-    def add_arguments(self, parser):
-        super(LXCInstallSubCommand, self).add_arguments(parser)
-        parser.add_argument(
-            '-n', '--lxc-name', default=LXC_NAME,
-            help='The LXC container name to setup. '
-                 '[DEFAULT={0}]'.format(LXC_NAME))
-        parser.add_argument(
-            '-A', '--lxc-arch', default=LXC_GUEST_ARCH,
-            help='The LXC container architecture. '
-                 '[DEFAULT={0}]'.format(LXC_GUEST_ARCH))
-        parser.add_argument(
-            '-r', '--lxc-os', default=LXC_GUEST_OS,
-            choices=LXC_GUEST_CHOICES,
-            help='The LXC container distro codename. '
-                 '[DEFAULT={0}]'.format(LXC_GUEST_OS))
-
-
-def main():
-    parser = ArgumentParser(description=__doc__)
-    parser.register_subcommand('install', InstallSubCommand)
-    parser.register_subcommand('update', UpdateSubCommand)
-    parser.register_subcommand('lxc-install', LXCInstallSubCommand)
-    args = parser.parse_args()
-    return args.main(args)
-
-
-if __name__ == '__main__':
-    sys.exit(main())

=== added file 'lpsetup/__init__.py'
--- lpsetup/__init__.py	1970-01-01 00:00:00 +0000
+++ lpsetup/__init__.py	2012-03-12 14:27:19 +0000
@@ -0,0 +1,16 @@
+#!/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 and update Launchpad development and testing environments."""
+
+__metaclass__ = type
+__all__ = [
+    'get_version',
+    ]
+
+VERSION = (0, 1, 0)
+
+
+def get_version():
+    return '.'.join(map(str, VERSION))

=== added file 'lpsetup/argparser.py'
--- lpsetup/argparser.py	1970-01-01 00:00:00 +0000
+++ lpsetup/argparser.py	2012-03-12 14:27:19 +0000
@@ -0,0 +1,456 @@
+#!/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).
+
+"""Argparse wrapper to add support for sub commands."""
+
+__metaclass__ = type
+__all__ = [
+    'ActionsBasedSubCommand',
+    'ArgumentParser',
+    'BaseSubCommand',
+    ]
+
+import argparse
+import os
+import subprocess
+import sys
+
+from lpsetup.exceptions import ValidationError
+
+
+class ArgumentParser(argparse.ArgumentParser):
+    """A customized argument parser for `argparse`."""
+
+    def __init__(self, *args, **kwargs):
+        self.actions = []
+        self.subparsers = None
+        super(ArgumentParser, self).__init__(*args, **kwargs)
+
+    def add_argument(self, *args, **kwargs):
+        """Override to store actions in a "public" instance attribute.
+
+            >>> parser = ArgumentParser()
+            >>> parser.add_argument('arg1')
+            >>> parser.add_argument('arg2')
+            >>> [action.dest for action in parser.actions]
+            ['help', 'arg1', 'arg2']
+        """
+        action = super(ArgumentParser, self).add_argument(*args, **kwargs)
+        self.actions.append(action)
+
+    def register_subcommand(self, name, subcommand_class, handler=None):
+        """Add a subcommand to this parser.
+
+        A sub command is registered giving the sub command `name` and class::
+
+            >>> parser = ArgumentParser()
+
+            >>> class SubCommand(BaseSubCommand):
+            ...     def handle(self, namespace):
+            ...         return self.name
+
+            >>> parser = ArgumentParser()
+            >>> _ = parser.register_subcommand('foo', SubCommand)
+
+        The `main` method of the subcommand class is added to namespace, and
+        can be used to actually handle the sub command execution.
+
+            >>> namespace = parser.parse_args(['foo'])
+            >>> namespace.main(namespace)
+            'foo'
+
+        A `handler` callable can also be provided to handle the subcommand
+        execution::
+
+            >>> handler = lambda namespace: 'custom handler'
+
+            >>> parser = ArgumentParser()
+            >>> _ = parser.register_subcommand(
+            ...         'bar', SubCommand, handler=handler)
+
+            >>> namespace = parser.parse_args(['bar'])
+            >>> namespace.main(namespace)
+            'custom handler'
+        """
+        if self.subparsers is None:
+            self.subparsers = self.add_subparsers(
+                title='subcommands',
+                help='Each subcommand accepts --h or --help to describe it.')
+        subcommand = subcommand_class(name, handler=handler)
+        parser = self.subparsers.add_parser(
+            subcommand.name, help=subcommand.help)
+        subcommand.add_arguments(parser)
+        parser.set_defaults(main=subcommand.main, get_parser=lambda: parser)
+        return subcommand
+
+    def get_args_from_namespace(self, namespace):
+        """Return a list of arguments taking values from `namespace`.
+
+        Having a parser defined as usual::
+
+            >>> parser = ArgumentParser()
+            >>> _ = parser.add_argument('--foo')
+            >>> _ = parser.add_argument('bar')
+            >>> namespace = parser.parse_args('--foo eggs spam'.split())
+
+        It is possible to recreate the argument list taking values from
+        a different namespace::
+
+            >>> namespace.foo = 'changed'
+            >>> parser.get_args_from_namespace(namespace)
+            ['--foo', 'changed', 'spam']
+        """
+        args = []
+        for action in self.actions:
+            dest = action.dest
+            option_strings = action.option_strings
+            value = getattr(namespace, dest, None)
+            if value:
+                if option_strings:
+                    args.append(option_strings[0])
+                if isinstance(value, list):
+                    args.extend(value)
+                elif not isinstance(value, bool):
+                    args.append(value)
+        return args
+
+    def _handle_help(self, namespace):
+        """Help sub command handler."""
+        command = namespace.command
+        help = self.prefix_chars + 'h'
+        args = [help] if command is None else [command, help]
+        self.parse_args(args)
+
+    def _add_help_subcommand(self):
+        """Add an help sub command to this parser."""
+        name = 'help'
+        choices = self.subparsers.choices.keys()
+        if name not in choices:
+            choices.append(name)
+            parser = self.subparsers.add_parser(
+                name, help='More help on a command.')
+            parser.add_argument('command', nargs='?', choices=choices)
+            parser.set_defaults(main=self._handle_help)
+
+    def parse_args(self, *args, **kwargs):
+        """Override to add an help sub command.
+
+        The help sub command is added only if other sub commands exist::
+
+            >>> stderr, sys.stderr = sys.stderr, sys.stdout
+            >>> parser = ArgumentParser()
+            >>> parser.parse_args(['help'])
+            Traceback (most recent call last):
+            SystemExit: 2
+            >>> sys.stderr = stderr
+
+            >>> class SubCommand(BaseSubCommand): pass
+            >>> _ = parser.register_subcommand('command', SubCommand)
+            >>> namespace = parser.parse_args(['help'])
+            >>> namespace.main(namespace)
+            Traceback (most recent call last):
+            SystemExit: 0
+        """
+        if self.subparsers is not None:
+            self._add_help_subcommand()
+        return super(ArgumentParser, self).parse_args(*args, **kwargs)
+
+
+class BaseSubCommand(object):
+    """Base class defining a sub command.
+
+    Objects of this class can be used to easily add sub commands to this
+    script as plugins, providing arguments, validating them, restarting
+    as root if needed.
+
+    Override `add_arguments()` to add arguments, `validators` to add
+    namespace validators, and `handle()` to manage sub command execution::
+
+        >>> def validator(namespace):
+        ...     namespace.bar = True
+
+        >>> class SubCommand(BaseSubCommand):
+        ...     help = 'Sub command example.'
+        ...     validators = (validator,)
+        ...
+        ...     def add_arguments(self, parser):
+        ...         super(SubCommand, self).add_arguments(parser)
+        ...         parser.add_argument('--foo')
+        ...
+        ...     def handle(self, namespace):
+        ...         return namespace
+
+    Register the sub command using `ArgumentParser.register_subcommand`::
+
+        >>> parser = ArgumentParser()
+        >>> sub_command = parser.register_subcommand('spam', SubCommand)
+
+    Now the subcommand has a name::
+
+        >>> sub_command.name
+        'spam'
+
+    The sub command handler can be called using `namespace.main()`::
+
+        >>> namespace = parser.parse_args('spam --foo eggs'.split())
+        >>> namespace = namespace.main(namespace)
+        >>> namespace.foo
+        'eggs'
+        >>> namespace.bar
+        True
+
+    The help attribute of sub command instances is used to generate
+    the command usage message::
+        >>> help = parser.format_help()
+        >>> 'spam' in help
+        True
+        >>> 'Sub command example.' in help
+        True
+    """
+
+    help = ''
+    needs_root = False
+    validators = ()
+
+    def __init__(self, name, handler=None):
+        self.name = name
+        self.handler = handler or self.handle
+
+    def __repr__(self):
+        return '<{klass}: {name}>'.format(
+            klass=self.__class__.__name__, name=self.name)
+
+    def init_namespace(self, namespace):
+        """Add `run_as_root` and `euid` names to the given `namespace`."""
+        euid = os.geteuid()
+        namespace.euid, namespace.run_as_root = euid, not euid
+
+    def get_needs_root(self, namespace):
+        """Return True if root is needed to run this subcommand.
+
+        Subclasses can override this to dynamically change this value
+        based on the given `namespace`.
+        """
+        return self.needs_root
+
+    def get_validators(self, namespace):
+        """Return an iterable of namespace validators for this sub command.
+
+        Subclasses can override this to dynamically change validators
+        based on the given `namespace`.
+        """
+        return self.validators
+
+    def validate(self, parser, namespace):
+        """Validate the current namespace.
+
+        The method `self.get_validators` can contain an iterable of objects
+        that are called once the arguments namespace is fully populated.
+        This allows cleaning and validating arguments that depend on
+        each other, or on the current environment.
+
+        Each validator is a callable object, takes the current namespace
+        and can raise ValidationError if the arguments are not valid::
+
+            >>> import sys
+            >>> stderr, sys.stderr = sys.stderr, sys.stdout
+            >>> def validator(namespace):
+            ...     raise ValidationError('nothing is going on')
+            >>> sub_command = BaseSubCommand('foo')
+            >>> sub_command.validators = [validator]
+            >>> sub_command.validate(ArgumentParser(), argparse.Namespace())
+            Traceback (most recent call last):
+            SystemExit: 2
+            >>> sys.stderr = stderr
+        """
+        for validator in self.get_validators(namespace):
+            try:
+                validator(namespace)
+            except ValidationError as err:
+                parser.error(err)
+
+    def restart_as_root(self, parser, namespace):
+        """Restart this script using *sudo*.
+
+        The arguments are recreated using the given `namespace`.
+        """
+        args = parser.get_args_from_namespace(namespace)
+        return subprocess.call(['sudo', sys.argv[0], self.name] + args)
+
+    def main(self, namespace):
+        """Entry point for subcommand execution.
+
+        This method takes care of:
+
+        - current argparse subparser retrieval
+        - namespace initialization
+        - namespace validation
+        - script restart as root (if this sub command needs to be run as root)
+
+        If everything is ok the sub command handler is called passing
+        the validated namespace.
+        """
+        parser = namespace.get_parser()
+        self.init_namespace(namespace)
+        self.validate(parser, namespace)
+        if self.get_needs_root(namespace) and not namespace.run_as_root:
+            return self.restart_as_root(parser, namespace)
+        return self.handler(namespace)
+
+    def handle(self, namespace):
+        """Default sub command handler.
+
+        Subclasses must either implement this method or provide another
+        callable handler during sub command registration.
+        """
+        raise NotImplementedError
+
+    def add_arguments(self, parser):
+        """Here subclasses can add arguments to the subparser."""
+        pass
+
+
+class ActionsBasedSubCommand(BaseSubCommand):
+    """A sub command that uses "actions" to handle its execution.
+
+    Actions are callables stored in the `actions` attribute, together
+    with the arguments they expect. Those arguments are strings
+    representing attributes of the argparse namespace::
+
+        >>> trace = []
+
+        >>> def action1(foo):
+        ...     trace.append('action1 received ' + foo)
+
+        >>> def action2(foo, bar):
+        ...     trace.append('action2 received {0} and {1}'.format(foo, bar))
+
+        >>> class SubCommand(ActionsBasedSubCommand):
+        ...     actions = (
+        ...         (action1, 'foo'),
+        ...         (action2, 'foo', 'bar'),
+        ...         )
+        ...
+        ...     def add_arguments(self, parser):
+        ...         super(SubCommand, self).add_arguments(parser)
+        ...         parser.add_argument('--foo')
+        ...         parser.add_argument('--bar')
+
+    This class implements an handler method that executes actions in the
+    order they are provided::
+
+        >>> parser = ArgumentParser()
+        >>> _ = parser.register_subcommand('sub', SubCommand)
+        >>> namespace = parser.parse_args('sub --foo eggs --bar spam'.split())
+        >>> namespace.main(namespace)
+        >>> trace
+        ['action1 received eggs', 'action2 received eggs and spam']
+
+    A special argument `-a` or `--actions` is automatically added to the
+    parser. It can be used to execute only one or a subset of actions::
+
+        >>> trace = []
+
+        >>> namespace = parser.parse_args('sub --foo eggs -a action1'.split())
+        >>> namespace.main(namespace)
+        >>> trace
+        ['action1 received eggs']
+
+    A special argument `--skip-actions` is automatically added to the
+    parser. It can be used to skip one or more actions::
+
+        >>> trace = []
+
+        >>> namespace = parser.parse_args(
+        ...     'sub --foo eggs --skip-actions action1'.split())
+        >>> namespace.main(namespace)
+        >>> trace
+        ['action2 received eggs and None']
+
+    The actions execution is stopped if an action raises
+    `subprocess.CalledProcessError`.
+    In that case, the error is returned by the handler.
+
+        >>> trace = []
+
+        >>> def erroneous_action(foo):
+        ...     raise subprocess.CalledProcessError(1, 'command')
+
+        >>> class SubCommandWithErrors(SubCommand):
+        ...     actions = (
+        ...         (action1, 'foo'),
+        ...         (erroneous_action, 'foo'),
+        ...         (action2, 'foo', 'bar'),
+        ...         )
+
+        >>> parser = ArgumentParser()
+        >>> _ = parser.register_subcommand('sub', SubCommandWithErrors)
+        >>> namespace = parser.parse_args('sub --foo eggs'.split())
+        >>> error = namespace.main(namespace)
+        >>> str(error)
+        "Command 'command' returned non-zero exit status 1"
+
+    The action `action2` is not executed::
+
+        >>> trace
+        ['action1 received eggs']
+    """
+
+    actions = ()
+
+    def __init__(self, *args, **kwargs):
+        super(ActionsBasedSubCommand, self).__init__(*args, **kwargs)
+        self._action_names = []
+        self._actions = {}
+        for action_args in self.actions:
+            action, args = action_args[0], action_args[1:]
+            action_name = self._get_action_name(action)
+            self._action_names.append(action_name)
+            self._actions[action_name] = (action, args)
+
+    def _get_action_name(self, action):
+        """Return the string representation of an action callable.
+
+        The name is retrieved using attributes lookup for `action_name`
+        and then `__name__`::
+
+            >>> def action1():
+            ...     pass
+            >>> action1.action_name = 'myaction'
+
+            >>> def action2():
+            ...     pass
+
+            >>> sub_command = ActionsBasedSubCommand('foo')
+            >>> sub_command._get_action_name(action1)
+            'myaction'
+            >>> sub_command._get_action_name(action2)
+            'action2'
+        """
+        try:
+            return action.action_name
+        except AttributeError:
+            return action.__name__
+
+    def add_arguments(self, parser):
+        super(ActionsBasedSubCommand, self).add_arguments(parser)
+        parser.add_argument(
+            '-a', '--actions', nargs='+', choices=self._action_names,
+            help='Call one or more internal functions.')
+        parser.add_argument(
+            '--skip-actions', nargs='+', choices=self._action_names,
+            help='Skip one or more internal functions.')
+
+    def handle(self, namespace):
+        skip_actions = namespace.skip_actions or []
+        action_names = filter(
+            lambda action_name: action_name not in skip_actions,
+            namespace.actions or self._action_names)
+        for action_name in action_names:
+            action, arg_names = self._actions[action_name]
+            args = [getattr(namespace, i) for i in arg_names]
+            try:
+                action(*args)
+            except subprocess.CalledProcessError as err:
+                return err

=== added file 'lpsetup/cli.py'
--- lpsetup/cli.py	1970-01-01 00:00:00 +0000
+++ lpsetup/cli.py	2012-03-12 14:27:19 +0000
@@ -0,0 +1,29 @@
+#!/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).
+
+"""Command line interface set up."""
+
+__metaclass__ = type
+__all__ = [
+    'main',
+    ]
+
+from lpsetup import (
+    __doc__ as description,
+    argparser,
+    )
+from lpsetup.subcommands import (
+    install,
+    lxcinstall,
+    update,
+    )
+
+
+def main():
+    parser = argparser.ArgumentParser(description=description)
+    parser.register_subcommand('install', install.SubCommand)
+    parser.register_subcommand('update', update.SubCommand)
+    parser.register_subcommand('lxc-install', lxcinstall.SubCommand)
+    args = parser.parse_args()
+    return args.main(args)

=== added file 'lpsetup/exceptions.py'
--- lpsetup/exceptions.py	1970-01-01 00:00:00 +0000
+++ lpsetup/exceptions.py	2012-03-12 14:27:19 +0000
@@ -0,0 +1,19 @@
+#!/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).
+
+"""Exceptions used during subcommand execution and args validation."""
+
+__metaclass__ = type
+__all__ = [
+    'LaunchpadError',
+    'ValidationError',
+    ]
+
+
+class LaunchpadError(Exception):
+    """Base exception for lpsetup."""
+
+
+class ValidationError(LaunchpadError):
+    """Argparse invalid arguments."""

=== added file 'lpsetup/handlers.py'
--- lpsetup/handlers.py	1970-01-01 00:00:00 +0000
+++ lpsetup/handlers.py	2012-03-12 14:27:19 +0000
@@ -0,0 +1,236 @@
+#!/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).
+
+"""Sub command arguments validation."""
+
+__metaclass__ = type
+__all__ = [
+    'handle_directories',
+    'handle_lpuser',
+    'handle_ssh_keys',
+    'handle_user',
+    'handle_userdata',
+    ]
+
+import os
+import pwd
+
+from shelltoolbox import (
+    bzr_whois,
+    get_user_home,
+    user_exists,
+    )
+
+from lpsetup.exceptions import ValidationError
+
+
+def handle_user(namespace):
+    """Handle user argument.
+
+    This validator populates namespace with `home_dir` name::
+
+        >>> import getpass
+        >>> username = getpass.getuser()
+
+        >>> import argparse
+        >>> namespace = argparse.Namespace(user=username)
+
+        >>> handle_user(namespace)
+        >>> namespace.home_dir == '/home/' + username
+        True
+
+    The validation fails if the current user is root and no user is provided::
+
+        >>> namespace = argparse.Namespace(user=None, euid=0)
+        >>> handle_user(namespace) # doctest: +ELLIPSIS
+        Traceback (most recent call last):
+        ValidationError: argument user ...
+    """
+    if namespace.user is None:
+        if not namespace.euid:
+            raise ValidationError('argument user can not be omitted if '
+                                  'the script is run as root.')
+        namespace.user = pwd.getpwuid(namespace.euid).pw_name
+    namespace.home_dir = get_user_home(namespace.user)
+
+
+def handle_lpuser(namespace):
+    """Handle lpuser argument.
+
+    If lpuser is not provided by namespace, the user name is used::
+
+        >>> import getpass
+        >>> username = getpass.getuser()
+
+        >>> import argparse
+        >>> namespace = argparse.Namespace(user=username, lpuser=None)
+        >>> handle_lpuser(namespace)
+        >>> namespace.lpuser == username
+        True
+    """
+    if namespace.lpuser is None:
+        namespace.lpuser = namespace.user
+
+
+def handle_userdata(namespace, whois=bzr_whois):
+    """Handle full_name and email arguments.
+
+    If they are not provided, this function tries to obtain them using
+    the given `whois` callable::
+
+        >>> import argparse
+        >>> namespace = argparse.Namespace(
+        ...     full_name=None, email=None, user='root')
+        >>> email = 'email@xxxxxxxxxxx'
+        >>> handle_userdata(namespace, lambda user: (user, email))
+        >>> namespace.full_name == namespace.user
+        True
+        >>> namespace.email == email
+        True
+
+    The validation fails if full_name or email are not provided and
+    they can not be obtained using the `whois` callable::
+
+        >>> namespace = argparse.Namespace(
+        ...     full_name=None, email=None, user='root')
+        >>> handle_userdata(namespace, lambda user: None) # doctest: +ELLIPSIS
+        Traceback (most recent call last):
+        ValidationError: arguments full-name ...
+
+        >>> namespace = argparse.Namespace(
+        ...     full_name=None, email=None, user='this_user_does_not_exist')
+        >>> handle_userdata(namespace) # doctest: +ELLIPSIS
+        Traceback (most recent call last):
+        ValidationError: arguments full-name ...
+
+    It does not make sense to provide only one argument::
+
+        >>> namespace = argparse.Namespace(full_name='Foo Bar', email=None)
+        >>> handle_userdata(namespace) # doctest: +ELLIPSIS
+        Traceback (most recent call last):
+        ValidationError: arguments full-name ...
+    """
+    args = (namespace.full_name, namespace.email)
+    if not all(args):
+        if any(args):
+            raise ValidationError(
+                'arguments full-name and email: '
+                'either none or both must be provided.')
+        if user_exists(namespace.user):
+            userdata = whois(namespace.user)
+            if userdata is None:
+                raise ValidationError(
+                    'arguments full-name and email are required: '
+                    'bzr user id not found.')
+            namespace.full_name, namespace.email = userdata
+        else:
+            raise ValidationError(
+                'arguments full-name and email are required: '
+                'system user not found.')
+
+
+def handle_ssh_keys(namespace):
+    r"""Handle private and public ssh keys.
+
+    Keys contained in the namespace are escaped::
+
+        >>> import argparse
+        >>> private = r'PRIVATE\nKEY'
+        >>> public = r'PUBLIC\nKEY'
+        >>> namespace = argparse.Namespace(
+        ...     private_key=private, public_key=public)
+        >>> handle_ssh_keys(namespace)
+        >>> namespace.private_key == private.decode('string-escape')
+        True
+        >>> namespace.public_key == public.decode('string-escape')
+        True
+        >>> namespace.valid_ssh_keys
+        True
+
+    Keys are None if they are not provided and can not be found in the
+    current home directory::
+
+        >>> namespace = argparse.Namespace(
+        ...     private_key=None, home_dir='/tmp/__does_not_exists__')
+        >>> handle_ssh_keys(namespace) # doctest: +ELLIPSIS
+        >>> print namespace.private_key
+        None
+        >>> print namespace.public_key
+        None
+        >>> namespace.valid_ssh_keys
+        False
+
+    If only one of private_key and public_key is provided, a
+    ValidationError will be raised.
+
+        >>> namespace = argparse.Namespace(
+        ...     private_key=private, public_key=None,
+        ...     home_dir='/tmp/__does_not_exists__')
+        >>> handle_ssh_keys(namespace) # doctest: +ELLIPSIS
+        Traceback (most recent call last):
+        ValidationError: arguments private-key...
+    """
+    namespace.valid_ssh_keys = True
+    for attr, filename in (
+        ('private_key', 'id_rsa'),
+        ('public_key', 'id_rsa.pub')):
+        value = getattr(namespace, attr, None)
+        if value:
+            setattr(namespace, attr, value.decode('string-escape'))
+        else:
+            path = os.path.join(namespace.home_dir, '.ssh', filename)
+            try:
+                value = open(path).read()
+            except IOError:
+                value = None
+                namespace.valid_ssh_keys = False
+            setattr(namespace, attr, value)
+    if bool(namespace.private_key) != bool(namespace.public_key):
+        raise ValidationError(
+            "arguments private-key and public-key: "
+            "both must be provided or neither must be provided.")
+
+
+def handle_directories(namespace):
+    """Handle checkout and dependencies directories.
+
+    The ~ construction is automatically expanded::
+
+        >>> import argparse
+        >>> namespace = argparse.Namespace(
+        ...     directory='~/launchpad', dependencies_dir='~/launchpad/deps',
+        ...     home_dir='/home/foo')
+        >>> handle_directories(namespace)
+        >>> namespace.directory
+        '/home/foo/launchpad'
+        >>> namespace.dependencies_dir
+        '/home/foo/launchpad/deps'
+
+    The validation fails for directories not residing inside the home::
+
+        >>> namespace = argparse.Namespace(
+        ...     directory='/tmp/launchpad',
+        ...     dependencies_dir='~/launchpad/deps',
+        ...     home_dir='/home/foo')
+        >>> handle_directories(namespace) # doctest: +ELLIPSIS
+        Traceback (most recent call last):
+        ValidationError: argument directory ...
+
+    The validation fails if the directory contains spaces::
+
+        >>> namespace = argparse.Namespace(directory='my directory')
+        >>> handle_directories(namespace) # doctest: +ELLIPSIS
+        Traceback (most recent call last):
+        ValidationError: argument directory ...
+    """
+    if ' ' in namespace.directory:
+        raise ValidationError('argument directory can not contain spaces.')
+    for attr in ('directory', 'dependencies_dir'):
+        directory = getattr(
+            namespace, attr).replace('~', namespace.home_dir)
+        if not directory.startswith(namespace.home_dir + os.path.sep):
+            raise ValidationError(
+                'argument {0} does not reside under the home '
+                'directory of the system user.'.format(attr))
+        setattr(namespace, attr, directory)

=== added file 'lpsetup/settings.py'
--- lpsetup/settings.py	1970-01-01 00:00:00 +0000
+++ lpsetup/settings.py	2012-03-12 14:27:19 +0000
@@ -0,0 +1,71 @@
+#!/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).
+
+"""Global settings and defaults for lpsetup."""
+
+
+APT_REPOSITORIES = (
+    'deb http://archive.ubuntu.com/ubuntu {distro} multiverse',
+    'deb http://archive.ubuntu.com/ubuntu {distro}-updates multiverse',
+    'deb http://archive.ubuntu.com/ubuntu {distro}-security multiverse',
+    'ppa:launchpad/ppa',
+    'ppa:bzr/ppa',
+    )
+BASE_PACKAGES = ['ssh', 'bzr', 'language-pack-en']
+CHECKOUT_DIR = '~/launchpad/branches'
+DEPENDENCIES_DIR = '~/launchpad/dependencies'
+DHCP_FILE = '/etc/dhcp/dhclient.conf'
+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'),
+    )
+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/bazaar.launchpad.dev/mirrors',
+    '/var/tmp/archive',
+    '/var/tmp/ppa',
+    )
+LP_BZR_LOCATIONS = (
+    ('submit_branch', '{checkout_dir}'),
+    ('public_branch', 'bzr+ssh://bazaar.launchpad.net/~{lpuser}/launchpad'),
+    ('public_branch:policy', 'appendpath'),
+    ('push_location', 'lp:~{lpuser}/launchpad'),
+    ('push_location:policy', 'appendpath'),
+    ('merge_target', '{checkout_dir}'),
+    ('submit_to', 'merge@xxxxxxxxxxxxxxxxxx'),
+    )
+LP_CHECKOUT = 'devel'
+LP_PACKAGES = [
+    'bzr', 'launchpad-developer-dependencies', 'apache2',
+    'apache2-mpm-worker', 'libapache2-mod-wsgi'
+    ]
+LP_REPOS = (
+    'http://bazaar.launchpad.net/~launchpad-pqm/launchpad/devel',
+    'lp:launchpad',
+    )
+LP_SOURCE_DEPS = (
+    'http://bazaar.launchpad.net/~launchpad/lp-source-dependencies/trunk')
+LXC_CONFIG_TEMPLATE = '/etc/lxc/local.conf'
+LXC_GUEST_ARCH = 'i386'
+LXC_GUEST_CHOICES = ('lucid', 'oneiric', 'precise')
+LXC_GUEST_OS = LXC_GUEST_CHOICES[0]
+LXC_NAME = 'lptests'
+LXC_OPTIONS = """
+lxc.network.type = veth
+lxc.network.link = {interface}
+lxc.network.flags = up
+"""
+LXC_PACKAGES = ['lxc', 'libvirt-bin']
+LXC_PATH = '/var/lib/lxc/'
+RESOLV_FILE = '/etc/resolv.conf'

=== added directory 'lpsetup/subcommands'
=== added file 'lpsetup/subcommands/__init__.py'
=== added file 'lpsetup/subcommands/install.py'
--- lpsetup/subcommands/install.py	1970-01-01 00:00:00 +0000
+++ lpsetup/subcommands/install.py	2012-03-12 14:27:19 +0000
@@ -0,0 +1,288 @@
+#!/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).
+
+"""Install subcommand: create a Launchpad development environment."""
+
+__metaclass__ = type
+__all__ = [
+    'make_launchpad',
+    'initialize',
+    'SubCommand',
+    'setup_apt',
+    'setup_codebase',
+    'setup_external_sourcecode',
+    'setup_launchpad',
+    ]
+
+from email.Utils import formataddr
+import os
+import pwd
+import subprocess
+
+from shelltoolbox import (
+    apt_get_install,
+    cd,
+    generate_ssh_keys,
+    get_su_command,
+    file_append,
+    mkdirs,
+    run,
+    su,
+    user_exists,
+    )
+
+from lpsetup import (
+    argparser,
+    handlers,
+    )
+from lpsetup.settings import (
+    APT_REPOSITORIES,
+    BASE_PACKAGES,
+    CHECKOUT_DIR,
+    DEPENDENCIES_DIR,
+    HOSTS_CONTENT,
+    HOSTS_FILE,
+    LP_APACHE_MODULES,
+    LP_APACHE_ROOTS,
+    LP_BZR_LOCATIONS,
+    LP_CHECKOUT,
+    LP_PACKAGES,
+    LP_REPOS,
+    LP_SOURCE_DEPS,
+    )
+from lpsetup.utils import call
+
+
+def setup_codebase(user, valid_ssh_keys, checkout_dir, dependencies_dir):
+    """Set up Launchpad repository and source dependencies.
+
+    Return True if new changes are pulled from bzr repository.
+    """
+    # Using real su because bzr uses uid.
+    if os.path.exists(checkout_dir):
+        # Pull the repository.
+        revno_args = ('bzr', 'revno', checkout_dir)
+        revno = run(*revno_args)
+        call(*get_su_command(user, ['bzr', 'pull', '-d', checkout_dir]))
+        changed = revno != run(*revno_args)
+    else:
+        # Branch the repository.
+        cmd = ('bzr', 'branch',
+               LP_REPOS[1] if valid_ssh_keys else LP_REPOS[0], checkout_dir)
+        call(*get_su_command(user, cmd))
+        changed = True
+    # Check repository integrity.
+    if subprocess.call(['bzr', 'status', '-q', checkout_dir]):
+        raise subprocess.CalledProcessError(
+            'Repository {0} is corrupted.'.format(checkout_dir))
+    # Set up source dependencies.
+    with su(user):
+        for subdir in ('eggs', 'yui', 'sourcecode'):
+            mkdirs(os.path.join(dependencies_dir, subdir))
+        download_cache = os.path.join(dependencies_dir, 'download-cache')
+        if os.path.exists(download_cache):
+            call('bzr', 'up', download_cache)
+        else:
+            call('bzr', 'co', '--lightweight', LP_SOURCE_DEPS, download_cache)
+    return changed
+
+
+def setup_external_sourcecode(
+    user, valid_ssh_keys, checkout_dir, dependencies_dir):
+    """Update and link external sourcecode."""
+    cmd = (
+        'utilities/update-sourcecode',
+        None if valid_ssh_keys else '--use-http',
+        os.path.join(dependencies_dir, 'sourcecode'),
+        )
+    with cd(checkout_dir):
+        # Using real su because update-sourcecode uses uid.
+        call(*get_su_command(user, cmd))
+        with su(user):
+            call('utilities/link-external-sourcecode', dependencies_dir)
+
+
+def make_launchpad(user, checkout_dir, install=False):
+    """Make and optionally install Launchpad."""
+    # Using real su because mailman make script uses uid.
+    call(*get_su_command(user, ['make', '-C', checkout_dir]))
+    if install:
+        call('make', '-C', checkout_dir, 'install')
+
+
+
+def initialize(
+    user, full_name, email, lpuser, private_key, public_key, valid_ssh_keys,
+    dependencies_dir, directory):
+    """Initialize host machine."""
+    # Install necessary deb packages.  This requires Oneiric or later.
+    call('apt-get', 'update')
+    apt_get_install(*BASE_PACKAGES)
+    # Create the user (if he does not exist).
+    if not user_exists(user):
+        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'):
+        generate_ssh_keys('/root/.ssh/')
+    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')
+        mkdirs(ssh_dir)
+        # Generate user ssh keys if none are supplied.
+        if not valid_ssh_keys:
+            generate_ssh_keys(ssh_dir)
+            private_key = open(os.path.join(ssh_dir, 'id_rsa')).read()
+            public_key = open(os.path.join(ssh_dir, 'id_rsa.pub')).read()
+        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 = run(
+            '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(contents + '\n')
+            os.chmod(filename, 0644)
+        os.chmod(priv_file, 0600)
+        # Set up bzr and Launchpad authentication.
+        call('bzr', 'whoami', formataddr([full_name, email]))
+        if valid_ssh_keys:
+            subprocess.call(['bzr', 'lp-login', lpuser])
+        # Set up the repository.
+        mkdirs(directory)
+        call('bzr', 'init-repo', '--2a', directory)
+    # Set up the codebase.
+    checkout_dir = os.path.join(directory, LP_CHECKOUT)
+    setup_codebase(user, valid_ssh_keys, checkout_dir, dependencies_dir)
+    # Set up bzr locations
+    with su(user) as env:
+        bzr_locations = os.path.join(env.home, '.bazaar', 'locations.conf')
+        file_append(bzr_locations, '[{0}]\n'.format(directory))
+        lines = ['{0} = {1}\n'.format(k, v) for k, v in LP_BZR_LOCATIONS]
+        for line in lines:
+            location = line.format(checkout_dir=checkout_dir, lpuser=lpuser)
+            file_append(bzr_locations, location)
+
+
+def setup_apt(no_repositories=True):
+    """Setup, update and upgrade deb packages."""
+    if not no_repositories:
+        distro = run('lsb_release', '-cs').strip()
+        # APT repository update.
+        for reposirory in APT_REPOSITORIES:
+            assume_yes = None if distro == 'lucid' else '-y'
+            call('add-apt-repository', assume_yes,
+                 reposirory.format(distro=distro))
+        # XXX frankban 2012-01-13 - Bug 892892: upgrading mountall in LXC
+        # containers currently does not work.
+        call("echo 'mountall hold' | dpkg --set-selections", shell=True)
+    call('apt-get', 'update')
+    # Install base and Launchpad deb packages.
+    apt_get_install(*LP_PACKAGES)
+
+
+def setup_launchpad(user, dependencies_dir, directory, valid_ssh_keys):
+    """Set up the Launchpad environment."""
+    # User configuration.
+    subprocess.call(['adduser', user, 'sudo'])
+    gid = pwd.getpwnam(user).pw_gid
+    subprocess.call(['addgroup', '--gid', str(gid), user])
+    # Set up Launchpad dependencies.
+    checkout_dir = os.path.join(directory, LP_CHECKOUT)
+    setup_external_sourcecode(
+        user, valid_ssh_keys, checkout_dir, dependencies_dir)
+    with su(user):
+        # Create Apache document roots, to avoid warnings.
+        mkdirs(*LP_APACHE_ROOTS)
+    # Set up Apache modules.
+    for module in LP_APACHE_MODULES.split():
+        call('a2enmod', module)
+    with cd(checkout_dir):
+        # Launchpad database setup.
+        call('utilities/launchpad-database-setup', user)
+    # Make and install launchpad.
+    make_launchpad(user, checkout_dir, install=True)
+    # Set up container hosts file.
+    lines = ['{0}\t{1}\n'.format(ip, names) for ip, names in HOSTS_CONTENT]
+    for line in lines:
+        file_append(HOSTS_FILE, line)
+
+
+class SubCommand(argparser.ActionsBasedSubCommand):
+    """Install the Launchpad environment."""
+
+    actions = (
+        (initialize,
+         'user', 'full_name', 'email', 'lpuser', 'private_key',
+         'public_key', 'valid_ssh_keys', 'dependencies_dir', 'directory'),
+        (setup_apt, 'no_repositories'),
+        (setup_launchpad,
+         'user', 'dependencies_dir', 'directory', 'valid_ssh_keys'),
+        )
+    help = __doc__
+    needs_root = True
+    validators = (
+        handlers.handle_user,
+        handlers.handle_lpuser,
+        handlers.handle_userdata,
+        handlers.handle_ssh_keys,
+        handlers.handle_directories,
+        )
+
+    def add_arguments(self, parser):
+        super(SubCommand, self).add_arguments(parser)
+        parser.add_argument(
+            '-u', '--user',
+            help='The name of the system user to be created or updated. '
+                 'The current user is used if this script is not run as '
+                 'root and this argument is omitted.')
+        parser.add_argument(
+            '-e', '--email',
+            help='The email of the user, used for bzr whoami. This argument '
+                 'can be omitted if a bzr id exists for current user.')
+        parser.add_argument(
+            '-f', '--full-name',
+            help='The full name of the user, used for bzr whoami. '
+                 'This argument can be omitted if a bzr id exists for '
+                 'current user.')
+        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 this argument is omitted and a keypair is '
+                 'not found in the home directory of the system user a new '
+                 'SSH keypair will be generated and the checkout of the '
+                 'Launchpad code will use HTTP rather than bzr+ssh.')
+        parser.add_argument(
+            '-b', '--public-key',
+            help='The SSH public key for the Launchpad user. '
+                 'If this argument is omitted and a keypair is not found '
+                 'in the home directory of the system user a new SSH '
+                 'keypair will be generated and the checkout of the '
+                 'Launchpad code will use HTTP rather than bzr+ssh.')
+        parser.add_argument(
+            '-d', '--dependencies-dir', default=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). '
+                 '[DEFAULT={0}]'.format(DEPENDENCIES_DIR))
+        parser.add_argument(
+            '-c', '--directory', default=CHECKOUT_DIR,
+            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). '
+                 '[DEFAULT={0}]'.format(CHECKOUT_DIR))
+        parser.add_argument(
+            '-N', '--no-repositories', action='store_true',
+            help='Do not add APT repositories.')

=== added file 'lpsetup/subcommands/lxcinstall.py'
--- lpsetup/subcommands/lxcinstall.py	1970-01-01 00:00:00 +0000
+++ lpsetup/subcommands/lxcinstall.py	2012-03-12 14:27:19 +0000
@@ -0,0 +1,185 @@
+#!/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).
+
+"""LXC install subcommand: create a Launchpad environment inside an LXC."""
+
+__metaclass__ = type
+__all__ = [
+    'create_lxc',
+    'initialize_lxc',
+    'SubCommand',
+    'setup_launchpad_lxc',
+    'start_lxc',
+    'stop_lxc',
+    'wait_for_lxc',
+    ]
+
+import os
+import shutil
+import subprocess
+import time
+
+from shelltoolbox import (
+    apt_get_install,
+    file_append,
+    file_prepend,
+    mkdirs,
+    ssh,
+    )
+
+from lpsetup.settings import (
+    BASE_PACKAGES,
+    DHCP_FILE,
+    LXC_CONFIG_TEMPLATE,
+    LXC_GUEST_ARCH,
+    LXC_GUEST_CHOICES,
+    LXC_GUEST_OS,
+    LXC_NAME,
+    LXC_OPTIONS,
+    LXC_PACKAGES,
+    RESOLV_FILE,
+    )
+from lpsetup.subcommands import install
+from lpsetup.utils import (
+    call,
+    get_container_path,
+    get_lxc_gateway,
+    lxc_stopped,
+    this_command,
+    )
+
+
+def create_lxc(user, lxc_name, lxc_arch, lxc_os):
+    """Create the LXC named `lxc_name` sharing `user` home directory.
+
+    The container will be used as development environment or as base template
+    for parallel testing using ephemeral instances.
+    """
+    # Install necessary deb packages.
+    apt_get_install(*LXC_PACKAGES)
+    # XXX 2012-02-02 gmb bug=925024:
+    #     These calls need to be removed once the lxc vs. apparmor bug
+    #     is resolved, since having apparmor enabled for lxc is very
+    #     much a Good Thing.
+    # Disable the apparmor profiles for lxc so that we don't have
+    # problems installing postgres.
+    call('ln', '-s',
+         '/etc/apparmor.d/usr.bin.lxc-start', '/etc/apparmor.d/disable/')
+    call('apparmor_parser', '-R', '/etc/apparmor.d/usr.bin.lxc-start')
+    # Update resolv file in order to get the ability to ssh into the LXC
+    # container using its name.
+    lxc_gateway_name, lxc_gateway_address = get_lxc_gateway()
+    file_prepend(RESOLV_FILE, 'nameserver {0}\n'.format(lxc_gateway_address))
+    file_append(
+        DHCP_FILE,
+        'prepend domain-name-servers {0};\n'.format(lxc_gateway_address))
+    # Container configuration template.
+    content = LXC_OPTIONS.format(interface=lxc_gateway_name)
+    with open(LXC_CONFIG_TEMPLATE, 'w') as f:
+        f.write(content)
+    # Creating container.
+    call(
+        'lxc-create',
+        '-t', 'ubuntu',
+        '-n', lxc_name,
+        '-f', LXC_CONFIG_TEMPLATE,
+        '--',
+        '-r {os} -a {arch} -b {user}'.format(
+            os=lxc_os, arch=lxc_arch, user=user),
+        )
+    # Set up root ssh key.
+    user_authorized_keys = os.path.expanduser(
+        '~' + 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(lxc_name, '/root/.ssh/')
+    mkdirs(dst)
+    shutil.copy(user_authorized_keys, dst)
+
+
+def start_lxc(lxc_name):
+    """Start the lxc instance named `lxc_name`."""
+    call('lxc-start', '-n', lxc_name, '-d')
+
+
+def wait_for_lxc(lxc_name, trials=60, sleep_seconds=1):
+    """Try to ssh as `user` into the LXC container named `lxc_name`."""
+    sshcall = ssh(lxc_name)
+    while True:
+        trials -= 1
+        try:
+            sshcall('true')
+        except subprocess.CalledProcessError:
+            if not trials:
+                raise
+            time.sleep(sleep_seconds)
+        else:
+            break
+
+
+def initialize_lxc(lxc_name, lxc_os):
+    """Initialize LXC container."""
+    base_packages = list(BASE_PACKAGES)
+    if lxc_os == 'lucid':
+        # Install argparse to be able to run this script inside a lucid lxc.
+        base_packages.append('python-argparse')
+    ssh(lxc_name)(
+        'DEBIAN_FRONTEND=noninteractive '
+        'apt-get install -y ' + ' '.join(base_packages))
+
+
+def setup_launchpad_lxc(
+    user, dependencies_dir, directory, valid_ssh_keys, lxc_name):
+    """Set up the Launchpad environment inside an LXC."""
+    # Use ssh to call this script from inside the container.
+    args = [
+        'install', '-u', user, '-a', 'setup_apt', 'setup_launchpad',
+        '-d', dependencies_dir, '-c', directory
+        ]
+    cmd = this_command(directory, args)
+    ssh(lxc_name)(cmd)
+
+
+def stop_lxc(lxc_name):
+    """Stop the lxc instance named `lxc_name`."""
+    ssh(lxc_name)('poweroff')
+    if not lxc_stopped(lxc_name):
+        subprocess.call(['lxc-stop', '-n', lxc_name])
+
+
+class SubCommand(install.SubCommand):
+    """Install the Launchpad environment inside an LXC."""
+
+    actions = (
+        (install.initialize,
+         'user', 'full_name', 'email', 'lpuser', 'private_key',
+         'public_key', 'valid_ssh_keys', 'dependencies_dir', 'directory'),
+        (create_lxc,
+         'user', 'lxc_name', 'lxc_arch', 'lxc_os'),
+        (start_lxc, 'lxc_name'),
+        (wait_for_lxc, 'lxc_name'),
+        (initialize_lxc,
+         'lxc_name', 'lxc_os'),
+        (setup_launchpad_lxc,
+         'user', 'dependencies_dir', 'directory', 'valid_ssh_keys',
+         'lxc_name'),
+        (stop_lxc, 'lxc_name'),
+        )
+    help = __doc__
+
+    def add_arguments(self, parser):
+        super(SubCommand, self).add_arguments(parser)
+        parser.add_argument(
+            '-n', '--lxc-name', default=LXC_NAME,
+            help='The LXC container name to setup. '
+                 '[DEFAULT={0}]'.format(LXC_NAME))
+        parser.add_argument(
+            '-A', '--lxc-arch', default=LXC_GUEST_ARCH,
+            help='The LXC container architecture. '
+                 '[DEFAULT={0}]'.format(LXC_GUEST_ARCH))
+        parser.add_argument(
+            '-r', '--lxc-os', default=LXC_GUEST_OS,
+            choices=LXC_GUEST_CHOICES,
+            help='The LXC container distro codename. '
+                 '[DEFAULT={0}]'.format(LXC_GUEST_OS))

=== added file 'lpsetup/subcommands/update.py'
--- lpsetup/subcommands/update.py	1970-01-01 00:00:00 +0000
+++ lpsetup/subcommands/update.py	2012-03-12 14:27:19 +0000
@@ -0,0 +1,101 @@
+#!/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).
+
+"""Update subcommand: update a Launchpad environment, pulling changes."""
+
+__metaclass__ = type
+__all__ = [
+    'link_sourcecode_in_branches',
+    'update_launchpad',
+    'SubCommand',
+    ]
+
+import os
+
+from shelltoolbox import su
+
+from lpsetup import (
+    argparser,
+    handlers,
+    )
+from lpsetup.settings import (
+    CHECKOUT_DIR,
+    DEPENDENCIES_DIR,
+    LP_CHECKOUT,
+    )
+from lpsetup.subcommands import install
+from lpsetup.utils import call
+
+
+def update_launchpad(user, valid_ssh_keys, dependencies_dir, directory, apt):
+    """Update the Launchpad environment."""
+    if apt:
+        install.setup_apt(no_repositories=True)
+    checkout_dir = os.path.join(directory, LP_CHECKOUT)
+    # Update the Launchpad codebase.
+    changed = install.setup_codebase(
+        user, valid_ssh_keys, checkout_dir, dependencies_dir)
+    install.setup_external_sourcecode(
+        user, valid_ssh_keys, checkout_dir, dependencies_dir)
+    if changed:
+        install.make_launchpad(user, checkout_dir, install=False)
+
+
+def link_sourcecode_in_branches(user, dependencies_dir, directory):
+    """Link external sourcecode for all branches in the project."""
+    checkout_dir = os.path.join(directory, LP_CHECKOUT)
+    cmd = os.path.join(checkout_dir, 'utilities', 'link-external-sourcecode')
+    with su(user):
+        for dirname in os.listdir(directory):
+            branch = os.path.join(directory, dirname)
+            sourcecode_dir = os.path.join(branch, 'sourcecode')
+            if (branch != checkout_dir and
+                os.path.exists(sourcecode_dir) and
+                os.path.isdir(sourcecode_dir)):
+                call(cmd, '--parent', dependencies_dir, '--target', branch)
+
+
+
+class SubCommand(argparser.ActionsBasedSubCommand):
+    """Update the Launchpad environment to latest version."""
+
+    actions = (
+        (update_launchpad,
+         'user', 'valid_ssh_keys', 'dependencies_dir', 'directory', 'apt'),
+        (link_sourcecode_in_branches,
+            'user', 'dependencies_dir', 'directory'),
+        )
+    help = __doc__
+    validators = (
+        handlers.handle_user,
+        handlers.handle_ssh_keys,
+        handlers.handle_directories,
+        )
+
+    def get_needs_root(self, namespace):
+        # Root is needed only if an apt update/upgrade is requested.
+        return namespace.apt
+
+    def add_arguments(self, parser):
+        super(SubCommand, self).add_arguments(parser)
+        parser.add_argument(
+            '-u', '--user',
+            help='The name of the system user used to update Launchpad. '
+                 'The current user is used if this script is not run as '
+                 'root and this argument is omitted.')
+        parser.add_argument(
+            '-d', '--dependencies-dir', default=DEPENDENCIES_DIR,
+            help='The directory of the Launchpad dependencies to be updated. '
+                 'The directory must reside under the home directory of the '
+                 'given user (see -u argument). '
+                 '[DEFAULT={0}]'.format(DEPENDENCIES_DIR))
+        parser.add_argument(
+            '-c', '--directory', default=CHECKOUT_DIR,
+            help='The directory of the Launchpad repository to be updated. '
+                 'The directory must reside under the home directory of the '
+                 'given user (see -u argument). '
+                 '[DEFAULT={0}]'.format(CHECKOUT_DIR))
+        parser.add_argument(
+            '-D', '--apt', action='store_true',
+            help='Also update deb packages.')

=== added directory 'lpsetup/tests'
=== added file 'lpsetup/tests/__init__.py'
=== added file 'lpsetup/utils.py'
--- lpsetup/utils.py	1970-01-01 00:00:00 +0000
+++ lpsetup/utils.py	2012-03-12 14:27:19 +0000
@@ -0,0 +1,111 @@
+#!/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).
+
+"""A collection of helper functions."""
+
+__metaclass__ = type
+__all__ = [
+    'call',
+    'get_container_path',
+    'get_lxc_gateway',
+    'lxc_in_state',
+    'lxc_running',
+    'lxc_stopped',
+    'this_command',
+    ]
+
+from functools import partial
+import os
+import platform
+import subprocess
+import time
+
+from shelltoolbox import (
+    join_command,
+    run,
+    )
+
+from lpsetup.settings import (
+    LP_CHECKOUT,
+    LXC_PATH,
+    )
+
+
+call = partial(run, stdout=None)
+
+
+def get_container_path(lxc_name, path='', base_path=LXC_PATH):
+    """Return the path of LXC container called `lxc_name`.
+
+    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, lxc_name, 'rootfs', path.lstrip('/'))
+
+
+def get_lxc_gateway():
+    """Return a tuple of gateway name and address.
+
+    The gateway name and address will change depending on which version
+    of Ubuntu the script is running on.
+    """
+    release_name = platform.linux_distribution()[2]
+    if release_name == 'oneiric':
+        return 'virbr0', '192.168.122.1'
+    else:
+        return 'lxcbr0', '10.0.3.1'
+
+
+def lxc_in_state(state, lxc_name, timeout=30):
+    """Return True if the LXC named `lxc_name` is in state `state`.
+
+    Return False otherwise.
+    """
+    while timeout:
+        try:
+            output = run('lxc-info', '-n', lxc_name, stderr=subprocess.STDOUT)
+        except subprocess.CalledProcessError:
+            pass
+        else:
+            if state in output:
+                return True
+        timeout -= 1
+        time.sleep(1)
+    return False
+
+
+lxc_running = partial(lxc_in_state, 'RUNNING')
+lxc_stopped = partial(lxc_in_state, 'STOPPED')
+
+
+def this_command(directory, args):
+    """Return a command line to re-launch this script using given `args`.
+
+    The command returned will execute this script from `directory`::
+
+        >>> import os
+        >>> script_name = os.path.basename(__file__)
+
+        >>> cmd = this_command('/home/user/lp/branches', ['install'])
+        >>> cmd == ('cd /home/user/lp/branches && devel/utilities/' +
+        ...         script_name + ' install')
+        True
+
+    Arguments are correctly quoted::
+
+        >>> cmd = this_command('/path', ['-arg', 'quote me'])
+        >>> cmd == ('cd /path && devel/utilities/' +
+        ...         script_name + " -arg 'quote me'")
+        True
+    """
+    script = join_command([
+        os.path.join(LP_CHECKOUT, 'utilities', os.path.basename(__file__)),
+        ] + list(args))
+    return join_command(['cd', directory]) + ' && ' + script

=== added file 'setup.py'
--- setup.py	1970-01-01 00:00:00 +0000
+++ setup.py	2012-03-12 14:27:19 +0000
@@ -0,0 +1,53 @@
+#!/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).
+
+from distutils.core import setup
+import os
+
+
+project_name = 'lpsetup'
+
+root_dir = os.path.dirname(__file__)
+if root_dir:
+    os.chdir(root_dir)
+
+data_files = []
+for dirpath, dirnames, filenames in os.walk(project_name):
+    for i, dirname in enumerate(dirnames):
+        if dirname.startswith('.'): del dirnames[i]
+    if '__init__.py' in filenames:
+        continue
+    elif filenames:
+        for f in filenames:
+            data_files.append(os.path.join(dirpath[len(project_name)+1:], f))
+
+project = __import__(project_name)
+long_description = open(os.path.join(root_dir, 'README.rst')).read()
+
+setup(
+    name=project_name,
+    version=project.get_version(),
+    description=project.__doc__,
+    long_description=long_description,
+    author='Francesco Banconi',
+    author_email='francesco.banconi@xxxxxxxxxxxxx',
+    url='https://launchpad.net/lpsetup',
+    scripts=['lp-setup'],
+    packages=[
+        'lpsetup',
+        'lpsetup.subcommands',
+        ],
+    package_data={project_name: data_files},
+    # install_requires=['python-shell-toolbox'],
+    classifiers=[
+        'Development Status :: 3 - Alpha',
+        'Environment :: Console',
+        'Intended Audience :: Developers',
+        'License :: OSI Approved :: GNU Affero General Public License v3',
+        'Operating System :: POSIX :: Linux',
+        'Programming Language :: Python',
+        'Topic :: Utilities'
+        ],
+
+    )