Merge lp:~vila/u1-test-utils/lxc into lp:u1-test-utils

Proposed by Vincent Ladeuil
Status: Merged
Approved by: Vincent Ladeuil
Approved revision: 119
Merged at revision: 93
Proposed branch: lp:~vila/u1-test-utils/lxc
Merge into: lp:u1-test-utils
Diff against target: 1423 lines (+556/-280)
3 files modified
setup_vm/bin/setup_vm.py (+364/-171)
setup_vm/tests/test_setup_vm.py (+176/-109)
setup_vm/vms.conf (+16/-0)
To merge this branch: bzr merge lp:~vila/u1-test-utils/lxc
Reviewer Review Type Date Requested Status
Leo Arias (community) code review. Approve
Review via email: mp+179236@code.launchpad.net

Commit message

Start implementing lxc support.

Description of the change

Here comes lxc support \o/

vms should now declare which class they are (kvm or lxc) via vm.class.

I thought about keeping kvm as a default for hiterycal raisins but... that
doesn't seem to be the most appropriate default ;)

I've yet to implement vm.backing for lxc so don't search for it.

Expect for that everything else should work but may need to be polished (doc
included).

To post a comment you must log in.
Revision history for this message
Leo Arias (elopio) wrote :

I think LXC would be a good default for vm.class.

29 + help='''Where lxc definitions are stored.'''

Why does it have three quotes?

94 + :return: False if the file is in the download cache, True if a download
95 + occurred.

This is more like: True if a download ocurred. False if the file is in the download cache and we are not forcing the download.

137 + def download(self):
138 + raise NotImplementedError(self.download)

Shouldn't this be the same call to wget you do on _download_in_cache?
If this should be implemented on the children classes, I think that would be a better errror message for the exception raised.

526 + # Create an lxc, relying on cloud-init to customize the base image.

This would be better as a docstring.

951 + tests.requires_feature(self, tests.sudo_feature)

This is already on the setUp.

1395 +[lxc1]

What's this lxc machine for? If you are using it for something, it should have a better name.

The branch looks really good. There are things I don't yet understand, from the libraries you are using or from cloud-init. But you have my +1. Next week I hope I will be able to give it a try.

review: Approve (code review.)
Revision history for this message
Ubuntu One Auto Pilot (otto-pilot) wrote :

There are additional revisions which have not been approved in review. Please seek review and approval of these new revisions.

Revision history for this message
Vincent Ladeuil (vila) wrote :

> I think LXC would be a good default for vm.class.
>
> 29 + help='''Where lxc definitions are stored.'''
>
> Why does it have three quotes?

So that all helps have 3 quotes.

>
> 94 + :return: False if the file is in the download cache, True if a
> download
> 95 + occurred.
>
> This is more like: True if a download ocurred. False if the file is in the
> download cache and we are not forcing the download.

Well, if we are forcing the download the file can't be in the cache, that's what 'force' do, it clears the cache.

>
> 137 + def download(self):
> 138 + raise NotImplementedError(self.download)
>
> Shouldn't this be the same call to wget you do on _download_in_cache?
> If this should be implemented on the children classes, I think that would be a
> better errror message for the exception raised.

Well, implementing lxc I discovered that lxc-create provides its own caching mechanism that cannot be bypassed.

So the API provided by setup_vm doesn't fit: the download will occur during lxc-create (called by --install) and not during --download.

We probably want to tweak setup_vm instead as I think lxc-create provides a better user experience:
- download if needed when creating a vm and requires it,
- find a way to force a new download if the user requires it (I've yet to see how to get that for lxc)

I filed hhtp://pad.lv/1210428 to track that.

>
> 526 + # Create an lxc, relying on cloud-init to customize the base image.
>
> This would be better as a docstring.

Done.
>
> 951 + tests.requires_feature(self, tests.sudo_feature)
>
> This is already on the setUp.

Fixed. Thanks for catching that one, these tests were changed multiple times
in this proposal and I lost track ;)

>
> 1395 +[lxc1]
>
> What's this lxc machine for? If you are using it for something, it should have
> a better name.

Sorry, that was for manual tests and I should have deleted it before
submitting (or even better, defined it in my ~/vms.conf...). Thanks for the
heads-up.

I'll leave lxc-precise-server-pristine in place for now as an example or
base for future uses but I don't need it myself (comments added to clarify).

>
> The branch looks really good. There are things I don't yet understand, from
> the libraries you are using or from cloud-init. But you have my +1. Next week
> I hope I will be able to give it a try.

Feel free to continue the discussion about the things you don't understand,
that may reveal bugs or at least lack of documentation.

Thanks for the review !

Revision history for this message
Leo Arias (elopio) :
review: Approve (code review.)
Revision history for this message
Ubuntu One Auto Pilot (otto-pilot) wrote :
Download full text (21.7 KiB)

The attempt to merge lp:~vila/u1-test-utils/lxc into lp:u1-test-utils failed. Below is the output from the failed tests.

Setting up the virtual environment.
[localhost] local: which virtualenv
[localhost] local: /usr/bin/python /usr/bin/virtualenv --version
[localhost] local: /usr/bin/python /usr/bin/virtualenv --distribute --clear .env
Not deleting .env/bin
New python executable in .env/bin/python
Installing distribute.............................................................................................................................................................................................done.
Installing pip...............done.
[localhost] local: . /mnt/tarmac/cache/u1-test-utils/trunk/.env/bin/activate && pip install -U -r requirements.txt
Downloading/unpacking bzr+ssh://bazaar.launchpad.net/~bloodearnest/localmail/trunk@36 (from -r requirements.txt (line 9))
  Checking out bzr+ssh://bazaar.launchpad.net/~bloodearnest/localmail/trunk (to revision 36) to /tmp/pip-pJTY95-build
  Running setup.py egg_info for package from bzr+ssh://bazaar.launchpad.net/~bloodearnest/localmail/trunk@36

Downloading/unpacking bzr+http://bazaar.launchpad.net/~ubuntuone-hackers/payclient/trunk@4 (from -r requirements.txt (line 10))
  Checking out http://bazaar.launchpad.net/~ubuntuone-hackers/payclient/trunk (to revision 4) to /tmp/pip-DUKT4G-build
  Running setup.py egg_info for package from bzr+http://bazaar.launchpad.net/~ubuntuone-hackers/payclient/trunk@4

    warning: no previously-included files matching '*.pyc' found anywhere in distribution
Downloading/unpacking bzr+http://bazaar.launchpad.net/~canonical-isd-qa/selenium-simple-test/trunk@tag:sst-0.2.4 (from -r requirements.txt (line 11))
  Checking out http://bazaar.launchpad.net/~canonical-isd-qa/selenium-simple-test/trunk (to revision tag:sst-0.2.4) to /tmp/pip-UsMJJ9-build
  Running setup.py egg_info for package from bzr+http://bazaar.launchpad.net/~canonical-isd-qa/selenium-simple-test/trunk@tag:sst-0.2.4

    warning: no files found matching 'testproj.db.original' under directory 'src/testproject'
Downloading/unpacking bzr+http://bazaar.launchpad.net/~canonical-isd-hackers/canonical-identity-provider/ssoclient@3 (from -r requirements.txt (line 12))
  Checking out http://bazaar.launchpad.net/~canonical-isd-hackers/canonical-identity-provider/ssoclient (to revision 3) to /tmp/pip-HSVF6F-build
  Running setup.py egg_info for package from bzr+http://bazaar.launchpad.net/~canonical-isd-hackers/canonical-identity-provider/ssoclient@3

Downloading/unpacking beautifulsoup4 (from -r requirements.txt (line 2))
  Running setup.py egg_info for package beautifulsoup4

Downloading/unpacking django==1.4 (from -r requirements.txt (line 3))
  Running setup.py egg_info for package django

Downloading/unpacking mock (from -r requirements.txt (line 4))
  Running setup.py egg_info for package mock

    warning: no files found matching '*.png' under directory 'docs'
    warning: no files found matching '*.css' under directory 'docs'
    warning: no files found matching '*.html' under directory 'docs'
    warning: no files found matching '*.js' under directory 'docs'
Downloading...

Revision history for this message
Ubuntu One Auto Pilot (otto-pilot) wrote :
Download full text (21.7 KiB)

The attempt to merge lp:~vila/u1-test-utils/lxc into lp:u1-test-utils failed. Below is the output from the failed tests.

Setting up the virtual environment.
[localhost] local: which virtualenv
[localhost] local: /usr/bin/python /usr/bin/virtualenv --version
[localhost] local: /usr/bin/python /usr/bin/virtualenv --distribute --clear .env
Not deleting .env/bin
New python executable in .env/bin/python
Installing distribute.............................................................................................................................................................................................done.
Installing pip...............done.
[localhost] local: . /mnt/tarmac/cache/u1-test-utils/trunk/.env/bin/activate && pip install -U -r requirements.txt
Downloading/unpacking bzr+ssh://bazaar.launchpad.net/~bloodearnest/localmail/trunk@36 (from -r requirements.txt (line 9))
  Checking out bzr+ssh://bazaar.launchpad.net/~bloodearnest/localmail/trunk (to revision 36) to /tmp/pip-oDnmSZ-build
  Running setup.py egg_info for package from bzr+ssh://bazaar.launchpad.net/~bloodearnest/localmail/trunk@36

