Merge lp:~frankban/lpsetup/lp-lxc-ip into lp:lpsetup

Proposed by Francesco Banconi
Status: Merged
Merged at revision: 15
Proposed branch: lp:~frankban/lpsetup/lp-lxc-ip
Merge into: lp:lpsetup
Diff against target: 766 lines (+677/-6)
10 files modified
.bzrignore (+1/-0)
README.rst (+31/-0)
lp-lxc-ip/lxcip.py (+201/-0)
lp-lxc-ip/tests/test_helpers.py (+86/-0)
lp-lxc-ip/tests/test_lxcip.py (+92/-0)
lp-lxc-ip/tests/test_utils.py (+123/-0)
lp-lxc-ip/tests/utils.py (+132/-0)
lpsetup/__init__.py (+1/-1)
lpsetup/tests/test_handlers.py (+5/-5)
setup.cfg (+5/-0)
To merge this branch: bzr merge lp:~frankban/lpsetup/lp-lxc-ip
Reviewer Review Type Date Requested Status
Gary Poster (community) Approve
Review via email: mp+103502@code.launchpad.net

Description of the change

== Changes ==

1. Introduced the lp-lxc-ip project.

*lp-lxc-ip* provides the ability to obtain the ip address of a running LXC container.
The application is contained in a standalone file lp-lxc-ip/lxcip.py that will be installed in /usr/local/bin/lp-lxc-ip by the debian packaging machinery. However, the file can also be used as is, from the source distribution, e.g.::

    sudo lp-lxc-ip/lxcip.py -n your_container

The application uses ctypes to call underlying C functions, in order to obtain the init pid (liblxc) and to switch the network namespace (libc). In the near future lpsetup will be refactored to use *lp-lxc-ip* for ssh connections to the container, rather than rely on the dns resolver.

2. Switched to nosetests.

== 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 5 0 100%
lpsetup.handlers 55 1 98% 55
lpsetup.settings 30 0 100%
lpsetup.subcommands 0 0 100%
lpsetup.utils 103 26 75% 67-71, 91-101, 117, 145, 155, 176-178, 196-202
---------------------------------------------------
TOTAL 324 34 90%
----------------------------------------------------------------------
Ran 48 tests in 0.564s

OK

$ cd lp-lxc-ip/ && sudo nosetests -v
test_error (tests.test_helpers.ErrorTest) ... ok
test_short (tests.test_helpers.OutputTest) ... ok
test_verbose (tests.test_helpers.OutputTest) ... ok
test_redirection (tests.test_helpers.RedirectStderrTest) ... ok
test_as_root (tests.test_helpers.RootRequiredTest) ... ok
test_as_unprivileged_user (tests.test_helpers.RootRequiredTest) ... ok
test_failure (tests.test_helpers.WrapTest) ... ok
test_success (tests.test_helpers.WrapTest) ... ok
test_get_ip (tests.test_lxcip.LXCIpTest) ... ok
test_invalid_interface (tests.test_lxcip.LXCIpTest) ... ok
test_invalid_name (tests.test_lxcip.LXCIpTest) ... ok
test_invalid_pid (tests.test_lxcip.LXCIpTest) ... ok
test_loopback (tests.test_lxcip.LXCIpTest) ... ok
test_not_root (tests.test_lxcip.LXCIpTest) ... ok
test_race_condition (tests.test_lxcip.LXCIpTest) ... ok
test_restart (tests.test_lxcip.LXCIpTest) ... ok
test_assertion (tests.test_utils.AssertOSErrorTest) ... ok
test_assertion_fails_different_exception (tests.test_utils.AssertOSErrorTest) ... ok
test_assertion_fails_different_message (tests.test_utils.AssertOSErrorTest) ... ok
test_assertion_fails_no_exception (tests.test_utils.AssertOSErrorTest) ... ok
test_create (tests.test_utils.LXCTest) ... ok
test_destroy (tests.test_utils.LXCTest) ... ok
test_start (tests.test_utils.LXCTest) ... ok
test_stop (tests.test_utils.LXCTest) ... ok
test_write_config (tests.test_utils.LXCTest) ... ok
test_mock (tests.test_utils.MockGeteuid) ... ok
test_failure (tests.test_utils.RetryTest) ... ok
test_success (tests.test_utils.RetryTest) ... ok

----------------------------------------------------------------------
Ran 28 tests in 11.473s

