← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~yellow/launchpad/lxcsetup into lp:launchpad

 

Francesco Banconi has proposed merging lp:~yellow/launchpad/lxcsetup into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~yellow/launchpad/lxcsetup/+merge/89660

= Summary =

This branch adds a script that can be used to set up a Launchpad environment
inside a LXC, useful for testing Launchpad.

== Proposed fix ==

https://dev.launchpad.net/ParallelTests describes how LXC containers offer 
a cheap way to run tests in parallel using ephemeral instances, obtaining the 
required isolation to workaround the existing globals (shared work dirs, 
hardcoded tcp ports, etc.). The `setuplxc.py` script creates a Launchpad 
environment from scratch, ready to be used by ephemerals (e.g. in a buildbot
slave context).


== Pre-implementation notes ==

During development we realized that the same approach (with few changes) 
should work to set up a Launchpad development environment. 
The developer can just:

- run the script (passing his current local username as user), e.g.::

    ./setuplxc.py -u username -e user@xxxxxxxxxxxxx -n 'Firstname Lastname' 
    -c lp-devel -d /home/username/lp-deps /home/username/lp-branches/

- ssh into the container (in the example above: ssh lp-devel)
- cd ~/lp-branches/devel
- make schema
- make run
- start hacking


== Implementation details ==

utilities/setuplxc.py:
* the script is implemented in Python
* requires Python 2.7
* must be run as root
* help on required arguments: utilities/setuplxc.py -h

utilities/launchpad-database-setup
* refactored so that it can be run by root


== Tests ==

The script uses internal helper functions providing doctests. To run them::
    
     python -m doctest -v utilities/setuplxc.py

A more extensive and complete testing approach would require to setup
a precise KVM.


== Demo and Q/A ==

To demo and Q/A this change, do the following:

 * Install precise in a virtual machine (e.g. KVM)

 * Copy the script inside the virtual machine, e.g. for kvm::

    kvm -hda precise_HDA.img -boot c -m 2000 -redir tcp:2222::22 &
    scp -P 2222 /utilities/setuplxc.py root@localhost:/tmp/

 * Run the script, e.g.::

    ssh -p 2222 root@localhost
    cd /tmp/
    ./setuplxc [arguments]


== lint ==

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  utilities/setuplxc.py

-- 
https://code.launchpad.net/~yellow/launchpad/lxcsetup/+merge/89660
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~yellow/launchpad/lxcsetup into lp:launchpad.
=== modified file 'utilities/launchpad-database-setup'
--- utilities/launchpad-database-setup	2011-09-06 01:38:11 +0000
+++ utilities/launchpad-database-setup	2012-01-23 11:06:17 +0000
@@ -18,9 +18,15 @@
 # https://dev.launchpad.net/DatabaseSetup which are intended for
 # initial Launchpad setup on an otherwise unconfigured postgresql instance
 
+if [ $(id -u) = 0 ]; then
+    SUDO=''
+else
+    SUDO='sudo'
+fi
+
 for pgversion in 8.4 8.3 8.2
 do
-  sudo grep -q "^auto" /etc/postgresql/$pgversion/main/start.conf
+  $SUDO grep -q "^auto" /etc/postgresql/$pgversion/main/start.conf
   if [ $? -eq 0 ]; then
     break
   fi
@@ -35,31 +41,31 @@
 echo "Using postgres $pgversion"
 
 # Make sure that we have the correct version running on port 5432
-sudo grep -q "port.*5432" /etc/postgresql/$pgversion/main/postgresql.conf
+$SUDO grep -q "port.*5432" /etc/postgresql/$pgversion/main/postgresql.conf
 if [ $? -ne 0 ]; then
   echo "Please check /etc/postgresql/$pgversion/main/postgresql.conf and"
   echo "ensure postgres is running on port 5432."
 fi;
 
 if [ -e /etc/init.d/postgresql-$pgversion ]; then
-    sudo /etc/init.d/postgresql-$pgversion stop
+    $SUDO /etc/init.d/postgresql-$pgversion stop
 else
     # This is Maverick.
-    sudo /etc/init.d/postgresql stop $pgversion
+    $SUDO /etc/init.d/postgresql stop $pgversion
 fi
 
 echo Purging postgresql data...
-sudo pg_dropcluster $pgversion main --stop-server
+$SUDO pg_dropcluster $pgversion main --stop-server
 echo Re-creating postgresql database...
 # Setting locale to C to make the server run in that locale.