Downloading/unpacking bzr+http://bazaar.launchpad.net/~ubuntuone-hackers/payclient/trunk@4 (from -r requirements.txt (line 10))
  Checking out http://bazaar.launchpad.net/~ubuntuone-hackers/payclient/trunk (to revision 4) to /tmp/pip-lArRRG-build
  Running setup.py egg_info for package from bzr+http://bazaar.launchpad.net/~ubuntuone-hackers/payclient/trunk@4

    warning: no previously-included files matching '*.pyc' found anywhere in distribution
Downloading/unpacking bzr+http://bazaar.launchpad.net/~canonical-isd-qa/selenium-simple-test/trunk@tag:sst-0.2.4 (from -r requirements.txt (line 11))
  Checking out http://bazaar.launchpad.net/~canonical-isd-qa/selenium-simple-test/trunk (to revision tag:sst-0.2.4) to /tmp/pip-bwzQrD-build
  Running setup.py egg_info for package from bzr+http://bazaar.launchpad.net/~canonical-isd-qa/selenium-simple-test/trunk@tag:sst-0.2.4

    warning: no files found matching 'testproj.db.original' under directory 'src/testproject'
Downloading/unpacking bzr+http://bazaar.launchpad.net/~canonical-isd-hackers/canonical-identity-provider/ssoclient@3 (from -r requirements.txt (line 12))
  Checking out http://bazaar.launchpad.net/~canonical-isd-hackers/canonical-identity-provider/ssoclient (to revision 3) to /tmp/pip-88gtUw-build
  Running setup.py egg_info for package from bzr+http://bazaar.launchpad.net/~canonical-isd-hackers/canonical-identity-provider/ssoclient@3

Downloading/unpacking beautifulsoup4 (from -r requirements.txt (line 2))
  Running setup.py egg_info for package beautifulsoup4

Downloading/unpacking django==1.4 (from -r requirements.txt (line 3))
  Running setup.py egg_info for package django

Downloading/unpacking mock (from -r requirements.txt (line 4))
  Running setup.py egg_info for package mock

    warning: no files found matching '*.png' under directory 'docs'
    warning: no files found matching '*.css' under directory 'docs'
    warning: no files found matching '*.html' under directory 'docs'
    warning: no files found matching '*.js' under directory 'docs'
Downloading...

Revision history for this message
Ubuntu One Auto Pilot (otto-pilot) wrote :
Download full text (21.9 KiB)

The attempt to merge lp:~vila/u1-test-utils/lxc into lp:u1-test-utils failed. Below is the output from the failed tests.

Setting up the virtual environment.
[localhost] local: which virtualenv
[localhost] local: /usr/bin/python /usr/bin/virtualenv --version
[localhost] local: /usr/bin/python /usr/bin/virtualenv --distribute --clear .env
Not deleting .env/bin
New python executable in .env/bin/python
Installing distribute.............................................................................................................................................................................................done.
Installing pip...............done.
[localhost] local: . /mnt/tarmac/cache/u1-test-utils/trunk/.env/bin/activate && pip install -U -r requirements.txt
Downloading/unpacking bzr+ssh://bazaar.launchpad.net/~bloodearnest/localmail/trunk@36 (from -r requirements.txt (line 9))
  Checking out bzr+ssh://bazaar.launchpad.net/~bloodearnest/localmail/trunk (to revision 36) to /tmp/pip-KtJOxw-build
  Running setup.py egg_info for package from bzr+ssh://bazaar.launchpad.net/~bloodearnest/localmail/trunk@36

Downloading/unpacking bzr+http://bazaar.launchpad.net/~ubuntuone-hackers/payclient/trunk@4 (from -r requirements.txt (line 10))
  Checking out http://bazaar.launchpad.net/~ubuntuone-hackers/payclient/trunk (to revision 4) to /tmp/pip-l1NQrT-build
  Running setup.py egg_info for package from bzr+http://bazaar.launchpad.net/~ubuntuone-hackers/payclient/trunk@4

    warning: no previously-included files matching '*.pyc' found anywhere in distribution
Downloading/unpacking bzr+http://bazaar.launchpad.net/~canonical-isd-qa/selenium-simple-test/trunk@tag:sst-0.2.4 (from -r requirements.txt (line 11))
  Checking out http://bazaar.launchpad.net/~canonical-isd-qa/selenium-simple-test/trunk (to revision tag:sst-0.2.4) to /tmp/pip-0aXiDS-build
  Running setup.py egg_info for package from bzr+http://bazaar.launchpad.net/~canonical-isd-qa/selenium-simple-test/trunk@tag:sst-0.2.4

    warning: no files found matching 'testproj.db.original' under directory 'src/testproject'
Downloading/unpacking bzr+http://bazaar.launchpad.net/~canonical-isd-hackers/canonical-identity-provider/ssoclient@3 (from -r requirements.txt (line 12))
  Checking out http://bazaar.launchpad.net/~canonical-isd-hackers/canonical-identity-provider/ssoclient (to revision 3) to /tmp/pip-RZdPEc-build
  Running setup.py egg_info for package from bzr+http://bazaar.launchpad.net/~canonical-isd-hackers/canonical-identity-provider/ssoclient@3

Downloading/unpacking beautifulsoup4 (from -r requirements.txt (line 2))
  Running setup.py egg_info for package beautifulsoup4

Downloading/unpacking django==1.4 (from -r requirements.txt (line 3))
  Running setup.py egg_info for package django

Downloading/unpacking mock (from -r requirements.txt (line 4))
  Running setup.py egg_info for package mock

    warning: no files found matching '*.png' under directory 'docs'
    warning: no files found matching '*.css' under directory 'docs'
    warning: no files found matching '*.html' under directory 'docs'
    warning: no files found matching '*.js' under directory 'docs'
Downloading...

lp:~vila/u1-test-utils/lxc updated
119. By Vincent Ladeuil