OK

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

Francesco, this is great! This uses all kinds of things I've never or barely used, so it was a nice learning experience to review; and the end result reads very well. The tests are very nice to have, and also read surprisingly well for what they are doing.

I had a lot of small comments from the discussion we had, but they were lost when I reloaded this page by mistake. Therefore, here's what I remember, plus some other bits I came up with while I was writing this up.

* In your README.rst, it would be mildly helpful to note that you can install nosetests by running `apt-get install python-nose`.

* Get SIOCGIFADDR and other constants from Python or C, rather than defining them yourself, if possible. We already determined in the call that termios does not have SIOCGIFADDR, so these may have to be ctypes imports.

* Define what those constants mean in some comments, at least enough for someone to basically follow along.

* AFAICT _redirect_stderr is generically useful to someone using ctypes. Do with that what you will.

* I suggest you add a comment or embellish the docstring to explain why _redirect_stderr is valuable with ctypes. You explain in line 190 of the diff, as you pointed out, but it would be nice to see the point while you are reading the function.

* I think it would be cleaner if __enter__ of SetNamespace had the self.set(pid) call.

* It would be nice if SetNamespace's __exit__ explained why 1 was an acceptable constant.

* When I ran the lxc-ip tests I got two failures: http://pastebin.ubuntu.com/945997/ (the main suite passed fine after I installed the python-shelltoolbox package from the yellow PPA). This seemed intermittent.

* I don't like the buffer overflow magic number determined experimentally. Could you check with Serge or someone else to see if `return get_init_pid(name[:85])` could get a magic number that was identified more systematically?

* Similarly, I don't understand why we truncate at :15 when AFAICT we ought to be able to truncate at :256 in "pack = struct.pack('256s', interface[:15])". A comment or something would be nice if possible.

* I'd like to have some more comments explaining what is going on in that ActiveState recipe. I think I mostly understand it now thanks to your explanation, but would be nice for others/the future.

* In test_redirection, I suggest you explain that you need to mimic C writing to stderr, which is why you do the ctypes dance.

* Mention in get_ip why it is required, as opposed to the similar Python function you mentioned.

* diff line 93 typo: ouput -> output

* diff line 73 typo: exits -> exist

Again, very nice branch. I do wish that the common usage pattern could be simpler/shorter than "ssh -A `sudo lp-lxc-ip -s -n lpdev`", but that's why we have aliases, I suppose. Thank you!

review: Approve
lp:~frankban/lpsetup/lp-lxc-ip updated
28. By Francesco Banconi

Trying to avoid race conditions in tests.

29. By Francesco Banconi

Changes from review.

30. By Francesco Banconi