-LC_ALL=C sudo pg_createcluster $pgversion main --encoding UNICODE
+LC_ALL=C $SUDO pg_createcluster $pgversion main --encoding UNICODE
 
 echo Applying postgresql configuration changes...
 
-sudo cp -a /etc/postgresql/$pgversion/main/pg_hba.conf \
+$SUDO cp -a /etc/postgresql/$pgversion/main/pg_hba.conf \
     /etc/postgresql/$pgversion/main/pg_hba.conf.old
-sudo grep -q Launchpad /etc/postgresql/$pgversion/main/pg_hba.conf || \
-sudo patch /etc/postgresql/$pgversion/main/pg_hba.conf <<'EOF'
+$SUDO grep -q Launchpad /etc/postgresql/$pgversion/main/pg_hba.conf || \
+$SUDO patch /etc/postgresql/$pgversion/main/pg_hba.conf <<'EOF'
 --- pg_hba.conf 2005-11-02 17:33:08.000000000 -0800
 +++ /tmp/pg_hba.conf    2005-11-03 07:32:46.932400423 -0800
 @@ -58,7 +58,10 @@
@@ -75,13 +81,13 @@
 
 
 EOF
-sudo chown --reference=/etc/postgresql/$pgversion/main/pg_hba.conf.old \
+$SUDO chown --reference=/etc/postgresql/$pgversion/main/pg_hba.conf.old \
     /etc/postgresql/$pgversion/main/pg_hba.conf
-sudo chmod --reference=/etc/postgresql/$pgversion/main/pg_hba.conf.old \
+$SUDO chmod --reference=/etc/postgresql/$pgversion/main/pg_hba.conf.old \
     /etc/postgresql/$pgversion/main/pg_hba.conf
 
-sudo grep -q Launchpad /etc/postgresql/$pgversion/main/postgresql.conf || \
-sudo tee -a /etc/postgresql/$pgversion/main/postgresql.conf <<'EOF'
+$SUDO grep -q Launchpad /etc/postgresql/$pgversion/main/postgresql.conf || \
+$SUDO tee -a /etc/postgresql/$pgversion/main/postgresql.conf <<'EOF'
 
 ##
 ## Launchpad configuration
@@ -98,25 +104,29 @@
 
 if [ "$pgversion" = 8.2 -o "$pgversion" = 8.3 ]
 then
-  sudo grep -q '^[[:space:]]*max_fsm_relations' /etc/postgresql/$pgversion/main/postgresql.conf || \
-  sudo tee -a /etc/postgresql/$pgversion/main/postgresql.conf <<'EOF'
+  $SUDO grep -q '^[[:space:]]*max_fsm_relations' /etc/postgresql/$pgversion/main/postgresql.conf || \
+  $SUDO tee -a /etc/postgresql/$pgversion/main/postgresql.conf <<'EOF'
 max_fsm_relations=2000
 
 EOF
 fi
 
 if [ -e /etc/init.d/postgresql-$pgversion ]; then
-    sudo /etc/init.d/postgresql-$pgversion start
+    $SUDO /etc/init.d/postgresql-$pgversion start
 else
     # This is Maverick.
-    sudo /etc/init.d/postgresql start $pgversion
+    $SUDO /etc/init.d/postgresql start $pgversion
 fi
 
 echo Waiting 10 seconds for postgresql to come up...
 sleep 10
 
 echo Creating postgresql user $USER
-sudo -u postgres /usr/lib/postgresql/$pgversion/bin/createuser -a -d $USER
+if [ $(id -u) = 0 ]; then
+    su postgres -c "/usr/lib/postgresql/$pgversion/bin/createuser -a -d $USER"
+else
+    sudo -u postgres /usr/lib/postgresql/$pgversion/bin/createuser -a -d $USER
+fi
 
 echo
 echo Looks like everything went ok.