Tricky... When RegistryOption was introduced in bzr-2.6 Option.help became a property and the attribute was renamed _help. The simplest fix here is to just drop the help property, we don't use it for now anyway.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'setup_vm/bin/setup_vm.py'
2--- setup_vm/bin/setup_vm.py 2013-08-08 10:10:01 +0000
3+++ setup_vm/bin/setup_vm.py 2013-08-09 09:36:11 +0000
4@@ -17,6 +17,7 @@
5 import bzrlib
6 from bzrlib import (
7 config,
8+ registry,
9 transport,
10 urlutils,
11 )
12@@ -107,6 +108,43 @@
13 *args, from_unicode=path_from_unicode, **kwargs)
14
15
16+if bzrlib.version_info < (2, 6):
17+ class RegistryOption(config.Option):
18+ """Option for a choice from a registry."""
19+
20+ def __init__(self, name, registry, default_from_env=None,
21+ help=None, invalid=None):
22+ """A registry based Option definition.
23+
24+ This overrides the base class so the conversion from a unicode
25+ string can take quoting into account.
26+ """
27+ super(RegistryOption, self).__init__(
28+ name, default=lambda: unicode(registry.default_key),
29+ default_from_env=default_from_env,
30+ from_unicode=self.from_unicode, help=help,
31+ invalid=invalid, unquote=False)
32+ self.registry = registry
33+
34+ def from_unicode(self, unicode_str):
35+ if not isinstance(unicode_str, basestring):
36+ raise TypeError
37+ try:
38+ return self.registry.get(unicode_str)
39+ except KeyError:
40+ raise ValueError(
41+ "Invalid value %s for %s."
42+ "See help for a list of possible values."
43+ % (unicode_str, self.name))
44+
45+else:
46+ RegistryOption = config.RegistryOption
47+
48+
49+# The VM classes are registered later (where they are defined)
50+vm_class_registry = registry.Registry()
51+
52+
53 def register(option):
54 config.option_registry.register(option)
55
56@@ -160,6 +198,9 @@
57 help='''\
58 Where libvirt (qemu) stores the vms config files.'''))
59
60+# The base directories where vms are stored for lxc
61+register(PathOption('vm.lxcs_dir', default='/var/lib/lxc',
62+ help='''Where lxc definitions are stored.'''))
63 # Isos and images download handling
64 register(config.Option('vm.iso_url',
65 default='http://cdimage.ubuntu.com/daily-live/current/',
66@@ -178,6 +219,10 @@
67 register(PathOption('vm.download_cache', default='{vm.images_dir}',
68 help='''Where downloads end up.'''))
69
70+
71+register(RegistryOption('vm.class', vm_class_registry,
72+ invalid='error',
73+ help='''The virtual machine technology to use.'''))
74 # The ubiquitous vm name
75 register(config.Option('vm.name', default=None, invalid='error',
76 help='''\
77@@ -633,6 +678,8 @@
78 # can't create any dir/file there. The fix is to only create a script
79 # that will be executed via runcmd so it will run later and avoid the
80 # issue. -- vila 2013-03-21
81+ # FIXME: Moreover, -pristine vms don't have bzr installed so this
82+ # cannot succeed there -- vila 2013-08-07
83 hook_content = '''#!/bin/sh
84 mkdir -p {dir_path}
85 chown {user}:{user} ~ubuntu
86@@ -739,28 +786,48 @@
87 return '#cloud-config-archive\n' + yaml.safe_dump(parts)
88
89
90-def vm_states(source=None):
91- """A dict of states for vms indexed by name.
92-
93- :param source: A list of lines as produced by virsh list --all without
94- decorations (header/footer).
95- """
96- if source is None:
97- retcode, out, err = run_subprocess(['virsh', 'list', '--all'])
98- # Get rid of header/footer
99- source = out.splitlines()[2:-1]
100- states = {}
101- for line in source:
102- caret_or_id, name, state = line.split(None, 2)
103- states[name] = state
104- return states
105-
106-
107 class VM(object):
108+ """A virtual machine relying on cloud-init to customize installation."""
109
110 def __init__(self, conf):
111 self.conf = conf
112 self._config_dir = None
113+ # Seed files
114+ self._meta_data_path = None
115+ self._user_data_path = None
116+
117+ def _download_in_cache(self, source_url, name, force=False):
118+ """Download ``name`` from ``source_url`` in ``vm.download_cache``.
119+
120+ :param source_url: The url where the file to download is located
121+
122+ :param name: The name of the file to download (also used as the name
123+ for the downloaded file).
124+
125+ :param force: Remove the file from the cache if present.
126+
127+ :return: False if the file is in the download cache, True if a download
128+ occurred.
129+ """
130+ source = urlutils.join(source_url, name)
131+ download_dir = self.conf.get('vm.download_cache')
132+ if not os.path.exists(download_dir):
133+ raise ConfigValueError('vm.download_cache', download_dir)
134+ target = os.path.join(download_dir, name)
135+ # FIXME: By default the download dir may be under root control, but if
136+ # a user chose to use a different one under his own control, it would
137+ # be nice to not require sudo usage. -- vila 2013-02-06
138+ if force:
139+ run_subprocess(['sudo', 'rm', '-f', target])
140+ if not os.path.exists(target):
141+ # FIXME: We do ask for a progress bar but it's not displayed
142+ # (run_subprocess capture both stdout and stderr) ! At least while
143+ # used interactively, it should. -- vila 2013-02-06
144+ run_subprocess(['sudo', 'wget', '--progress=dot:mega', '-O',
145+ target, source])
146+ return True
147+ else:
148+ return False
149
150 def ensure_dir(self, path):
151 try:
152@@ -793,52 +860,88 @@
153 for key in keys:
154 self._ssh_keygen(key)
155
156+ def create_meta_data(self):
157+ self.ensure_config_dir()
158+ self._meta_data_path = os.path.join(self._config_dir, 'meta-data')
159+ with open(self._meta_data_path, 'w') as f:
160+ f.write(self.conf.get('vm.meta_data'))
161+
162+ def create_user_data(self):
163+ ci_user_data = CIUserData(self.conf)
164+ ci_user_data.populate()
165+ self.ensure_config_dir()
166+ self._user_data_path = os.path.join(self._config_dir, 'user-data')
167+ with open(self._user_data_path, 'w') as f:
168+ f.write(ci_user_data.dump())
169+
170+ def download(self, force=False):
171+ raise NotImplementedError(self.download)
172+
173+ def parse_console_during_install(self, cmd):
174+ """Parse the console output until the end of the install.
175+
176+ We added a specific part for cloud-init to ensure we properly detect
177+ the end of the run.
178+
179+ :param cmd: The install command (used for error display).
180+ """
181+ console = FileMonitor(self._console_path)
182+ try:
183+ for line in console.parse():
184+# FIXME: We need some way to activate this dynamically (conf var defaulting to
185+# env var OR cmdline parameter ? -- vila 2013-02-11
186+# print "read: [%s]" % (line,) # so useful for debug...
187+ pass
188+ except (ConsoleEOFError, CloudInitError):
189+ # FIXME: No test covers this path -- vila 2013-02-15
190+ err_lines = ['Suspicious line from cloud-init.\n',
191+ '\t' + console.lines[-1],
192+ 'Check the configuration:\n']
193+ with open(self._meta_data_path) as f:
194+ err_lines.append('meta-data content:\n')
195+ err_lines.extend(f.readlines())
196+ with open(self._user_data_path) as f:
197+ err_lines.append('user-data content:\n')
198+ err_lines.extend(f.readlines())
199+ raise CommandError(cmd, console.proc.returncode,
200+ '\n'.join(console.lines),
201+ ''.join(err_lines))
202+
203+
204+def kvm_states(source=None):
205+ """A dict of states for kvms indexed by name.
206+
207+ :param source: A list of lines as produced by virsh list --all without
208+ decorations (header/footer).
209+ """
210+ if source is None:
211+ retcode, out, err = run_subprocess(['virsh', 'list', '--all'])
212+ # Get rid of header/footer
213+ source = out.splitlines()[2:-1]
214+ states = {}
215+ for line in source:
216+ caret_or_id, name, state = line.split(None, 2)
217+ states[name] = state
218+ return states
219+
220
221 class Kvm(VM):
222
223 def __init__(self, conf):
224 super(Kvm, self).__init__(conf)
225- # Seed files
226- self._meta_data_path = None
227- self._user_data_path = None
228 # Disk paths
229+ self._disk_image_path = None
230 self._seed_path = None
231- self._disk_image_path = None
232
233 self._console_path = None
234
235- def _download_in_cache(self, source_url, name, force=False):
236- """Download ``name`` from ``source_url`` in ``vm.download_cache``.
237-
238- :param source_url: The url where the file to download is located
239-
240- :param name: The name of the file to download (also used as the name
241- for the downloaded file).
242-
243- :param force: Remove the file from the cache if present.
244-
245- :return: False if the file is in the download cache, True if a download
246- occurred.
247- """
248- source = urlutils.join(source_url, name)
249- download_dir = self.conf.get('vm.download_cache')
250- if not os.path.exists(download_dir):
251- raise ConfigValueError('vm.download_cache', download_dir)
252- target = os.path.join(download_dir, name)
253- # FIXME: By default the download dir may be under root control, but if
254- # a user chose to use a different one under his own control, it would
255- # be nice to not require sudo usage. -- vila 2013-02-06
256- if force:
257- run_subprocess(['sudo', 'rm', '-f', target])
258- if not os.path.exists(target):
259- # FIXME: We do ask for a progress bar but it's not displayed
260- # (run_subprocess capture both stdout and stderr) ! At least while
261- # used interactively, it should. -- vila 2013-02-06
262- run_subprocess(['sudo', 'wget', '--progress=dot:mega', '-O',
263- target, source])
264- return True
265- else:
266- return False
267+ def state(self):
268+ states = kvm_states()
269+ try:
270+ state = states[self.conf.get('vm.name')]
271+ except KeyError:
272+ state = None
273+ return state
274
275 def download_iso(self, force=False):
276 """Download the iso to install the vm.
277@@ -860,21 +963,10 @@
278 self.conf.get('vm.cloud_image_name'),
279 force=force)
280
281- def create_meta_data(self):
282- self.ensure_config_dir()
283- self._meta_data_path = os.path.join(self._config_dir, 'meta-data')
284- with open(self._meta_data_path, 'w') as f:
285- f.write(self.conf.get('vm.meta_data'))
286-
287- def create_user_data(self):
288- ci_user_data = CIUserData(self.conf)
289- ci_user_data.populate()
290- self.ensure_config_dir()
291- self._user_data_path = os.path.join(self._config_dir, 'user-data')
292- with open(self._user_data_path, 'w') as f:
293- f.write(ci_user_data.dump())
294-
295- def create_seed(self):
296+ def download(self, force=False):
297+ return self.download_cloud_image(force)
298+
299+ def create_seed_image(self):
300 if self._meta_data_path is None:
301 self.create_meta_data()
302 if self._user_data_path is None:
303@@ -886,7 +978,9 @@
304 # We create the seed in the ``vm.images_dir`` directory, so
305 # ``sudo`` is required
306 ['sudo',
307- 'genisoimage', '-output', seed_path, '-volid', 'cidata',
308+ 'genisoimage', '-output', seed_path,
309+ # cloud-init relies on the volid to discover its data
310+ '-volid', 'cidata',
311 '-joliet', '-rock', '-input-charset', 'default',
312 '-graft-points',
313 'user-data=%s' % (self._user_data_path,),
314@@ -895,35 +989,51 @@
315 self._seed_path = seed_path
316
317 def create_disk_image(self):
318- raise NotImplementedError(self.create_disk_image)
319-
320- def _wait_for_install_with_seed(self):
321+ if self.conf.get('vm.backing') is None:
322+ self.create_disk_image_from_cloud_image()
323+ else:
324+ self.create_disk_image_from_backing()
325+
326+ def create_disk_image_from_cloud_image(self):
327+ """Create a disk image from a cloud one."""
328+ cloud_image_path = os.path.join(
329+ self.conf.get('vm.download_cache'),
330+ self.conf.get('vm.cloud_image_name'))
331+ disk_image_path = os.path.join(
332+ self.conf.get('vm.images_dir'),
333+ self.conf.expand_options('{vm.name}.qcow2'))
334+ run_subprocess(
335+ ['sudo', 'qemu-img', 'convert',
336+ '-O', 'qcow2', cloud_image_path, disk_image_path])
337+ run_subprocess(
338+ ['sudo', 'qemu-img', 'resize',
339+ disk_image_path, self.conf.get('vm.disk_size')])
340+ self._disk_image_path = disk_image_path
341+
342+ def create_disk_image_from_backing(self):
343+ """Create a disk image backed by an existing one."""
344+ backing_image_path = os.path.join(
345+ self.conf.get('vm.images_dir'),
346+ self.conf.expand_options('{vm.backing}'))
347+ disk_image_path = os.path.join(
348+ self.conf.get('vm.images_dir'),
349+ self.conf.expand_options('{vm.name}.qcow2'))
350+ run_subprocess(
351+ ['sudo', 'qemu-img', 'create', '-f', 'qcow2',
352+ '-b', backing_image_path, disk_image_path])
353+ run_subprocess(
354+ ['sudo', 'qemu-img', 'resize',
355+ disk_image_path, self.conf.get('vm.disk_size')])
356+ self._disk_image_path = disk_image_path
357+
358+ def parse_console_during_install(self, cmd):
359+ """See Vm.parse_console_during_install."""
360 # The console is created by virt-install which requires sudo but
361 # creates the file 0600 for libvirt-qemu. We give read access to all
362 # otherwise 'tail -f' requires sudo and can't be killed anymore.
363 run_subprocess(['sudo', 'chmod', '0644', self._console_path])
364 # While `virt-install` is running, let's connect to the console
365- console = FileMonitor(self._console_path)
366- try:
367- for line in console.parse():
368-# FIXME: We need some way to activate this dynamically (conf var defaulting to
369-# env var OR cmdline parameter ? -- vila 2013-02-11
370-# print "read: [%s]" % (line,) # so useful for debug...
371- pass
372- except (ConsoleEOFError, CloudInitError):
373- # FIXME: No test covers this path -- vila 2013-02-15
374- err_lines = ['Suspicious line from cloud-init.\n',
375- '\t' + console.lines[-1],
376- 'Check the configuration:\n']
377- with open(self._meta_data_path) as f:
378- err_lines.append('meta-data content:\n')
379- err_lines.extend(f.readlines())
380- with open(self._user_data_path) as f:
381- err_lines.append('user-data content:\n')
382- err_lines.extend(f.readlines())
383- raise CommandError(console.cmd, console.proc.returncode,
384- '\n'.join(console.lines),
385- ''.join(err_lines))
386+ super(Kvm, self).parse_console_during_install(cmd)
387
388 def install(self):
389 # Create a kvm, relying on cloud-init to customize the base image.
390@@ -942,7 +1052,7 @@
391 # a warning and terminate console and self.install_proc.
392 # -- vila 2013-02-07
393 if self._seed_path is None:
394- self.create_seed()
395+ self.create_seed_image()
396 if self._disk_image_path is None:
397 self.create_disk_image()
398 # FIXME: Install time is probably a good time to delete the
399@@ -952,30 +1062,31 @@
400 self._console_path = os.path.join(
401 self.conf.get('vm.images_dir'),
402 '%s.console' % (self.conf.get('vm.name'),))
403- run_subprocess(
404- ['sudo', 'virt-install',
405- # To ensure we're not bitten again by http://pad.lv/1157272 where
406- # virt-install wrongly detect virtualbox. -- vila 2013-03-20
407- '--connect', 'qemu:///system',
408- # Without --noautoconsole, virt-install will relay the console,
409- # that's not appropriate for our needs so we'll connect later
410- # ourselves
411- '--noautoconsole',
412- # We define the console as a file so we can monitor the install
413- # via 'tail -f'
414- '--serial', 'file,path=%s' % (self._console_path,),
415- '--network', self.conf.get('vm.network'),
416- # Anticipate that we'll need a graphic card defined
417- '--graphics', 'spice',
418- '--name', self.conf.get('vm.name'),
419- '--ram', self.conf.get('vm.ram_size'),
420- '--vcpus', self.conf.get('vm.cpus'),
421- '--disk', 'path=%s,format=qcow2' % (self._disk_image_path,),
422- '--disk', 'path=%s' % (self._seed_path,),
423- # We just boot, cloud-init will handle the installs we need
424- '--boot', 'hd', '--hvm',
425- ])
426- self._wait_for_install_with_seed()
427+ virt_install = [
428+ 'sudo', 'virt-install',
429+ # To ensure we're not bitten again by http://pad.lv/1157272 where
430+ # virt-install wrongly detect virtualbox. -- vila 2013-03-20
431+ '--connect', 'qemu:///system',
432+ # Without --noautoconsole, virt-install will relay the console,
433+ # that's not appropriate for our needs so we'll connect later
434+ # ourselves
435+ '--noautoconsole',
436+ # We define the console as a file so we can monitor the install
437+ # via 'tail -f'
438+ '--serial', 'file,path=%s' % (self._console_path,),
439+ '--network', self.conf.get('vm.network'),
440+ # Anticipate that we'll need a graphic card defined
441+ '--graphics', 'spice',
442+ '--name', self.conf.get('vm.name'),
443+ '--ram', self.conf.get('vm.ram_size'),
444+ '--vcpus', self.conf.get('vm.cpus'),
445+ '--disk', 'path=%s,format=qcow2' % (self._disk_image_path,),
446+ '--disk', 'path=%s' % (self._seed_path,),
447+ # We just boot, cloud-init will handle the installs we need
448+ '--boot', 'hd', '--hvm',
449+ ]
450+ run_subprocess(virt_install)
451+ self.parse_console_during_install(virt_install)
452 # We've seen the console signaling halt, but the vm will need a bit
453 # more time to get there so we help it a bit.
454 if self.conf.get('vm.release') in ('precise', 'quantal'):
455@@ -984,7 +1095,7 @@
456 self.poweroff()
457 vm_name = self.conf.get('vm.name')
458 while True:
459- state = vm_states()[vm_name]
460+ state = self.state()
461 # We expect the vm's state to be 'in shutdown' but in some rare
462 # occasions we may catch 'running' before getting 'in shutdown'.
463 if state in ('in shutdown', 'running'):
464@@ -1016,44 +1127,135 @@
465 '--remove-all-storage'])
466
467
468-class KvmFromCloudImage(Kvm):
469-
470- def create_disk_image(self, src_name=None, dst_name=None):
471- """Create a disk image from a cloud one."""
472- if src_name is None:
473- src_name = self.conf.get('vm.cloud_image_name')
474- if dst_name is None:
475- dst_name = self.conf.expand_options('{vm.name}.qcow2')
476- cloud_image_path = os.path.join(
477- self.conf.get('vm.download_cache'), src_name)
478- disk_image_path = os.path.join(
479- self.conf.get('vm.images_dir'), dst_name)
480- run_subprocess(
481- ['sudo', 'qemu-img', 'convert',
482- '-O', 'qcow2', cloud_image_path, disk_image_path])
483- run_subprocess(
484- ['sudo', 'qemu-img', 'resize',
485- disk_image_path, self.conf.get('vm.disk_size')])
486- self._disk_image_path = disk_image_path
487-
488-
489-class KvmFromBacking(Kvm):
490-
491- def create_disk_image(self, src_name=None, dst_name=None):
492- """Create a disk image backed by an existing one."""
493- backing_image_path = os.path.join(
494- self.conf.get('vm.images_dir'),
495- self.conf.expand_options('{vm.backing}'))
496- disk_image_path = os.path.join(
497- self.conf.get('vm.images_dir'),
498- self.conf.expand_options('{vm.name}.qcow2'))
499- run_subprocess(
500- ['sudo', 'qemu-img', 'create', '-f', 'qcow2',
501- '-b', backing_image_path, disk_image_path])
502- run_subprocess(
503- ['sudo', 'qemu-img', 'resize',
504- disk_image_path, self.conf.get('vm.disk_size')])
505- self._disk_image_path = disk_image_path
506+vm_class_registry.register('kvm', Kvm, 'Kernel-based virtual machine')
507+
508+
509+def lxc_info(vm_name, source=None):
510+ """Parse state info from the lxc-info output.
511+
512+ :param vm_name: The vm we want to query about.
513+
514+ :param source: A list of lines as produced by virsh list --all without
515+ decorations (header/footer).
516+ """
517+ if source is None:
518+ retcode, out, err = run_subprocess(['sudo', 'lxc-info', '-n', vm_name])
519+ source = out.splitlines()
520+ state_line, pid_line = source
521+ _, state = state_line.split(None, 1)
522+ _, pid = pid_line.split(None, 1)
523+ return dict(state=state, pid=pid)
524+
525+
526+class Lxc(VM):
527+
528+ def __init__(self, conf):
529+ super(Lxc, self).__init__(conf)
530+ self._guest_seed_path = None
531+ self._fstab_path = None
532+
533+ def state(self):
534+ info = lxc_info(self.conf.get('vm.name'))
535+ return info['state']
536+
537+ def download(self, force=False):
538+ # FIXME: lxc-create provides its own cache. download(True) should just
539+ # ensure we clear that cache from the previous download. Should we add
540+ # a warning ? Specialize the cache for Kvm only ?-- vila 2013-08-07
541+ return True
542+
543+ def create_seed_files(self):
544+ if self._meta_data_path is None:
545+ self.create_meta_data()
546+ if self._user_data_path is None:
547+ self.create_user_data()
548+ self._fstab_path = os.path.join(self._config_dir, 'fstab')
549+ self._guest_seed_path = os.path.join(
550+ self.conf.get('vm.lxcs_dir'),
551+ self.conf.get('vm.name'),
552+ 'rootfs/var/lib/cloud/seed/nocloud-net')
553+ with open(self._fstab_path, 'w') as f:
554+ # Add a entry so cloud-init find the seed files
555+ f.write('%s %s none bind 0 0\n' % (self._config_dir,
556+ self._guest_seed_path))
557+
558+ def install(self):
559+ '''Create an lxc, relying on cloud-init to customize the base image.
560+
561+ There are two processes involvded here:
562+ - lxc-create creates the vm.
563+ - progress is monitored via the console to detect cloud-final.
564+
565+ Once cloud-init has finished, the vm can be powered off.
566+ '''
567+ # FIXME: If the install doesn't finish after $time, emit a warning and
568+ # terminate self.install_proc.
569+ # FIXME: If we can't connect to the console, emit a warning and
570+ # terminate console and self.install_proc.
571+ # FIXME: If we don't receive anything on the console after $time2, emit
572+ # a warning and terminate console and self.install_proc.
573+ # -- vila 2013-02-07
574+ if self._fstab_path is None:
575+ self.create_seed_files()
576+ # FIXME: Install time is probably a good time to delete the
577+ # console. While it makes sense to accumulate for all runs for a given
578+ # install, keeping them without any limit nor roration is likely to
579+ # cause issues at some point... -- vila 2013-02-20
580+ self._console_path = os.path.join(
581+ # FIXME: We use _config_dir instead of 'vm.images_dir' as kvm does
582+ # because the later is owned by root so we can't create a file
583+ # there. It would be nice to check if the same trick can be used
584+ # for kvm to simplify. -- vila 2013-08-07
585+ self._config_dir,
586+ '%s.console' % (self.conf.get('vm.name'),))
587+ # Create/empty the file so we get access to it (otherwise it will be
588+ # owned by root).
589+ open(self._console_path, 'w').close()
590+ # FIXME: Some feedback would be nice during lxc creation, not sure
591+ # about which errors to expect there either -- vila 2013-08-07
592+ run_subprocess(
593+ ['sudo', 'lxc-create',
594+ '-n', self.conf.get('vm.name'),
595+ '-t', 'ubuntu-cloud',
596+ '--',
597+ '-r', self.conf.get('vm.release'),
598+ '-a', self.conf.get('vm.cpu_model'),
599+ '-C', # From cloud image, implying download/cache
600+ ])
601+ # Now we add the cloud-init data seed and do lxc-start to trigger all
602+ # our customizations monitoring the lxc-start output from the host.
603+ mkdir_seed_path = 'mkdir -p %s' % (self._guest_seed_path,)
604+ lxc_start = ['sudo', 'lxc-start',
605+ '-n', self.conf.get('vm.name'),
606+ '--define', 'lxc.hook.pre-start=%s' % (mkdir_seed_path,),
607+ '--define', 'lxc.mount=%s' % (self._fstab_path,),
608+ '--console-log', self._console_path,
609+ # Daemonize or: 1) it fails with a spurious return code,
610+ # 2) We can't monitor the logfile
611+ '-d',
612+ ]
613+ run_subprocess(lxc_start)
614+ self.parse_console_during_install(lxc_start)
615+
616+ def poweroff(self):
617+ return run_subprocess(
618+ ['sudo', 'lxc-stop', '-n', self.conf.get('vm.name')])
619+
620+ def undefine(self):
621+ try:
622+ return run_subprocess(
623+ ['sudo', 'lxc-destroy', '-n', self.conf.get('vm.name')])
624+ except CommandError as e:
625+ # FIXME: No test -- vila 2013-08-08
626+ if e.err.endswith('does not exist\n'):
627+ # Fine. lxc-info makes no distinction between a stopped vm and
628+ # a non-existing one.
629+ pass
630+ else:
631+ raise
632+
633+
634+vm_class_registry.register('lxc', Lxc, 'Linux container virtual machine')
635
636
637 class ArgParser(argparse.ArgumentParser):
638@@ -1108,10 +1310,7 @@
639 class Download(Command):
640
641 def run(self):
642- # FIXME: what needs to be downloaded should depend on the type of the
643- # vm (possibly errors if there is nothing to download). -- vila
644- # 2013-02-06
645- self.vm.download_cloud_image(force=True)
646+ self.vm.download(force=True)
647
648
649 class SshKeyGen(Command):
650@@ -1124,13 +1323,11 @@
651
652 def run(self):
653 vm_name = self.vm.conf.get('vm.name')
654- state = vm_states().get(vm_name, None)
655- if state == 'shut off':
656+ state = self.vm.state()
657+ if state in('shut off', 'STOPPED'):
658 self.vm.undefine()
659- elif state == 'running':
660+ elif state in ('running', 'RUNNING'):
661 raise SetupVmError('{name} is running', name=vm_name)
662- # FIXME: The installation method may vary depending on the vm type.
663- # -- vila 2013-02-06
664 self.vm.install()
665
666
667@@ -1142,11 +1339,7 @@
668 ns = arg_parser.parse_args(args, out=out, err=err)
669
670 conf = VmStack(ns.name)
671- with_backing = conf.get('vm.backing')
672- if with_backing is None:
673- vm = KvmFromCloudImage(conf)
674- else:
675- vm = KvmFromBacking(conf)
676+ vm = conf.get('vm.class')(conf)
677 if ns.download:
678 cmds.append(Download(vm))
679 if ns.ssh_keygen:
680
681=== modified file 'setup_vm/tests/test_setup_vm.py'
682--- setup_vm/tests/test_setup_vm.py 2013-08-08 10:10:01 +0000
683+++ setup_vm/tests/test_setup_vm.py 2013-08-09 09:36:11 +0000
684@@ -5,13 +5,13 @@
685 import testtools
686
687 from setup_vm import tests
688-from setup_vm.bin import setup_vm as sm
689+from setup_vm.bin import setup_vm as svm
690
691
692 def requires_known_reference_image(test):
693 # We need a pre-seeded download cache from the user running the tests
694 # as downloading the cloud image is too long.
695- user_conf = sm.VmStack(None)
696+ user_conf = svm.VmStack(None)
697 download_cache = user_conf.get('vm.download_cache')
698 if download_cache is None:
699 test.skip('vm.download_cache is not set')
700@@ -42,15 +42,15 @@
701 # Also isolate from the system environment
702 self.etc_dir = os.path.join(self.test_base_dir, 'etc')
703 os.mkdir(self.etc_dir)
704- self.patch(sm, 'system_config_dir', lambda: self.etc_dir)
705+ self.patch(svm, 'system_config_dir', lambda: self.etc_dir)
706
707
708 class TestVmMatcher(TestCaseWithHome):
709
710 def setUp(self):
711 super(TestVmMatcher, self).setUp()
712- self.store = sm.VmStore('.', 'foo.conf')
713- self.matcher = sm.VmMatcher(self.store, 'test')
714+ self.store = svm.VmStore('.', 'foo.conf')
715+ self.matcher = svm.VmMatcher(self.store, 'test')
716
717 def test_empty_section_always_matches(self):
718 self.store._load_from_string('foo=bar')
719@@ -75,7 +75,7 @@
720
721 def setUp(self):
722 super(TestVmStores, self).setUp()
723- self.conf = sm.VmStack('foo')
724+ self.conf = svm.VmStack('foo')
725
726 def test_default_in_empty_stack(self):
727 self.assertEqual('1024', self.conf.get('vm.ram_size'))
728@@ -97,10 +97,11 @@
729
730
731 class TestVmStack(TestCaseWithHome):
732+ """Test config option values."""
733
734 def setUp(self):
735 super(TestVmStack, self).setUp()
736- self.conf = sm.VmStack('foo')
737+ self.conf = svm.VmStack('foo')
738 self.conf.store._load_from_string('''
739 vm.release=raring
740 vm.cpu_model=amd64
741@@ -145,7 +146,7 @@
742 class TestPathOption(TestCaseWithHome):
743
744 def assertConverted(self, expected, value):
745- option = sm.PathOption('foo', help='A path.')
746+ option = svm.PathOption('foo', help='A path.')
747 self.assertEquals(expected, option.convert_from_unicode(None, value))
748
749 def test_absolute_path(self):
750@@ -161,8 +162,11 @@
751
752 class TestDownload(TestCaseWithHome):
753
754-# FIXME: Needs parametrization against vm.{cloud_image_name|iso_name} and
755-# {download_iso|download_cloud_image} -- vila 2013-02-07
756+# FIXME: Needs parametrization against
757+# vm.{cloud_image_name|cloud_tarball_name|iso_name} and
758+# {download_iso|download_cloud_image|download_cloud_tarball} {Lxc|Kvm}... May
759+# be we just need to test _download_in_cache() now that it's implemented at Vm
760+# level and be done -- vila 2013-08-06
761
762 def setUp(self):
763 tests.requires_feature(self, tests.sudo_feature)
764@@ -171,60 +175,61 @@
765 super(TestDownload, self).setUp()
766 download_cache = os.path.join(self.test_base_dir, 'downloads')
767 os.mkdir(download_cache)
768- self.conf = sm.VmStack('foo')
769+ self.conf = svm.VmStack('foo')
770 self.conf.store._load_from_string('''
771 vm.iso_name=MD5SUMS
772 vm.cloud_image_name=MD5SUMS
773+vm.cloud_tarball_name=MD5SUMS
774 vm.release=raring
775 vm.cpu_model=amd64
776 vm.download_cache=%s
777 ''' % (download_cache,))
778
779 def test_download_iso(self):
780- vm = sm.Kvm(self.conf)
781+ vm = svm.Kvm(self.conf)
782 self.assertTrue(vm.download_iso())
783 # Trying to download again will find the file in the cache
784 self.assertFalse(vm.download_iso())
785 # Forcing the download even when the file is present
786 self.assertTrue(vm.download_iso(force=True))
787
788+ def test_download_unknown_iso_fail(self):
789+ self.conf.set('vm.iso_name', 'I-dont-exist')
790+ vm = svm.Kvm(self.conf)
791+ self.assertRaises(svm.CommandError, vm.download_iso)
792+
793+ def test_download_iso_with_unknown_cache_fail(self):
794+ dl_cache = os.path.join(self.test_base_dir, 'I-dont-exist')
795+ self.conf.set('vm.download_cache', dl_cache)
796+ vm = svm.Kvm(self.conf)
797+ self.assertRaises(svm.ConfigValueError, vm.download_iso)
798+
799 def test_download_cloud_image(self):
800- vm = sm.Kvm(self.conf)
801+ vm = svm.Kvm(self.conf)
802 self.assertTrue(vm.download_cloud_image())
803 # Trying to download again will find the file in the cache
804 self.assertFalse(vm.download_cloud_image())
805 # Forcing the download even when the file is present
806 self.assertTrue(vm.download_cloud_image(force=True))
807
808- def test_download_unknown_iso_fail(self):
809- self.conf.set('vm.iso_name', 'I-dont-exist')
810- vm = sm.Kvm(self.conf)
811- self.assertRaises(sm.CommandError, vm.download_iso)
812-
813 def test_download_unknown_cloud_image_fail(self):
814 self.conf.set('vm.cloud_image_name', 'I-dont-exist')
815- vm = sm.Kvm(self.conf)
816- self.assertRaises(sm.CommandError, vm.download_cloud_image)
817-
818- def test_download_iso_with_unknown_cache_fail(self):
819- dl_cache = os.path.join(self.test_base_dir, 'I-dont-exist')
820- self.conf.set('vm.download_cache', dl_cache)
821- vm = sm.Kvm(self.conf)
822- self.assertRaises(sm.ConfigValueError, vm.download_iso)
823+ vm = svm.Kvm(self.conf)
824+ self.assertRaises(svm.CommandError, vm.download_cloud_image)
825
826 def test_download_cloud_image_with_unknown_cache_fail(self):
827 dl_cache = os.path.join(self.test_base_dir, 'I-dont-exist')
828 self.conf.set('vm.download_cache', dl_cache)
829- vm = sm.Kvm(self.conf)
830- self.assertRaises(sm.ConfigValueError, vm.download_cloud_image)
831+ vm = svm.Kvm(self.conf)
832+ self.assertRaises(svm.ConfigValueError, vm.download_cloud_image)
833
834
835 class TestMetaData(TestCaseWithHome):
836
837 def setUp(self):
838 super(TestMetaData, self).setUp()
839- self.conf = sm.VmStack('foo')
840- self.vm = sm.Kvm(self.conf)
841+ self.conf = svm.VmStack('foo')
842+ self.vm = svm.Kvm(self.conf)
843 images_dir = os.path.join(self.test_base_dir, 'images')
844 os.mkdir(images_dir)
845 config_dir = os.path.join(self.test_base_dir, 'config')
846@@ -248,10 +253,10 @@
847 class TestYaml(testtools.TestCase):
848
849 def yaml_load(self, *args, **kwargs):
850- return sm.yaml.safe_load(*args, **kwargs)
851+ return svm.yaml.safe_load(*args, **kwargs)
852
853 def yaml_dump(self, *args, **kwargs):
854- return sm.yaml.safe_dump(*args, **kwargs)
855+ return svm.yaml.safe_dump(*args, **kwargs)
856
857 def test_load_scalar(self):
858 self.assertEqual(
859@@ -294,13 +299,13 @@
860
861 def setUp(self):
862 super(TestLaunchpadAccess, self).setUp()
863- self.conf = sm.VmStack('foo')
864- self.vm = sm.Kvm(self.conf)
865- self.ci_data = sm.CIUserData(self.conf)
866+ self.conf = svm.VmStack('foo')
867+ self.vm = svm.Kvm(self.conf)
868+ self.ci_data = svm.CIUserData(self.conf)
869
870 def test_cant_find_private_key(self):
871 self.conf.store._load_from_string('vm.launchpad_id = I-dont-exist')
872- e = self.assertRaises(sm.ConfigPathNotFound,
873+ e = self.assertRaises(svm.ConfigPathNotFound,
874 self.ci_data.set_launchpad_access)
875 key_path = '~/.ssh/I-dont-exist@setup_vm'
876 self.assertEqual(key_path, e.path)
877@@ -340,8 +345,8 @@
878
879 def setUp(self):
880 super(TestCIUserData, self).setUp()
881- self.conf = sm.VmStack('foo')
882- self.ci_data = sm.CIUserData(self.conf)
883+ self.conf = svm.VmStack('foo')
884+ self.ci_data = svm.CIUserData(self.conf)
885
886 def test_empty_config(self):
887 self.ci_data.populate()
888@@ -445,11 +450,11 @@
889
890 def test_bad_type_ssh_keys(self):
891 self.conf.store._load_from_string('vm.ssh_keys = I-dont-exist')
892- self.assertRaises(sm.ConfigValueError, self.ci_data.populate)
893+ self.assertRaises(svm.ConfigValueError, self.ci_data.populate)
894
895 def test_unknown_ssh_keys(self):
896 self.conf.store._load_from_string('vm.ssh_keys = rsa.pub')
897- self.assertRaises(sm.ConfigPathNotFound, self.ci_data.populate)
898+ self.assertRaises(svm.ConfigPathNotFound, self.ci_data.populate)
899
900 def test_good_ssh_authorized_keys(self):
901 paths = ('home.pub', 'work.pub')
902@@ -464,15 +469,15 @@
903
904 def test_unknown_ssh_authorized_keys(self):
905 self.conf.store._load_from_string('vm.ssh_authorized_keys = rsa.pub')
906- self.assertRaises(sm.ConfigPathNotFound, self.ci_data.populate)
907+ self.assertRaises(svm.ConfigPathNotFound, self.ci_data.populate)
908
909 def test_unknown_root_script(self):
910 self.conf.store._load_from_string('vm.root_script = I-dont-exist')
911- self.assertRaises(sm.ConfigPathNotFound, self.ci_data.populate)
912+ self.assertRaises(svm.ConfigPathNotFound, self.ci_data.populate)
913
914 def test_unknown_ubuntu_script(self):
915 self.conf.store._load_from_string('vm.ubuntu_script = I-dont-exist')
916- self.assertRaises(sm.ConfigPathNotFound, self.ci_data.populate)
917+ self.assertRaises(svm.ConfigPathNotFound, self.ci_data.populate)
918
919 def test_expansion_error_in_script(self):
920 root_script_content = '''#!/bin/sh
921@@ -490,7 +495,7 @@
922 def test_unknown_uploaded_scripts(self):
923 self.conf.store._load_from_string(
924 '''vm.uploaded_scripts = I-dont-exist ''')
925- self.assertRaises(sm.ConfigPathNotFound,
926+ self.assertRaises(svm.ConfigPathNotFound,
927 self.ci_data.populate)
928
929 def test_root_script(self):
930@@ -584,8 +589,8 @@
931
932 def setUp(self):
933 super(TestCreateUserData, self).setUp()
934- self.conf = sm.VmStack('foo')
935- self.vm = sm.Kvm(self.conf)
936+ self.conf = svm.VmStack('foo')
937+ self.vm = svm.Kvm(self.conf)
938
939 def test_empty_config(self):
940 config_dir = os.path.join(self.test_base_dir, 'config')
941@@ -604,13 +609,12 @@
942 self.assertEqual("- {content: '#cloud-config\n", user_data[1])
943
944
945-class TestSeed(TestCaseWithHome):
946+class TestSeedData(TestCaseWithHome):
947
948 def setUp(self):
949- tests.requires_feature(self, tests.sudo_feature)
950- super(TestSeed, self).setUp()
951- self.conf = sm.VmStack('foo')
952- self.vm = sm.Kvm(self.conf)
953+ super(TestSeedData, self).setUp()
954+ self.conf = svm.VmStack('foo')
955+ self.vm = svm.VM(self.conf)
956 images_dir = os.path.join(self.test_base_dir, 'images')
957 os.mkdir(images_dir)
958 config_dir = os.path.join(self.test_base_dir, 'config')
959@@ -629,9 +633,28 @@
960 self.vm.create_user_data()
961 self.assertTrue(os.path.exists(self.vm._user_data_path))
962
963- def test_create_seed(self):
964+
965+class TestSeedImage(TestCaseWithHome):
966+
967+ def setUp(self):
968+ tests.requires_feature(self, tests.sudo_feature)
969+ tests.requires_feature(self, tests.qemu_img_feature)
970+ super(TestSeedImage, self).setUp()
971+ self.conf = svm.VmStack('foo')
972+ self.vm = svm.Kvm(self.conf)
973+ images_dir = os.path.join(self.test_base_dir, 'images')
974+ os.mkdir(images_dir)
975+ config_dir = os.path.join(self.test_base_dir, 'config')
976+ self.conf.store._load_from_string('''
977+vm.name=foo
978+vm.release=raring
979+vm.config_dir=%s
980+vm.images_dir=%s
981+''' % (config_dir, images_dir,))
982+
983+ def test_create_seed_image(self):
984 self.assertTrue(self.vm._seed_path is None)
985- self.vm.create_seed()
986+ self.vm.create_seed_image()
987 self.assertFalse(self.vm._seed_path is None)
988 self.assertTrue(os.path.exists(self.vm._seed_path))
989
990@@ -642,8 +665,8 @@
991 tests.requires_feature(self, tests.sudo_feature)
992 tests.requires_feature(self, tests.qemu_img_feature)
993 super(TestImageFromCloud, self).setUp()
994- self.conf = sm.VmStack('foo')
995- self.vm = sm.KvmFromCloudImage(self.conf)
996+ self.conf = svm.VmStack('foo')
997+ self.vm = svm.Kvm(self.conf)
998 images_dir = os.path.join(self.test_base_dir, 'images')
999 os.mkdir(images_dir)
1000 download_cache_dir = os.path.join(self.test_base_dir, 'download')
1001@@ -661,7 +684,7 @@
1002 cloud_image_path = os.path.join(self.conf.get('vm.download_cache'),
1003 self.conf.get('vm.cloud_image_name'))
1004 # We need a fake cloud image that can be converted
1005- sm.run_subprocess(
1006+ svm.run_subprocess(
1007 ['sudo', 'qemu-img', 'create',
1008 cloud_image_path, self.conf.get('vm.disk_size')])
1009 self.assertTrue(self.vm._disk_image_path is None)
1010@@ -681,7 +704,7 @@
1011 images_dir = os.path.join(self.test_base_dir, 'images')
1012 os.mkdir(images_dir)
1013 # Create a shared config
1014- conf = sm.VmStack(None)
1015+ conf = svm.VmStack(None)
1016 conf.store._load_from_string('''
1017 vm.release=raring
1018 vm.images_dir=%s
1019@@ -698,22 +721,21 @@
1020 # To bypass creating a real vm, we start from the cloud image that is a
1021 # real and bootable one, so we just convert it. That also makes it
1022 # available in vm.images_dir
1023- temp_vm = sm.KvmFromCloudImage(
1024- sm.VmStack('selftest-from-cloud'))
1025+ temp_vm = svm.Kvm(svm.VmStack('selftest-from-cloud'))
1026 temp_vm.create_disk_image()
1027
1028 def test_create_image_with_backing(self):
1029- vm = sm.KvmFromBacking(sm.VmStack('selftest-backing'))
1030+ vm = svm.Kvm(svm.VmStack('selftest-backing'))
1031 self.assertTrue(vm._disk_image_path is None)
1032 vm.create_disk_image()
1033 self.assertFalse(vm._disk_image_path is None)
1034 self.assertTrue(os.path.exists(vm._disk_image_path))
1035
1036
1037-class TestVmStates(testtools.TestCase):
1038+class TestKvmStates(testtools.TestCase):
1039
1040 def assertStates(self, expected, lines):
1041- self.assertEqual(expected, sm.vm_states(lines))
1042+ self.assertEqual(expected, svm.kvm_states(lines))
1043
1044 def test_empty(self):
1045 self.assertStates({}, [])
1046@@ -728,34 +750,59 @@
1047 '19 bar running'])
1048
1049
1050+class TestLxcInfo(testtools.TestCase):
1051+
1052+ def assertInfo(self, expected, lines):
1053+ self.assertEqual(expected, svm.lxc_info('foo', lines))
1054+
1055+ def test_empty(self):
1056+ self.assertRaises(ValueError,
1057+ self.assertInfo, dict(state='STOPPED', pid=-1), [])
1058+
1059+ def test_garbage(self):
1060+ self.assertRaises(ValueError, self.assertInfo, None, [''])
1061+
1062+ def test_stopped(self):
1063+ # From a real life sample
1064+ self.assertInfo({'state': 'STOPPED', 'pid': '-1'},
1065+ ['state: STOPPED',
1066+ 'pid: -1'])
1067+
1068+ def test_running(self):
1069+ # From a real life sample
1070+ self.assertInfo({'state': 'RUNNING', 'pid': '30937'},
1071+ ['state: RUNNING',
1072+ 'pid: 30937'])
1073+
1074+
1075 class TestConsoleParsing(testtools.TestCase):
1076
1077 def _parse_console_monitor(self, string):
1078- mon = sm.ConsoleMonitor(StringIO(string))
1079+ mon = svm.ConsoleMonitor(StringIO(string))
1080 lines = []
1081 for line in mon.parse():
1082 lines.append(line)
1083 return lines
1084
1085 def test_fails_on_empty(self):
1086- self.assertRaises(sm.ConsoleEOFError,
1087+ self.assertRaises(svm.ConsoleEOFError,
1088 self._parse_console_monitor, '')
1089
1090 def test_fail_on_knwon_cloud_init_errors(self):
1091 self.assertRaises(
1092- sm.CloudInitError,
1093+ svm.CloudInitError,
1094 self._parse_console_monitor,
1095 'Failed loading yaml blob\n')
1096 self.assertRaises(
1097- sm.CloudInitError,
1098+ svm.CloudInitError,
1099 self._parse_console_monitor,
1100 'Unhandled non-multipart userdata starting\n')
1101 self.assertRaises(
1102- sm.CloudInitError,
1103+ svm.CloudInitError,
1104 self._parse_console_monitor,
1105 "failed to render string to stdout: cannot find 'uptime'\n")
1106 self.assertRaises(
1107- sm.CloudInitError,
1108+ svm.CloudInitError,
1109 self._parse_console_monitor,
1110 "Failed loading of cloud config "
1111 "'/var/lib/cloud/instance/cloud-config.txt'. "
1112@@ -781,7 +828,7 @@
1113 def _parse_file_monitor(self, string):
1114 with open('console', 'w') as f:
1115 f.write(string)
1116- mon = sm.FileMonitor('console')
1117+ mon = svm.FileMonitor('console')
1118 for line in mon.parse():
1119 pass
1120 return mon.lines
1121@@ -803,19 +850,19 @@
1122
1123 def xtest_fails_on_empty_file(self):
1124 # FIXME: We need some sort of timeout there...
1125- self.assertRaises(sm.CommandError, self._parse_file_monitor, '')
1126+ self.assertRaises(svm.CommandError, self._parse_file_monitor, '')
1127
1128 def test_fail_on_knwon_cloud_init_errors_with_file(self):
1129 self.assertRaises(
1130- sm.CloudInitError,
1131+ svm.CloudInitError,
1132 self._parse_file_monitor,
1133 'Failed loading yaml blob\n')
1134 self.assertRaises(
1135- sm.CloudInitError,
1136+ svm.CloudInitError,
1137 self._parse_file_monitor,
1138 'Unhandled non-multipart userdata starting\n')
1139 self.assertRaises(
1140- sm.CloudInitError,
1141+ svm.CloudInitError,
1142 self._parse_file_monitor,
1143 "failed to render string to stdout: cannot find 'uptime'\n")
1144
1145@@ -831,11 +878,11 @@
1146 os.chmod(self.test_base_dir, 0755)
1147 # We also need to sudo rm it as root created some files there
1148 self.addCleanup(
1149- sm.run_subprocess,
1150+ svm.run_subprocess,
1151 ['sudo', 'rm', '-fr',
1152 os.path.join(self.test_base_dir, 'home', '.virtinst')])
1153- self.conf = sm.VmStack('selftest-seed')
1154- self.vm = sm.KvmFromCloudImage(self.conf)
1155+ self.conf = svm.VmStack('selftest-seed')
1156+ self.vm = svm.Kvm(self.conf)
1157 images_dir = os.path.join(self.test_base_dir, 'images')
1158 os.mkdir(images_dir, 0755)
1159 config_dir = os.path.join(self.test_base_dir, 'config')
1160@@ -851,14 +898,10 @@
1161 vm.disk_size=8G
1162 ''' % (config_dir, images_dir, download_cache, reference_cloud_image_name))
1163
1164- def assertVmState(self, expected):
1165- states = sm.vm_states()
1166- self.assertEqual(expected, states[self.vm.conf.get('vm.name')])
1167-
1168 def test_install_with_seed(self):
1169 self.addCleanup(self.vm.undefine)
1170 self.vm.install()
1171- self.assertVmState('shut off')
1172+ self.assertEqual('shut off', self.vm.state())
1173
1174
1175 class TestInstallWithBacking(TestCaseWithHome):
1176@@ -872,17 +915,17 @@
1177 os.chmod(self.test_base_dir, 0755)
1178 # We also need to sudo rm it as root created some files there
1179 self.addCleanup(
1180- sm.run_subprocess,
1181+ svm.run_subprocess,
1182 ['sudo', 'rm', '-fr',
1183 os.path.join(self.test_base_dir, 'home', '.virtinst')])
1184- self.conf = sm.VmStack('selftest-backing')
1185- self.vm = sm.KvmFromBacking(self.conf)
1186+ self.conf = svm.VmStack('selftest-backing')
1187+ self.vm = svm.Kvm(self.conf)
1188 # We'll share the images_dir between vms
1189 images_dir = os.path.join(self.test_base_dir, 'images')
1190 os.mkdir(images_dir, 0755)
1191 config_dir = os.path.join(self.test_base_dir, 'config')
1192 # Create a shared config
1193- conf = sm.VmStack(None)
1194+ conf = svm.VmStack(None)
1195 conf.store._load_from_string('''
1196 vm.release=raring
1197 vm.config_dir=%s
1198@@ -899,27 +942,22 @@
1199 ''' % (config_dir, images_dir, download_cache_dir, reference_cloud_image_name))
1200 conf.store.save()
1201 # Fake a previous install by just re-using the reference cloud image
1202- temp_vm = sm.KvmFromCloudImage(
1203- sm.VmStack('selftest-from-cloud'))
1204+ temp_vm = svm.Kvm(svm.VmStack('selftest-from-cloud'))
1205 temp_vm.create_disk_image()
1206
1207- def assertVmState(self, vm, expected):
1208- states = sm.vm_states()
1209- self.assertEqual(expected, states[vm.conf.get('vm.name')])
1210-
1211 def test_install_with_backing(self):
1212- vm = sm.KvmFromBacking(sm.VmStack('selftest-backing'))
1213+ vm = svm.Kvm(svm.VmStack('selftest-backing'))
1214 self.addCleanup(vm.undefine)
1215 vm.install()
1216- self.assertVmState(vm, 'shut off')
1217+ self.assertEqual('shut off', vm.state())
1218
1219
1220 class TestSshKeyGen(TestCaseWithHome):
1221
1222 def setUp(self):
1223 super(TestSshKeyGen, self).setUp()
1224- self.conf = sm.VmStack(None)
1225- self.vm = sm.VM(self.conf)
1226+ self.conf = svm.VmStack(None)
1227+ self.vm = svm.VM(self.conf)
1228 self.config_dir = os.path.join(self.test_base_dir, 'config')
1229
1230 def load_config(self, more):
1231@@ -973,7 +1011,7 @@
1232 self.err = StringIO()
1233
1234 def parse_args(self, args):
1235- return sm.arg_parser.parse_args(args, self.out, self.err)
1236+ return svm.arg_parser.parse_args(args, self.out, self.err)
1237
1238 def test_nothing(self):
1239 self.assertRaises(SystemExit, self.parse_args, [])
1240@@ -991,37 +1029,66 @@
1241 self.assertTrue(ns.download)
1242
1243
1244-class TestBuildCommands(testtools.TestCase):
1245+class TestBuildCommands(TestCaseWithHome):
1246
1247 def setUp(self):
1248 super(TestBuildCommands, self).setUp()
1249 self.out = StringIO()
1250 self.err = StringIO()
1251+ self.conf = svm.VmStack('foo')
1252+ self.conf.store._load_from_string('''\
1253+[foo]
1254+vm.name=foo
1255+vm.class=lxc
1256+''')
1257+ self.conf.store.save()
1258
1259 def build_commands(self, args):
1260- return sm.build_commands(args, self.out, self.err)
1261+ return svm.build_commands(args, self.out, self.err)
1262
1263 def test_install(self):
1264 cmds = self.build_commands(['--install', 'foo'])
1265 self.assertEqual(1, len(cmds))
1266- self.assertTrue(isinstance(cmds[0], sm.Install))
1267+ self.assertTrue(isinstance(cmds[0], svm.Install))
1268
1269 def test_download(self):
1270 cmds = self.build_commands(['--download', 'foo'])
1271 self.assertEqual(1, len(cmds))
1272- self.assertTrue(isinstance(cmds[0], sm.Download))
1273+ self.assertTrue(isinstance(cmds[0], svm.Download))
1274
1275 def test_ssh_keygen(self):
1276 cmds = self.build_commands(['--ssh-keygen', 'foo'])
1277 self.assertEqual(1, len(cmds))
1278- self.assertTrue(isinstance(cmds[0], sm.SshKeyGen))
1279+ self.assertTrue(isinstance(cmds[0], svm.SshKeyGen))
1280
1281 def test_download_and_install(self):
1282 cmds = self.build_commands(['--install', '--download', 'foo'])
1283 self.assertEqual(2, len(cmds))
1284 # Download comes first
1285- self.assertTrue(isinstance(cmds[0], sm.Download))
1286- self.assertTrue(isinstance(cmds[1], sm.Install))
1287+ self.assertTrue(isinstance(cmds[0], svm.Download))
1288+ self.assertTrue(isinstance(cmds[1], svm.Install))
1289+
1290+
1291+class TestVmClass(testtools.TestCase):
1292+
1293+ def test_class_mandatory(self):
1294+ conf = svm.VmStack('I-dont-exist')
1295+ self.assertRaises(errors.ConfigOptionValueError, conf.get, 'vm.class')
1296+
1297+ def test_lxc(self):
1298+ conf = svm.VmStack('I-dont-exist')
1299+ conf.store._load_from_string('''vm.class=lxc''')
1300+ self.assertIs(svm.Lxc, conf.get('vm.class'))
1301+
1302+ def test_kvm(self):
1303+ conf = svm.VmStack('I-dont-exist')
1304+ conf.store._load_from_string('''vm.class=kvm''')
1305+ self.assertIs(svm.Kvm, conf.get('vm.class'))
1306+
1307+ def test_bogus(self):
1308+ conf = svm.VmStack('I-dont-exist')
1309+ conf.store._load_from_string('''vm.class=bogus''')
1310+ self.assertRaises(errors.ConfigOptionValueError, conf.get, 'vm.class')
1311
1312
1313 # FIXME: This needs to be parametrized for KvmFromCloudImage and
1314@@ -1031,7 +1098,7 @@
1315
1316 def setUp(self):
1317 super(TestInstall, self).setUp()
1318- self.conf = sm.VmStack('I-dont-exist')
1319+ self.conf = svm.VmStack('I-dont-exist')
1320 self.conf.store._load_from_string('''
1321 vm.name=I-dont-exist
1322 vm.release=raring
1323@@ -1039,13 +1106,13 @@
1324 ''')
1325 self.states = []
1326
1327- def vm_states(source=None):
1328+ def kvm_states(source=None):
1329 return self.states
1330- self.patch(sm, 'vm_states', vm_states)
1331+ self.patch(svm, 'kvm_states', kvm_states)
1332 self.vm = None
1333
1334 def install(self):
1335- class FakeKvm(sm.Kvm):
1336+ class FakeKvm(svm.Kvm):
1337
1338 def __init__(self, conf):
1339 super(FakeKvm, self).__init__(conf)
1340@@ -1063,13 +1130,13 @@
1341 self.install_called = True
1342
1343 self.vm = FakeKvm(self.conf)
1344- cmd = sm.Install(self.vm)
1345+ cmd = svm.Install(self.vm)
1346 cmd.run()
1347
1348 def test_install_while_running(self):
1349 self.conf.set('vm.name', 'foo')
1350 self.states = {'foo': 'running'}
1351- self.assertRaises(sm.SetupVmError, self.install)
1352+ self.assertRaises(svm.SetupVmError, self.install)
1353 self.assertFalse(self.vm.install_called)
1354 self.assertFalse(self.vm.undefine_called)
1355
1356
1357=== modified file 'setup_vm/vms.conf'
1358--- setup_vm/vms.conf 2013-07-29 03:31:03 +0000
1359+++ setup_vm/vms.conf 2013-08-09 09:36:11 +0000
1360@@ -39,12 +39,23 @@
1361
1362 [precise-server-pristine]
1363 vm.name=precise-server-pristine
1364+vm.class=kvm
1365+vm.release=precise
1366+vm.packages=bzr, avahi-daemon, emacs23
1367+vm.update=True
1368+
1369+[lxc-precise-server-pristine]
1370+# This is an example of an lxc precise server, if you start using it as a
1371+# base for a real server, update/remove this comment.
1372+vm.name=lxc-precise-server-pristine
1373+vm.class=lxc
1374 vm.release=precise
1375 vm.packages=bzr, avahi-daemon, emacs23
1376 vm.update=True
1377
1378 [sso]
1379 vm.name=sso
1380+vm.class=kvm
1381 vm.release=precise
1382 vm.backing=precise-server-pristine.qcow2
1383 vm.apt_sources={ppa.canonical-isd-hackers-dependencies}
1384@@ -55,6 +66,7 @@
1385
1386 [pay]
1387 vm.name=pay
1388+vm.class=kvm
1389 vm.release=precise
1390 vm.backing=precise-server-pristine.qcow2
1391 vm.apt_sources={ppa.canonical-isd-hackers-dependencies}, {ppa.canonical-isd-hackers-private}
1392@@ -65,6 +77,7 @@
1393
1394 [u1]
1395 vm.name=u1
1396+vm.class=kvm
1397 vm.release=precise
1398 vm.backing=precise-server-pristine.qcow2
1399 vm.apt_sources={ppa.ubuntuone_hackers}
1400@@ -75,6 +88,7 @@
1401
1402 [curucu]
1403 vm.name=curucu
1404+vm.class=kvm
1405 vm.release=precise
1406 vm.backing=precise-server-pristine.qcow2
1407 vm.apt_sources={ppa.ubuntuone_hackers}
1408@@ -85,6 +99,7 @@
1409
1410 [saucy-desktop-pristine]
1411 vm.name=saucy-desktop-pristine
1412+vm.class=kvm
1413 vm.release=saucy
1414 # python-unittest2 is not strictly required here but works around sst
1415 # insisting on installing it locally.
1416@@ -96,6 +111,7 @@
1417
1418 [purchase-testing]
1419 vm.name=purchase-testing
1420+vm.class=kvm
1421 vm.release=saucy
1422 vm.backing=saucy-desktop-pristine.qcow2
1423 vm.apt_sources=deb http://ppa.launchpad.net/ubuntuone/dashpurchase-testing/ubuntu {vm.release} main|4BD0ECAE,deb http://ppa.launchpad.net/vila/selenium/ubuntu {vm.release} main|5703355D,ppa:ubuntuone/nightlies

Subscribers

People subscribed via source and target branches

to all changes: