launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #07736
[Merge] lp:~frankban/launchpad/remove-setuplxc into lp:launchpad
Francesco Banconi has proposed merging lp:~frankban/launchpad/remove-setuplxc into lp:launchpad.
Requested reviews:
Francesco Banconi (frankban)
For more details, see:
https://code.launchpad.net/~frankban/launchpad/remove-setuplxc/+merge/105321
Deleted setuplxc. lpsetup is now used in parallel tests.
--
https://code.launchpad.net/~frankban/launchpad/remove-setuplxc/+merge/105321
Your team Launchpad code reviewers is subscribed to branch lp:launchpad.
=== removed file 'utilities/setuplxc.py'
--- utilities/setuplxc.py 2012-05-07 20:00:52 +0000
+++ utilities/setuplxc.py 1970-01-01 00:00:00 +0000
@@ -1,1192 +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 an LXC test environment for Launchpad testing."""
-
-__metaclass__ = type
-__all__ = [
- 'ArgumentParser',
- 'cd',
- 'create_lxc',
- 'create_scripts',
- 'file_append',
- 'file_prepend',
- 'get_container_path',
- 'get_user_home',
- 'get_user_ids',
- 'initialize_host',
- 'initialize_lxc',
- 'SetupLXCError',
- 'ssh',
- 'SSHError',
- 'stop_lxc',
- 'su',
- 'user_exists',
- 'ValidationError',
- ]
-
-# To run doctests: python -m doctest -v setuplxc.py
-
-from collections import namedtuple, OrderedDict
-from contextlib import contextmanager
-from email.Utils import parseaddr, formataddr
-import argparse
-import os
-import platform
-import pwd
-import re
-import shutil
-import subprocess
-import sys
-import textwrap
-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',
- # XXX 2012-03-19 frankban bug=955006:
- # The yellow PPA contains an updated version of testrepository
- # that fixes the encoding issue.
- 'ppa:yellow/ppa',
- )
-DEPENDENCIES_DIR = '~/dependencies'
-DHCP_FILE = '/etc/dhcp/dhclient.conf'
-HOST_PACKAGES = [
- 'ssh', 'lxc', 'libvirt-bin', 'bzr', 'testrepository',
- 'python-shelltoolbox', 'subunit',
- ]
-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_DEB_DEPENDENCIES = (
- 'bzr launchpad-developer-dependencies apache2 '
- 'apache2-mpm-worker libapache2-mod-wsgi')
-LP_REPOSITORIES = (
- '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_OS = 'lucid'
-LXC_HOSTS_CONTENT = (
- ('127.0.0.88',
- 'launchpad.dev answers.launchpad.dev archive.launchpad.dev '
- 'api.launchpad.dev bazaar.launchpad.dev '
- 'bazaar-internal.launchpad.dev blueprints.launchpad.dev '
- 'bugs.launchpad.dev code.launchpad.dev feeds.launchpad.dev '
- 'keyserver.launchpad.dev lists.launchpad.dev ppa.launchpad.dev '
- 'private-ppa.launchpad.dev testopenid.dev translations.launchpad.dev '
- 'xmlrpc-private.launchpad.dev xmlrpc.launchpad.dev'),
- )
-LXC_NAME = 'lptests'
-LXC_OPTIONS = """
-lxc.network.type = veth
-lxc.network.link = {interface}
-lxc.network.flags = up
-"""
-LXC_PATH = '/var/lib/lxc/'
-RESOLV_FILE = '/etc/resolv.conf'
-SSH_KEY_NAME = 'id_rsa'
-
-
-Env = namedtuple('Env', 'uid gid home')
-
-
-class SetupLXCError(Exception):
- """Base exception for setuplxc."""
-
-
-class SSHError(SetupLXCError):
- """Errors occurred during SSH connection."""
-
-
-class ValidationError(SetupLXCError):
- """Argparse invalid arguments."""
-
-
-def bzr_whois(user):
- """Return fullname and email of bzr `user`.
-
- Return None if the given `user` does not have a bzr user id.
- """
- with su(user):
- try:
- whoami = subprocess.check_output(['bzr', 'whoami'])
- except (subprocess.CalledProcessError, OSError):
- return None
- return parseaddr(whoami)
-
-
-@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 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'
- """
- with open(filename, 'a+') as f:
- content = f.read()
- if line not in content:
- if content.endswith('\n'):
- 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(path):
- """Generate ssh key pair, saving them inside the given `directory`.
-
- >>> generate_ssh_keys('/tmp/id_rsa')
- 0
- >>> 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')
- """
- return subprocess.call([
- 'ssh-keygen', '-q', '-t', 'rsa', '-N', '', '-f', path])
-
-
-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 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_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 get_user_home(user):
- """Return the home directory of the given `user`.
-
- >>> get_user_home('root')
- '/root'
- """
- return pwd.getpwnam(user).pw_dir
-
-
-def ssh(location, user=None, key=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 tuple(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')
-
- The ssh key path can be optionally provided::
-
- >>> root_sshcall = ssh('example.com', key='/tmp/foo', caller=caller)
- >>> root_sshcall('ls -l') # doctest: +ELLIPSIS
- ('ssh', '-t', ..., '-i', '/tmp/foo', '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)
- """
- sshcmd = [
- 'ssh',
- '-t',
- '-t', # Yes, this second -t is deliberate. See `man ssh`.
- '-o', 'StrictHostKeyChecking=no',
- '-o', 'UserKnownHostsFile=/dev/null',
- ]
- if key is not None:
- sshcmd.extend(['-i', key])
- if user is not None:
- location = '{}@{}'.format(user, location)
- sshcmd.extend([location, '--'])
-
- def _sshcall(cmd, ignore_errors=False):
- command = sshcmd + [cmd]
- if caller(command) and not ignore_errors:
- raise SSHError('Error running command: ' + ' '.join(command))
-
- return _sshcall
-
-
-@contextmanager
-def su(user):
- """A context manager to temporary run the script as a different user."""
- uid, gid = get_user_ids(user)
- os.setegid(gid)
- os.seteuid(uid)
- current_home = os.getenv('HOME')
- home = get_user_home(user)
- os.environ['HOME'] = home
- try:
- yield Env(uid, gid, home)
- finally:
- os.setegid(os.getgid())
- os.seteuid(os.getuid())
- if current_home is not None:
- os.environ['HOME'] = current_home
-
-
-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
-
-
-class ArgumentParser(argparse.ArgumentParser):
- """A customized parser for argparse."""
-
- validators = ()
-
- 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 _validate(self, namespace):
- for validator in self.validators:
- try:
- validator(namespace)
- except ValidationError as err:
- self.error(err.message)
-
- def parse_args(self, *args, **kwargs):
- """Override to add further arguments cleaning and validation.
-
- `self.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')
- >>> parser = ArgumentParser()
- >>> parser.validators = [validator]
- >>> parser.parse_args([])
- Traceback (most recent call last):
- SystemExit: 2
- >>> sys.stderr = stderr
- """
- namespace = super(ArgumentParser, self).parse_args(*args, **kwargs)
- self._validate(namespace)
- return namespace
-
-
-def handle_users(namespace, euid=None):
- """Handle user and lpuser arguments.
-
- 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_users(namespace)
- >>> namespace.lpuser == username
- True
-
- This validator populates namespace with `home_dir` and `run_as_root`
- names::
-
- >>> handle_users(namespace, euid=0)
- >>> namespace.home_dir == '/home/' + username
- True
- >>> namespace.run_as_root
- True
-
- The validation fails if the current user is root and no user is provided::
-
- >>> namespace = argparse.Namespace(user=None)
- >>> handle_users(namespace, euid=0) # doctest: +ELLIPSIS
- Traceback (most recent call last):
- ValidationError: argument user ...
- """
- if euid is None:
- euid = os.geteuid()
- if namespace.user is None:
- if not euid:
- raise ValidationError('argument user can not be omitted if '
- 'the script is run as root.')
- namespace.user = pwd.getpwuid(euid).pw_name
- if namespace.lpuser is None:
- namespace.lpuser = namespace.user
- namespace.home_dir = get_user_home(namespace.user)
- namespace.run_as_root = not euid
-
-
-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='foo')
- >>> 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='foo')
- >>> handle_userdata(namespace, lambda user: None) # 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.')
- 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
-
-
-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,
- ... ssh_key_name='id_rsa', home_dir='/tmp/')
- >>> handle_ssh_keys(namespace)
- >>> namespace.private_key == private.decode('string-escape')
- True
- >>> namespace.public_key == public.decode('string-escape')
- True
-
- After this handler is called, the ssh key path is present as an attribute
- of the namespace::
-
- >>> namespace.ssh_key_path
- '/tmp/.ssh/id_rsa'
-
- Keys are None if they are not provided and can not be found in the
- current home directory::
-
- >>> namespace = argparse.Namespace(
- ... private_key=None, public_key=None, ssh_key_name='id_rsa',
- ... home_dir='/tmp/__does_not_exists__')
- >>> handle_ssh_keys(namespace) # doctest: +ELLIPSIS
- >>> print namespace.private_key
- None
- >>> print namespace.public_key
- None
-
- 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, ssh_key_name='id_rsa',
- ... home_dir='/tmp/__does_not_exists__')
- >>> handle_ssh_keys(namespace) # doctest: +ELLIPSIS
- Traceback (most recent call last):
- ValidationError: arguments private-key...
- """
- namespace.ssh_key_path = os.path.join(
- namespace.home_dir, '.ssh', namespace.ssh_key_name)
- for attr, path in (
- ('private_key', namespace.ssh_key_path),
- ('public_key', namespace.ssh_key_path + '.pub')):
- value = getattr(namespace, attr)
- if value:
- setattr(namespace, attr, value.decode('string-escape'))
- else:
- try:
- value = open(path).read()
- except IOError:
- value = None
- 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 {} does not reside under the home '
- 'directory of the system user.'.format(attr))
- setattr(namespace, attr, directory)
-
-
-parser = ArgumentParser(description=__doc__)
-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(
- '-a', '--actions', nargs='+',
- choices=('initialize_host', 'create_scripts', 'create_lxc',
- 'initialize_lxc', 'stop_lxc'),
- help='Only for debugging. Call one or more internal functions.')
-parser.add_argument(
- '-n', '--lxc-name', default=LXC_NAME,
- metavar='LXC_NAME (default={})'.format(LXC_NAME),
- help='The LXC container name.')
-parser.add_argument(
- '-s', '--ssh-key-name', default=SSH_KEY_NAME,
- metavar='SSH_KEY_NAME (default={})'.format(SSH_KEY_NAME),
- help='The ssh key name used to connect to the LXC container.')
-parser.add_argument(
- '-d', '--dependencies-dir', default=DEPENDENCIES_DIR,
- metavar='DEPENDENCIES_DIR (default={})'.format(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(
- '-U', '--use-urandom', action='store_true',
- help='Use /dev/urandom to feed /dev/random and avoid entropy exhaustion.')
-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).')
-parser.validators = (
- handle_users,
- handle_userdata,
- handle_ssh_keys,
- handle_directories,
- )
-
-
-def initialize_host(
- user, fullname, email, lpuser, private_key, public_key, ssh_key_path,
- use_urandom, 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])
- 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)
- # Generate user ssh keys if none are supplied.
- valid_ssh_keys = True
- pub_key_path = ssh_key_path + '.pub'
- if private_key is None:
- generate_ssh_keys(ssh_key_path)
- private_key = open(ssh_key_path).read()
- public_key = open(pub_key_path).read()
- valid_ssh_keys = False
- 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 [
- (ssh_key_path, private_key, 'w'),
- (pub_key_path, public_key, 'w'),
- (auth_file, public_key, 'a'),
- (known_hosts, known_host_content, 'a'),
- ]:
- with open(filename, mode) as f:
- f.write('{}\n'.format(contents))
- os.chmod(filename, 0644)
- os.chmod(ssh_key_path, 0600)
- # Set up bzr and Launchpad authentication.
- subprocess.call(['bzr', 'whoami', formataddr((fullname, email))])
- if valid_ssh_keys:
- 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.
- repository = LP_REPOSITORIES[1] if valid_ssh_keys else LP_REPOSITORIES[0]
- subprocess.call([
- 'su', '-', user, '-c',
- 'bzr branch {} "{}"'.format(repository, checkout_dir)])
- with su(user) as env:
- # Set up source dependencies.
- for subdir in ('eggs', 'yui', 'sourcecode'):
- path = os.path.join(dependencies_dir, subdir)
- if not os.path.exists(path):
- os.makedirs(path)
- with cd(dependencies_dir):
- with su(user) as env:
- subprocess.call([
- 'bzr', 'co', '--lightweight',
- LP_SOURCE_DEPS, 'download-cache'])
- # rng-tools is used to set /dev/urandom as random data source, avoiding
- # entropy exhaustion during automated parallel tests.
- if use_urandom:
- subprocess.call(['apt-get', '-y', 'install', 'rng-tools'])
- file_append('/etc/default/rng-tools', 'HRNGDEVICE=/dev/urandom')
- subprocess.call(['/etc/init.d/rng-tools', 'start'])
-
-
-def create_scripts(user, lxcname, ssh_key_path):
- """Create scripts to update the Launchpad environment and run tests."""
- # Leases path in lucid differs from the one in oneiric/precise.
- mapping = {
- 'leases1': get_container_path(
- lxcname, '/var/lib/dhcp3/dhclient.eth0.leases'),
- 'leases2': get_container_path(
- lxcname, '/var/lib/dhcp/dhclient.eth0.leases'),
- 'lxcname': lxcname,
- 'pattern':
- r's/.* ([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}).*/\1/',
- 'ssh_key_path': ssh_key_path,
- 'user': user,
- }
- # We need a script that will run the LP build inside LXC. It is run as
- # root (see below) but drops root once inside the LXC container.
- build_script_file = '/usr/local/bin/launchpad-lxc-build'
- with open(build_script_file, 'w') as script:
- script.write(textwrap.dedent("""\
- #!/bin/sh
- set -ux
- truncate -c -s0 {leases1}
- truncate -c -s0 {leases2}
-
- lxc-start -n {lxcname} -d
- lxc-wait -n {lxcname} -s RUNNING
-
- exit_code=0
- delay=30
- while [ "$delay" -gt 0 -a ! -s {leases1} -a ! -s {leases2} ]
- do
- delay=$(( $delay - 1 ))
- sleep 1
- done
-
- [ -s {leases1} ] && LEASES={leases1} || LEASES={leases2}
- IP_ADDRESS=`grep fixed-address $LEASES | \\
- tail -n 1 | sed -r '{pattern}'`
-
- if [ 0 -eq $? -a -n "$IP_ADDRESS" ]; then
- for i in $(seq 1 30); do
- su {user} -c "/usr/bin/ssh -o StrictHostKeyChecking=no \\
- -i '{ssh_key_path}' $IP_ADDRESS make -C $PWD schema"
- if [ ! 255 -eq $? ]; then
- # If ssh returns 255 then its connection failed.
- # Anything else is either success (status 0) or a
- # failure from whatever we ran over the SSH connection.
- # In those cases we want to stop looping, so we break
- # here.
- break;
- fi
- sleep 1
- done
- else
- echo "could not get IP address - aborting." >&2
- echo "content of $LEASES:" >&2
- cat $LEASES >&2
- exit_code=-1
- fi
-
- lxc-stop -n {lxcname}
- lxc-wait -n {lxcname} -s STOPPED
- exit $exit_code
- """.format(**mapping)))
- os.chmod(build_script_file, 0555)
- # We need a script to test launchpad using LXC ephemeral instances.
- test_script_file = '/usr/local/bin/launchpad-lxc-test'
- with open(test_script_file, 'w') as script:
- # We intentionally generate a very long line for the
- # lxc-start-ephemeral command below because ssh does not propagate
- # quotes the way we want. E.g.,
- # touch a; touch b; ssh localhost -- ls "a b"
- # succeeds, when it should say that the file "a b" does not exist.
- script.write(textwrap.dedent(re.sub(' {2,}', ' ', """\
- #!/bin/sh
- set -uex
- lxc-start-ephemeral -u {user} -S '{ssh_key_path}' -o {lxcname} -- \
- "xvfb-run --error-file=/var/tmp/xvfb-errors.log \
- --server-args='-screen 0 1024x768x24' \
- -a $PWD/bin/test --shuffle --subunit $@"
- """).format(**mapping)))
- os.chmod(test_script_file, 0555)
- # Create a script for cleaning up cruft possibly left by previous lxc
- # ephemeral containers that were not properly shut down.
- cleanup_script_file = '/usr/local/bin/launchpad-lxc-cleanup'
- with open(cleanup_script_file, 'w') as script:
- script.write(textwrap.dedent('''\
- #!/usr/bin/python
- # Cleanup remnants of LXC containers from previous runs.
-
- # Runs of LXC may leave cruft laying around that interferes with
- # the next run. These items need to be cleaned up.
-
- # 1) Shut down all running containers
-
- # 2) for every /var/lib/lxc/lptests-tmp-* directory:
- # umount [directory]/ephemeralbind
- # umount [directory]
- # rm -rf [directory]
-
- # 3) for every /tmp/lxc-lp-* (or something like that?) directory:
- # umount [directory]
- # rm -rf [directory]
-
- # Assumptions:
- # * This script is run as root.
-
- import glob
- import os.path
- import re
- from shelltoolbox import run
- import shutil
- import time
-
-
- LP_TEST_DIR_PATTERN = "/var/lib/lxc/lptests-tmp-*"
- LXC_LP_DIR_PATTERN = "/tmp/lxc-lp-*"
- PID_RE = re.compile("pid:\s+(\d+)")
-
-
- class Scrubber(object):
- """Scrubber will cleanup after lxc ephemeral uncleanliness.
-
- All running containers will be killed.
-
- Those directories corresponding the lp_test_dir_pattern will
- be unmounted and removed. The 'ephemeralbind' subdirectories
- will be unmounted.
-
- The directories corresponding to the lxc_lp_dir_pattern will
- be unmounted and removed. No subdirectories will need
- unmounting.
- """
- def __init__(self, user='buildbot',
- lp_test_dir_pattern=LP_TEST_DIR_PATTERN,
- lxc_lp_dir_pattern=LXC_LP_DIR_PATTERN):
- self.lp_test_dir_pattern = lp_test_dir_pattern
- self.lxc_lp_dir_pattern = lxc_lp_dir_pattern
- self.user = user
-
- def umount(self, dir_):
- if os.path.ismount(dir_):
- run("umount", dir_)
-
- def scrubdir(self, dir_, extra):
- dirs = [dir_]
- if extra is not None:
- dirs.insert(0, os.path.join(dir_, extra))
- for d in dirs:
- self.umount(d)
- shutil.rmtree(dir_)
-
- def scrub(self, pattern, extra=None):
- for dir_ in glob.glob(pattern):
- if os.path.isdir(dir_):
- self.scrubdir(dir_, extra)
-
- def getPid(self, container):
- info = run("lxc-info", "-n", container)
- # lxc-info returns a string containing 'RUNNING' for those
- # containers that are running followed by 'pid: <pid>', so
- # that must be parsed.
- if 'RUNNING' in info:
- match = PID_RE.search(info)
- if match:
- return match.group(1)
- return None
-
- def getRunningContainers(self):
- """Get the running containers.
-
- Returns a list of (name, pid) tuples.
- """
- output = run("lxc-ls")
- containers = set(output.split())
- pidlist = [(c, self.getPid(c)) for c in containers]
- return [(c,p) for c,p in pidlist if p is not None]
-
- def killer(self):
- """Kill all running ephemeral containers."""
- pids = self.getRunningContainers()
- if len(pids) > 0:
- # We can do this the easy way...
- for name, pid in pids:
- run("/usr/bin/lxc-stop", "-n", name)
- time.sleep(2)
- pids = self.getRunningContainers()
- # ...or, the hard way.
- for name, pid in pids:
- run("kill", "-9", pid)
-
- def run(self):
- self.killer()
- self.scrub(self.lp_test_dir_pattern, "ephemeralbind")
- self.scrub(self.lxc_lp_dir_pattern)
-
-
- if __name__ == '__main__':
- scrubber = Scrubber()
- scrubber.run()
- '''))
- os.chmod(cleanup_script_file, 0555)
-
- # Add a file to sudoers.d that will let the buildbot user run the above.
- sudoers_file = '/etc/sudoers.d/launchpad-' + user
- with open(sudoers_file, 'w') as sudoers:
- sudoers.write('{} ALL = (ALL) NOPASSWD:'.format(user))
- sudoers.write(' {},'.format(build_script_file))
- sudoers.write(' {},'.format(cleanup_script_file))
- sudoers.write(' {}\n'.format(test_script_file))
- # The sudoers must have this mode or it will be ignored.
- os.chmod(sudoers_file, 0440)
- # XXX 2012-03-13 frankban bug=944386:
- # Disable hardlink restriction. This workaround needs
- # to be removed once the kernel bug is resolved.
- procfile = '/proc/sys/kernel/yama/protected_nonaccess_hardlinks'
- with open(procfile, 'w') as f:
- f.write('0\n')
-
-
-def create_lxc(user, lxcname, ssh_key_path):
- """Create the LXC container that will be used for ephemeral instances."""
- # 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.
- if not os.path.exists('/etc/apparmor.d/disable/usr.bin.lxc-start'):
- subprocess.call([
- 'ln', '-s',
- '/etc/apparmor.d/usr.bin.lxc-start',
- '/etc/apparmor.d/disable/'])
- subprocess.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 {}\n'.format(lxc_gateway_address))
- file_append(
- DHCP_FILE,
- 'prepend domain-name-servers {};\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.
- exit_code = subprocess.call([
- 'lxc-create',
- '-t', 'ubuntu',
- '-n', lxcname,
- '-f', LXC_CONFIG_TEMPLATE,
- '--',
- '-r {} -a i386 -b {}'.format(LXC_GUEST_OS, user),
- ])
- if exit_code:
- raise SetupLXCError('Unable to create the LXC container.')
- # XXX 2012-04-18 frankban bug=974584:
- # Add a line to the container's fstab to be able to create semaphores
- # in lxc. This workaround needs to be removed once the lxc bug is
- # resolved for lucid containers too.
- file_append(
- '/var/lib/lxc/{}/fstab'.format(lxcname),
- 'none dev/shm tmpfs defaults 0 0\n')
- subprocess.call(['lxc-start', '-n', lxcname, '-d'])
- # Set up root ssh key.
- user_authorized_keys = os.path.expanduser(
- '~' + user + '/.ssh/authorized_keys')
- 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.
- sshcall = ssh(lxcname, user, key=ssh_key_path)
- trials = 60
- while True:
- trials -= 1
- try:
- sshcall('true')
- except SSHError:
- if not trials:
- raise
- time.sleep(1)
- else:
- break
-
-
-def initialize_lxc(user, dependencies_dir, directory, lxcname, ssh_key_path):
- """Set up the Launchpad development environment inside the LXC container.
- """
- root_sshcall = ssh(lxcname, key=ssh_key_path)
- sshcall = ssh(lxcname, user, key=ssh_key_path)
- # APT repository update.
- for apt_repository in APT_REPOSITORIES:
- repository = apt_repository.format(distro=LXC_GUEST_OS)
- assume_yes = '' if LXC_GUEST_OS == 'lucid' else '-y'
- root_sshcall('add-apt-repository {} "{}"'.format(
- assume_yes, repository))
- # XXX frankban 2012-01-13 - Bug 892892: upgrading mountall in LXC
- # containers currently does not work.
- root_sshcall("echo 'mountall hold' | dpkg --set-selections")
- # Upgrading packages.
- root_sshcall(
- 'apt-get update && DEBIAN_FRONTEND=noninteractive LANG=C apt-get -y '
- 'install {}'.format(LP_DEB_DEPENDENCIES))
- # We install lxc in the guest so that lxc-execute will work on the
- # container. We use --no-install-recommends at the recommendation
- # of the Canonical lxc maintainers because all we need is a file
- # that the base lxc package installs, and so that packages we
- # don't need and that might even cause problems inside the
- # container are not around.
- root_sshcall(
- 'DEBIAN_FRONTEND=noninteractive apt-get -y '
- '--no-install-recommends install lxc')
- # User configuration.
- root_sshcall('adduser {} sudo'.format(user))
- pygetgid = 'import pwd; print pwd.getpwnam("{}").pw_gid'.format(user)
- gid = "`python -c '{}'`".format(pygetgid)
- root_sshcall('addgroup --gid {} {}'.format(gid, user), ignore_errors=True)
- # Set up Launchpad dependencies.
- checkout_dir = os.path.join(directory, LP_CHECKOUT)
- sshcall(
- ('cd {} && utilities/update-sourcecode --use-http '
- '"{}/sourcecode"').format(checkout_dir, dependencies_dir))
- sshcall(
- 'cd {} && utilities/link-external-sourcecode "{}"'.format(
- checkout_dir, dependencies_dir))
- # Create Apache document roots, to avoid warnings.
- sshcall(' && '.join('mkdir -p {}'.format(i) for i in LP_APACHE_ROOTS))
- # Set up Apache modules.
- for module in LP_APACHE_MODULES.split():
- root_sshcall('a2enmod {}'.format(module))
- # Launchpad database setup.
- root_sshcall(
- 'cd {} && utilities/launchpad-database-setup {}'.format(
- checkout_dir, user))
- sshcall('cd {} && make'.format(checkout_dir))
- # Set up container hosts file.
- lines = ['{}\t{}'.format(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.
- root_sshcall('cd {} && make install'.format(checkout_dir))
- # XXX benji 2012-03-19 bug=959352: this is so graphviz will work in an
- # ephemeral container
- root_sshcall('mkdir -p /rootfs/usr/lib')
- root_sshcall('ln -s /usr/lib/graphviz /rootfs/usr/lib/graphviz')
-
-
-def stop_lxc(lxcname, ssh_key_path):
- """Stop the lxc instance named `lxcname`."""
- ssh(lxcname, key=ssh_key_path)('poweroff')
- timeout = 30
- while timeout:
- try:
- output = subprocess.check_output([
- 'lxc-info', '-n', lxcname], stderr=subprocess.STDOUT)
- except subprocess.CalledProcessError:
- pass
- else:
- if 'STOPPED' in output:
- break
- timeout -= 1
- time.sleep(1)
- else:
- subprocess.call(['lxc-stop', '-n', lxcname])
-
-
-def main(
- user, fullname, email, lpuser, private_key, public_key, actions,
- lxc_name, ssh_key_path, use_urandom, dependencies_dir, directory):
- function_args_map = OrderedDict((
- ('initialize_host', (
- user, fullname, email, lpuser, private_key, public_key,
- ssh_key_path, use_urandom, dependencies_dir, directory)),
- ('create_scripts', (user, lxc_name, ssh_key_path)),
- ('create_lxc', (user, lxc_name, ssh_key_path)),
- ('initialize_lxc', (
- user, dependencies_dir, directory, lxc_name, ssh_key_path)),
- ('stop_lxc', (lxc_name, ssh_key_path)),
- ))
- if actions is None:
- actions = function_args_map.keys()
- scope = globals()
- for action in actions:
- try:
- scope[action](*function_args_map[action])
- except SetupLXCError as err:
- return err
-
-
-if __name__ == '__main__':
- args = parser.parse_args()
- if args.run_as_root:
- exit_code = main(
- args.user,
- args.full_name,
- args.email,
- args.lpuser,
- args.private_key,
- args.public_key,
- args.actions,
- args.lxc_name,
- args.ssh_key_path,
- args.use_urandom,
- args.dependencies_dir,
- args.directory,
- )
- else:
- # If the script is run as normal user, restart it as root using
- # all the collected arguments. Note that this step requires user
- # interaction: running this script as root is still required
- # for non-interactive setup of the Launchpad environment.
- exit_code = subprocess.call(
- ['sudo', sys.argv[0]] + parser.get_args_from_namespace(args))
- sys.exit(exit_code)
Follow ups