=== added file 'utilities/setuplxc.py'
--- utilities/setuplxc.py	1970-01-01 00:00:00 +0000
+++ utilities/setuplxc.py	2012-01-23 11:06:17 +0000
@@ -0,0 +1,554 @@
+#!/usr/bin/env python
+# Copyright 2012 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Create an LXC test environment for Launchpad testing."""
+
+__metaclass__ = type
+__all__ = [
+    'cd',
+    'create_lxc',
+    'error',
+    'file_append',
+    'file_insert',
+    'get_container_path',
+    'get_user_ids',
+    'initialize_host',
+    'initialize_lxc',
+    'Namespace',
+    'ssh',
+    'stop_lxc',
+    'su',
+    'user_exists',
+    ]
+
+# This script is run as root.
+# To run doctests: python -m doctest -v setuplxc.py
+
+from collections import namedtuple, OrderedDict
+from contextlib import contextmanager
+import argparse
+import os
+import pwd
+import shutil
+import subprocess
+import sys
+import time
+
+
+DEPENDENCIES_DIR = '~/dependencies'
+DHCP_FILE = '/etc/dhcp/dhclient.conf'
+HOST_PACKAGES = ['ssh', 'lxc', 'libvirt-bin', 'bzr', 'language-pack-en']
+HOSTS_FILE = '/etc/hosts'
+LP_APACHE_MODULES = 'proxy proxy_http rewrite ssl deflate headers'
+LP_APACHE_ROOTS = (
+    '/var/tmp/bazaar.launchpad.dev/static',
+    '/var/tmp/archive',
+    '/var/tmp/ppa',
+    )
+LP_CHECKOUT = 'devel'
+LP_REPOSITORY = 'lp:launchpad'
+LP_SOURCE_DEPS = (
+    'http://bazaar.launchpad.net/~launchpad/lp-source-dependencies/trunk')
+LXC_CONFIG_TEMPLATE = '/etc/lxc/local.conf'
+LXC_GATEWAY = '10.0.3.1'
+LXC_GUEST_OS = 'lucid'
+LXC_HOSTS_CONTENT = (
+    ('127.0.0.88',
+        'launchpad.dev answers.launchpad.dev archive.launchpad.dev '
+        'api.launchpad.dev bazaar-internal.launchpad.dev beta.launchpad.dev '
+        'blueprints.launchpad.dev bugs.launchpad.dev code.launchpad.dev '
+        'feeds.launchpad.dev id.launchpad.dev keyserver.launchpad.dev '
+        'lists.launchpad.dev openid.launchpad.dev '
+        'ubuntu-openid.launchpad.dev ppa.launchpad.dev '
+        'private-ppa.launchpad.dev testopenid.dev translations.launchpad.dev '
+        'xmlrpc-private.launchpad.dev xmlrpc.launchpad.dev'),
+    ('127.0.0.99', 'bazaar.launchpad.dev'),
+    )
+LXC_NAME = 'lptests'
+LXC_OPTIONS = (
+    ('lxc.network.type', 'veth'),
+    ('lxc.network.link', 'lxcbr0'),
+    ('lxc.network.flags', 'up'),
+    )
+LXC_PATH = '/var/lib/lxc/'
+LXC_REPOS = (
+    'deb http://archive.ubuntu.com/ubuntu '
+    'lucid main universe multiverse',
+    'deb http://archive.ubuntu.com/ubuntu '
+    'lucid-updates main universe multiverse',
+    'deb http://archive.ubuntu.com/ubuntu '
+    'lucid-security main universe multiverse',
+    'deb http://ppa.launchpad.net/launchpad/ppa/ubuntu lucid main',
+    'deb http://ppa.launchpad.net/bzr/ppa/ubuntu lucid main',
+    )
+RESOLV_FILE = '/etc/resolv.conf'
+
+
+Env = namedtuple('Env', 'uid gid home')
+
+
+@contextmanager
+def ssh(location, user=None):
+    """Return a callable that can be used to run shell commands into another
+    host using ssh.
+
+    The ssh `location` and, optionally, `user` must be given.
+    If the user is None then the current user is used for the connection.
+    """
+    if user is not None:
+        location = '%s@%s' % (user, location)
+
+    def _sshcall(cmd):
+        sshcmd = (
+            'ssh',
+            '-o', 'StrictHostKeyChecking=no',
+            '-o', 'UserKnownHostsFile=/dev/null',
+            location,
+            '--', cmd,
+            )
+        return subprocess.call(sshcmd)
+
+    yield _sshcall
+
+
+def get_user_ids(user):
+    """Return the uid and gid of given `user`, e.g.::
+
+        >>> get_user_ids('root')
+        (0, 0)
+    """
+    userdata = pwd.getpwnam(user)
+    return userdata.pw_uid, userdata.pw_gid
+
+
+@contextmanager
+def su(user):
+    """A context manager to temporary run the Python interpreter as a
+    different user.
+    """
+    uid, gid = get_user_ids(user)
+    os.setegid(gid)
+    os.seteuid(uid)
+    current_home = os.getenv('HOME')
+    home = os.path.join(os.path.sep, 'home', user)
+    os.environ['HOME'] = home
+    yield Env(uid, gid, home)
+    os.setegid(os.getgid())
+    os.seteuid(os.getuid())
+    os.environ['HOME'] = current_home
+
+
+@contextmanager
+def cd(directory):
+    """A context manager to temporary change current working dir, e.g.::
+
+        >>> import os
+        >>> os.chdir('/tmp')
+        >>> with cd('/bin'): print os.getcwd()
+        /bin
+        >>> os.getcwd()
+        '/tmp'
+    """
+    cwd = os.getcwd()
+    os.chdir(directory)
+    yield
+    os.chdir(cwd)
+
+
+def get_container_path(lxcname, path='', base_path=LXC_PATH):
+    """Return the path of LXC container called `lxcname`.
+    If a `path` is given, return that path inside the container, e.g.::
+
+        >>> get_container_path('mycontainer')
+        '/var/lib/lxc/mycontainer/rootfs/'
+        >>> get_container_path('mycontainer', '/etc/apt/')
+        '/var/lib/lxc/mycontainer/rootfs/etc/apt/'
+        >>> get_container_path('mycontainer', 'home')
+        '/var/lib/lxc/mycontainer/rootfs/home'
+    """
+    return os.path.join(base_path, lxcname, 'rootfs', path.lstrip('/'))
+
+
+def file_insert(filename, line):
+    """Insert given `line`, if not present, at the beginning of `filename`,
+    e.g.::
+
+        >>> import tempfile
+        >>> f = tempfile.NamedTemporaryFile('w', delete=False)
+        >>> f.write('line1\\n')
+        >>> f.close()
+        >>> file_insert(f.name, 'line0\\n')
+        >>> open(f.name).read()
+        'line0\\nline1\\n'
+        >>> file_insert(f.name, 'line0\\n')
+        >>> open(f.name).read()
+        'line0\\nline1\\n'
+    """
+    with open(filename, 'r+') as f:
+        lines = f.readlines()
+        if lines[0] != line:
+            lines.insert(0, line)
+            f.seek(0)
+            f.writelines(lines)
+
+
+def file_append(filename, content):
+    """Append given `content`, if not present, at the end of `filename`,
+    e.g.::
+
+        >>> import tempfile
+        >>> f = tempfile.NamedTemporaryFile('w', delete=False)
+        >>> f.write('line1\\n')
+        >>> f.close()
+        >>> file_append(f.name, 'new content\\n')
+        >>> open(f.name).read()
+        'line1\\nnew content\\n'
+        >>> file_append(f.name, 'content')
+        >>> open(f.name).read()
+        'line1\\nnew content\\n'
+    """
+    with open(filename, 'r+') as f:
+        current_content = f.read()
+        if content not in current_content:
+            f.seek(0)
+            f.write(current_content + content)
+
+
+def user_exists(username):
+    """Return True if given `username` exists, e.g.::
+
+        >>> user_exists('root')
+        True
+        >>> user_exists('_this_user_does_not_exist_')
+        False
+    """
+    try:
+        pwd.getpwnam(username)
+    except KeyError:
+        return False
+    return True
+
+
+def error(msg):
+    """Print out the error message and quit the script."""
+    print 'ERROR: %s' % msg
+    sys.exit(1)
+
+
+parser = argparse.ArgumentParser(description=__doc__)
+parser.add_argument(
+    '-u', '--user', required=True,
+    help='The name of the system user to be created or updated.')
+parser.add_argument(
+    '-e', '--email', required=True,
+    help='The email of the user, used for bzr whoami.')
+parser.add_argument(
+    '-n', '--name', required=True,
+    help='The full name of the user, used fo bzr whoami.')
+parser.add_argument(
+    '-l', '--lpuser',
+    help=('The name of the Launchpad user that will be used to check out '
+          'dependencies.  If not provided, the system user name is used.'))
+parser.add_argument(
+    '-v', '--private-key',
+    help=('The SSH private key for the Launchpad user (without passphrase). '
+          'If the system user already exists with SSH key pair set up, '
+          'this argument can be omitted.'))
+parser.add_argument(
+    '-b', '--public-key',
+    help=('The SSH public key for the Launchpad user. '
+          'If the system user already exists with SSH key pair set up, '
+          'this argument can be omitted.'))
+parser.add_argument(
+    '-a', '--actions', nargs='+',
+    choices=('initialize_host', 'create_lxc', 'initialize_lxc', 'stop_lxc'),
+    help='Only for debugging. Call one or more internal functions.')
+parser.add_argument(
+    '-c', '--lxc-name', default=LXC_NAME,
+    metavar='LXC_NAME (default=%s)' % LXC_NAME,
+    help='The LXC container name.')
+parser.add_argument(
+    '-d', '--dependencies-dir', default=DEPENDENCIES_DIR,
+    metavar='DEPENDENCIES_DIR (default=%s)' % DEPENDENCIES_DIR,
+    help=('The directory of the Launchpad dependencies to be created. '
+          'The directory must reside under the home directory of the '
+          'given user (see -u argument).'))
+parser.add_argument(
+    'directory',
+    help=('The directory of the Launchpad repository to be created. '
+          'The directory must reside under the home directory of the '
+          'given user (see -u argument).'))
+
+
+def initialize_host(
+    user, fullname, email, lpuser, private_key, public_key,
+    dependencies_dir, directory):
+    """Initialize host machine."""
+    # Install necessary deb packages.  This requires Oneiric or later.
+    subprocess.call(['apt-get', 'update'])
+    subprocess.call(['apt-get', '-y', 'install'] + HOST_PACKAGES)
+    # Create the user (if he does not exist).
+    if not user_exists(user):
+        subprocess.call(['useradd', '-m', '-s', '/bin/bash', '-U', user])
+    # Generate root ssh keys if they do not exist.
+    if not os.path.exists('/root/.ssh/id_rsa.pub'):
+        subprocess.call([
+            'ssh-keygen', '-q', '-t', 'rsa', '-N', '',
+            '-f', '/root/.ssh/id_rsa'])
+    with su(user) as env:
+        # Set up the user's ssh directory.  The ssh key must be associated
+        # with the lpuser's Launchpad account.
+        ssh_dir = os.path.join(env.home, '.ssh')
+        if not os.path.exists(ssh_dir):
+            os.makedirs(ssh_dir)
+        priv_file = os.path.join(ssh_dir, 'id_rsa')
+        pub_file = os.path.join(ssh_dir, 'id_rsa.pub')
+        auth_file = os.path.join(ssh_dir, 'authorized_keys')
+        known_hosts = os.path.join(ssh_dir, 'known_hosts')
+        known_host_content = subprocess.check_output([
+            'ssh-keyscan', '-t', 'rsa', 'bazaar.launchpad.net'])
+        for filename, contents, mode in [
+            (priv_file, private_key, 'w'),
+            (pub_file, public_key, 'w'),
+            (auth_file, public_key, 'a'),
+            (known_hosts, known_host_content, 'a'),
+            ]:
+            with open(filename, mode) as f:
+                f.write('%s\n' % contents)
+            os.chmod(filename, 0644)
+        os.chmod(priv_file, 0600)
+        # Set up bzr and Launchpad authentication.
+        subprocess.call(['bzr', 'whoami', '"%s <%s>"' % (fullname, email)])
+        subprocess.call(['bzr', 'lp-login', lpuser])
+        # Set up the repository.
+        if not os.path.exists(directory):
+            os.makedirs(directory)
+        subprocess.call(['bzr', 'init-repo', directory])
+        checkout_dir = os.path.join(directory, LP_CHECKOUT)
+    # bzr branch does not work well with seteuid.
+    os.system(
+        "su - %s -c 'bzr branch %s %s'" % (user, LP_REPOSITORY, checkout_dir))
+    with su(user) as env:
+        # Set up source dependencies.
+        os.makedirs('%s/eggs' % dependencies_dir)
+        os.makedirs('%s/yui' % dependencies_dir)
+        os.makedirs('%s/sourcecode' % dependencies_dir)
+        with cd(dependencies_dir):
+            subprocess.call([
+                'bzr', 'co', '--lightweight',
+                LP_SOURCE_DEPS, 'download-cache'])
+    # Update resolv file in order to get the ability to ssh into the LXC
+    # container using its name.
+    file_insert(RESOLV_FILE, 'nameserver %s\n' % LXC_GATEWAY)
+    file_append(DHCP_FILE, 'prepend domain-name-servers %s;\n' % LXC_GATEWAY)
+
+
+def create_lxc(user, lxcname):
+    """Create the LXC container that will be used for ephemeral instances."""
+    # Container configuration template.
+    content = ''.join('%s=%s\n' % i for i in LXC_OPTIONS)
+    with open(LXC_CONFIG_TEMPLATE, 'w') as f:
+        f.write(content)
+    # Creating container.
+    exit_code = subprocess.call([
+        'lxc-create',
+        '-t', 'ubuntu',
+        '-n', lxcname,
+        '-f', LXC_CONFIG_TEMPLATE,
+        '--',
+        '-r %s -a i386 -b %s' % (LXC_GUEST_OS, user)
+        ])
+    if exit_code:
+        error('Unable to create the LXC container.')
+    subprocess.call(['lxc-start', '-n', lxcname, '-d'])
+    # Set up root ssh key.
+    user_authorized_keys = os.path.join(
+        os.path.sep, 'home', user, '.ssh/authorized_keys')
+    with open(user_authorized_keys, 'a') as f:
+        f.write(open('/root/.ssh/id_rsa.pub').read())
+    dst = get_container_path(lxcname, '/root/.ssh/')
+    if not os.path.exists(dst):
+        os.makedirs(dst)
+    shutil.copy(user_authorized_keys, dst)
+    # SSH into the container.
+    with ssh(lxcname, user) as sshcall:
+        timeout = 60
+        while timeout:
+            if not sshcall('true'):
+                break
+            timeout -= 1
+            time.sleep(1)
+        else:
+            error('Unable to SSH into LXC.')
+
+
+def initialize_lxc(user, dependencies_dir, directory, lxcname):
+    """Set up the Launchpad development environment inside the LXC container.
+    """
+    with ssh(lxcname) as sshcall:
+        # APT repository update.
+        sources = get_container_path(lxcname, '/etc/apt/sources.list')
+        with open(sources, 'w') as f:
+            f.write('\n'.join(LXC_REPOS))
+        # XXX frankban 2012-01-13 - Bug 892892: upgrading mountall in LXC
+        # containers currently does not work.
+        sshcall("echo 'mountall hold' | dpkg --set-selections")
+        # Upgrading packages.
+        sshcall(
+            'apt-get update && '
+            'DEBIAN_FRONTEND=noninteractive '
+            'apt-get -y --allow-unauthenticated install language-pack-en')
+        sshcall(
+            'DEBIAN_FRONTEND=noninteractive '
+            'apt-get -y --allow-unauthenticated install '
+            'bzr launchpad-developer-dependencies apache2 apache2-mpm-worker')
+        # User configuration.
+        sshcall('adduser %s sudo' % user)
+        pygetgid = 'import pwd; print pwd.getpwnam("%s").pw_gid' % user
+        gid = "`python -c '%s'`" % pygetgid
+        sshcall('addgroup --gid %s %s' % (gid, user))
+    with ssh(lxcname, user) as sshcall:
+        # Set up Launchpad dependencies.
+        checkout_dir = os.path.join(directory, LP_CHECKOUT)
+        sshcall(
+            'cd %s && utilities/update-sourcecode %s/sourcecode' % (
+            checkout_dir, dependencies_dir))
+        sshcall(
+            'cd %s && utilities/link-external-sourcecode %s' % (
+            checkout_dir, dependencies_dir))
+        # Create Apache document roots, to avoid warnings.
+        sshcall(' && '.join('mkdir -p %s' % i for i in LP_APACHE_ROOTS))
+    with ssh(lxcname) as sshcall:
+        # Set up Apache modules.
+        for module in LP_APACHE_MODULES.split():
+            sshcall('a2enmod %s' % module)
+        # Launchpad database setup.
+        sshcall(
+            'cd %s && utilities/launchpad-database-setup %s' % (
+            checkout_dir, user))
+    with ssh(lxcname, user) as sshcall:
+        sshcall('cd %s && make' % checkout_dir)
+    # Set up container hosts file.
+    lines = ['%s\t%s' % (ip, names) for ip, names in LXC_HOSTS_CONTENT]
+    lxc_hosts_file = get_container_path(lxcname, HOSTS_FILE)
+    file_append(lxc_hosts_file, '\n'.join(lines))
+    # Make and install launchpad.
+    with ssh(lxcname) as sshcall:
+        sshcall('cd %s && make install' % checkout_dir)
+
+
+def stop_lxc(lxcname):
+    """Stop the lxc instance named `lxcname`."""
+    with ssh(lxcname) as sshcall:
+        sshcall('poweroff')
+    time.sleep(5)
+    subprocess.call(['lxc-stop', '-n', lxcname])
+
+
+def main(
+    user, fullname, email, lpuser, private_key, public_key, actions,
+    lxc_name, dependencies_dir, directory):
+    function_args_map = OrderedDict((
+        ('initialize_host', (user, fullname, email, lpuser, private_key,
+                             public_key, dependencies_dir, directory)),
+        ('create_lxc', (user, lxc_name)),
+        ('initialize_lxc', (user, dependencies_dir, directory, lxc_name)),
+        ('stop_lxc', (lxc_name,)),
+        ))
+    if actions is None:
+        actions = function_args_map.keys()
+    scope = globals()
+    for action in actions:
+        scope[action](*function_args_map[action])
+
+
+class Namespace(object):
+    """A namespace for argparse.
+
+    Add methods for further arguments validation.
+    This class implements ssh key validation, e.g.::
+
+        >>> args = parser.parse_args('-u example_user -e example@xxxxxxxxxxx '
+        ...                          '-n exampleuser -v PRIVATE -b PUBLIC '
+        ...                          '/home/example_user/launchpad/'.split(),
+        ...                          namespace=Namespace())
+        >>> args.are_valid()
+        True
+        >>> args = parser.parse_args('-u example_user -e example@xxxxxxxxxxx '
+        ...                          '-n exampleuser -b PUBLIC '
+        ...                          '/home/example_user/launchpad/'.split(),
+        ...                          namespace=Namespace())
+        >>> args.are_valid()
+        False
+        >>> args.error_message # doctest: +ELLIPSIS
+        'argument private_key ...'
+
+    and directory validation::
+
+        >>> args = parser.parse_args('-u example_user -e example@xxxxxxxxxxx '
+        ...                          '-n exampleuser -v PRIVATE -b PUBLIC '
+        ...                          '/home/'.split(),
+        ...                          namespace=Namespace())
+        >>> args.are_valid()
+        False
+        >>> args.error_message # doctest: +ELLIPSIS
+        'argument directory ...'
+    """
+    _errors = None
+
+    def __repr__(self):
+        return repr(vars(self))
+
+    @property
+    def error_message(self):
+        return '\n'.join(self._errors)
+
+    def _get_ssh_key(self, attr, filename):
+        value = getattr(self, attr)
+        if value:
+            return value.decode('string-escape')
+        try:
+            return open(filename).read()
+        except IOError:
+            self._errors.append(
+                'argument %s is required if the system user '
+                'does not exists with SSH key pair set up.' % attr)
+
+    def _get_directory(self, attr, home_dir):
+        directory = getattr(self, attr).replace('~', home_dir)
+        if not directory.startswith(home_dir + os.path.sep):
+            self._errors.append('argument %s does not reside under the home '
+                                'directory of the system user.' % attr)
+        return directory
+
+    def are_valid(self):
+        self._errors = []
+        home_dir = os.path.join(os.path.sep, 'home', self.user)
+        if self.lpuser is None:
+            self.lpuser = self.user
+        self.private_key = self._get_ssh_key(
+            'private_key', os.path.join(home_dir, '.ssh', 'id_rsa'))
+        self.public_key = self._get_ssh_key(
+            'public_key', os.path.join(home_dir, '.ssh', 'id_rsa.pub'))
+        self.directory = self._get_directory('directory', home_dir)
+        self.dependencies_dir = self._get_directory(
+            'dependencies_dir', home_dir)
+        return not self._errors
+
+
+if __name__ == '__main__':
+    args = parser.parse_args(namespace=Namespace())
+    if args.are_valid():
+        main(args.user,
+             args.name,
+             args.email,
+             args.lpuser,
+             args.private_key,
+             args.public_key,
+             args.actions,
+             args.lxc_name,
+             args.dependencies_dir,
+             args.directory)
+    else:
+        parser.error(args.error_message)