Merge lp:~frankban/lpsetup/use-lxcip into lp:lpsetup

Proposed by Francesco Banconi
Status: Merged
Approved by: Gary Poster
Approved revision: 24
Merged at revision: 18
Proposed branch: lp:~frankban/lpsetup/use-lxcip
Merge into: lp:lpsetup
Diff against target: 523 lines (+215/-60)
7 files modified
lp-setup (+3/-1)
lpsetup/cli.py (+5/-1)
lpsetup/exceptions.py (+7/-3)
lpsetup/settings.py (+2/-3)
lpsetup/subcommands/lxcinstall.py (+27/-37)
lpsetup/tests/test_utils.py (+84/-3)
lpsetup/utils.py (+87/-12)
To merge this branch: bzr merge lp:~frankban/lpsetup/use-lxcip
Reviewer Review Type Date Requested Status
Gary Poster (community) Approve
Review via email: mp+104350@code.launchpad.net

Description of the change

== 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.

Removed the `wait_for_lxc` step: each ssh call systematically retries to obtain the ip address and establish the connection, for ~30 seconds.

== 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

To post a comment you must log in.
Revision history for this message
Gary Poster (gary) wrote :

Another great branch, frankban.

A summary of our IRC conversation follows. I requested some small changes.

 * This is a great improvement to simplicity and robustness.
 * Let's not catch MemoryError in cli.main(): the traceback might in fact be a valuable diagnostic.
 * I don't think we should retry sshlxc, because we only want to retry the lxcip call and verify that the sshd is up. Once the sshd is up, we should not retry a failed command. It may have had an effect before it blew up, and there are no guarantees that a retry is appropriate. Instead, we probably should retry the lxcip call, and use wait_for_lxc to verify that the sshd is ready before we try the command (once).

Thank you!

review: Approve
lp:~frankban/lpsetup/use-lxcip updated
25. By Francesco Banconi