Comments.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file '.bzrignore'
2--- .bzrignore 2012-03-14 17:16:45 +0000
3+++ .bzrignore 2012-04-27 08:29:19 +0000
4@@ -1,3 +1,4 @@
5+.coverage
6 .installed.cfg
7 bin
8 build
9
10=== modified file 'README.rst'
11--- README.rst 2012-03-12 16:20:59 +0000
12+++ README.rst 2012-04-27 08:29:19 +0000
13@@ -32,3 +32,34 @@
14 Get help on commands::
15
16 lp-setup help [command]
17+
18+
19+Testing the application
20+~~~~~~~~~~~~~~~~~~~~~~~
21+
22+To run *lpsetup* tests install nose (`apt-get install python-nose`)
23+and run `nosetests` from this directory.
24+
25+
26+lp-lxc-ip
27+=========
28+
29+This project is a standalone application that can be used to obtain the
30+ip address of a running LXC container, e.g.::
31+
32+ sudo lp-lxc-ip -n mycontainer
33+
34+See `lp-lxc-ip --help` for an explanation of the other options.
35+
36+In the source distribution, the script is present under the `lp-lxc-ip`
37+directory, named `lxcip.py`.
38+
39+
40+Testing the application
41+~~~~~~~~~~~~~~~~~~~~~~~
42+
43+*lp-lxc-ip* must be tested by root: the test run creates and destroy LXC
44+containers, so it can take some minutes to complete::
45+
46+ cd lp-lxc-ip
47+ sudo nosetests
48
49=== added directory 'lp-lxc-ip'
50=== added file 'lp-lxc-ip/lxcip.py'
51--- lp-lxc-ip/lxcip.py 1970-01-01 00:00:00 +0000
52+++ lp-lxc-ip/lxcip.py 2012-04-27 08:29:19 +0000
53@@ -0,0 +1,201 @@
54+#!/usr/bin/env python
55+# Copyright 2012 Canonical Ltd. This software is licensed under the
56+# GNU Affero General Public License version 3 (see the file LICENSE).
57+
58+"""Display the ip address of a container."""
59+
60+import argparse
61+from contextlib import closing, contextmanager
62+import ctypes
63+import fcntl
64+from functools import wraps
65+import os
66+import socket
67+import struct
68+import sys
69+
70+
71+# The namespace type to use with setns(2): in this case we want to
72+# reassociate this thread with the network namespace.
73+CLONE_NEWNET = 0x40000000
74+ERRORS = {
75+ 'not_connected': 'unable to find the container ip address',
76+ 'not_found': 'the container does not exist or is not running',
77+ 'not_installed': 'lxc does not seem to be installed',
78+ 'not_root': 'you must be root',
79+ }
80+INTERFACE = 'eth0'
81+# The ioctl command to retrieve the interface address.
82+SIOCGIFADDR = 0x8915
83+
84+
85+def _parse_args():
86+ """Parse the command line arguments."""
87+ parser = argparse.ArgumentParser(description=__doc__)
88+ parser.add_argument(
89+ '-n', '--name', required=True,
90+ help='The name of the container. ')
91+ parser.add_argument(
92+ '-i', '--interface', default=INTERFACE,
93+ help='The network interface used to obtain the ip address. '
94+ '[DEFAULT={}]'.format(INTERFACE))
95+ parser.add_argument(
96+ '-s', '--short', action='store_true',
97+ help='Display the ip address using the short output format.')
98+ namespace = parser.parse_args()
99+ return namespace.name, namespace.interface, namespace.short
100+
101+
102+def _error(code):
103+ """Return an OSError containing given `msg`."""
104+ return OSError(
105+ '{}: error: {}'.format(os.path.basename(sys.argv[0]), ERRORS[code]))
106+
107+
108+def _output(name, ip, short):
109+ """Format the output displaying the ip address of the container."""
110+ return ip if short else '{}: {}'.format(name, ip)
111+
112+
113+def _wrap(function, error_code):
114+ """Add error handling to the given C `function`.
115+
116+ If the function returns an error, the wrapped function raises an
117+ OSError using a message corresponding to the given `error_code`.
118+ """
119+ def errcheck(result, func, arguments):
120+ if result < 0:
121+ raise _error(error_code)
122+ return result
123+ function.errcheck = errcheck
124+ return function
125+
126+
127+@contextmanager
128+def redirect_stderr(path):
129+ """Redirect system stderr to `path`.
130+
131+ This context manager does not use normal sys.std* Python redirection
132+ because we also want to intercept and redirect stderr written by
133+ underlying C functions called using ctypes.
134+ """
135+ fd = sys.stderr.fileno()
136+ backup = os.dup(fd)
137+ new_fd = os.open(path, os.O_WRONLY)
138+ sys.stderr.flush()
139+ os.dup2(new_fd, fd)
140+ os.close(new_fd)
141+ try:
142+ yield
143+ finally:
144+ sys.stderr.flush()
145+ os.dup2(backup, fd)
146+
147+
148+def root_required(func):
149+ """A decorator checking for current user effective id.
150+
151+ The decorated function is only executed if the current user is root.
152+ Otherwise, an OSError is raised.
153+ """
154+ @wraps(func)
155+ def decorated(*args, **kwargs):
156+ if os.geteuid():
157+ raise _error('not_root')
158+ return func(*args, **kwargs)
159+ return decorated
160+
161+
162+class SetNamespace(object):
163+ """A context manager to switch the network namespace for this thread.
164+
165+ A namespace is one of the entries in /proc/[pid]/ns/.
166+ """
167+ def __init__(self, pid, nstype=CLONE_NEWNET):
168+ libc = ctypes.cdll.LoadLibrary('libc.so.6')
169+ self._pid = pid
170+ self._nstype = nstype
171+ self._setns = _wrap(libc.setns, 'not_connected')
172+
173+ def __enter__(self):
174+ """Switch the namespace."""
175+ self.set(self._pid)
176+ return self
177+
178+ def __exit__(self, exc_type, exc_val, exc_tb):
179+ """Restore normal namespace."""
180+ # To restore the namespace we use the file descriptor associated
181+ # with the hosts's init process. In Linux the init pid is always 1.
182+ self.set(1)
183+
184+ def set(self, pid):
185+ try:
186+ fd = os.open('/proc/{}/ns/net'.format(pid), os.O_RDONLY)
187+ except OSError:
188+ raise _error('not_found')
189+ self._setns(fd, self._nstype)
190+ os.close(fd)
191+
192+
193+@root_required
194+def get_pid(name):
195+ """Return the pid of an LXC, given its `name`.
196+
197+ Raise OSError if LXC is not installed or the container is not found.
198+ """
199+ try:
200+ liblxc = ctypes.cdll.LoadLibrary('/usr/lib/lxc/liblxc.so.0')
201+ except OSError:
202+ raise _error('not_installed')
203+ get_init_pid = _wrap(liblxc.get_init_pid, 'not_found')
204+ # Redirect the system stderr in order to get rid of the error raised by
205+ # the underlying C function call if the container is not found.
206+ with redirect_stderr('/dev/null'):
207+ # XXX 2012-04-27 frankban bug=944386:
208+ # Slice the container name to avoid buffer overflows when calling
209+ # the underlying C function. Here the magic number seems to be 85.
210+ return get_init_pid(name[:85])
211+
212+
213+@root_required
214+def get_ip(pid, interface):
215+ """Return the ip address of LXC `interface`, given the container's `pid`.
216+
217+ Raise OSError if the container is not found or the ip address is not
218+ retreivable.
219+
220+ Note that `socket.gethostbyname` is not usable in this context: it uses
221+ the system's dns resolver that by default does not resolve lxc names.
222+ """
223+ with SetNamespace(pid):
224+ # Retrieve the ip address for the given network interface.
225+ # Original from http://code.activestate.com/recipes/
226+ # 439094-get-the-ip-address-associated-with-a-network-inter/
227+ with closing(socket.socket(socket.AF_INET, socket.SOCK_DGRAM)) as s:
228+ # Slice the interface because the buffer size used to hold an
229+ # interface name, including its terminating zero byte,
230+ # is 16 in Linux (see /usr/include/linux/if.h).
231+ pack = struct.pack('256s', interface[:15])
232+ try:
233+ # Use the ioctl Unix routine to request SIOCGIFADDR.
234+ binary_ip = fcntl.ioctl(s.fileno(), SIOCGIFADDR, pack)[20:24]
235+ # Convert the packet ipv4 address to its standard dotted-quad
236+ # string representation.
237+ ip = socket.inet_ntoa(binary_ip)
238+ except (IOError, socket.error):
239+ raise _error('not_connected')
240+ return ip
241+
242+
243+def main():
244+ name, interface, short = _parse_args()
245+ try:
246+ pid = get_pid(name)
247+ ip = get_ip(pid, interface)
248+ except (KeyboardInterrupt, OSError) as err:
249+ return err
250+ print _output(name, ip, short)
251+
252+
253+if __name__ == '__main__':
254+ sys.exit(main())
255
256=== added directory 'lp-lxc-ip/tests'
257=== added file 'lp-lxc-ip/tests/__init__.py'
258=== added file 'lp-lxc-ip/tests/test_helpers.py'
259--- lp-lxc-ip/tests/test_helpers.py 1970-01-01 00:00:00 +0000
260+++ lp-lxc-ip/tests/test_helpers.py 2012-04-27 08:29:19 +0000
261@@ -0,0 +1,86 @@
262+#!/usr/bin/env python
263+# Copyright 2012 Canonical Ltd. This software is licensed under the
264+# GNU Affero General Public License version 3 (see the file LICENSE).
265+
266+"""Tests for the helpers functions in the lxcip module."""
267+
268+import ctypes
269+import tempfile
270+import unittest
271+
272+import lxcip
273+import utils
274+
275+
276+class ErrorTest(unittest.TestCase):
277+
278+ def test_error(self):
279+ # Ensure the error message is correctly formatted.
280+ error = lxcip._error('not_found')
281+ self.assertIsInstance(error, OSError)
282+ self.assertIn(lxcip.ERRORS['not_found'], str(error))
283+
284+
285+class OutputTest(unittest.TestCase):
286+
287+ name = 'lxc'
288+ ip = '10.0.3.100'
289+
290+ def test_short(self):
291+ # Ensure the short output just displays the ip address.
292+ output = lxcip._output(self.name, self.ip, True)
293+ self.assertEqual(self.ip, output)
294+
295+ def test_verbose(self):
296+ # Ensure the verbose output is correctly generated.
297+ output = lxcip._output(self.name, self.ip, False)
298+ self.assertEqual('{}: {}'.format(self.name, self.ip), output)
299+
300+
301+class RedirectStderrTest(unittest.TestCase):
302+
303+ def test_redirection(self):
304+ # Ensure system stderr is correctly redirected to file.
305+ # Ctypes is used here to ensure the context manager correctly
306+ # redirects stderr coming from low level C functions.
307+ libc = ctypes.cdll.LoadLibrary('libc.so.6')
308+ stderr = ctypes.c_void_p.in_dll(libc, 'stderr')
309+ error = 'you should not see this...'
310+ with tempfile.NamedTemporaryFile(delete=False) as f:
311+ with lxcip.redirect_stderr(f.name):
312+ libc.fprintf(stderr, error)
313+ self.assertEqual(error, f.read())
314+
315+
316+class RootRequiredTest(utils.ErrorTestMixin, unittest.TestCase):
317+
318+ @lxcip.root_required
319+ def _function(self, result):
320+ return result
321+
322+ def test_as_root(self):
323+ # Ensure the function is normally executed fi the current user
324+ # is root.
325+ self.assertTrue(1, self._function(1))
326+
327+ def test_as_unprivileged_user(self):
328+ # The function raises an OSError if executed by an unprivileged user.
329+ with utils.mock_geteuid(1000):
330+ with self.assertOSError('not_root'):
331+ self._function(1)
332+
333+
334+class WrapTest(utils.ErrorTestMixin, unittest.TestCase):
335+
336+ def wrap_and_run(self, function):
337+ wrapped = lxcip._wrap(function, 'not_found')
338+ return wrapped.errcheck(wrapped(), wrapped, [])
339+
340+ def test_success(self):
341+ # Ensure the wrapper correctly returns the function return code.
342+ self.assertEqual(1, self.wrap_and_run(lambda: 1))
343+
344+ def test_failure(self):
345+ # Ensure the wrapper raises an OSError if the wrapped function fails.
346+ with self.assertOSError('not_found'):
347+ self.wrap_and_run(lambda: -1)
348
349=== added file 'lp-lxc-ip/tests/test_lxcip.py'
350--- lp-lxc-ip/tests/test_lxcip.py 1970-01-01 00:00:00 +0000
351+++ lp-lxc-ip/tests/test_lxcip.py 2012-04-27 08:29:19 +0000
352@@ -0,0 +1,92 @@
353+#!/usr/bin/env python
354+# Copyright 2012 Canonical Ltd. This software is licensed under the
355+# GNU Affero General Public License version 3 (see the file LICENSE).
356+
357+"""Tests for the lxcip module."""
358+
359+import unittest
360+
361+import lxcip
362+import utils
363+
364+
365+lxc = utils.LXC()
366+
367+
368+def setup():
369+ """Create and start an LXC container to be used during tests."""
370+ lxc.create()
371+ lxc.start()
372+
373+
374+def teardown():
375+ """Destroy the LXC container created for tests."""
376+ lxc.destroy()
377+
378+
379+class LXCIpTest(utils.ErrorTestMixin, unittest.TestCase):
380+
381+ name = lxc.name
382+
383+ def test_get_ip(self):
384+ # Ensure the ip address of the container is correctly retrieved.
385+ pid = utils.get_pid(self.name)
386+ ip = utils.get_ip(pid, lxcip.INTERFACE)
387+ self.assertEqual(self.name, utils.gethostbyaddr(ip)[0])
388+
389+ def test_loopback(self):
390+ # Ensure the loopback ip address is correctly retrieved.
391+ pid = utils.get_pid(self.name)
392+ ip = utils.get_ip(pid, 'lo')
393+ self.assertEqual('127.0.0.1', ip)
394+
395+ def test_invalid_interface(self):
396+ # Ensure an OSError is raised if the ip addres is requested for
397+ # a non existent interface.
398+ pid = utils.get_pid(self.name)
399+ with self.assertOSError('not_connected'):
400+ lxcip.get_ip(pid, '__does_not_exist__')
401+
402+ def test_invalid_name(self):
403+ # If the container does not exist or is not running, trying to obtain
404+ # the PID raises an OSError.
405+ with self.assertOSError('not_found'):
406+ lxcip.get_pid('__does_not_exist__')
407+
408+ def test_invalid_pid(self):
409+ # If the container does not exist or is not running, trying to obtain
410+ # the ip address raises an OSError.
411+ with self.assertOSError('not_found'):
412+ lxcip.get_ip(0, lxcip.INTERFACE)
413+
414+ def test_not_root(self):
415+ # An OSerror is raised by get_pid and get_ip if the current user
416+ # is not root.
417+ with utils.mock_geteuid(1000):
418+ with self.assertOSError('not_root'):
419+ lxcip.get_pid(self.name)
420+ with self.assertOSError('not_root'):
421+ lxcip.get_ip(1, lxcip.INTERFACE)
422+
423+ def test_restart(self):
424+ # Ensure the functions work as expected if the container is
425+ # stopped and then restarted.
426+ lxc.stop()
427+ with self.assertOSError('not_found'):
428+ lxcip.get_pid(self.name)
429+ lxc.start()
430+ pid = utils.get_pid(self.name)
431+ ip = utils.get_ip(pid, lxcip.INTERFACE)
432+ self.assertEqual(self.name, utils.gethostbyaddr(ip)[0])
433+
434+ def test_race_condition(self):
435+ # Ensure the application fails gracefully if the container is
436+ # stopped during the script execution.
437+ pid = utils.get_pid(self.name)
438+ lxc.stop()
439+ with self.assertOSError('not_found'):
440+ lxcip.get_ip(pid, lxcip.INTERFACE)
441+ lxc.start()
442+ # Wait for the container to be up again before proceeding with
443+ # other tests.
444+ utils.get_pid(self.name)
445
446=== added file 'lp-lxc-ip/tests/test_utils.py'
447--- lp-lxc-ip/tests/test_utils.py 1970-01-01 00:00:00 +0000
448+++ lp-lxc-ip/tests/test_utils.py 2012-04-27 08:29:19 +0000
449@@ -0,0 +1,123 @@
450+#!/usr/bin/env python
451+# Copyright 2012 Canonical Ltd. This software is licensed under the
452+# GNU Affero General Public License version 3 (see the file LICENSE).
453+
454+"""Tests for the lxcip test utilities."""
455+
456+import os
457+import unittest
458+
459+import lxcip
460+import utils
461+
462+
463+class AssertOSErrorTest(utils.ErrorTestMixin, unittest.TestCase):
464+
465+ def test_assertion(self):
466+ # Ensure the assertion does not fail if OSError is raised with the
467+ # correct message.
468+ with self.assertOSError('not_found'):
469+ raise OSError(lxcip.ERRORS['not_found'])
470+
471+ def test_assertion_fails_different_message(self):
472+ # Ensure the assertion fails if OSError is raised with
473+ # a different message.
474+ with self.assertRaises(AssertionError):
475+ with self.assertOSError('not_found'):
476+ raise OSError('example error text')
477+
478+ def test_assertion_fails_no_exception(self):
479+ # Ensure the assertion fails if OSError is not raised.
480+ with self.assertRaises(AssertionError) as cm:
481+ with self.assertOSError('not_found'):
482+ pass
483+ self.assertEqual('OSError not raised', str(cm.exception))
484+
485+ def test_assertion_fails_different_exception(self):
486+ # Ensure the assertion function does not swallow other exceptions.
487+ with self.assertRaises(TypeError):
488+ with self.assertOSError('not_found'):
489+ raise TypeError
490+
491+
492+class MockGeteuid(unittest.TestCase):
493+
494+ def test_mock(self):
495+ # Ensure os.geteuid return value is correctly changed and then
496+ # restored.
497+ current_value = os.geteuid()
498+ with utils.mock_geteuid(9000):
499+ self.assertEqual(9000, os.geteuid())
500+ self.assertEqual(current_value, os.geteuid())
501+
502+
503+class LXCTest(unittest.TestCase):
504+
505+ config = (('k1', 'v1'), ('k2', 'v2'))
506+
507+ def setUp(self):
508+ self.lxc = utils.LXC(config=self.config, caller=lambda cmd: cmd)
509+
510+ def test_write_config(self):
511+ # Ensure the LXC template configuration is correctly saved in a
512+ # temporary file.
513+ filename = self.lxc._write_config()
514+ expected = ''.join('{}={}\n'.format(k, v) for k, v in self.config)
515+ with open(filename) as f:
516+ self.assertEqual(expected, f.read())
517+
518+ def test_create(self):
519+ # Ensure the LXC creation command is correctly generated.
520+ cmd = self.lxc.create()
521+ expected = [
522+ 'lxc-create', '-t', self.lxc.template, '-n', self.lxc.name]
523+ self.assertItemsEqual(expected, cmd[:5])
524+
525+ def test_destroy(self):
526+ # Ensure the LXC destruction command is correctly generated.
527+ cmd = self.lxc.destroy()
528+ expected = ['lxc-destroy', '-f', '-n', self.lxc.name]
529+ self.assertItemsEqual(expected, cmd)
530+
531+ def test_start(self):
532+ # Ensure the LXC start command is correctly generated.
533+ cmd = self.lxc.start()
534+ expected = ['lxc-start', '-n', self.lxc.name, '-d']
535+ self.assertItemsEqual(expected, cmd)
536+
537+ def test_stop(self):
538+ # Ensure the LXC stop command is correctly generated.
539+ cmd = self.lxc.stop()
540+ expected = ['lxc-stop', '-n', self.lxc.name]
541+ self.assertItemsEqual(expected, cmd)
542+
543+
544+class RetryTest(unittest.TestCase):
545+
546+ error = 'error after {} tries'
547+
548+ def setUp(self):
549+ self.tries = 0
550+
551+ @utils.retry(OSError, tries=10, delay=0)
552+ def _success(self):
553+ self.tries += 1
554+ if self.tries == 5:
555+ return True
556+ raise OSError
557+
558+ @utils.retry(OSError, tries=10, delay=0)
559+ def _failure(self):
560+ self.tries += 1
561+ raise OSError(self.error.format(self.tries))
562+
563+ def test_success(self):
564+ # Ensure the decorated function correctly returns without errors
565+ # after several tries.
566+ self.assertTrue(self._success())
567+
568+ def test_failure(self):
569+ # Ensure the decorated function raises the last error.
570+ with self.assertRaises(OSError) as cm:
571+ self._failure()
572+ self.assertEqual(self.error.format(10), str(cm.exception))
573
574=== added file 'lp-lxc-ip/tests/utils.py'
575--- lp-lxc-ip/tests/utils.py 1970-01-01 00:00:00 +0000
576+++ lp-lxc-ip/tests/utils.py 2012-04-27 08:29:19 +0000
577@@ -0,0 +1,132 @@
578+#!/usr/bin/env python
579+# Copyright 2012 Canonical Ltd. This software is licensed under the
580+# GNU Affero General Public License version 3 (see the file LICENSE).
581+
582+"""An LXC manager that allows containers creation and destruction."""
583+
584+from contextlib import contextmanager
585+from functools import wraps
586+import os
587+import socket
588+import subprocess
589+import tempfile
590+import time
591+
592+import lxcip
593+
594+
595+NAME = 'lp-lxc-ip-tests'
596+
597+
598+class ErrorTestMixin(object):
599+
600+ @contextmanager
601+ def assertOSError(self, error_code):
602+ try:
603+ yield
604+ except OSError as err:
605+ self.assertIn(lxcip.ERRORS[error_code], str(err))
606+ else:
607+ self.fail('OSError not raised')
608+
609+
610+@contextmanager
611+def mock_geteuid(euid):
612+ """A context manager to temporary mock os.geteuid."""
613+ os.geteuid, backup = lambda: euid, os.geteuid
614+ try:
615+ yield
616+ finally:
617+ os.geteuid = backup
618+
619+
620+class LXC(object):
621+ """Create, start, stop, destroy LXC containers."""
622+
623+ def __init__(
624+ self, name=NAME, template='ubuntu', release='precise',
625+ arch='i386', config=None, caller=None):
626+ self.arch = arch
627+ if caller is None:
628+ self._caller = subprocess.check_output
629+ else:
630+ self._caller = caller
631+ if config is None:
632+ self.config = (
633+ ('lxc.network.type', 'veth'),
634+ ('lxc.network.link', 'lxcbr0'),
635+ ('lxc.network.flags', 'up'),
636+ )
637+ else:
638+ self.config = config
639+ self.name = name
640+ self.release = release
641+ self.template = template
642+
643+ def _get_config(self):
644+ return ''.join('{}={}\n'.format(k, v) for k, v in self.config)
645+
646+ def _write_config(self):
647+ """Write the configuration template in a temporary file.
648+
649+ Return the temporary file path.
650+ """
651+ with tempfile.NamedTemporaryFile(delete=False) as f:
652+ f.write(self._get_config())
653+ return f.name
654+
655+ def _call(self, cmd):
656+ """Run the external program `cmd`."""
657+ return self._caller(cmd)
658+
659+ def create(self):
660+ """Create the container."""
661+ cmd = [
662+ 'lxc-create',
663+ '-t', self.template,
664+ '-n', self.name,
665+ '-f', self._write_config(),
666+ '--',
667+ '-r {} -a {}'.format(self.release, self.arch)
668+ ]
669+ return self._call(cmd)
670+
671+ def destroy(self):
672+ """Destroy the container."""
673+ return self._call(['lxc-destroy', '-f', '-n', self.name])
674+
675+ def start(self):
676+ """Start the container."""
677+ return self._call(['lxc-start', '-n', self.name, '-d'])
678+
679+ def stop(self):
680+ """Stop the container."""
681+ return self._call(['lxc-stop', '-n', self.name])
682+
683+
684+def retry(exception, tries=100, delay=0.1):
685+ """If the decorated function raises `exception`, wait and try it again.
686+
687+ Raise the exception raised by the last call if the function does not
688+ exit normally after 100 tries.
689+
690+ Original from http://wiki.python.org/moin/PythonDecoratorLibrary#Retry.
691+ """
692+ def decorator(func):
693+ @wraps(func)
694+ def decorated(*args, **kwargs):
695+ mtries = tries
696+ while mtries:
697+ try:
698+ return func(*args, **kwargs)
699+ except exception as err:
700+ time.sleep(delay)
701+ mtries -= 1
702+ raise err
703+ return decorated
704+ return decorator
705+
706+
707+get_pid = retry(OSError)(lxcip.get_pid)
708+get_ip = retry(OSError)(lxcip.get_ip)
709+gethostbyaddr = retry(socket.herror)(socket.gethostbyaddr)
710
711=== modified file 'lpsetup/__init__.py'
712--- lpsetup/__init__.py 2012-03-22 10:15:25 +0000
713+++ lpsetup/__init__.py 2012-04-27 08:29:19 +0000
714@@ -9,7 +9,7 @@
715 'get_version',
716 ]
717
718-VERSION = (0, 1, 2)
719+VERSION = (0, 1, 3)
720
721
722 def get_version():
723
724=== modified file 'lpsetup/tests/test_handlers.py'
725--- lpsetup/tests/test_handlers.py 2012-04-20 14:30:25 +0000
726+++ lpsetup/tests/test_handlers.py 2012-04-27 08:29:19 +0000
727@@ -154,7 +154,7 @@
728
729 class HandleTestingTest(unittest.TestCase):
730
731- context = {
732+ ctx = {
733 'create_scripts': True,
734 'install_subunit': False,
735 'use_urandom': False,
736@@ -162,16 +162,16 @@
737
738 def test_true(self):
739 # Ensure aliased options are set to True if testing is True.
740- namespace = argparse.Namespace(testing=True, **self.context)
741+ namespace = argparse.Namespace(testing=True, **self.ctx)
742 handle_testing(namespace)
743- for key in self.context:
744+ for key in self.ctx:
745 self.assertTrue(getattr(namespace, key))
746
747 def test_false(self):
748 # Ensure no changes are made to aliased options if testing is False.
749- namespace = argparse.Namespace(testing=False, **self.context)
750+ namespace = argparse.Namespace(testing=False, **self.ctx)
751 handle_testing(namespace)
752- for key, value in self.context.items():
753+ for key, value in self.ctx.items():
754 self.assertEqual(value, getattr(namespace, key))
755
756
757
758=== added file 'setup.cfg'
759--- setup.cfg 1970-01-01 00:00:00 +0000
760+++ setup.cfg 2012-04-27 08:29:19 +0000
761@@ -0,0 +1,5 @@
762+[nosetests]
763+detailed-errors=1
764+exclude=handle_testing
765+with-coverage=1
766+cover-package=lpsetup

Subscribers

People subscribed via source and target branches

to all changes: