Merge lp:~vila/u1-test-utils/lxc into lp:u1-test-utils
- lxc
- Merge into trunk
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 |
Related bugs: |
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).
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.
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 NotImplementedE
>
> 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://
>
> 526 + # Create an lxc, relying on cloud-init to customize the base image.
>
> This would be better as a docstring.
Done.
>
> 951 + tests.requires_
>
> 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-
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 !
Leo Arias (elopio) : | # |
Ubuntu One Auto Pilot (otto-pilot) wrote : | # |
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.
Installing pip....
[localhost] local: . /mnt/tarmac/
Downloading/
Checking out bzr+ssh:
Running setup.py egg_info for package from bzr+ssh:
Downloading/
Checking out http://
Running setup.py egg_info for package from bzr+http://
warning: no previously-included files matching '*.pyc' found anywhere in distribution
Downloading/
Checking out http://
Running setup.py egg_info for package from bzr+http://
warning: no files found matching 'testproj.
Downloading/
Checking out http://
Running setup.py egg_info for package from bzr+http://
Downloading/
Running setup.py egg_info for package beautifulsoup4
Downloading/
Running setup.py egg_info for package django
Downloading/
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...
Ubuntu One Auto Pilot (otto-pilot) wrote : | # |
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.
Installing pip....
[localhost] local: . /mnt/tarmac/
Downloading/
Checking out bzr+ssh:
Running setup.py egg_info for package from bzr+ssh:
Downloading/
Checking out http://
Running setup.py egg_info for package from bzr+http://
warning: no previously-included files matching '*.pyc' found anywhere in distribution
Downloading/
Checking out http://
Running setup.py egg_info for package from bzr+http://
warning: no files found matching 'testproj.
Downloading/
Checking out http://
Running setup.py egg_info for package from bzr+http://
Downloading/
Running setup.py egg_info for package beautifulsoup4
Downloading/
Running setup.py egg_info for package django
Downloading/
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...
Ubuntu One Auto Pilot (otto-pilot) wrote : | # |
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.
Installing pip....
[localhost] local: . /mnt/tarmac/
Downloading/
Checking out bzr+ssh:
Running setup.py egg_info for package from bzr+ssh:
Downloading/
Checking out http://
Running setup.py egg_info for package from bzr+http://
warning: no previously-included files matching '*.pyc' found anywhere in distribution
Downloading/
Checking out http://
Running setup.py egg_info for package from bzr+http://
warning: no files found matching 'testproj.
Downloading/
Checking out http://
Running setup.py egg_info for package from bzr+http://
Downloading/
Running setup.py egg_info for package beautifulsoup4
Downloading/
Running setup.py egg_info for package django
Downloading/
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...
- 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
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 |
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): rror(self. download)
138 + raise NotImplementedE
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.