Changes from review.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lp-setup'
2--- lp-setup 2012-03-12 10:11:31 +0000
3+++ lp-setup 2012-05-02 16:37:17 +0000
4@@ -4,8 +4,10 @@
5
6 """Command line interface entry point for lpsetup."""
7
8+import sys
9+
10 from lpsetup import cli
11
12
13 if __name__ == '__main__':
14- cli.main()
15+ sys.exit(cli.main())
16
17=== modified file 'lpsetup/cli.py'
18--- lpsetup/cli.py 2012-03-12 10:11:31 +0000
19+++ lpsetup/cli.py 2012-05-02 16:37:17 +0000
20@@ -12,6 +12,7 @@
21 from lpsetup import (
22 __doc__ as description,
23 argparser,
24+ exceptions,
25 )
26 from lpsetup.subcommands import (
27 install,
28@@ -26,4 +27,7 @@
29 parser.register_subcommand('update', update.SubCommand)
30 parser.register_subcommand('lxc-install', lxcinstall.SubCommand)
31 args = parser.parse_args()
32- return args.main(args)
33+ try:
34+ return args.main(args)
35+ except (exceptions.ExecutionError, KeyboardInterrupt) as err:
36+ return err
37
38=== modified file 'lpsetup/exceptions.py'
39--- lpsetup/exceptions.py 2012-03-09 10:14:32 +0000
40+++ lpsetup/exceptions.py 2012-05-02 16:37:17 +0000
41@@ -6,14 +6,18 @@
42
43 __metaclass__ = type
44 __all__ = [
45- 'LaunchpadError',
46+ 'LPSetupError',
47 'ValidationError',
48 ]
49
50
51-class LaunchpadError(Exception):
52+class LPSetupError(Exception):
53 """Base exception for lpsetup."""
54
55
56-class ValidationError(LaunchpadError):
57+class ValidationError(LPSetupError):
58 """Argparse invalid arguments."""
59+
60+
61+class ExecutionError(LPSetupError):
62+ """Error occurring during script execution."""
63
64=== modified file 'lpsetup/settings.py'
65--- lpsetup/settings.py 2012-04-20 13:49:21 +0000
66+++ lpsetup/settings.py 2012-05-02 16:37:17 +0000
67@@ -15,7 +15,6 @@
68 BASE_PACKAGES = ['ssh', 'bzr']
69 CHECKOUT_DIR = '~/launchpad/branches'
70 DEPENDENCIES_DIR = '~/launchpad/dependencies'
71-DHCP_FILE = '/etc/dhcp/dhclient.conf'
72 HOSTS_CONTENT = (
73 ('127.0.0.88',
74 'launchpad.dev answers.launchpad.dev archive.launchpad.dev '
75@@ -57,10 +56,11 @@
76 LP_SOURCE_DEPS = (
77 'http://bazaar.launchpad.net/~launchpad/lp-source-dependencies/trunk')
78 LPSETUP_PPA = 'ppa:yellow/ppa'
79-LXC_CONFIG_TEMPLATE = '/etc/lxc/local.conf'
80+LXC_CONFIG_TEMPLATE = '/etc/lxc/lp-setup.conf'
81 LXC_GUEST_ARCH = 'i386'
82 LXC_GUEST_CHOICES = ('lucid', 'oneiric', 'precise')
83 LXC_GUEST_OS = LXC_GUEST_CHOICES[0]
84+LXC_IP_COMMAND = ('/usr/bin/lp-lxc-ip', '-s', '-i', 'eth0', '-n')
85 LXC_LEASES = (
86 '/var/lib/dhcp3/dhclient.eth0.leases',
87 '/var/lib/dhcp/dhclient.eth0.leases',
88@@ -75,6 +75,5 @@
89 """
90 LXC_PACKAGES = ('lxc', 'libvirt-bin')
91 LXC_PATH = '/var/lib/lxc/'
92-RESOLV_FILE = '/etc/resolv.conf'
93 SCRIPTS = ('lp-setup-lxc-build', 'lp-setup-lxc-cleanup', 'lp-setup-lxc-test')
94 SSH_KEY_NAME = 'id_rsa'
95
96=== modified file 'lpsetup/subcommands/lxcinstall.py'
97--- lpsetup/subcommands/lxcinstall.py 2012-04-20 14:37:23 +0000
98+++ lpsetup/subcommands/lxcinstall.py 2012-05-02 16:37:17 +0000
99@@ -13,26 +13,24 @@
100 'setup_launchpad_lxc',
101 'start_lxc',
102 'stop_lxc',
103- 'wait_for_lxc',
104 ]
105
106 import os
107 import shutil
108 import subprocess
109-import time
110
111 from shelltoolbox import (
112 apt_get_install,
113 file_append,
114- file_prepend,
115 mkdirs,
116- ssh,
117 )
118
119-from lpsetup import handlers
120+from lpsetup import (
121+ exceptions,
122+ handlers,
123+ )
124 from lpsetup.settings import (
125 BASE_PACKAGES,
126- DHCP_FILE,
127 LPSETUP_PPA,
128 LXC_CONFIG_TEMPLATE,
129 LXC_GUEST_ARCH,
130@@ -42,7 +40,6 @@
131 LXC_NAME,
132 LXC_OPTIONS,
133 LXC_PACKAGES,
134- RESOLV_FILE,
135 SCRIPTS,
136 )
137 from lpsetup.subcommands import install
138@@ -52,6 +49,8 @@
139 get_lxc_gateway,
140 lxc_stopped,
141 render_to_file,
142+ retry,
143+ sshlxc as ssh,
144 this_command,
145 )
146
147@@ -111,15 +110,12 @@
148 call('ln', '-s',
149 '/etc/apparmor.d/usr.bin.lxc-start', '/etc/apparmor.d/disable/')
150 call('apparmor_parser', '-R', '/etc/apparmor.d/usr.bin.lxc-start')
151- # Update resolv file in order to get the ability to ssh into the LXC
152- # container using its name.
153- lxc_gateway_name, lxc_gateway_address = get_lxc_gateway()
154- file_prepend(RESOLV_FILE, 'nameserver {0}\n'.format(lxc_gateway_address))
155- file_append(
156- DHCP_FILE,
157- 'prepend domain-name-servers {0};\n'.format(lxc_gateway_address))
158 # Container configuration template.
159- content = LXC_OPTIONS.format(interface=lxc_gateway_name)
160+ lxc_gateway = get_lxc_gateway()
161+ if lxc_gateway is None:
162+ raise exceptions.ExecutionError(
163+ 'Error: LXC bridge interface not found.')
164+ content = LXC_OPTIONS.format(interface=lxc_gateway)
165 with open(LXC_CONFIG_TEMPLATE, 'w') as f:
166 f.write(content)
167 # Creating container.
168@@ -152,37 +148,31 @@
169 call('lxc-start', '-n', lxc_name, '-d')
170
171
172-def wait_for_lxc(lxc_name, ssh_key_path, trials=60, sleep_seconds=1):
173- """Try to ssh as `user` into the LXC container named `lxc_name`."""
174- sshcall = ssh(lxc_name, key=ssh_key_path)
175- while True:
176- trials -= 1
177- try:
178- sshcall('true')
179- except subprocess.CalledProcessError:
180- if not trials:
181- raise
182- time.sleep(sleep_seconds)
183- else:
184- break
185+def wait_for_lxc(lxc_name, ssh_key_path):
186+ """Try to ssh into the LXC container named `lxc_name`."""
187+ retry_ssh = retry(subprocess.CalledProcessError)(ssh)
188+ retry_ssh(lxc_name, 'true', key=ssh_key_path)
189
190
191 def initialize_lxc(lxc_name, ssh_key_path, lxc_os):
192 """Initialize LXC container."""
193 base_packages = list(BASE_PACKAGES) + ['python-software-properties']
194- sshcall = ssh(lxc_name, key=ssh_key_path)
195- sshcall(
196+ ssh(lxc_name,
197 'DEBIAN_FRONTEND=noninteractive '
198- 'apt-get install -y ' + ' '.join(base_packages))
199+ 'apt-get install -y ' + ' '.join(base_packages),
200+ key=ssh_key_path)
201 args = {
202 'assume_yes': '' if lxc_os == 'lucid' else '-y',
203 'repository': LPSETUP_PPA,
204 }
205- sshcall('apt-add-repository {assume_yes} {repository}'.format(**args))
206- sshcall('apt-get clean && apt-get update')
207- sshcall(
208+ ssh(lxc_name,
209+ 'apt-add-repository {assume_yes} {repository}'.format(**args),
210+ key=ssh_key_path)
211+ ssh(lxc_name, 'apt-get clean && apt-get update', key=ssh_key_path)
212+ ssh(lxc_name,
213 'DEBIAN_FRONTEND=noninteractive '
214- 'apt-get upgrade -y && apt-get install -y lpsetup')
215+ 'apt-get upgrade -y && apt-get install -y lpsetup',
216+ key=ssh_key_path)
217
218
219 def setup_launchpad_lxc(
220@@ -194,12 +184,12 @@
221 '-d', dependencies_dir, '-c', directory
222 ]
223 cmd = this_command(directory, args)
224- ssh(lxc_name, key=ssh_key_path)(cmd)
225+ ssh(lxc_name, cmd, key=ssh_key_path)
226
227
228 def stop_lxc(lxc_name, ssh_key_path):
229 """Stop the lxc instance named `lxc_name`."""
230- ssh(lxc_name, key=ssh_key_path)('poweroff')
231+ ssh(lxc_name, 'poweroff', key=ssh_key_path)
232 if not lxc_stopped(lxc_name):
233 subprocess.call(['lxc-stop', '-n', lxc_name])
234
235
236=== modified file 'lpsetup/tests/test_utils.py'
237--- lpsetup/tests/test_utils.py 2012-04-11 10:40:05 +0000
238+++ lpsetup/tests/test_utils.py 2012-05-02 16:37:17 +0000
239@@ -11,8 +11,6 @@
240 import tempfile
241 import unittest
242
243-from shelltoolbox import run
244-
245 from lpsetup.settings import (
246 LXC_LP_DIR_PATTERN,
247 LXC_LP_TEST_DIR_PATTERN,
248@@ -20,8 +18,11 @@
249 )
250 from lpsetup.utils import (
251 get_container_path,
252+ get_lxc_gateway,
253+ get_network_interfaces,
254 get_running_containers,
255 render_to_file,
256+ retry,
257 Scrubber,
258 this_command,
259 )
260@@ -45,6 +46,55 @@
261 get_container_path('mycontainer', 'home'))
262
263
264+class GetLXCGatewayTest(unittest.TestCase):
265+
266+ interfaces = ('eth0', 'virbr0', 'lxcbr0', 'lo')
267+
268+ def test_found(self):
269+ # Ensure the first gateway found is correctly returned.
270+ gw = get_lxc_gateway(interface_lookup=lambda: self.interfaces)
271+ self.assertEqual(self.interfaces[2], gw)
272+
273+ def test_fallback(self):
274+ # If the first interface can not be found, a fallback is returned.
275+ gw = get_lxc_gateway(interface_lookup=lambda: self.interfaces[:2])
276+ self.assertEqual(self.interfaces[1], gw)
277+
278+ def test_not_found(self):
279+ # Ensure the function returns None if the gateway can not be found.
280+ gw = get_lxc_gateway(interface_lookup=lambda: self.interfaces[:1])
281+ self.assertIsNone(gw)
282+
283+
284+class GetNetworkInterfacesTest(unittest.TestCase):
285+
286+ interfaces = ('eth0', 'eth1', 'lo')
287+
288+ def create_path(self, interfaces):
289+ path = tempfile.mkdtemp()
290+ for interface in interfaces:
291+ os.mkdir(os.path.join(path, interface))
292+ return path
293+
294+ def test_all(self):
295+ # Ensure all interfaces are correctly returned.
296+ path = self.create_path(self.interfaces)
297+ interfaces = get_network_interfaces(path=path)
298+ self.assertItemsEqual(self.interfaces, interfaces)
299+
300+ def test_exclude(self):
301+ # Ensure interfaces can be excluded from the returned list.
302+ path = self.create_path(self.interfaces)
303+ interfaces = get_network_interfaces(path=path, exclude=['lo'])
304+ self.assertItemsEqual(self.interfaces[:-1], interfaces)
305+
306+ def test_none(self):
307+ # An empty list is returned if interfaces are not found.
308+ path = self.create_path([])
309+ interfaces = get_network_interfaces(path=path, exclude=['lo'])
310+ self.assertEqual([], interfaces)
311+
312+
313 class GetRunningContainersTest(unittest.TestCase):
314
315 def assertRunning(self, expected, containers):
316@@ -86,7 +136,38 @@
317 self.assertEqual(expected, f.read())
318
319
320-class TestScrubber(unittest.TestCase):
321+class RetryTest(unittest.TestCase):
322+
323+ error = 'error after {} tries'
324+
325+ def setUp(self):
326+ self.tries = 0
327+
328+ @retry(OSError, tries=10, delay=0)
329+ def _success(self):
330+ self.tries += 1
331+ if self.tries == 5:
332+ return True
333+ raise OSError
334+
335+ @retry(OSError, tries=10, delay=0)
336+ def _failure(self):
337+ self.tries += 1
338+ raise OSError(self.error.format(self.tries))
339+
340+ def test_success(self):
341+ # Ensure the decorated function correctly returns without errors
342+ # after several tries.
343+ self.assertTrue(self._success())
344+
345+ def test_failure(self):
346+ # Ensure the decorated function raises the last error.
347+ with self.assertRaises(OSError) as cm:
348+ self._failure()
349+ self.assertEqual(self.error.format(10), str(cm.exception))
350+
351+
352+class ScrubberTest(unittest.TestCase):
353
354 def make_dirs(self, tmp_path, pattern, num=10, extra=None):
355 dir_ = os.path.split(pattern)[0]
356
357=== modified file 'lpsetup/utils.py'
358--- lpsetup/utils.py 2012-04-11 10:40:05 +0000
359+++ lpsetup/utils.py 2012-05-02 16:37:17 +0000
360@@ -9,18 +9,24 @@
361 'call',
362 'get_container_path',
363 'get_lxc_gateway',
364+ 'get_network_interfaces',
365+ 'get_running_containers',
366 'lxc_in_state',
367- 'lxc_running',
368+ 'lxc_ip',
369 'lxc_stopped',
370 'render_to_file',
371+ 'retry',
372 'Scrubber',
373+ 'sshlxc',
374 'this_command',
375 ]
376
377-from functools import partial
378+from functools import (
379+ partial,
380+ wraps,
381+ )
382 import glob
383 import os
384-import platform
385 import re
386 import subprocess
387 import shutil
388@@ -28,18 +34,24 @@
389 import time
390
391 from shelltoolbox import (
392+ command,
393 join_command,
394 run,
395+ ssh,
396 )
397
398+from lpsetup import exceptions
399 from lpsetup.settings import (
400 LXC_LP_DIR_PATTERN,
401 LXC_LP_TEST_DIR_PATTERN,
402+ LXC_IP_COMMAND,
403 LXC_PATH,
404 )
405
406
407 PID_RE = re.compile("pid:\s+(\d+)")
408+
409+
410 call = partial(run, stdout=None)
411
412
413@@ -58,17 +70,29 @@
414 return os.path.join(base_path, lxc_name, 'rootfs', path.lstrip('/'))
415
416
417-def get_lxc_gateway():
418- """Return a tuple of gateway name and address.
419+def get_lxc_gateway(candidates=('lxcbr0', 'virbr0'), interface_lookup=None):
420+ """Return the gateway bridge name to be used by LXC containers.
421
422- The gateway name and address will change depending on which version
423- of Ubuntu the script is running on.
424+ The gateway name will change depending on which version of Ubuntu the
425+ script is running on: if *lxcbr0* does not exist, *virbr0* will be used.
426+ If both interfaces don't exist, None is returned.
427 """
428- release_name = platform.linux_distribution()[2]
429- if release_name == 'oneiric':
430- return 'virbr0', '192.168.122.1'
431+ if interface_lookup is None:
432+ interfaces = get_network_interfaces()
433 else:
434- return 'lxcbr0', '10.0.3.1'
435+ interfaces = interface_lookup()
436+ for interface in candidates:
437+ if interface in interfaces:
438+ return interface
439+
440+
441+def get_network_interfaces(path='/sys/class/net/', exclude=()):
442+ """Return a list of the network interfaces up in this system."""
443+ return [
444+ i for i in os.listdir(path)
445+ if i not in exclude and
446+ os.path.isdir(os.path.join(path, i))
447+ ]
448
449
450 def get_running_containers(containers=None):
451@@ -101,7 +125,6 @@
452 return False
453
454
455-lxc_running = partial(lxc_in_state, 'RUNNING')
456 lxc_stopped = partial(lxc_in_state, 'STOPPED')
457
458
459@@ -123,6 +146,29 @@
460 return contents
461
462
463+def retry(exception, tries=60, delay=0.5):
464+ """If the decorated function raises `exception`, wait and try it again.
465+
466+ Raise the exception raised by the last call if the function does not
467+ exit normally after 100 tries.
468+
469+ Original from http://wiki.python.org/moin/PythonDecoratorLibrary#Retry.
470+ """
471+ def decorator(func):
472+ @wraps(func)
473+ def decorated(*args, **kwargs):
474+ mtries = tries
475+ while mtries:
476+ try:
477+ return func(*args, **kwargs)
478+ except exception as err:
479+ time.sleep(delay)
480+ mtries -= 1
481+ raise err
482+ return decorated
483+ return decorator
484+
485+
486 class Scrubber(object):
487 """Scrubber will cleanup after lxc ephemeral uncleanliness.
488
489@@ -207,6 +253,35 @@
490 self.scrub(self.lxc_lp_dir_pattern)
491
492
493+@retry(subprocess.CalledProcessError)
494+def lxc_ip(name):
495+ """Return the ip address of the LXC container called `name`.
496+
497+ This function will try to obtain the ip address of the container for
498+ ~30 seconds before raising a `subprocess.CalledProcessError`.
499+ """
500+ cmd = command(*LXC_IP_COMMAND)
501+ return cmd(name).strip()
502+
503+
504+def sshlxc(name, cmd, **kwargs):
505+ """Run ssh shell commands.
506+
507+ The ssh connection is made resolving the ip address of the LXC
508+ container called `name`.
509+
510+ Raise `lpsetup.exceptions.ExecutionError` if the ip address is not found.
511+ Raise `subprocess.CalledProcessError` if the ssh command returns an error.
512+ """
513+ try:
514+ ip = lxc_ip(name)
515+ except subprocess.CalledProcessError as err:
516+ msg = 'Error: unable to find the ip address of the LXC container: '
517+ raise exceptions.ExecutionError(msg + str(err))
518+ sshcall = ssh(ip, **kwargs)
519+ return sshcall(cmd)
520+
521+
522 def this_command(directory, args):
523 """Return a command line to re-launch this script using given `args`.
524

Subscribers

People subscribed via source and target branches

to all changes: