yellow team mailing list archive
-
yellow team
-
Mailing list archive
-
Message #00792
[Merge] lp:~frankban/lpsetup/use-lxcip into lp:lpsetup
Francesco Banconi has proposed merging lp:~frankban/lpsetup/use-lxcip into lp:lpsetup.
Requested reviews:
Launchpad Yellow Squad (yellow)
For more details, see:
https://code.launchpad.net/~frankban/lpsetup/use-lxcip/+merge/104350
== Changes ==
lpsetup no longer changes resolv.conf and dhcp files.
Each ssh connection is made using the container's ip address, obtained from *lp-lxc-ip*.
Changed the lxc template filename, and how the bridge interface is retrieved: now the script looks for interface existence: lxcbr0 first, virbr0 as a fallback, error otherwise (see get_lxc_gateway).
Fixed the script error handling: now lpsetup exists with the expected return code.
== Tests ==
$ nosetests
........................................................
Name Stmts Miss Cover Missing
---------------------------------------------------
lpsetup 6 1 83% 16
lpsetup.argparser 125 6 95% 113, 221, 278-279, 298, 307
lpsetup.exceptions 6 0 100%
lpsetup.handlers 55 1 98% 55
lpsetup.settings 29 0 100%
lpsetup.subcommands 0 0 100%
lpsetup.utils 125 28 78% 80, 114-124, 132-133, 148, 199, 209, 230-232, 250-256, 275-277
---------------------------------------------------
TOTAL 346 36 90%
----------------------------------------------------------------------
Ran 56 tests in 0.579s
OK
--
https://code.launchpad.net/~frankban/lpsetup/use-lxcip/+merge/104350
Your team Launchpad Yellow Squad is requested to review the proposed merge of lp:~frankban/lpsetup/use-lxcip into lp:lpsetup.
=== modified file 'lp-setup'
--- lp-setup 2012-03-12 10:11:31 +0000
+++ lp-setup 2012-05-02 09:24:19 +0000
@@ -4,8 +4,10 @@
"""Command line interface entry point for lpsetup."""
+import sys
+
from lpsetup import cli
if __name__ == '__main__':
- cli.main()
+ sys.exit(cli.main())
=== modified file 'lpsetup/cli.py'
--- lpsetup/cli.py 2012-03-12 10:11:31 +0000
+++ lpsetup/cli.py 2012-05-02 09:24:19 +0000
@@ -12,6 +12,7 @@
from lpsetup import (
__doc__ as description,
argparser,
+ exceptions,
)
from lpsetup.subcommands import (
install,
@@ -26,4 +27,7 @@
parser.register_subcommand('update', update.SubCommand)
parser.register_subcommand('lxc-install', lxcinstall.SubCommand)
args = parser.parse_args()
- return args.main(args)
+ try:
+ return args.main(args)
+ except (exceptions.ExecutionError, KeyboardInterrupt, MemoryError) as err:
+ return err
=== modified file 'lpsetup/exceptions.py'
--- lpsetup/exceptions.py 2012-03-09 10:14:32 +0000
+++ lpsetup/exceptions.py 2012-05-02 09:24:19 +0000
@@ -6,14 +6,18 @@
__metaclass__ = type
__all__ = [
- 'LaunchpadError',
+ 'LPSetupError',
'ValidationError',
]
-class LaunchpadError(Exception):
+class LPSetupError(Exception):
"""Base exception for lpsetup."""
-class ValidationError(LaunchpadError):
+class ValidationError(LPSetupError):
"""Argparse invalid arguments."""
+
+
+class ExecutionError(LPSetupError):
+ """Error occurring during script execution."""
=== modified file 'lpsetup/settings.py'
--- lpsetup/settings.py 2012-04-20 13:49:21 +0000
+++ lpsetup/settings.py 2012-05-02 09:24:19 +0000
@@ -15,7 +15,6 @@
BASE_PACKAGES = ['ssh', 'bzr']
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 '
@@ -57,10 +56,11 @@
LP_SOURCE_DEPS = (
'http://bazaar.launchpad.net/~launchpad/lp-source-dependencies/trunk')
LPSETUP_PPA = 'ppa:yellow/ppa'
-LXC_CONFIG_TEMPLATE = '/etc/lxc/local.conf'
+LXC_CONFIG_TEMPLATE = '/etc/lxc/lp-setup.conf'
LXC_GUEST_ARCH = 'i386'
LXC_GUEST_CHOICES = ('lucid', 'oneiric', 'precise')
LXC_GUEST_OS = LXC_GUEST_CHOICES[0]
+LXC_IP_COMMAND = ('/usr/bin/lp-lxc-ip', '-s', '-i', 'eth0', '-n')
LXC_LEASES = (
'/var/lib/dhcp3/dhclient.eth0.leases',
'/var/lib/dhcp/dhclient.eth0.leases',
@@ -75,6 +75,5 @@
"""
LXC_PACKAGES = ('lxc', 'libvirt-bin')
LXC_PATH = '/var/lib/lxc/'
-RESOLV_FILE = '/etc/resolv.conf'
SCRIPTS = ('lp-setup-lxc-build', 'lp-setup-lxc-cleanup', 'lp-setup-lxc-test')
SSH_KEY_NAME = 'id_rsa'
=== modified file 'lpsetup/subcommands/lxcinstall.py'
--- lpsetup/subcommands/lxcinstall.py 2012-04-20 14:37:23 +0000
+++ lpsetup/subcommands/lxcinstall.py 2012-05-02 09:24:19 +0000
@@ -13,26 +13,24 @@
'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 import handlers
+from lpsetup import (
+ exceptions,
+ handlers,
+ )
from lpsetup.settings import (
BASE_PACKAGES,
- DHCP_FILE,
LPSETUP_PPA,
LXC_CONFIG_TEMPLATE,
LXC_GUEST_ARCH,
@@ -42,7 +40,6 @@
LXC_NAME,
LXC_OPTIONS,
LXC_PACKAGES,
- RESOLV_FILE,
SCRIPTS,
)
from lpsetup.subcommands import install
@@ -52,6 +49,7 @@
get_lxc_gateway,
lxc_stopped,
render_to_file,
+ sshlxc as ssh,
this_command,
)
@@ -111,15 +109,12 @@
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)
+ lxc_gateway = get_lxc_gateway()
+ if lxc_gateway is None:
+ raise exceptions.ExecutionError(
+ 'Error: LXC bridge interface not found.')
+ content = LXC_OPTIONS.format(interface=lxc_gateway)
with open(LXC_CONFIG_TEMPLATE, 'w') as f:
f.write(content)
# Creating container.
@@ -152,37 +147,25 @@
call('lxc-start', '-n', lxc_name, '-d')
-def wait_for_lxc(lxc_name, ssh_key_path, trials=60, sleep_seconds=1):
- """Try to ssh as `user` into the LXC container named `lxc_name`."""
- sshcall = ssh(lxc_name, key=ssh_key_path)
- 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, ssh_key_path, lxc_os):
"""Initialize LXC container."""
base_packages = list(BASE_PACKAGES) + ['python-software-properties']
- sshcall = ssh(lxc_name, key=ssh_key_path)
- sshcall(
+ ssh(lxc_name,
'DEBIAN_FRONTEND=noninteractive '
- 'apt-get install -y ' + ' '.join(base_packages))
+ 'apt-get install -y ' + ' '.join(base_packages),
+ key=ssh_key_path)
args = {
'assume_yes': '' if lxc_os == 'lucid' else '-y',
'repository': LPSETUP_PPA,
}
- sshcall('apt-add-repository {assume_yes} {repository}'.format(**args))
- sshcall('apt-get clean && apt-get update')
- sshcall(
+ ssh(lxc_name,
+ 'apt-add-repository {assume_yes} {repository}'.format(**args),
+ key=ssh_key_path)
+ ssh(lxc_name, 'apt-get clean && apt-get update', key=ssh_key_path)
+ ssh(lxc_name,
'DEBIAN_FRONTEND=noninteractive '
- 'apt-get upgrade -y && apt-get install -y lpsetup')
+ 'apt-get upgrade -y && apt-get install -y lpsetup',
+ key=ssh_key_path)
def setup_launchpad_lxc(
@@ -194,12 +177,12 @@
'-d', dependencies_dir, '-c', directory
]
cmd = this_command(directory, args)
- ssh(lxc_name, key=ssh_key_path)(cmd)
+ ssh(lxc_name, cmd, key=ssh_key_path)
def stop_lxc(lxc_name, ssh_key_path):
"""Stop the lxc instance named `lxc_name`."""
- ssh(lxc_name, key=ssh_key_path)('poweroff')
+ ssh(lxc_name, 'poweroff', key=ssh_key_path)
if not lxc_stopped(lxc_name):
subprocess.call(['lxc-stop', '-n', lxc_name])
@@ -218,8 +201,6 @@
'user', 'lxc_name', 'lxc_arch', 'lxc_os', 'install_subunit'),
(start_lxc,
'lxc_name'),
- (wait_for_lxc,
- 'lxc_name', 'ssh_key_path'),
(initialize_lxc,
'lxc_name', 'ssh_key_path', 'lxc_os'),
(setup_launchpad_lxc,
=== modified file 'lpsetup/tests/test_utils.py'
--- lpsetup/tests/test_utils.py 2012-04-11 10:40:05 +0000
+++ lpsetup/tests/test_utils.py 2012-05-02 09:24:19 +0000
@@ -11,8 +11,6 @@
import tempfile
import unittest
-from shelltoolbox import run
-
from lpsetup.settings import (
LXC_LP_DIR_PATTERN,
LXC_LP_TEST_DIR_PATTERN,
@@ -20,8 +18,11 @@
)
from lpsetup.utils import (
get_container_path,
+ get_lxc_gateway,
+ get_network_interfaces,
get_running_containers,
render_to_file,
+ retry,
Scrubber,
this_command,
)
@@ -45,6 +46,55 @@
get_container_path('mycontainer', 'home'))
+class GetLXCGatewayTest(unittest.TestCase):
+
+ interfaces = ('eth0', 'virbr0', 'lxcbr0', 'lo')
+
+ def test_found(self):
+ # Ensure the first gateway found is correctly returned.
+ gw = get_lxc_gateway(interface_lookup=lambda: self.interfaces)
+ self.assertEqual(self.interfaces[2], gw)
+
+ def test_fallback(self):
+ # If the first interface can not be found, a fallback is returned.
+ gw = get_lxc_gateway(interface_lookup=lambda: self.interfaces[:2])
+ self.assertEqual(self.interfaces[1], gw)
+
+ def test_not_found(self):
+ # Ensure the function returns None if the gateway can not be found.
+ gw = get_lxc_gateway(interface_lookup=lambda: self.interfaces[:1])
+ self.assertIsNone(gw)
+
+
+class GetNetworkInterfacesTest(unittest.TestCase):
+
+ interfaces = ('eth0', 'eth1', 'lo')
+
+ def create_path(self, interfaces):
+ path = tempfile.mkdtemp()
+ for interface in interfaces:
+ os.mkdir(os.path.join(path, interface))
+ return path
+
+ def test_all(self):
+ # Ensure all interfaces are correctly returned.
+ path = self.create_path(self.interfaces)
+ interfaces = get_network_interfaces(path=path)
+ self.assertItemsEqual(self.interfaces, interfaces)
+
+ def test_exclude(self):
+ # Ensure interfaces can be excluded from the returned list.
+ path = self.create_path(self.interfaces)
+ interfaces = get_network_interfaces(path=path, exclude=['lo'])
+ self.assertItemsEqual(self.interfaces[:-1], interfaces)
+
+ def test_none(self):
+ # An empty list is returned if interfaces are not found.
+ path = self.create_path([])
+ interfaces = get_network_interfaces(path=path, exclude=['lo'])
+ self.assertEqual([], interfaces)
+
+
class GetRunningContainersTest(unittest.TestCase):
def assertRunning(self, expected, containers):
@@ -86,7 +136,38 @@
self.assertEqual(expected, f.read())
-class TestScrubber(unittest.TestCase):
+class RetryTest(unittest.TestCase):
+
+ error = 'error after {} tries'
+
+ def setUp(self):
+ self.tries = 0
+
+ @retry(OSError, tries=10, delay=0)
+ def _success(self):
+ self.tries += 1
+ if self.tries == 5:
+ return True
+ raise OSError
+
+ @retry(OSError, tries=10, delay=0)
+ def _failure(self):
+ self.tries += 1
+ raise OSError(self.error.format(self.tries))
+
+ def test_success(self):
+ # Ensure the decorated function correctly returns without errors
+ # after several tries.
+ self.assertTrue(self._success())
+
+ def test_failure(self):
+ # Ensure the decorated function raises the last error.
+ with self.assertRaises(OSError) as cm:
+ self._failure()
+ self.assertEqual(self.error.format(10), str(cm.exception))
+
+
+class ScrubberTest(unittest.TestCase):
def make_dirs(self, tmp_path, pattern, num=10, extra=None):
dir_ = os.path.split(pattern)[0]
=== modified file 'lpsetup/utils.py'
--- lpsetup/utils.py 2012-04-11 10:40:05 +0000
+++ lpsetup/utils.py 2012-05-02 09:24:19 +0000
@@ -9,18 +9,24 @@
'call',
'get_container_path',
'get_lxc_gateway',
+ 'get_network_interfaces',
+ 'get_running_containers',
'lxc_in_state',
- 'lxc_running',
+ 'lxc_ip',
'lxc_stopped',
'render_to_file',
+ 'retry',
'Scrubber',
+ 'sshlxc',
'this_command',
]
-from functools import partial
+from functools import (
+ partial,
+ wraps,
+ )
import glob
import os
-import platform
import re
import subprocess
import shutil
@@ -28,18 +34,23 @@
import time
from shelltoolbox import (
+ command,
join_command,
run,
+ ssh,
)
from lpsetup.settings import (
LXC_LP_DIR_PATTERN,
LXC_LP_TEST_DIR_PATTERN,
+ LXC_IP_COMMAND,
LXC_PATH,
)
PID_RE = re.compile("pid:\s+(\d+)")
+
+
call = partial(run, stdout=None)
@@ -58,17 +69,29 @@
return os.path.join(base_path, lxc_name, 'rootfs', path.lstrip('/'))
-def get_lxc_gateway():
- """Return a tuple of gateway name and address.
+def get_lxc_gateway(candidates=('lxcbr0', 'virbr0'), interface_lookup=None):
+ """Return the gateway bridge name to be used by LXC containers.
- The gateway name and address will change depending on which version
- of Ubuntu the script is running on.
+ The gateway name will change depending on which version of Ubuntu the
+ script is running on: if *lxcbr0* does not exist, *virbr0* will be used.
+ If both interfaces don't exist, None is returned.
"""
- release_name = platform.linux_distribution()[2]
- if release_name == 'oneiric':
- return 'virbr0', '192.168.122.1'
+ if interface_lookup is None:
+ interfaces = get_network_interfaces()
else:
- return 'lxcbr0', '10.0.3.1'
+ interfaces = interface_lookup()
+ for interface in candidates:
+ if interface in interfaces:
+ return interface
+
+
+def get_network_interfaces(path='/sys/class/net/', exclude=()):
+ """Return a list of the network interfaces up in this system."""
+ return [
+ i for i in os.listdir(path)
+ if i not in exclude and
+ os.path.isdir(os.path.join(path, i))
+ ]
def get_running_containers(containers=None):
@@ -101,7 +124,15 @@
return False
-lxc_running = partial(lxc_in_state, 'RUNNING')
+def lxc_ip(name):
+ """Return the ip address of the LXC container called `name`.
+
+ Raise `subprocess.CalledProcessError` if the ip address can not be found.
+ """
+ cmd = command(*LXC_IP_COMMAND)
+ return cmd(name).strip()
+
+
lxc_stopped = partial(lxc_in_state, 'STOPPED')
@@ -123,6 +154,29 @@
return contents
+def retry(exception, tries=100, delay=0.1):
+ """If the decorated function raises `exception`, wait and try it again.
+
+ Raise the exception raised by the last call if the function does not
+ exit normally after 100 tries.
+
+ Original from http://wiki.python.org/moin/PythonDecoratorLibrary#Retry.
+ """
+ def decorator(func):
+ @wraps(func)
+ def decorated(*args, **kwargs):
+ mtries = tries
+ while mtries:
+ try:
+ return func(*args, **kwargs)
+ except exception as err:
+ time.sleep(delay)
+ mtries -= 1
+ raise err
+ return decorated
+ return decorator
+
+
class Scrubber(object):
"""Scrubber will cleanup after lxc ephemeral uncleanliness.
@@ -207,6 +261,22 @@
self.scrub(self.lxc_lp_dir_pattern)
+@retry(subprocess.CalledProcessError, tries=60, delay=0.5)
+def sshlxc(name, cmd, **kwargs):
+ """Run ssh shell commands.
+
+ The ssh connection is made resolving the ip address of the LXC
+ container called `name`.
+
+ This function will try to obtain the ip address of the container and
+ then connect to the container for ~30 seconds before raising a
+ `subprocess.CalledProcessError`.
+ """
+ ip = lxc_ip(name)
+ sshcall = ssh(ip, **kwargs)
+ return sshcall(cmd)
+
+
def this_command(directory, args):
"""Return a command line to re-launch this script using given `args`.
Follow ups