Merge lp:~soren/nova/virt-test-improvements into lp:~hudson-openstack/nova/trunk
- virt-test-improvements
- Merge into trunk
Status: | Rejected | ||||
---|---|---|---|---|---|
Rejected by: | Soren Hansen | ||||
Proposed branch: | lp:~soren/nova/virt-test-improvements | ||||
Merge into: | lp:~hudson-openstack/nova/trunk | ||||
Diff against target: |
2428 lines (+1976/-107) 10 files modified
nova/image/fake.py (+9/-1) nova/image/service.py (+200/-0) nova/tests/fake_libvirt_utils.py (+94/-0) nova/tests/fakelibvirt.py (+773/-0) nova/tests/test_fakelibvirt.py (+391/-0) nova/tests/test_image.py (+11/-0) nova/tests/test_libvirt.py (+184/-2) nova/tests/test_virt_drivers.py (+41/-4) nova/virt/libvirt/connection.py (+44/-100) nova/virt/libvirt/utils.py (+229/-0) |
||||
To merge this branch: | bzr merge lp:~soren/nova/virt-test-improvements | ||||
Related bugs: |
|
||||
Related blueprints: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Rick Harris (community) | Needs Fixing | ||
Review via email: mp+73644@code.launchpad.net |
Commit message
Extend test_virt_drivers to test the libvirt driver
Description of the change
Extend test_virt_driver to also test libvirt driver.
To support this, I've added a fake libvirt implementation. It's supposed
to expose an API and behaviour identical to that of libvirt itself
except without actually running any VM's or setting up any firewall or
anything, but still responding correctly when asked for a domain's XML,
a list of defined domains, running domains, etc.
I've also split out everything from libvirt.connection that is
potentially destructive or otherwise undesirable to run during testing,
and moved it to a new nova.virt.
things separately as well as stub version of it for testing. I hope
eventually to make it similar to fakelibvirt in style (e.g. keep track
of files created and deleted and attempts to open a file that it doesn't
know about, you'll get proper exceptions with proper errnos set and
whatnot).
Soren Hansen (soren) wrote : | # |
Soren Hansen (soren) wrote : | # |
> Whoa, hang on. Found a bug.
...and fixed it, by the way. Reviews welcome.
Rick Harris (rconradharris) wrote : | # |
Nice work here Soren.
Some notes:
Received this error when running the tests: http://
> 911 + if not auth == [[VIR_CRED_
> 912 + 'root',
> 913 + None]:
Might be better as:
if auth != [[VIR_CRED_
> 914 + raise Exception("Please extend fake libvirt module to support this "
> 915 + "auth method")
i18n.
> 1420 + try:
> 1421 + dst_fd, dst_path = tempfile.mkstemp()
> 1422 + os.close(dst_fd)
> 1423 +
> 1424 + src_fd, src_path = tempfile.mkstemp()
> 1425 + with os.fdopen(src_fd) as fp:
> 1426 + fp.write('canary')
> 1427 +
> 1428 + libvirt_
> 1429 + with open(dst_path, 'r') as fp:
> 1430 + self.assertEqua
> 1431 + finally:
> 1432 + os.unlink(src_path)
> 1433 + os.unlink(dst_path)
Not likely to happen, but if an exception was is raised after the first
`mkstemp()` but before the second `mkstemp()`, the `finally` will attempt to
unlink `src_path` which hasn't been defined yet.
Also, in that first `mkstemp`, if that were to raise an exception, then
`dest_path` wouldn't have been bound as a local variable (since the RHS raised
before assigning to the LHS). To handle that, the `mkstemp` should go outside of the `try`.
This should solve both problems:
dst_fd, dst_path = tempfile.mkstemp()
try:
src_fd, src_path = tempfile.mkstemp()
try:
with os.fdopen(src_fd) as fp:
with open(dst_path, 'r') as fp:
finally:
finally:
> 1454 + try:
> 1455 + dst_fd, dst_path = tempfile.mkstemp()
Same deal here:
dst_fd, dst_path = tempfile.mkstemp()
try:
...
Few more places like this in the file.
> 1990 +# Copyright 2010 United States Government as represented by the
> 1991 +# Administrator of the National Aeronautics and Space Administration.
> 1992 +# All Rights Reserved.
> 1993 +# Copyright (c) 2010 Citrix Systems, Inc.
> 1994 +# Copyright (c) 2011 Piston Cloud Computing, Inc
> 1995 +# Copyright (c) 2011 OpenStack LLC
Given that this is a new file, should we just start it out with:
# Copyright (c) 2011 OpenStack LLC
No real opinion here, just tossing out the question.
> 2105 + if umask:
> 2106 + saved_umask = os.umask(umask)
> 2107 +
> 2108 + with open(path, 'w') as f:
> 2109 + f.write(contents)
> 2110 +
> 2111 + if umask:
> 2112 + os.umask(
Should this be inside a try/finally?
if umask:
saved_umask = os.umask(umask)
try:
with open(path, 'w') as f:
finally:
if umask:
Soren Hansen (soren) wrote : | # |
> Nice work here Soren.
>
> Some notes:
>
>
> Received this error when running the tests:
> http://
Which OS are you running this on? Can you give me the outout of "uname -a" and see if its df(1) command can somehow be pursuaded to provide its output in bytes?
>> + raise Exception("Please extend fake libvirt module to support this "
>> + "auth method")
> i18n.
I added i18n here, but generally, I don't really think doing so in general for strings that only exist in the tests is super useful. Kind of like how we don't i18n enable our code comments.
> > 1420 + try:
> > 1421 + dst_fd, dst_path = tempfile.mkstemp()
> > 1422 + os.close(dst_fd)
> > 1423 +
> > 1424 + src_fd, src_path = tempfile.mkstemp()
> > 1425 + with os.fdopen(src_fd) as fp:
> > 1426 + fp.write('canary')
> > 1427 +
> > 1428 + libvirt_
> > 1429 + with open(dst_path, 'r') as fp:
> > 1430 + self.assertEqua
> > 1431 + finally:
> > 1432 + os.unlink(src_path)
> > 1433 + os.unlink(dst_path)
> Not likely to happen, but if an exception was is raised after the first
> `mkstemp()` but before the second `mkstemp()`, the `finally` will attempt to
> unlink `src_path` which hasn't been defined yet.
Fixed.
> > 1990 +# Copyright 2010 United States Government as represented by the
> > 1991 +# Administrator of the National Aeronautics and Space
> Administration.
> > 1992 +# All Rights Reserved.
> > 1993 +# Copyright (c) 2010 Citrix Systems, Inc.
> > 1994 +# Copyright (c) 2011 Piston Cloud Computing, Inc
> > 1995 +# Copyright (c) 2011 OpenStack LLC
>
> Given that this is a new file, should we just start it out with:
Parts of its contents are based on an existing file, so the copyright carries over. This was intentional.
> > 2105 + if umask:
> > 2106 + saved_umask = os.umask(umask)
> > 2107 +
> > 2108 + with open(path, 'w') as f:
> > 2109 + f.write(contents)
> > 2110 +
> > 2111 + if umask:
> > 2112 + os.umask(
> Should this be inside a try/finally?
Good call. Fixed.
Thanks!
- 1394. By Soren Hansen
-
Move the calls to mkstemp outside the try block so that only files that have been created are cleaned up.
- 1395. By Soren Hansen
-
i18n for exception strings in tests.
- 1396. By Soren Hansen
-
Move resetting umask back to its original value into a finally block.
- 1397. By Soren Hansen
-
Merge trunk
- 1398. By Soren Hansen
-
Also fix call to reboot()
- 1399. By Soren Hansen
-
Merge trunk
- 1400. By Soren Hansen
-
Extend fakelibvirt to properly support snapshotCreateXML
Brian Lamar (blamar) wrote : | # |
Overall I don't have a lot of code-specific comments. I will say that this does bring some much needed testing to the libvirt layer which historically has lacked some of the bigger-picture tests like the ones you've added.
However, I can't help but think about how overly complex this change is:
347 === added file 'nova/tests/
1112 === added file 'nova/tests/
I have always been told that if you're spending time writing tests for your tests then take a step back and think about why. In this case we're creating a 100% fake libvirt module which provides all of the functionality (we need/use) for our tests.
If a test fails, we have absolutely no way of knowing if it's an error in our driver or in our test code. Now we're debugging our test's tests which seems to be to be unneeded. Which version of the libvirt API does the fake libvirt module follow? I say leave the libvirt code up to the libvirt maintainers and the nova code, which interfaces with libvirt, to us.
We could use the libvirt 'test' driver, which provides basically what you said:
> to expose an API and behaviour identical to that of libvirt itself
> except without actually running any VM's or setting up any firewall or
> anything, but still responding correctly when asked for a domain's XML,
> a list of defined domains, running domains, etc.
However, one thing I can see is that the 'test' drive doesn't support a lot of nwfilter things that we run in our tests.
Example of 'test' driver, for anyone who hasn't used it: http://
So I guess long story short I don't want to hold this merge prop up if we think it's going to increase libvirt reliability, however for the future in my opinion we should shoot for:
Small Tests: Simple stubbing out of libvirt methods we call, or use of Mox to make sure we're
not the interaction of our code with libvirt's code.
Medium Tests: Use the 'test' driver to test as much as it allows. Sometimes we might have to
to make sure that the functions we're calling are the correct ones and that the
Large Tests: Have a libvirt process running on Jenkins or somewhere else that runs a full test
suite of QEMU, LXC, and any other driver we support. These tests make sure that
http://
Also, perhaps this was discussed elsewhere and I missed up, but feel free to point me to doc/discussions on this topic.
Brian Lamar (blamar) wrote : | # |
Two quick code comments:
43 === added file 'nova/image/
I think this is a remnant of the past, it was moved in trunk a bit ago. I think it might have been left after a trunk merge.
1841 + try:
1842 + virt_dom = self._lookup_
1843 + except exception.
1844 + raise exception.
Why change the exception type here? Seems like an InstanceNotFound exception makes more sense to me.
Soren Hansen (soren) wrote : | # |
Moved to Gerrit:
Unmerged revisions
- 1400. By Soren Hansen
-
Extend fakelibvirt to properly support snapshotCreateXML
- 1399. By Soren Hansen
-
Merge trunk
- 1398. By Soren Hansen
-
Also fix call to reboot()
- 1397. By Soren Hansen
-
Merge trunk
- 1396. By Soren Hansen
-
Move resetting umask back to its original value into a finally block.
- 1395. By Soren Hansen
-
i18n for exception strings in tests.
- 1394. By Soren Hansen
-
Move the calls to mkstemp outside the try block so that only files that have been created are cleaned up.
- 1393. By Soren Hansen
-
Accidentally included a patch to enable profiling. Reverting.
- 1392. By Soren Hansen
-
Merge trunk
- 1391. By Soren Hansen
-
For some reason I thought qemu-img was being run as root.
Preview Diff
1 | === modified file 'nova/image/fake.py' |
2 | --- nova/image/fake.py 2011-09-01 03:13:21 +0000 |
3 | +++ nova/image/fake.py 2011-09-16 15:21:29 +0000 |
4 | @@ -54,7 +54,7 @@ |
5 | 'ramdisk_id': FLAGS.null_kernel, |
6 | 'architecture': 'x86_64'}} |
7 | |
8 | - image2 = {'id': 'fake', |
9 | + image2 = {'id': '5', |
10 | 'name': 'fakeimage123456', |
11 | 'created_at': timestamp, |
12 | 'updated_at': timestamp, |
13 | @@ -111,6 +111,7 @@ |
14 | self.create(None, image3) |
15 | self.create(None, image4) |
16 | self.create(None, image5) |
17 | + self._imagedata = {} |
18 | super(_FakeImageService, self).__init__() |
19 | |
20 | def index(self, context, filters=None, marker=None, limit=None): |
21 | @@ -125,6 +126,11 @@ |
22 | """Return list of detailed image information.""" |
23 | return copy.deepcopy(self.images.values()) |
24 | |
25 | + def get(self, context, image_id, data): |
26 | + metadata = self.show(context, image_id) |
27 | + data.write(self._imagedata.get(image_id, '')) |
28 | + return metadata |
29 | + |
30 | def show(self, context, image_id): |
31 | """Get data about specified image. |
32 | |
33 | @@ -167,6 +173,8 @@ |
34 | |
35 | metadata['id'] = image_id |
36 | self.images[image_id] = copy.deepcopy(metadata) |
37 | + if data: |
38 | + self._imagedata[image_id] = data.read() |
39 | return self.images[image_id] |
40 | |
41 | def update(self, context, image_id, metadata, data=None): |
42 | |
43 | === added file 'nova/image/service.py' |
44 | --- nova/image/service.py 1970-01-01 00:00:00 +0000 |
45 | +++ nova/image/service.py 2011-09-16 15:21:29 +0000 |
46 | @@ -0,0 +1,200 @@ |
47 | +# vim: tabstop=4 shiftwidth=4 softtabstop=4 |
48 | + |
49 | +# Copyright 2010 OpenStack LLC. |
50 | +# All Rights Reserved. |
51 | +# |
52 | +# Licensed under the Apache License, Version 2.0 (the "License"); you may |
53 | +# not use this file except in compliance with the License. You may obtain |
54 | +# a copy of the License at |
55 | +# |
56 | +# http://www.apache.org/licenses/LICENSE-2.0 |
57 | +# |
58 | +# Unless required by applicable law or agreed to in writing, software |
59 | +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
60 | +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
61 | +# License for the specific language governing permissions and limitations |
62 | +# under the License. |
63 | + |
64 | + |
65 | +from nova import utils |
66 | + |
67 | + |
68 | +class BaseImageService(object): |
69 | + """Base class for providing image search and retrieval services. |
70 | + |
71 | + ImageService exposes two concepts of metadata: |
72 | + |
73 | + 1. First-class attributes: This is metadata that is common to all |
74 | + ImageService subclasses and is shared across all hypervisors. These |
75 | + attributes are defined by IMAGE_ATTRS. |
76 | + |
77 | + 2. Properties: This is metdata that is specific to an ImageService, |
78 | + and Image, or a particular hypervisor. Any attribute not present in |
79 | + BASE_IMAGE_ATTRS should be considered an image property. |
80 | + |
81 | + This means that ImageServices will return BASE_IMAGE_ATTRS as keys in the |
82 | + metadata dict, all other attributes will be returned as keys in the nested |
83 | + 'properties' dict. |
84 | + |
85 | + """ |
86 | + |
87 | + BASE_IMAGE_ATTRS = ['id', 'name', 'created_at', 'updated_at', |
88 | + 'deleted_at', 'deleted', 'status', 'is_public'] |
89 | + |
90 | + # NOTE(sirp): ImageService subclasses may override this to aid translation |
91 | + # between BaseImageService attributes and additional metadata stored by |
92 | + # the ImageService subclass |
93 | + SERVICE_IMAGE_ATTRS = [] |
94 | + |
95 | + def index(self, context, *args, **kwargs): |
96 | + """List images. |
97 | + |
98 | + :returns: a sequence of mappings with the following signature |
99 | + {'id': opaque id of image, 'name': name of image} |
100 | + |
101 | + """ |
102 | + raise NotImplementedError |
103 | + |
104 | + def detail(self, context, *args, **kwargs): |
105 | + """Detailed information about an images. |
106 | + |
107 | + :returns: a sequence of mappings with the following signature |
108 | + {'id': opaque id of image, |
109 | + 'name': name of image, |
110 | + 'created_at': creation datetime object, |
111 | + 'updated_at': modification datetime object, |
112 | + 'deleted_at': deletion datetime object or None, |
113 | + 'deleted': boolean indicating if image has been deleted, |
114 | + 'status': string description of image status, |
115 | + 'is_public': boolean indicating if image is public |
116 | + } |
117 | + |
118 | + If the service does not implement a method that provides a detailed |
119 | + set of information about images, then the method should raise |
120 | + NotImplementedError, in which case Nova will emulate this method |
121 | + with repeated calls to show() for each image received from the |
122 | + index() method. |
123 | + |
124 | + """ |
125 | + raise NotImplementedError |
126 | + |
127 | + def show(self, context, image_id): |
128 | + """Detailed information about an image. |
129 | + |
130 | + :returns: a mapping with the following signature: |
131 | + {'id': opaque id of image, |
132 | + 'name': name of image, |
133 | + 'created_at': creation datetime object, |
134 | + 'updated_at': modification datetime object, |
135 | + 'deleted_at': deletion datetime object or None, |
136 | + 'deleted': boolean indicating if image has been deleted, |
137 | + 'status': string description of image status, |
138 | + 'is_public': boolean indicating if image is public |
139 | + }, ... |
140 | + |
141 | + :raises: NotFound if the image does not exist |
142 | + |
143 | + """ |
144 | + raise NotImplementedError |
145 | + |
146 | + def get(self, context, image_id, data): |
147 | + """Get an image. |
148 | + |
149 | + :param data: a file-like object to hold binary image data |
150 | + :returns: a dict containing image metadata, writes image data to data. |
151 | + :raises: NotFound if the image does not exist |
152 | + |
153 | + """ |
154 | + raise NotImplementedError |
155 | + |
156 | + def create(self, context, metadata, data=None): |
157 | + """Store the image metadata and data. |
158 | + |
159 | + :returns: the new image metadata. |
160 | + :raises: AlreadyExists if the image already exist. |
161 | + |
162 | + """ |
163 | + raise NotImplementedError |
164 | + |
165 | + def update(self, context, image_id, metadata, data=None): |
166 | + """Update the given image metadata and data and return the metadata. |
167 | + |
168 | + :raises: NotFound if the image does not exist. |
169 | + |
170 | + """ |
171 | + raise NotImplementedError |
172 | + |
173 | + def delete(self, context, image_id): |
174 | + """Delete the given image. |
175 | + |
176 | + :raises: NotFound if the image does not exist. |
177 | + |
178 | + """ |
179 | + raise NotImplementedError |
180 | + |
181 | + @staticmethod |
182 | + def _is_image_available(context, image_meta): |
183 | + """Check image availability. |
184 | + |
185 | + Images are always available if they are public or if the user is an |
186 | + admin. |
187 | + |
188 | + Otherwise, we filter by project_id (if present) and then fall-back to |
189 | + images owned by user. |
190 | + |
191 | + """ |
192 | + # FIXME(sirp): We should be filtering by user_id on the Glance side |
193 | + # for security; however, we can't do that until we get authn/authz |
194 | + # sorted out. Until then, filtering in Nova. |
195 | + if image_meta['is_public'] or context.is_admin: |
196 | + return True |
197 | + |
198 | + properties = image_meta['properties'] |
199 | + |
200 | + if context.project_id and ('project_id' in properties): |
201 | + return str(properties['project_id']) == str(context.project_id) |
202 | + |
203 | + try: |
204 | + user_id = properties['user_id'] |
205 | + except KeyError: |
206 | + return False |
207 | + |
208 | + return str(user_id) == str(context.user_id) |
209 | + |
210 | + @classmethod |
211 | + def _translate_to_base(cls, metadata): |
212 | + """Return a metadata dictionary that is BaseImageService compliant. |
213 | + |
214 | + This is used by subclasses to expose only a metadata dictionary that |
215 | + is the same across ImageService implementations. |
216 | + |
217 | + """ |
218 | + return cls._propertify_metadata(metadata, cls.BASE_IMAGE_ATTRS) |
219 | + |
220 | + @classmethod |
221 | + def _translate_to_service(cls, metadata): |
222 | + """Return a metadata dict that is usable by the ImageService subclass. |
223 | + |
224 | + As an example, Glance has additional attributes (like 'location'); the |
225 | + BaseImageService considers these properties, but we need to translate |
226 | + these back to first-class attrs for sending to Glance. This method |
227 | + handles this by allowing you to specify the attributes an ImageService |
228 | + considers first-class. |
229 | + |
230 | + """ |
231 | + if not cls.SERVICE_IMAGE_ATTRS: |
232 | + raise NotImplementedError(_('Cannot use this without specifying ' |
233 | + 'SERVICE_IMAGE_ATTRS for subclass')) |
234 | + return cls._propertify_metadata(metadata, cls.SERVICE_IMAGE_ATTRS) |
235 | + |
236 | + @staticmethod |
237 | + def _propertify_metadata(metadata, keys): |
238 | + """Move unknown keys to a nested 'properties' dict. |
239 | + |
240 | + :returns: a new dict with the keys moved. |
241 | + |
242 | + """ |
243 | + flattened = utils.flatten_dict(metadata) |
244 | + attributes, properties = utils.partition_dict(flattened, keys) |
245 | + attributes['properties'] = properties |
246 | + return attributes |
247 | |
248 | === added file 'nova/tests/fake_libvirt_utils.py' |
249 | --- nova/tests/fake_libvirt_utils.py 1970-01-01 00:00:00 +0000 |
250 | +++ nova/tests/fake_libvirt_utils.py 2011-09-16 15:21:29 +0000 |
251 | @@ -0,0 +1,94 @@ |
252 | +# vim: tabstop=4 shiftwidth=4 softtabstop=4 |
253 | + |
254 | +# Copyright (c) 2011 OpenStack LLC |
255 | +# |
256 | +# Licensed under the Apache License, Version 2.0 (the "License"); you may |
257 | +# not use this file except in compliance with the License. You may obtain |
258 | +# a copy of the License at |
259 | +# |
260 | +# http://www.apache.org/licenses/LICENSE-2.0 |
261 | +# |
262 | +# Unless required by applicable law or agreed to in writing, software |
263 | +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
264 | +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
265 | +# License for the specific language governing permissions and limitations |
266 | +# under the License. |
267 | + |
268 | +import StringIO |
269 | + |
270 | +files = {} |
271 | + |
272 | + |
273 | +def create_image(disk_format, path, size): |
274 | + pass |
275 | + |
276 | + |
277 | +def create_cow_image(backing_file, path): |
278 | + pass |
279 | + |
280 | + |
281 | +def get_disk_size(path): |
282 | + pass |
283 | + |
284 | + |
285 | +def copy_image(src, dest): |
286 | + pass |
287 | + |
288 | + |
289 | +def mkfs(fs, path): |
290 | + pass |
291 | + |
292 | + |
293 | +def ensure_tree(path): |
294 | + pass |
295 | + |
296 | + |
297 | +def write_to_file(path, contents, umask=None): |
298 | + pass |
299 | + |
300 | + |
301 | +def chown(path, owner): |
302 | + pass |
303 | + |
304 | + |
305 | +def extract_snapshot(disk_path, snapshot_name, out_path): |
306 | + files[out_path] = '' |
307 | + |
308 | + |
309 | +class File(object): |
310 | + def __init__(self, path, mode=None): |
311 | + self.fp = StringIO.StringIO(files[path]) |
312 | + |
313 | + def __enter__(self): |
314 | + return self.fp |
315 | + |
316 | + def __exit__(self, *args): |
317 | + return |
318 | + |
319 | + |
320 | +def file_open(path, mode=None): |
321 | + return File(path, mode) |
322 | + |
323 | + |
324 | +def load_file(path): |
325 | + return '' |
326 | + |
327 | + |
328 | +def get_open_port(start_port, end_port): |
329 | + # Return the port in the middle |
330 | + return int((start_port + end_port) / 2) |
331 | + |
332 | + |
333 | +def run_ajaxterm(cmd, token, port): |
334 | + pass |
335 | + |
336 | + |
337 | +def get_fs_info(path): |
338 | + return {'total': 128 * (1024 ** 3), |
339 | + 'used': 44 * (1024 ** 3), |
340 | + 'free': 84 * (1024 ** 3)} |
341 | + |
342 | + |
343 | +def fetch_image(context, target, image_id, user_id, project_id, |
344 | + size=None): |
345 | + pass |
346 | |
347 | === added file 'nova/tests/fakelibvirt.py' |
348 | --- nova/tests/fakelibvirt.py 1970-01-01 00:00:00 +0000 |
349 | +++ nova/tests/fakelibvirt.py 2011-09-16 15:21:29 +0000 |
350 | @@ -0,0 +1,773 @@ |
351 | +# vim: tabstop=4 shiftwidth=4 softtabstop=4 |
352 | +# |
353 | +# Copyright 2010 OpenStack LLC |
354 | +# |
355 | +# Licensed under the Apache License, Version 2.0 (the "License"); you may |
356 | +# not use this file except in compliance with the License. You may obtain |
357 | +# a copy of the License at |
358 | +# |
359 | +# http://www.apache.org/licenses/LICENSE-2.0 |
360 | +# |
361 | +# Unless required by applicable law or agreed to in writing, software |
362 | +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
363 | +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
364 | +# License for the specific language governing permissions and limitations |
365 | +# under the License. |
366 | + |
367 | +from xml.etree.ElementTree import fromstring as xml_to_tree |
368 | +from xml.etree.ElementTree import ParseError |
369 | +import uuid |
370 | + |
371 | +# Allow passing None to the various connect methods |
372 | +# (i.e. allow the client to rely on default URLs) |
373 | +allow_default_uri_connection = True |
374 | + |
375 | +# string indicating the CPU arch |
376 | +node_arch = 'x86_64' # or 'i686' (or whatever else uname -m might return) |
377 | + |
378 | +# memory size in kilobytes |
379 | +node_kB_mem = 4096 |
380 | + |
381 | +# the number of active CPUs |
382 | +node_cpus = 2 |
383 | + |
384 | +# expected CPU frequency |
385 | +node_mhz = 800 |
386 | + |
387 | +# the number of NUMA cell, 1 for unusual NUMA topologies or uniform |
388 | +# memory access; check capabilities XML for the actual NUMA topology |
389 | +node_nodes = 1 # NUMA nodes |
390 | + |
391 | +# number of CPU sockets per node if nodes > 1, total number of CPU |
392 | +# sockets otherwise |
393 | +node_sockets = 1 |
394 | + |
395 | +# number of cores per socket |
396 | +node_cores = 2 |
397 | + |
398 | +# number of threads per core |
399 | +node_threads = 1 |
400 | + |
401 | +# CPU model |
402 | +node_cpu_model = "Penryn" |
403 | + |
404 | +# CPU vendor |
405 | +node_cpu_vendor = "Intel" |
406 | + |
407 | + |
408 | +def _reset(): |
409 | + global allow_default_uri_connection |
410 | + allow_default_uri_connection = True |
411 | + |
412 | +# virDomainState |
413 | +VIR_DOMAIN_NOSTATE = 0 |
414 | +VIR_DOMAIN_RUNNING = 1 |
415 | +VIR_DOMAIN_BLOCKED = 2 |
416 | +VIR_DOMAIN_PAUSED = 3 |
417 | +VIR_DOMAIN_SHUTDOWN = 4 |
418 | +VIR_DOMAIN_SHUTOFF = 5 |
419 | +VIR_DOMAIN_CRASHED = 6 |
420 | + |
421 | +VIR_CPU_COMPARE_ERROR = -1 |
422 | +VIR_CPU_COMPARE_INCOMPATIBLE = 0 |
423 | +VIR_CPU_COMPARE_IDENTICAL = 1 |
424 | +VIR_CPU_COMPARE_SUPERSET = 2 |
425 | + |
426 | +VIR_CRED_AUTHNAME = 2 |
427 | +VIR_CRED_NOECHOPROMPT = 7 |
428 | + |
429 | +# libvirtError enums |
430 | +# (Intentionally different from what's in libvirt. We do this to check, |
431 | +# that consumers of the library are using the symbolic names rather than |
432 | +# hardcoding the numerical values) |
433 | +VIR_FROM_QEMU = 100 |
434 | +VIR_FROM_DOMAIN = 200 |
435 | +VIR_FROM_NWFILTER = 330 |
436 | +VIR_ERR_XML_DETAIL = 350 |
437 | +VIR_ERR_NO_DOMAIN = 420 |
438 | +VIR_ERR_NO_NWFILTER = 620 |
439 | + |
440 | + |
441 | +def _parse_disk_info(element): |
442 | + disk_info = {} |
443 | + disk_info['type'] = element.get('type', 'file') |
444 | + disk_info['device'] = element.get('device', 'disk') |
445 | + |
446 | + driver = element.find('./driver') |
447 | + if driver is not None: |
448 | + disk_info['driver_name'] = driver.get('name') |
449 | + disk_info['driver_type'] = driver.get('type') |
450 | + |
451 | + source = element.find('./source') |
452 | + if source is not None: |
453 | + disk_info['source'] = source.get('file') |
454 | + if not disk_info['source']: |
455 | + disk_info['source'] = source.get('dev') |
456 | + |
457 | + if not disk_info['source']: |
458 | + disk_info['source'] = source.get('path') |
459 | + |
460 | + target = element.find('./target') |
461 | + if target is not None: |
462 | + disk_info['target_dev'] = target.get('dev') |
463 | + disk_info['target_bus'] = target.get('bus') |
464 | + |
465 | + return disk_info |
466 | + |
467 | + |
468 | +class libvirtError(Exception): |
469 | + def __init__(self, error_code, error_domain, msg): |
470 | + self.error_code = error_code |
471 | + self.error_domain = error_domain |
472 | + Exception(self, msg) |
473 | + |
474 | + def get_error_code(self): |
475 | + return self.error_code |
476 | + |
477 | + def get_error_domain(self): |
478 | + return self.error_domain |
479 | + |
480 | + |
481 | +class NWFilter(object): |
482 | + def __init__(self, connection, xml): |
483 | + self._connection = connection |
484 | + |
485 | + self._xml = xml |
486 | + self._parse_xml(xml) |
487 | + |
488 | + def _parse_xml(self, xml): |
489 | + tree = xml_to_tree(xml) |
490 | + root = tree.find('.') |
491 | + self._name = root.get('name') |
492 | + |
493 | + def undefine(self): |
494 | + self._connection._remove_filter(self) |
495 | + |
496 | + |
497 | +class Domain(object): |
498 | + def __init__(self, connection, xml, running=False, transient=False): |
499 | + self._connection = connection |
500 | + if running: |
501 | + connection._mark_running(self) |
502 | + |
503 | + self._state = running and VIR_DOMAIN_RUNNING or VIR_DOMAIN_SHUTOFF |
504 | + self._transient = transient |
505 | + self._def = self._parse_definition(xml) |
506 | + self._has_saved_state = False |
507 | + self._snapshots = {} |
508 | + |
509 | + def _parse_definition(self, xml): |
510 | + try: |
511 | + tree = xml_to_tree(xml) |
512 | + except ParseError: |
513 | + raise libvirtError(VIR_ERR_XML_DETAIL, VIR_FROM_DOMAIN, |
514 | + "Invalid XML.") |
515 | + |
516 | + definition = {} |
517 | + |
518 | + name = tree.find('./name') |
519 | + if name is not None: |
520 | + definition['name'] = name.text |
521 | + |
522 | + uuid_elem = tree.find('./uuid') |
523 | + if uuid_elem is not None: |
524 | + definition['uuid'] = uuid_elem.text |
525 | + else: |
526 | + definition['uuid'] = str(uuid.uuid4()) |
527 | + |
528 | + vcpu = tree.find('./vcpu') |
529 | + if vcpu is not None: |
530 | + definition['vcpu'] = int(vcpu.text) |
531 | + |
532 | + memory = tree.find('./memory') |
533 | + if memory is not None: |
534 | + definition['memory'] = int(memory.text) |
535 | + |
536 | + os = {} |
537 | + os_type = tree.find('./os/type') |
538 | + if os_type is not None: |
539 | + os['type'] = os_type.text |
540 | + os['arch'] = os_type.get('arch', node_arch) |
541 | + |
542 | + os_kernel = tree.find('./os/kernel') |
543 | + if os_kernel is not None: |
544 | + os['kernel'] = os_kernel.text |
545 | + |
546 | + os_initrd = tree.find('./os/initrd') |
547 | + if os_initrd is not None: |
548 | + os['initrd'] = os_initrd.text |
549 | + |
550 | + os_cmdline = tree.find('./os/cmdline') |
551 | + if os_cmdline is not None: |
552 | + os['cmdline'] = os_cmdline.text |
553 | + |
554 | + os_boot = tree.find('./os/boot') |
555 | + if os_boot is not None: |
556 | + os['boot_dev'] = os_boot.get('dev') |
557 | + |
558 | + definition['os'] = os |
559 | + |
560 | + features = {} |
561 | + |
562 | + acpi = tree.find('./features/acpi') |
563 | + if acpi is not None: |
564 | + features['acpi'] = True |
565 | + |
566 | + definition['features'] = features |
567 | + |
568 | + devices = {} |
569 | + |
570 | + device_nodes = tree.find('./devices') |
571 | + if device_nodes is not None: |
572 | + disks_info = [] |
573 | + disks = device_nodes.findall('./disk') |
574 | + for disk in disks: |
575 | + disks_info += [_parse_disk_info(disk)] |
576 | + devices['disks'] = disks_info |
577 | + |
578 | + nics_info = [] |
579 | + nics = device_nodes.findall('./interface') |
580 | + for nic in nics: |
581 | + nic_info = {} |
582 | + nic_info['type'] = nic.get('type') |
583 | + |
584 | + mac = nic.find('./mac') |
585 | + if mac is not None: |
586 | + nic_info['mac'] = mac.get('address') |
587 | + |
588 | + source = nic.find('./source') |
589 | + if source is not None: |
590 | + if nic_info['type'] == 'network': |
591 | + nic_info['source'] = source.get('network') |
592 | + elif nic_info['type'] == 'bridge': |
593 | + nic_info['source'] = source.get('bridge') |
594 | + |
595 | + nics_info += [nic_info] |
596 | + |
597 | + devices['nics'] = nics_info |
598 | + |
599 | + definition['devices'] = devices |
600 | + |
601 | + return definition |
602 | + |
603 | + def create(self): |
604 | + self.createWithFlags(0) |
605 | + |
606 | + def createWithFlags(self, flags): |
607 | + # FIXME: Not handling flags at the moment |
608 | + self._state = VIR_DOMAIN_RUNNING |
609 | + self._connection._mark_running(self) |
610 | + self._has_saved_state = False |
611 | + |
612 | + def isActive(self): |
613 | + return int(self._state == VIR_DOMAIN_RUNNING) |
614 | + |
615 | + def undefine(self): |
616 | + self._connection._undefine(self) |
617 | + |
618 | + def destroy(self): |
619 | + self._state = VIR_DOMAIN_SHUTOFF |
620 | + self._connection._mark_not_running(self) |
621 | + |
622 | + def name(self): |
623 | + return self._def['name'] |
624 | + |
625 | + def UUIDString(self): |
626 | + return self._def['uuid'] |
627 | + |
628 | + def interfaceStats(self, device): |
629 | + return [10000242400, 1234, 0, 2, 213412343233, 34214234, 23, 3] |
630 | + |
631 | + def blockStats(self, device): |
632 | + return [2, 10000242400, 234, 2343424234, 34] |
633 | + |
634 | + def suspend(self): |
635 | + self._state = VIR_DOMAIN_PAUSED |
636 | + |
637 | + def info(self): |
638 | + return [VIR_DOMAIN_RUNNING, |
639 | + long(self._def['memory']), |
640 | + long(self._def['memory']), |
641 | + self._def['vcpu'], |
642 | + 123456789L] |
643 | + |
644 | + def attachDevice(self, xml): |
645 | + disk_info = _parse_disk_info(xml_to_tree(xml)) |
646 | + disk_info['_attached'] = True |
647 | + self._def['devices']['disks'] += [disk_info] |
648 | + return True |
649 | + |
650 | + def detachDevice(self, xml): |
651 | + disk_info = _parse_disk_info(xml_to_tree(xml)) |
652 | + disk_info['_attached'] = True |
653 | + return disk_info in self._def['devices']['disks'] |
654 | + |
655 | + def XMLDesc(self, flags): |
656 | + disks = '' |
657 | + for disk in self._def['devices']['disks']: |
658 | + disks += '''<disk type='%(type)s' device='%(device)s'> |
659 | + <driver name='%(driver_name)s' type='%(driver_type)s'/> |
660 | + <source file='%(source)s'/> |
661 | + <target dev='%(target_dev)s' bus='%(target_bus)s'/> |
662 | + <address type='drive' controller='0' bus='0' unit='0'/> |
663 | + </disk>''' % disk |
664 | + |
665 | + nics = '' |
666 | + for nic in self._def['devices']['nics']: |
667 | + nics += '''<interface type='%(type)s'> |
668 | + <mac address='%(mac)s'/> |
669 | + <source %(type)s='%(source)s'/> |
670 | + <address type='pci' domain='0x0000' bus='0x00' slot='0x03' |
671 | + function='0x0'/> |
672 | + </interface>''' % nic |
673 | + |
674 | + return '''<domain type='kvm'> |
675 | + <name>%(name)s</name> |
676 | + <uuid>%(uuid)s</uuid> |
677 | + <memory>%(memory)s</memory> |
678 | + <currentMemory>%(memory)s</currentMemory> |
679 | + <vcpu>%(vcpu)s</vcpu> |
680 | + <os> |
681 | + <type arch='%(arch)s' machine='pc-0.12'>hvm</type> |
682 | + <boot dev='hd'/> |
683 | + </os> |
684 | + <features> |
685 | + <acpi/> |
686 | + <apic/> |
687 | + <pae/> |
688 | + </features> |
689 | + <clock offset='localtime'/> |
690 | + <on_poweroff>destroy</on_poweroff> |
691 | + <on_reboot>restart</on_reboot> |
692 | + <on_crash>restart</on_crash> |
693 | + <devices> |
694 | + <emulator>/usr/bin/kvm</emulator> |
695 | + %(disks)s |
696 | + <controller type='ide' index='0'> |
697 | + <address type='pci' domain='0x0000' bus='0x00' slot='0x01' |
698 | + function='0x1'/> |
699 | + </controller> |
700 | + %(nics)s |
701 | + <serial type='pty'> |
702 | + <source pty='/dev/pts/27'/> |
703 | + <target port='0'/> |
704 | + </serial> |
705 | + <console type='pty'> |
706 | + <target type='serial' port='0'/> |
707 | + </console> |
708 | + <input type='tablet' bus='usb'/> |
709 | + <input type='mouse' bus='ps2'/> |
710 | + <graphics type='vnc' port='-1' autoport='yes'/> |
711 | + <video> |
712 | + <model type='cirrus' vram='9216' heads='1'/> |
713 | + <address type='pci' domain='0x0000' bus='0x00' slot='0x02' |
714 | + function='0x0'/> |
715 | + </video> |
716 | + <memballoon model='virtio'> |
717 | + <address type='pci' domain='0x0000' bus='0x00' slot='0x04' |
718 | + function='0x0'/> |
719 | + </memballoon> |
720 | + </devices> |
721 | +</domain>''' % {'name': self._def['name'], |
722 | + 'uuid': self._def['uuid'], |
723 | + 'memory': self._def['memory'], |
724 | + 'vcpu': self._def['vcpu'], |
725 | + 'arch': self._def['os']['arch'], |
726 | + 'disks': disks, |
727 | + 'nics': nics} |
728 | + |
729 | + def managedSave(self, flags): |
730 | + self._connection._mark_not_running(self) |
731 | + self._has_saved_state = True |
732 | + |
733 | + def resume(self): |
734 | + self._state = VIR_DOMAIN_RUNNING |
735 | + |
736 | + def snapshotCreateXML(self, xml, flags): |
737 | + tree = xml_to_tree(xml) |
738 | + name = tree.find('./name').text |
739 | + snapshot = DomainSnapshot(name, self) |
740 | + self._snapshots[name] = snapshot |
741 | + return snapshot |
742 | + |
743 | + |
744 | +class DomainSnapshot(object): |
745 | + def __init__(self, name, domain): |
746 | + self._name = name |
747 | + self._domain = domain |
748 | + |
749 | + def delete(self, flags): |
750 | + del self._domain._snapshots[self._name] |
751 | + |
752 | + |
753 | +class Connection(object): |
754 | + def __init__(self, uri, readonly): |
755 | + if not uri: |
756 | + if allow_default_uri_connection: |
757 | + uri = 'qemu:///session' |
758 | + else: |
759 | + raise Exception("URI was None, but fake libvirt is configured" |
760 | + " to not accept this.") |
761 | + |
762 | + uri_whitelist = ['qemu:///system', |
763 | + 'qemu:///session', |
764 | + 'xen:///system', |
765 | + 'uml:///system'] |
766 | + |
767 | + if uri not in uri_whitelist: |
768 | + raise libvirtError(5, 0, |
769 | + "libvir: error : no connection driver " |
770 | + "available for No connection for URI %s" % uri) |
771 | + |
772 | + self.readonly = readonly |
773 | + self._uri = uri |
774 | + self._vms = {} |
775 | + self._running_vms = {} |
776 | + self._id_counter = 0 |
777 | + self._nwfilters = {} |
778 | + |
779 | + def _add_filter(self, nwfilter): |
780 | + self._nwfilters[nwfilter._name] = nwfilter |
781 | + |
782 | + def _remove_filter(self, nwfilter): |
783 | + del self._nwfilters[nwfilter._name] |
784 | + |
785 | + def _mark_running(self, dom): |
786 | + self._running_vms[self._id_counter] = dom |
787 | + self._id_counter += 1 |
788 | + |
789 | + def _mark_not_running(self, dom): |
790 | + if dom._transient: |
791 | + self._undefine(dom) |
792 | + |
793 | + for (k, v) in self._running_vms.iteritems(): |
794 | + if v == dom: |
795 | + del self._running_vms[k] |
796 | + return |
797 | + |
798 | + def _undefine(self, dom): |
799 | + del self._vms[dom.name()] |
800 | + |
801 | + def getInfo(self): |
802 | + return [node_arch, |
803 | + node_kB_mem, |
804 | + node_cpus, |
805 | + node_mhz, |
806 | + node_nodes, |
807 | + node_sockets, |
808 | + node_cores, |
809 | + node_threads] |
810 | + |
811 | + def listDomainsID(self): |
812 | + return self._running_vms.keys() |
813 | + |
814 | + def lookupByID(self, id): |
815 | + if id in self._running_vms: |
816 | + return self._running_vms[id] |
817 | + raise libvirtError(VIR_ERR_NO_DOMAIN, VIR_FROM_QEMU, |
818 | + 'Domain not found: no domain with matching ' |
819 | + 'id %d' % id) |
820 | + |
821 | + def lookupByName(self, name): |
822 | + if name in self._vms: |
823 | + return self._vms[name] |
824 | + raise libvirtError(VIR_ERR_NO_DOMAIN, VIR_FROM_QEMU, |
825 | + 'Domain not found: no domain with matching ' |
826 | + 'name "%s"' % name) |
827 | + |
828 | + def defineXML(self, xml): |
829 | + dom = Domain(connection=self, running=False, transient=False, xml=xml) |
830 | + self._vms[dom.name()] = dom |
831 | + return dom |
832 | + |
833 | + def createXML(self, xml, flags): |
834 | + dom = Domain(connection=self, running=True, transient=True, xml=xml) |
835 | + self._vms[dom.name()] = dom |
836 | + return dom |
837 | + |
838 | + def getType(self): |
839 | + if self._uri == 'qemu:///system': |
840 | + return 'QEMU' |
841 | + |
842 | + def getVersion(self): |
843 | + return 14000 |
844 | + |
845 | + def getCapabilities(self): |
846 | + return '''<capabilities> |
847 | + <host> |
848 | + <uuid>cef19ce0-0ca2-11df-855d-b19fbce37686</uuid> |
849 | + <cpu> |
850 | + <arch>x86_64</arch> |
851 | + <model>Penryn</model> |
852 | + <vendor>Intel</vendor> |
853 | + <topology sockets='1' cores='2' threads='1'/> |
854 | + <feature name='xtpr'/> |
855 | + <feature name='tm2'/> |
856 | + <feature name='est'/> |
857 | + <feature name='vmx'/> |
858 | + <feature name='ds_cpl'/> |
859 | + <feature name='monitor'/> |
860 | + <feature name='pbe'/> |
861 | + <feature name='tm'/> |
862 | + <feature name='ht'/> |
863 | + <feature name='ss'/> |
864 | + <feature name='acpi'/> |
865 | + <feature name='ds'/> |
866 | + <feature name='vme'/> |
867 | + </cpu> |
868 | + <migration_features> |
869 | + <live/> |
870 | + <uri_transports> |
871 | + <uri_transport>tcp</uri_transport> |
872 | + </uri_transports> |
873 | + </migration_features> |
874 | + <secmodel> |
875 | + <model>apparmor</model> |
876 | + <doi>0</doi> |
877 | + </secmodel> |
878 | + </host> |
879 | + |
880 | + <guest> |
881 | + <os_type>hvm</os_type> |
882 | + <arch name='i686'> |
883 | + <wordsize>32</wordsize> |
884 | + <emulator>/usr/bin/qemu</emulator> |
885 | + <machine>pc-0.14</machine> |
886 | + <machine canonical='pc-0.14'>pc</machine> |
887 | + <machine>pc-0.13</machine> |
888 | + <machine>pc-0.12</machine> |
889 | + <machine>pc-0.11</machine> |
890 | + <machine>pc-0.10</machine> |
891 | + <machine>isapc</machine> |
892 | + <domain type='qemu'> |
893 | + </domain> |
894 | + <domain type='kvm'> |
895 | + <emulator>/usr/bin/kvm</emulator> |
896 | + <machine>pc-0.14</machine> |
897 | + <machine canonical='pc-0.14'>pc</machine> |
898 | + <machine>pc-0.13</machine> |
899 | + <machine>pc-0.12</machine> |
900 | + <machine>pc-0.11</machine> |
901 | + <machine>pc-0.10</machine> |
902 | + <machine>isapc</machine> |
903 | + </domain> |
904 | + </arch> |
905 | + <features> |
906 | + <cpuselection/> |
907 | + <deviceboot/> |
908 | + <pae/> |
909 | + <nonpae/> |
910 | + <acpi default='on' toggle='yes'/> |
911 | + <apic default='on' toggle='no'/> |
912 | + </features> |
913 | + </guest> |
914 | + |
915 | + <guest> |
916 | + <os_type>hvm</os_type> |
917 | + <arch name='x86_64'> |
918 | + <wordsize>64</wordsize> |
919 | + <emulator>/usr/bin/qemu-system-x86_64</emulator> |
920 | + <machine>pc-0.14</machine> |
921 | + <machine canonical='pc-0.14'>pc</machine> |
922 | + <machine>pc-0.13</machine> |
923 | + <machine>pc-0.12</machine> |
924 | + <machine>pc-0.11</machine> |
925 | + <machine>pc-0.10</machine> |
926 | + <machine>isapc</machine> |
927 | + <domain type='qemu'> |
928 | + </domain> |
929 | + <domain type='kvm'> |
930 | + <emulator>/usr/bin/kvm</emulator> |
931 | + <machine>pc-0.14</machine> |
932 | + <machine canonical='pc-0.14'>pc</machine> |
933 | + <machine>pc-0.13</machine> |
934 | + <machine>pc-0.12</machine> |
935 | + <machine>pc-0.11</machine> |
936 | + <machine>pc-0.10</machine> |
937 | + <machine>isapc</machine> |
938 | + </domain> |
939 | + </arch> |
940 | + <features> |
941 | + <cpuselection/> |
942 | + <deviceboot/> |
943 | + <acpi default='on' toggle='yes'/> |
944 | + <apic default='on' toggle='no'/> |
945 | + </features> |
946 | + </guest> |
947 | + |
948 | + <guest> |
949 | + <os_type>hvm</os_type> |
950 | + <arch name='arm'> |
951 | + <wordsize>32</wordsize> |
952 | + <emulator>/usr/bin/qemu-system-arm</emulator> |
953 | + <machine>integratorcp</machine> |
954 | + <machine>vexpress-a9</machine> |
955 | + <machine>syborg</machine> |
956 | + <machine>musicpal</machine> |
957 | + <machine>mainstone</machine> |
958 | + <machine>n800</machine> |
959 | + <machine>n810</machine> |
960 | + <machine>n900</machine> |
961 | + <machine>cheetah</machine> |
962 | + <machine>sx1</machine> |
963 | + <machine>sx1-v1</machine> |
964 | + <machine>beagle</machine> |
965 | + <machine>beaglexm</machine> |
966 | + <machine>tosa</machine> |
967 | + <machine>akita</machine> |
968 | + <machine>spitz</machine> |
969 | + <machine>borzoi</machine> |
970 | + <machine>terrier</machine> |
971 | + <machine>connex</machine> |
972 | + <machine>verdex</machine> |
973 | + <machine>lm3s811evb</machine> |
974 | + <machine>lm3s6965evb</machine> |
975 | + <machine>realview-eb</machine> |
976 | + <machine>realview-eb-mpcore</machine> |
977 | + <machine>realview-pb-a8</machine> |
978 | + <machine>realview-pbx-a9</machine> |
979 | + <machine>versatilepb</machine> |
980 | + <machine>versatileab</machine> |
981 | + <domain type='qemu'> |
982 | + </domain> |
983 | + </arch> |
984 | + <features> |
985 | + <deviceboot/> |
986 | + </features> |
987 | + </guest> |
988 | + |
989 | + <guest> |
990 | + <os_type>hvm</os_type> |
991 | + <arch name='mips'> |
992 | + <wordsize>32</wordsize> |
993 | + <emulator>/usr/bin/qemu-system-mips</emulator> |
994 | + <machine>malta</machine> |
995 | + <machine>mipssim</machine> |
996 | + <machine>magnum</machine> |
997 | + <machine>pica61</machine> |
998 | + <machine>mips</machine> |
999 | + <domain type='qemu'> |
1000 | + </domain> |
1001 | + </arch> |
1002 | + <features> |
1003 | + <deviceboot/> |
1004 | + </features> |
1005 | + </guest> |
1006 | + |
1007 | + <guest> |
1008 | + <os_type>hvm</os_type> |
1009 | + <arch name='mipsel'> |
1010 | + <wordsize>32</wordsize> |
1011 | + <emulator>/usr/bin/qemu-system-mipsel</emulator> |
1012 | + <machine>malta</machine> |
1013 | + <machine>mipssim</machine> |
1014 | + <machine>magnum</machine> |
1015 | + <machine>pica61</machine> |
1016 | + <machine>mips</machine> |
1017 | + <domain type='qemu'> |
1018 | + </domain> |
1019 | + </arch> |
1020 | + <features> |
1021 | + <deviceboot/> |
1022 | + </features> |
1023 | + </guest> |
1024 | + |
1025 | + <guest> |
1026 | + <os_type>hvm</os_type> |
1027 | + <arch name='sparc'> |
1028 | + <wordsize>32</wordsize> |
1029 | + <emulator>/usr/bin/qemu-system-sparc</emulator> |
1030 | + <machine>SS-5</machine> |
1031 | + <machine>leon3_generic</machine> |
1032 | + <machine>SS-10</machine> |
1033 | + <machine>SS-600MP</machine> |
1034 | + <machine>SS-20</machine> |
1035 | + <machine>Voyager</machine> |
1036 | + <machine>LX</machine> |
1037 | + <machine>SS-4</machine> |
1038 | + <machine>SPARCClassic</machine> |
1039 | + <machine>SPARCbook</machine> |
1040 | + <machine>SS-1000</machine> |
1041 | + <machine>SS-2000</machine> |
1042 | + <machine>SS-2</machine> |
1043 | + <domain type='qemu'> |
1044 | + </domain> |
1045 | + </arch> |
1046 | + </guest> |
1047 | + |
1048 | + <guest> |
1049 | + <os_type>hvm</os_type> |
1050 | + <arch name='ppc'> |
1051 | + <wordsize>32</wordsize> |
1052 | + <emulator>/usr/bin/qemu-system-ppc</emulator> |
1053 | + <machine>g3beige</machine> |
1054 | + <machine>virtex-ml507</machine> |
1055 | + <machine>mpc8544ds</machine> |
1056 | + <machine canonical='bamboo-0.13'>bamboo</machine> |
1057 | + <machine>bamboo-0.13</machine> |
1058 | + <machine>bamboo-0.12</machine> |
1059 | + <machine>ref405ep</machine> |
1060 | + <machine>taihu</machine> |
1061 | + <machine>mac99</machine> |
1062 | + <machine>prep</machine> |
1063 | + <domain type='qemu'> |
1064 | + </domain> |
1065 | + </arch> |
1066 | + <features> |
1067 | + <deviceboot/> |
1068 | + </features> |
1069 | + </guest> |
1070 | + |
1071 | +</capabilities>''' |
1072 | + |
1073 | + def compareCPU(self, xml, flags): |
1074 | + tree = xml_to_tree(xml) |
1075 | + |
1076 | + arch_node = tree.find('./arch') |
1077 | + if arch_node is not None: |
1078 | + if arch_node.text not in ['x86_64', 'i686']: |
1079 | + return VIR_CPU_COMPARE_INCOMPATIBLE |
1080 | + |
1081 | + model_node = tree.find('./model') |
1082 | + if model_node is not None: |
1083 | + if model_node.text != node_cpu_model: |
1084 | + return VIR_CPU_COMPARE_INCOMPATIBLE |
1085 | + |
1086 | + vendor_node = tree.find('./vendor') |
1087 | + if vendor_node is not None: |
1088 | + if vendor_node.text != node_cpu_vendor: |
1089 | + return VIR_CPU_COMPARE_INCOMPATIBLE |
1090 | + |
1091 | + # The rest of the stuff libvirt implements is rather complicated |
1092 | + # and I don't think it adds much value to replicate it here. |
1093 | + |
1094 | + return VIR_CPU_COMPARE_IDENTICAL |
1095 | + |
1096 | + def nwfilterLookupByName(self, name): |
1097 | + try: |
1098 | + return self._nwfilters[name] |
1099 | + except KeyError: |
1100 | + raise libvirtError(VIR_ERR_NO_NWFILTER, VIR_FROM_NWFILTER, |
1101 | + "no nwfilter with matching name %s" % name) |
1102 | + |
1103 | + def nwfilterDefineXML(self, xml): |
1104 | + nwfilter = NWFilter(self, xml) |
1105 | + self._add_filter(nwfilter) |
1106 | + |
1107 | + |
1108 | +def openReadOnly(uri): |
1109 | + return Connection(uri, readonly=True) |
1110 | + |
1111 | + |
1112 | +def openAuth(uri, auth, flags): |
1113 | + if flags != 0: |
1114 | + raise Exception(_("Please extend mock libvirt module to support " |
1115 | + "flags")) |
1116 | + |
1117 | + if auth != [[VIR_CRED_AUTHNAME, VIR_CRED_NOECHOPROMPT], |
1118 | + 'root', |
1119 | + None]: |
1120 | + raise Exception(_("Please extend fake libvirt module to support " |
1121 | + "this auth method")) |
1122 | + |
1123 | + return Connection(uri, readonly=False) |
1124 | |
1125 | === added file 'nova/tests/test_fakelibvirt.py' |
1126 | --- nova/tests/test_fakelibvirt.py 1970-01-01 00:00:00 +0000 |
1127 | +++ nova/tests/test_fakelibvirt.py 2011-09-16 15:21:29 +0000 |
1128 | @@ -0,0 +1,391 @@ |
1129 | +# vim: tabstop=4 shiftwidth=4 softtabstop=4 |
1130 | +# |
1131 | +# Copyright 2010 OpenStack LLC |
1132 | +# |
1133 | +# Licensed under the Apache License, Version 2.0 (the "License"); you may |
1134 | +# not use this file except in compliance with the License. You may obtain |
1135 | +# a copy of the License at |
1136 | +# |
1137 | +# http://www.apache.org/licenses/LICENSE-2.0 |
1138 | +# |
1139 | +# Unless required by applicable law or agreed to in writing, software |
1140 | +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
1141 | +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
1142 | +# License for the specific language governing permissions and limitations |
1143 | +# under the License. |
1144 | + |
1145 | +from nova import test |
1146 | + |
1147 | +from xml.etree.ElementTree import fromstring as xml_to_tree |
1148 | + |
1149 | +import fakelibvirt as libvirt |
1150 | + |
1151 | + |
1152 | +def get_vm_xml(name="testname", uuid=None, source_type='file', |
1153 | + interface_type='bridge'): |
1154 | + uuid_tag = '' |
1155 | + if uuid: |
1156 | + uuid_tag = '<uuid>%s</uuid>' % (uuid,) |
1157 | + |
1158 | + return '''<domain type='kvm'> |
1159 | + <name>%(name)s</name> |
1160 | +%(uuid_tag)s |
1161 | + <memory>128000</memory> |
1162 | + <vcpu>1</vcpu> |
1163 | + <os> |
1164 | + <type>hvm</type> |
1165 | + <kernel>/somekernel</kernel> |
1166 | + <cmdline>root=/dev/sda</cmdline> |
1167 | + <boot dev='hd'/> |
1168 | + </os> |
1169 | + <features> |
1170 | + <acpi/> |
1171 | + </features> |
1172 | + <devices> |
1173 | + <disk type='file' device='disk'> |
1174 | + <driver name='qemu' type='qcow2'/> |
1175 | + <source %(source_type)s='/somefile'/> |
1176 | + <target dev='vda' bus='virtio'/> |
1177 | + </disk> |
1178 | + <interface type='%(interface_type)s'> |
1179 | + <mac address='05:26:3e:31:28:1f'/> |
1180 | + <source %(interface_type)s='br100'/> |
1181 | + </interface> |
1182 | + <input type='mouse' bus='ps2'/> |
1183 | + <graphics type='vnc' port='5901' autoport='yes' keymap='en-us'/> |
1184 | + </devices> |
1185 | +</domain>''' % {'name': name, |
1186 | + 'uuid_tag': uuid_tag, |
1187 | + 'source_type': source_type, |
1188 | + 'interface_type': interface_type} |
1189 | + |
1190 | + |
1191 | +class FakeLibvirtTests(test.TestCase): |
1192 | + def setUp(self): |
1193 | + super(FakeLibvirtTests, self).setUp() |
1194 | + libvirt._reset() |
1195 | + |
1196 | + def get_openReadOnly_curry_func(self): |
1197 | + return lambda uri: libvirt.openReadOnly(uri) |
1198 | + |
1199 | + def get_openAuth_curry_func(self): |
1200 | + return lambda uri: libvirt.openAuth(uri, |
1201 | + [[libvirt.VIR_CRED_AUTHNAME, |
1202 | + libvirt.VIR_CRED_NOECHOPROMPT], |
1203 | + 'root', |
1204 | + None], 0) |
1205 | + |
1206 | + def _test_connect_method_accepts_None_uri_by_default(self, conn_method): |
1207 | + conn = conn_method(None) |
1208 | + self.assertNotEqual(conn, None, "Connecting to fake libvirt failed") |
1209 | + |
1210 | + def test_openReadOnly_accepts_None_uri_by_default(self): |
1211 | + conn_method = self.get_openReadOnly_curry_func() |
1212 | + self._test_connect_method_accepts_None_uri_by_default(conn_method) |
1213 | + |
1214 | + def test_openAuth_accepts_None_uri_by_default(self): |
1215 | + conn_method = self.get_openAuth_curry_func() |
1216 | + self._test_connect_method_accepts_None_uri_by_default(conn_method) |
1217 | + |
1218 | + def _test_connect_method_can_refuse_None_uri(self, conn_method): |
1219 | + libvirt.allow_default_uri_connection = False |
1220 | + self.assertRaises(Exception, conn_method, None) |
1221 | + |
1222 | + def test_openReadOnly_can_refuse_None_uri(self): |
1223 | + conn_method = self.get_openReadOnly_curry_func() |
1224 | + self._test_connect_method_can_refuse_None_uri(conn_method) |
1225 | + |
1226 | + def test_openAuth_can_refuse_None_uri(self): |
1227 | + conn_method = self.get_openAuth_curry_func() |
1228 | + self._test_connect_method_can_refuse_None_uri(conn_method) |
1229 | + |
1230 | + def _test_connect_method_refuses_invalid_URI(self, conn_method): |
1231 | + self.assertRaises(libvirt.libvirtError, conn_method, 'blah') |
1232 | + |
1233 | + def test_openReadOnly_refuses_invalid_URI(self): |
1234 | + conn_method = self.get_openReadOnly_curry_func() |
1235 | + self._test_connect_method_refuses_invalid_URI(conn_method) |
1236 | + |
1237 | + def test_openAuth_refuses_invalid_URI(self): |
1238 | + conn_method = self.get_openAuth_curry_func() |
1239 | + self._test_connect_method_refuses_invalid_URI(conn_method) |
1240 | + |
1241 | + def test_getInfo(self): |
1242 | + conn = libvirt.openReadOnly(None) |
1243 | + res = conn.getInfo() |
1244 | + self.assertIn(res[0], ('i686', 'x86_64')) |
1245 | + self.assertTrue(1024 <= res[1] <= 16384, |
1246 | + "Memory unusually high or low.") |
1247 | + self.assertTrue(1 <= res[2] <= 32, |
1248 | + "Active CPU count unusually high or low.") |
1249 | + self.assertTrue(800 <= res[3] <= 4500, |
1250 | + "CPU speed unusually high or low.") |
1251 | + self.assertTrue(res[2] <= (res[5] * res[6]), |
1252 | + "More active CPUs than num_sockets*cores_per_socket") |
1253 | + |
1254 | + def test_createXML_detects_invalid_xml(self): |
1255 | + self._test_XML_func_detects_invalid_xml('createXML', [0]) |
1256 | + |
1257 | + def test_defineXML_detects_invalid_xml(self): |
1258 | + self._test_XML_func_detects_invalid_xml('defineXML', []) |
1259 | + |
1260 | + def _test_XML_func_detects_invalid_xml(self, xmlfunc_name, args): |
1261 | + conn = self.get_openAuth_curry_func()('qemu:///system') |
1262 | + try: |
1263 | + getattr(conn, xmlfunc_name)("this is not valid </xml>", *args) |
1264 | + except libvirt.libvirtError, e: |
1265 | + self.assertEqual(e.get_error_code(), libvirt.VIR_ERR_XML_DETAIL) |
1266 | + self.assertEqual(e.get_error_domain(), libvirt.VIR_FROM_DOMAIN) |
1267 | + return |
1268 | + raise self.failureException("Invalid XML didn't raise libvirtError") |
1269 | + |
1270 | + def test_defineXML_defines_domain(self): |
1271 | + conn = self.get_openAuth_curry_func()('qemu:///system') |
1272 | + conn.defineXML(get_vm_xml()) |
1273 | + dom = conn.lookupByName('testname') |
1274 | + self.assertEqual('testname', dom.name()) |
1275 | + self.assertEqual(0, dom.isActive()) |
1276 | + dom.undefine() |
1277 | + self.assertRaises(libvirt.libvirtError, |
1278 | + conn.lookupByName, |
1279 | + 'testname') |
1280 | + |
1281 | + def test_blockStats(self): |
1282 | + conn = self.get_openAuth_curry_func()('qemu:///system') |
1283 | + conn.createXML(get_vm_xml(), 0) |
1284 | + dom = conn.lookupByName('testname') |
1285 | + blockstats = dom.blockStats('vda') |
1286 | + self.assertEqual(len(blockstats), 5) |
1287 | + for x in blockstats: |
1288 | + self.assertTrue(type(x) in [int, long]) |
1289 | + |
1290 | + def test_attach_detach(self): |
1291 | + conn = self.get_openAuth_curry_func()('qemu:///system') |
1292 | + conn.createXML(get_vm_xml(), 0) |
1293 | + dom = conn.lookupByName('testname') |
1294 | + xml = '''<disk type='block'> |
1295 | + <driver name='qemu' type='raw'/> |
1296 | + <source dev='/dev/nbd0'/> |
1297 | + <target dev='/dev/vdc' bus='virtio'/> |
1298 | + </disk>''' |
1299 | + self.assertTrue(dom.attachDevice(xml)) |
1300 | + self.assertTrue(dom.detachDevice(xml)) |
1301 | + |
1302 | + def test_info(self): |
1303 | + conn = self.get_openAuth_curry_func()('qemu:///system') |
1304 | + conn.createXML(get_vm_xml(), 0) |
1305 | + dom = conn.lookupByName('testname') |
1306 | + info = dom.info() |
1307 | + self.assertEqual(info[0], libvirt.VIR_DOMAIN_RUNNING) |
1308 | + self.assertEqual(info[1], 128000) |
1309 | + self.assertTrue(info[2] <= 128000) |
1310 | + self.assertEqual(info[3], 1) |
1311 | + self.assertTrue(type(info[4]) in [int, long]) |
1312 | + |
1313 | + def test_createXML_runs_domain(self): |
1314 | + conn = self.get_openAuth_curry_func()('qemu:///system') |
1315 | + conn.createXML(get_vm_xml(), 0) |
1316 | + dom = conn.lookupByName('testname') |
1317 | + self.assertEqual('testname', dom.name()) |
1318 | + self.assertEqual(1, dom.isActive()) |
1319 | + dom.destroy() |
1320 | + try: |
1321 | + dom = conn.lookupByName('testname') |
1322 | + except libvirt.libvirtError as e: |
1323 | + self.assertEqual(e.get_error_code(), libvirt.VIR_ERR_NO_DOMAIN) |
1324 | + self.assertEqual(e.get_error_domain(), libvirt.VIR_FROM_QEMU) |
1325 | + return |
1326 | + self.fail("lookupByName succeeded for destroyed non-defined VM") |
1327 | + |
1328 | + def test_defineXML_remembers_uuid(self): |
1329 | + conn = self.get_openAuth_curry_func()('qemu:///system') |
1330 | + uuid = 'b21f957d-a72f-4b93-b5a5-45b1161abb02' |
1331 | + conn.defineXML(get_vm_xml(uuid=uuid)) |
1332 | + dom = conn.lookupByName('testname') |
1333 | + self.assertEquals(dom.UUIDString(), uuid) |
1334 | + |
1335 | + def test_createWithFlags(self): |
1336 | + conn = self.get_openAuth_curry_func()('qemu:///system') |
1337 | + conn.defineXML(get_vm_xml()) |
1338 | + dom = conn.lookupByName('testname') |
1339 | + self.assertFalse(dom.isActive(), 'Defined domain was running.') |
1340 | + dom.createWithFlags(0) |
1341 | + self.assertTrue(dom.isActive(), |
1342 | + 'Domain wasn\'t running after createWithFlags') |
1343 | + |
1344 | + def test_listDomainsId_and_lookupById(self): |
1345 | + conn = self.get_openAuth_curry_func()('qemu:///system') |
1346 | + self.assertEquals(conn.listDomainsID(), []) |
1347 | + conn.defineXML(get_vm_xml()) |
1348 | + dom = conn.lookupByName('testname') |
1349 | + dom.createWithFlags(0) |
1350 | + self.assertEquals(len(conn.listDomainsID()), 1) |
1351 | + |
1352 | + dom_id = conn.listDomainsID()[0] |
1353 | + self.assertEquals(conn.lookupByID(dom_id), dom) |
1354 | + |
1355 | + dom_id = conn.listDomainsID()[0] |
1356 | + try: |
1357 | + conn.lookupByID(dom_id + 1) |
1358 | + except libvirt.libvirtError, e: |
1359 | + self.assertEqual(e.get_error_code(), libvirt.VIR_ERR_NO_DOMAIN) |
1360 | + self.assertEqual(e.get_error_domain(), libvirt.VIR_FROM_QEMU) |
1361 | + return |
1362 | + raise self.failureException("Looking up an invalid domain ID didn't " |
1363 | + "raise libvirtError") |
1364 | + |
1365 | + def test_define_and_retrieve(self): |
1366 | + conn = self.get_openAuth_curry_func()('qemu:///system') |
1367 | + self.assertEquals(conn.listDomainsID(), []) |
1368 | + conn.defineXML(get_vm_xml()) |
1369 | + dom = conn.lookupByName('testname') |
1370 | + xml = dom.XMLDesc(0) |
1371 | + xml_to_tree(xml) |
1372 | + |
1373 | + def _test_accepts_source_type(self, source_type): |
1374 | + conn = self.get_openAuth_curry_func()('qemu:///system') |
1375 | + self.assertEquals(conn.listDomainsID(), []) |
1376 | + conn.defineXML(get_vm_xml(source_type=source_type)) |
1377 | + dom = conn.lookupByName('testname') |
1378 | + xml = dom.XMLDesc(0) |
1379 | + tree = xml_to_tree(xml) |
1380 | + elem = tree.find('./devices/disk/source') |
1381 | + self.assertEquals(elem.get('file'), '/somefile') |
1382 | + |
1383 | + def test_accepts_source_dev(self): |
1384 | + self._test_accepts_source_type('dev') |
1385 | + |
1386 | + def test_accepts_source_path(self): |
1387 | + self._test_accepts_source_type('path') |
1388 | + |
1389 | + def test_network_type_bridge_sticks(self): |
1390 | + self._test_network_type_sticks('bridge') |
1391 | + |
1392 | + def test_network_type_network_sticks(self): |
1393 | + self._test_network_type_sticks('network') |
1394 | + |
1395 | + def _test_network_type_sticks(self, network_type): |
1396 | + conn = self.get_openAuth_curry_func()('qemu:///system') |
1397 | + self.assertEquals(conn.listDomainsID(), []) |
1398 | + conn.defineXML(get_vm_xml(interface_type=network_type)) |
1399 | + dom = conn.lookupByName('testname') |
1400 | + xml = dom.XMLDesc(0) |
1401 | + tree = xml_to_tree(xml) |
1402 | + elem = tree.find('./devices/interface') |
1403 | + self.assertEquals(elem.get('type'), network_type) |
1404 | + elem = elem.find('./source') |
1405 | + self.assertEquals(elem.get(network_type), 'br100') |
1406 | + |
1407 | + def test_getType(self): |
1408 | + conn = self.get_openAuth_curry_func()('qemu:///system') |
1409 | + self.assertEquals(conn.getType(), 'QEMU') |
1410 | + |
1411 | + def test_getVersion(self): |
1412 | + conn = self.get_openAuth_curry_func()('qemu:///system') |
1413 | + self.assertTrue(type(conn.getVersion()) is int) |
1414 | + |
1415 | + def test_getCapabilities(self): |
1416 | + conn = self.get_openAuth_curry_func()('qemu:///system') |
1417 | + xml_to_tree(conn.getCapabilities()) |
1418 | + |
1419 | + def test_nwfilter_define_undefine(self): |
1420 | + conn = self.get_openAuth_curry_func()('qemu:///system') |
1421 | + # Will raise an exception if it's not valid XML |
1422 | + xml = '''<filter name='nova-instance-instance-789' chain='root'> |
1423 | + <uuid>946878c6-3ad3-82b2-87f3-c709f3807f58</uuid> |
1424 | + </filter>''' |
1425 | + |
1426 | + conn.nwfilterDefineXML(xml) |
1427 | + nwfilter = conn.nwfilterLookupByName('nova-instance-instance-789') |
1428 | + nwfilter.undefine() |
1429 | + try: |
1430 | + conn.nwfilterLookupByName('nova-instance-instance-789320334') |
1431 | + except libvirt.libvirtError, e: |
1432 | + self.assertEqual(e.get_error_code(), libvirt.VIR_ERR_NO_NWFILTER) |
1433 | + self.assertEqual(e.get_error_domain(), libvirt.VIR_FROM_NWFILTER) |
1434 | + return |
1435 | + raise self.failureException("Invalid NWFilter name didn't" |
1436 | + " raise libvirtError") |
1437 | + |
1438 | + def test_compareCPU_compatible(self): |
1439 | + conn = self.get_openAuth_curry_func()('qemu:///system') |
1440 | + |
1441 | + xml = '''<cpu> |
1442 | + <arch>%s</arch> |
1443 | + <model>%s</model> |
1444 | + <vendor>%s</vendor> |
1445 | + <topology sockets="%d" cores="%d" threads="%d"/> |
1446 | + </cpu>''' % (libvirt.node_arch, |
1447 | + libvirt.node_cpu_model, |
1448 | + libvirt.node_cpu_vendor, |
1449 | + libvirt.node_sockets, |
1450 | + libvirt.node_cores, |
1451 | + libvirt.node_threads) |
1452 | + self.assertEqual(conn.compareCPU(xml, 0), |
1453 | + libvirt.VIR_CPU_COMPARE_IDENTICAL) |
1454 | + |
1455 | + def test_compareCPU_incompatible_vendor(self): |
1456 | + conn = self.get_openAuth_curry_func()('qemu:///system') |
1457 | + |
1458 | + xml = '''<cpu> |
1459 | + <arch>%s</arch> |
1460 | + <model>%s</model> |
1461 | + <vendor>%s</vendor> |
1462 | + <topology sockets="%d" cores="%d" threads="%d"/> |
1463 | + </cpu>''' % (libvirt.node_arch, |
1464 | + libvirt.node_cpu_model, |
1465 | + "AnotherVendor", |
1466 | + libvirt.node_sockets, |
1467 | + libvirt.node_cores, |
1468 | + libvirt.node_threads) |
1469 | + self.assertEqual(conn.compareCPU(xml, 0), |
1470 | + libvirt.VIR_CPU_COMPARE_INCOMPATIBLE) |
1471 | + |
1472 | + def test_compareCPU_incompatible_arch(self): |
1473 | + conn = self.get_openAuth_curry_func()('qemu:///system') |
1474 | + |
1475 | + xml = '''<cpu> |
1476 | + <arch>%s</arch> |
1477 | + <model>%s</model> |
1478 | + <vendor>%s</vendor> |
1479 | + <topology sockets="%d" cores="%d" threads="%d"/> |
1480 | + </cpu>''' % ('not-a-valid-arch', |
1481 | + libvirt.node_cpu_model, |
1482 | + libvirt.node_cpu_vendor, |
1483 | + libvirt.node_sockets, |
1484 | + libvirt.node_cores, |
1485 | + libvirt.node_threads) |
1486 | + self.assertEqual(conn.compareCPU(xml, 0), |
1487 | + libvirt.VIR_CPU_COMPARE_INCOMPATIBLE) |
1488 | + |
1489 | + def test_compareCPU_incompatible_model(self): |
1490 | + conn = self.get_openAuth_curry_func()('qemu:///system') |
1491 | + |
1492 | + xml = '''<cpu> |
1493 | + <arch>%s</arch> |
1494 | + <model>%s</model> |
1495 | + <vendor>%s</vendor> |
1496 | + <topology sockets="%d" cores="%d" threads="%d"/> |
1497 | + </cpu>''' % (libvirt.node_arch, |
1498 | + "AnotherModel", |
1499 | + libvirt.node_cpu_vendor, |
1500 | + libvirt.node_sockets, |
1501 | + libvirt.node_cores, |
1502 | + libvirt.node_threads) |
1503 | + self.assertEqual(conn.compareCPU(xml, 0), |
1504 | + libvirt.VIR_CPU_COMPARE_INCOMPATIBLE) |
1505 | + |
1506 | + def test_compareCPU_compatible_unspecified_model(self): |
1507 | + conn = self.get_openAuth_curry_func()('qemu:///system') |
1508 | + |
1509 | + xml = '''<cpu> |
1510 | + <arch>%s</arch> |
1511 | + <vendor>%s</vendor> |
1512 | + <topology sockets="%d" cores="%d" threads="%d"/> |
1513 | + </cpu>''' % (libvirt.node_arch, |
1514 | + libvirt.node_cpu_vendor, |
1515 | + libvirt.node_sockets, |
1516 | + libvirt.node_cores, |
1517 | + libvirt.node_threads) |
1518 | + self.assertEqual(conn.compareCPU(xml, 0), |
1519 | + libvirt.VIR_CPU_COMPARE_IDENTICAL) |
1520 | |
1521 | === modified file 'nova/tests/test_image.py' |
1522 | --- nova/tests/test_image.py 2011-08-05 19:28:22 +0000 |
1523 | +++ nova/tests/test_image.py 2011-09-16 15:21:29 +0000 |
1524 | @@ -16,6 +16,7 @@ |
1525 | # under the License. |
1526 | |
1527 | import datetime |
1528 | +import StringIO |
1529 | |
1530 | from nova import context |
1531 | from nova import exception |
1532 | @@ -127,6 +128,16 @@ |
1533 | index = self.image_service.index(self.context) |
1534 | self.assertEquals(len(index), 0) |
1535 | |
1536 | + def test_create_then_get(self): |
1537 | + blob = 'some data' |
1538 | + s1 = StringIO.StringIO(blob) |
1539 | + self.image_service.create(self.context, |
1540 | + {'id': '32', 'foo': 'bar'}, |
1541 | + data=s1) |
1542 | + s2 = StringIO.StringIO() |
1543 | + self.image_service.get(self.context, '32', data=s2) |
1544 | + self.assertEquals(s2.getvalue(), blob, 'Did not get blob back intact') |
1545 | + |
1546 | |
1547 | class FakeImageTestCase(_ImageTestCase): |
1548 | def setUp(self): |
1549 | |
1550 | === modified file 'nova/tests/test_libvirt.py' |
1551 | --- nova/tests/test_libvirt.py 2011-09-14 16:15:09 +0000 |
1552 | +++ nova/tests/test_libvirt.py 2011-09-16 15:21:29 +0000 |
1553 | @@ -35,9 +35,12 @@ |
1554 | from nova.api.ec2 import cloud |
1555 | from nova.compute import power_state |
1556 | from nova.compute import vm_states |
1557 | +from nova.virt import disk |
1558 | +from nova.virt import images |
1559 | from nova.virt import driver |
1560 | from nova.virt.libvirt import connection |
1561 | from nova.virt.libvirt import firewall |
1562 | +from nova.virt.libvirt import utils as libvirt_utils |
1563 | from nova.tests import fake_network |
1564 | |
1565 | libvirt = None |
1566 | @@ -733,8 +736,8 @@ |
1567 | os.path.getsize("/test/disk").AndReturn(10 * 1024 * 1024 * 1024) |
1568 | # another is qcow image, so qemu-img should be mocked. |
1569 | self.mox.StubOutWithMock(utils, "execute") |
1570 | - utils.execute('qemu-img', 'info', '/test/disk.local').\ |
1571 | - AndReturn((ret, '')) |
1572 | + utils.execute('qemu-img', 'info', |
1573 | + '/test/disk.local').AndReturn((ret, '')) |
1574 | |
1575 | self.mox.ReplayAll() |
1576 | conn = connection.LibvirtConnection(False) |
1577 | @@ -1423,3 +1426,182 @@ |
1578 | self.assertEqual(original_filter_count - len(fakefilter.filters), 2) |
1579 | |
1580 | db.instance_destroy(admin_ctxt, instance_ref['id']) |
1581 | + |
1582 | + |
1583 | +class LibvirtUtilsTestCase(test.TestCase): |
1584 | + def test_create_image(self): |
1585 | + self.mox.StubOutWithMock(utils, 'execute') |
1586 | + utils.execute('qemu-img', 'create', '-f', 'raw', |
1587 | + '/some/path', '10G') |
1588 | + utils.execute('qemu-img', 'create', '-f', 'qcow2', |
1589 | + '/some/stuff', '1234567891234') |
1590 | + # Start test |
1591 | + self.mox.ReplayAll() |
1592 | + libvirt_utils.create_image('raw', '/some/path', '10G') |
1593 | + libvirt_utils.create_image('qcow2', '/some/stuff', '1234567891234') |
1594 | + |
1595 | + def test_create_cow_image(self): |
1596 | + self.mox.StubOutWithMock(utils, 'execute') |
1597 | + utils.execute('qemu-img', 'create', '-f', 'qcow2', |
1598 | + '-o', 'cluster_size=2M,backing_file=/some/path', |
1599 | + '/the/new/cow') |
1600 | + # Start test |
1601 | + self.mox.ReplayAll() |
1602 | + libvirt_utils.create_cow_image('/some/path', '/the/new/cow') |
1603 | + |
1604 | + def test_get_disk_size(self): |
1605 | + self.mox.StubOutWithMock(utils, 'execute') |
1606 | + utils.execute('qemu-img', |
1607 | + 'info', |
1608 | + '/some/path').AndReturn(('''image: 00000001 |
1609 | +file format: raw |
1610 | +virtual size: 4.4M (4592640 bytes) |
1611 | +disk size: 4.4M''', '')) |
1612 | + |
1613 | + # Start test |
1614 | + self.mox.ReplayAll() |
1615 | + self.assertEquals(libvirt_utils.get_disk_size('/some/path'), 4592640) |
1616 | + |
1617 | + def test_copy_image(self): |
1618 | + dst_fd, dst_path = tempfile.mkstemp() |
1619 | + try: |
1620 | + os.close(dst_fd) |
1621 | + |
1622 | + src_fd, src_path = tempfile.mkstemp() |
1623 | + try: |
1624 | + with os.fdopen(src_fd) as fp: |
1625 | + fp.write('canary') |
1626 | + |
1627 | + libvirt_utils.copy_image(src_path, dst_path) |
1628 | + with open(dst_path, 'r') as fp: |
1629 | + self.assertEquals(fp.read(), 'canary') |
1630 | + finally: |
1631 | + os.unlink(src_path) |
1632 | + finally: |
1633 | + os.unlink(dst_path) |
1634 | + |
1635 | + def test_mkfs(self): |
1636 | + self.mox.StubOutWithMock(utils, 'execute') |
1637 | + utils.execute('mkfs', '-t', 'ext4', '/my/block/dev') |
1638 | + utils.execute('mkswap', '/my/swap/block/dev') |
1639 | + self.mox.ReplayAll() |
1640 | + |
1641 | + libvirt_utils.mkfs('ext4', '/my/block/dev') |
1642 | + libvirt_utils.mkfs('swap', '/my/swap/block/dev') |
1643 | + |
1644 | + def test_ensure_tree(self): |
1645 | + tmpdir = tempfile.mkdtemp() |
1646 | + try: |
1647 | + testdir = '%s/foo/bar/baz' % (tmpdir,) |
1648 | + libvirt_utils.ensure_tree(testdir) |
1649 | + self.assertTrue(os.path.isdir(testdir)) |
1650 | + finally: |
1651 | + shutil.rmtree(tmpdir) |
1652 | + |
1653 | + def test_write_to_file(self): |
1654 | + dst_fd, dst_path = tempfile.mkstemp() |
1655 | + try: |
1656 | + os.close(dst_fd) |
1657 | + |
1658 | + libvirt_utils.write_to_file(dst_path, 'hello') |
1659 | + with open(dst_path, 'r') as fp: |
1660 | + self.assertEquals(fp.read(), 'hello') |
1661 | + finally: |
1662 | + os.unlink(dst_path) |
1663 | + |
1664 | + def test_write_to_file_with_umask(self): |
1665 | + dst_fd, dst_path = tempfile.mkstemp() |
1666 | + try: |
1667 | + os.close(dst_fd) |
1668 | + os.unlink(dst_path) |
1669 | + |
1670 | + libvirt_utils.write_to_file(dst_path, 'hello', umask=0277) |
1671 | + with open(dst_path, 'r') as fp: |
1672 | + self.assertEquals(fp.read(), 'hello') |
1673 | + mode = os.stat(dst_path).st_mode |
1674 | + self.assertEquals(mode & 0277, 0) |
1675 | + finally: |
1676 | + os.unlink(dst_path) |
1677 | + |
1678 | + def test_chown(self): |
1679 | + self.mox.StubOutWithMock(utils, 'execute') |
1680 | + utils.execute('chown', 'soren', '/some/path', run_as_root=True) |
1681 | + self.mox.ReplayAll() |
1682 | + libvirt_utils.chown('/some/path', 'soren') |
1683 | + |
1684 | + def test_extract_snapshot(self): |
1685 | + self.mox.StubOutWithMock(utils, 'execute') |
1686 | + utils.execute('qemu-img', 'convert', '-f', 'qcow2', '-O', 'raw', |
1687 | + '-s', 'snap1', '/path/to/disk/image', '/extracted/snap') |
1688 | + |
1689 | + # Start test |
1690 | + self.mox.ReplayAll() |
1691 | + libvirt_utils.extract_snapshot('/path/to/disk/image', |
1692 | + 'snap1', '/extracted/snap') |
1693 | + |
1694 | + def test_load_file(self): |
1695 | + dst_fd, dst_path = tempfile.mkstemp() |
1696 | + try: |
1697 | + os.close(dst_fd) |
1698 | + |
1699 | + # We have a test for write_to_file. If that is sound, this suffices |
1700 | + libvirt_utils.write_to_file(dst_path, 'hello') |
1701 | + self.assertEquals(libvirt_utils.load_file(dst_path), 'hello') |
1702 | + finally: |
1703 | + os.unlink(dst_path) |
1704 | + |
1705 | + def test_file_open(self): |
1706 | + dst_fd, dst_path = tempfile.mkstemp() |
1707 | + try: |
1708 | + os.close(dst_fd) |
1709 | + |
1710 | + # We have a test for write_to_file. If that is sound, this suffices |
1711 | + libvirt_utils.write_to_file(dst_path, 'hello') |
1712 | + with libvirt_utils.file_open(dst_path, 'r') as fp: |
1713 | + self.assertEquals(fp.read(), 'hello') |
1714 | + finally: |
1715 | + os.unlink(dst_path) |
1716 | + |
1717 | + def test_run_ajaxterm(self): |
1718 | + self.mox.StubOutWithMock(utils, 'execute') |
1719 | + token = 's3cr3tt0ken' |
1720 | + shell_cmd = 'shell-cmd.py' |
1721 | + port = 2048 |
1722 | + utils.execute(mox.IgnoreArg(), |
1723 | + '--command', shell_cmd, |
1724 | + '-t', token, |
1725 | + '-p', port) |
1726 | + |
1727 | + # Start test |
1728 | + self.mox.ReplayAll() |
1729 | + libvirt_utils.run_ajaxterm(shell_cmd, token, port) |
1730 | + |
1731 | + def test_get_fs_info(self): |
1732 | + stdout, stderr = utils.execute('df', '-B1', '/tmp') |
1733 | + info_line = stdout.split('\n')[1] |
1734 | + _dev, total, used, free, _percentage, _mntpnt = re.split('\s+', |
1735 | + info_line) |
1736 | + |
1737 | + fs_info = libvirt_utils.get_fs_info('/tmp') |
1738 | + self.assertEquals(int(total), fs_info['total']) |
1739 | + self.assertEquals(int(free), fs_info['free']) |
1740 | + self.assertEquals(int(used), fs_info['used']) |
1741 | + |
1742 | + def test_fetch_image(self): |
1743 | + self.mox.StubOutWithMock(images, 'fetch') |
1744 | + self.mox.StubOutWithMock(disk, 'extend') |
1745 | + |
1746 | + context = 'opaque context' |
1747 | + target = '/tmp/targetfile' |
1748 | + image_id = '4' |
1749 | + user_id = 'fake' |
1750 | + project_id = 'fake' |
1751 | + images.fetch(context, image_id, target, user_id, project_id) |
1752 | + images.fetch(context, image_id, target, user_id, project_id) |
1753 | + disk.extend(target, '10G') |
1754 | + |
1755 | + self.mox.ReplayAll() |
1756 | + libvirt_utils.fetch_image(context, target, image_id, |
1757 | + user_id, project_id) |
1758 | + libvirt_utils.fetch_image(context, target, image_id, |
1759 | + user_id, project_id, size='10G') |
1760 | |
1761 | === modified file 'nova/tests/test_virt_drivers.py' |
1762 | --- nova/tests/test_virt_drivers.py 2011-09-08 21:10:03 +0000 |
1763 | +++ nova/tests/test_virt_drivers.py 2011-09-16 15:21:29 +0000 |
1764 | @@ -16,6 +16,7 @@ |
1765 | |
1766 | import base64 |
1767 | import netaddr |
1768 | +import os |
1769 | import sys |
1770 | import traceback |
1771 | |
1772 | @@ -24,6 +25,7 @@ |
1773 | from nova import image |
1774 | from nova import log as logging |
1775 | from nova import test |
1776 | +from nova import utils |
1777 | from nova.tests import utils as test_utils |
1778 | |
1779 | libvirt = None |
1780 | @@ -484,7 +486,42 @@ |
1781 | self.driver_module = nova.virt.fake |
1782 | super(FakeConnectionTestCase, self).setUp() |
1783 | |
1784 | -# Before long, we'll add the real hypervisor drivers here as well |
1785 | -# with whatever instrumentation they need to work independently of |
1786 | -# their hypervisor. This way, we can verify that they all act the |
1787 | -# same. |
1788 | + |
1789 | +class LibvirtConnTestCase(_VirtDriverTestCase): |
1790 | + def setUp(self): |
1791 | + # Put fakelibvirt in place |
1792 | + if 'libvirt' in sys.modules: |
1793 | + self.saved_libvirt = sys.modules['libvirt'] |
1794 | + else: |
1795 | + self.saved_libvirt = None |
1796 | + |
1797 | + import fakelibvirt |
1798 | + import fake_libvirt_utils |
1799 | + |
1800 | + sys.modules['libvirt'] = fakelibvirt |
1801 | + |
1802 | + import nova.virt.libvirt.connection |
1803 | + import nova.virt.libvirt.firewall |
1804 | + |
1805 | + nova.virt.libvirt.connection.libvirt = fakelibvirt |
1806 | + nova.virt.libvirt.connection.libvirt_utils = fake_libvirt_utils |
1807 | + nova.virt.libvirt.firewall.libvirt = fakelibvirt |
1808 | + |
1809 | + # Point _VirtDriverTestCase at the right module |
1810 | + self.driver_module = nova.virt.libvirt.connection |
1811 | + super(LibvirtConnTestCase, self).setUp() |
1812 | + FLAGS.rescue_image_id = "2" |
1813 | + FLAGS.rescue_kernel_id = "3" |
1814 | + FLAGS.rescue_ramdisk_id = None |
1815 | + |
1816 | + def tearDown(self): |
1817 | + super(LibvirtConnTestCase, self).setUp() |
1818 | + |
1819 | + # Restore libvirt |
1820 | + import nova.virt.libvirt.connection |
1821 | + import nova.virt.libvirt.firewall |
1822 | + if self.saved_libvirt: |
1823 | + sys.modules['libvirt'] = self.saved_libvirt |
1824 | + nova.virt.libvirt.connection.libvirt = self.saved_libvirt |
1825 | + nova.virt.libvirt.connection.libvirt_utils = self.saved_libvirt |
1826 | + nova.virt.libvirt.firewall.libvirt = self.saved_libvirt |
1827 | |
1828 | === modified file 'nova/virt/libvirt/connection.py' |
1829 | --- nova/virt/libvirt/connection.py 2011-09-16 01:57:03 +0000 |
1830 | +++ nova/virt/libvirt/connection.py 2011-09-16 15:21:29 +0000 |
1831 | @@ -70,7 +70,7 @@ |
1832 | from nova.virt import disk |
1833 | from nova.virt import driver |
1834 | from nova.virt import images |
1835 | -from nova.virt.libvirt import netutils |
1836 | +from nova.virt.libvirt import utils as libvirt_utils |
1837 | |
1838 | |
1839 | libvirt = None |
1840 | @@ -125,8 +125,6 @@ |
1841 | 'Define block migration behavior.') |
1842 | flags.DEFINE_integer('live_migration_bandwidth', 0, |
1843 | 'Define live migration behavior') |
1844 | -flags.DEFINE_string('qemu_img', 'qemu-img', |
1845 | - 'binary to use for qemu-img commands') |
1846 | flags.DEFINE_string('libvirt_vif_type', 'bridge', |
1847 | 'Type of VIF to create.') |
1848 | flags.DEFINE_string('libvirt_vif_driver', |
1849 | @@ -396,7 +394,10 @@ |
1850 | to support this command. |
1851 | |
1852 | """ |
1853 | - virt_dom = self._lookup_by_name(instance['name']) |
1854 | + try: |
1855 | + virt_dom = self._lookup_by_name(instance['name']) |
1856 | + except exception.InstanceNotFound: |
1857 | + raise exception.InstanceNotRunning() |
1858 | |
1859 | (image_service, image_id) = nova.image.get_image_service( |
1860 | context, instance['image_ref']) |
1861 | @@ -444,20 +445,10 @@ |
1862 | # Export the snapshot to a raw image |
1863 | temp_dir = tempfile.mkdtemp() |
1864 | out_path = os.path.join(temp_dir, snapshot_name) |
1865 | - qemu_img_cmd = (FLAGS.qemu_img, |
1866 | - 'convert', |
1867 | - '-f', |
1868 | - 'qcow2', |
1869 | - '-O', |
1870 | - 'raw', |
1871 | - '-s', |
1872 | - snapshot_name, |
1873 | - disk_path, |
1874 | - out_path) |
1875 | - utils.execute(*qemu_img_cmd) |
1876 | + libvirt_utils.extract_snapshot(disk_path, snapshot_name, out_path) |
1877 | |
1878 | # Upload that image to the image service |
1879 | - with open(out_path) as image_file: |
1880 | + with libvirt_utils.file_open(out_path) as image_file: |
1881 | image_service.update(context, |
1882 | image_href, |
1883 | metadata, |
1884 | @@ -549,7 +540,8 @@ |
1885 | rescue_images = {'image_id': FLAGS.rescue_image_id, |
1886 | 'kernel_id': FLAGS.rescue_kernel_id, |
1887 | 'ramdisk_id': FLAGS.rescue_ramdisk_id} |
1888 | - self._create_image(context, instance, xml, '.rescue', rescue_images) |
1889 | + self._create_image(context, instance, xml, '.rescue', rescue_images, |
1890 | + network_info=network_info) |
1891 | self._create_new_domain(xml) |
1892 | |
1893 | def _wait_for_rescue(): |
1894 | @@ -572,14 +564,14 @@ |
1895 | return timer.start(interval=0.5, now=True) |
1896 | |
1897 | @exception.wrap_exception() |
1898 | - def unrescue(self, instance, network_info): |
1899 | + def unrescue(self, instance, callback, network_info): |
1900 | """Reboot the VM which is being rescued back into primary images. |
1901 | |
1902 | Because reboot destroys and re-creates instances, unresue should |
1903 | simply call reboot. |
1904 | |
1905 | """ |
1906 | - self.reboot(instance, network_info) |
1907 | + self.reboot(instance, network_info, 'SOFT') |
1908 | |
1909 | @exception.wrap_exception() |
1910 | def poll_rescued_instances(self, timeout): |
1911 | @@ -641,18 +633,12 @@ |
1912 | fp.write(data) |
1913 | return fpath |
1914 | |
1915 | - def _dump_file(self, fpath): |
1916 | - fp = open(fpath, 'r+') |
1917 | - contents = fp.read() |
1918 | - LOG.info(_('Contents of file %(fpath)s: %(contents)r') % locals()) |
1919 | - return contents |
1920 | - |
1921 | @exception.wrap_exception() |
1922 | def get_console_output(self, instance): |
1923 | console_log = os.path.join(FLAGS.instances_path, instance['name'], |
1924 | 'console.log') |
1925 | |
1926 | - utils.execute('chown', os.getuid(), console_log, run_as_root=True) |
1927 | + libvirt_utils.chown(console_log, os.getuid()) |
1928 | |
1929 | if FLAGS.libvirt_type == 'xen': |
1930 | # Xen is special |
1931 | @@ -666,23 +652,10 @@ |
1932 | else: |
1933 | fpath = console_log |
1934 | |
1935 | - return self._dump_file(fpath) |
1936 | + return libvirt_utils.load_file(fpath) |
1937 | |
1938 | @exception.wrap_exception() |
1939 | def get_ajax_console(self, instance): |
1940 | - def get_open_port(): |
1941 | - start_port, end_port = FLAGS.ajaxterm_portrange.split("-") |
1942 | - for i in xrange(0, 100): # don't loop forever |
1943 | - port = random.randint(int(start_port), int(end_port)) |
1944 | - # netcat will exit with 0 only if the port is in use, |
1945 | - # so a nonzero return value implies it is unused |
1946 | - cmd = 'netcat', '0.0.0.0', port, '-w', '1' |
1947 | - try: |
1948 | - stdout, stderr = utils.execute(*cmd, process_input='') |
1949 | - except exception.ProcessExecutionError: |
1950 | - return port |
1951 | - raise Exception(_('Unable to find an open port')) |
1952 | - |
1953 | def get_pty_for_instance(instance_name): |
1954 | virt_dom = self._lookup_by_name(instance_name) |
1955 | xml = virt_dom.XMLDesc(0) |
1956 | @@ -693,17 +666,15 @@ |
1957 | source = serial.getElementsByTagName('source')[0] |
1958 | return source.getAttribute('path') |
1959 | |
1960 | - port = get_open_port() |
1961 | + start_port, end_port = FLAGS.ajaxterm_portrange.split("-") |
1962 | + port = libvirt_utils.get_open_port(int(start_port), int(end_port)) |
1963 | token = str(uuid.uuid4()) |
1964 | host = instance['host'] |
1965 | |
1966 | ajaxterm_cmd = 'sudo socat - %s' \ |
1967 | % get_pty_for_instance(instance['name']) |
1968 | |
1969 | - cmd = ['%s/tools/ajaxterm/ajaxterm.py' % utils.novadir(), |
1970 | - '--command', ajaxterm_cmd, '-t', token, '-p', port] |
1971 | - |
1972 | - utils.execute(cmd) |
1973 | + libvirt_utils.run_ajaxterm(ajaxterm_cmd, token, port) |
1974 | return {'token': token, 'host': host, 'port': port} |
1975 | |
1976 | def get_host_ip_addr(self): |
1977 | @@ -746,7 +717,7 @@ |
1978 | if not os.path.exists(target): |
1979 | base_dir = os.path.join(FLAGS.instances_path, '_base') |
1980 | if not os.path.exists(base_dir): |
1981 | - os.mkdir(base_dir) |
1982 | + libvirt_utils.ensure_tree(base_dir) |
1983 | base = os.path.join(base_dir, fname) |
1984 | |
1985 | @utils.synchronized(fname) |
1986 | @@ -757,18 +728,9 @@ |
1987 | call_if_not_exists(base, fn, *args, **kwargs) |
1988 | |
1989 | if cow: |
1990 | - utils.execute('qemu-img', 'create', '-f', 'qcow2', '-o', |
1991 | - 'cluster_size=2M,backing_file=%s' % base, |
1992 | - target) |
1993 | + libvirt_utils.create_cow_image(base, target) |
1994 | else: |
1995 | - utils.execute('cp', base, target) |
1996 | - |
1997 | - def _fetch_image(self, context, target, image_id, user_id, project_id, |
1998 | - size=None): |
1999 | - """Grab image and optionally attempt to resize it""" |
2000 | - images.fetch(context, image_id, target, user_id, project_id) |
2001 | - if size: |
2002 | - disk.extend(target, size) |
2003 | + libvirt_utils.copy_image(base, target) |
2004 | |
2005 | def _create_local(self, target, local_size, prefix='G', fs_format=None): |
2006 | """Create a blank image of specified size""" |
2007 | @@ -776,9 +738,10 @@ |
2008 | if not fs_format: |
2009 | fs_format = FLAGS.default_local_format |
2010 | |
2011 | - utils.execute('truncate', target, '-s', "%d%c" % (local_size, prefix)) |
2012 | + libvirt_utils.create_image('raw', target, |
2013 | + '%d%c' % (local_size, prefix)) |
2014 | if fs_format: |
2015 | - utils.execute('mkfs', '-t', fs_format, target) |
2016 | + libvirt_utils.mkfs(fs_format, target) |
2017 | |
2018 | def _create_ephemeral(self, target, local_size, fs_label, os_type): |
2019 | self._create_local(target, local_size) |
2020 | @@ -786,8 +749,8 @@ |
2021 | |
2022 | def _create_swap(self, target, swap_gb): |
2023 | """Create a swap file of specified size""" |
2024 | - self._create_local(target, swap_gb) |
2025 | - utils.execute('mkswap', target) |
2026 | + libvirt_utils.create_image('raw', target, '%dG' % swap_gb) |
2027 | + libvirt_utils.mkfs(target, 'swap') |
2028 | |
2029 | def _create_image(self, context, inst, libvirt_xml, suffix='', |
2030 | disk_images=None, network_info=None, |
2031 | @@ -802,20 +765,17 @@ |
2032 | fname + suffix) |
2033 | |
2034 | # ensure directories exist and are writable |
2035 | - utils.execute('mkdir', '-p', basepath(suffix='')) |
2036 | + libvirt_utils.ensure_tree(basepath(suffix='')) |
2037 | |
2038 | LOG.info(_('instance %s: Creating image'), inst['name']) |
2039 | - f = open(basepath('libvirt.xml'), 'w') |
2040 | - f.write(libvirt_xml) |
2041 | - f.close() |
2042 | + libvirt_utils.write_to_file(basepath('libvirt.xml'), libvirt_xml) |
2043 | |
2044 | if FLAGS.libvirt_type == 'lxc': |
2045 | container_dir = '%s/rootfs' % basepath(suffix='') |
2046 | - utils.execute('mkdir', '-p', container_dir) |
2047 | + libvirt_utils.ensure_tree(container_dir) |
2048 | |
2049 | # NOTE(vish): No need add the suffix to console.log |
2050 | - os.close(os.open(basepath('console.log', ''), |
2051 | - os.O_CREAT | os.O_WRONLY, 0660)) |
2052 | + libvirt_utils.write_to_file(basepath('console.log', ''), '', 007) |
2053 | |
2054 | if not disk_images: |
2055 | disk_images = {'image_id': inst['image_ref'], |
2056 | @@ -824,7 +784,7 @@ |
2057 | |
2058 | if disk_images['kernel_id']: |
2059 | fname = '%08x' % int(disk_images['kernel_id']) |
2060 | - self._cache_image(fn=self._fetch_image, |
2061 | + self._cache_image(fn=libvirt_utils.fetch_image, |
2062 | context=context, |
2063 | target=basepath('kernel'), |
2064 | fname=fname, |
2065 | @@ -833,7 +793,7 @@ |
2066 | project_id=inst['project_id']) |
2067 | if disk_images['ramdisk_id']: |
2068 | fname = '%08x' % int(disk_images['ramdisk_id']) |
2069 | - self._cache_image(fn=self._fetch_image, |
2070 | + self._cache_image(fn=libvirt_utils.fetch_image, |
2071 | context=context, |
2072 | target=basepath('ramdisk'), |
2073 | fname=fname, |
2074 | @@ -852,7 +812,7 @@ |
2075 | |
2076 | if not self._volume_in_mapping(self.default_root_device, |
2077 | block_device_info): |
2078 | - self._cache_image(fn=self._fetch_image, |
2079 | + self._cache_image(fn=libvirt_utils.fetch_image, |
2080 | context=context, |
2081 | target=basepath('disk'), |
2082 | fname=root_fname, |
2083 | @@ -914,7 +874,7 @@ |
2084 | |
2085 | if config_drive_id: |
2086 | fname = '%08x' % int(config_drive_id) |
2087 | - self._cache_image(fn=self._fetch_image, |
2088 | + self._cache_image(fn=libvirt_utils.fetch_image, |
2089 | target=basepath('disk.config'), |
2090 | fname=fname, |
2091 | image_id=config_drive_id, |
2092 | @@ -1001,7 +961,7 @@ |
2093 | ' data into image %(img_id)s (%(e)s)') % locals()) |
2094 | |
2095 | if FLAGS.libvirt_type == 'uml': |
2096 | - utils.execute('chown', 'root', basepath('disk'), run_as_root=True) |
2097 | + libvirt_utils.chown(basepath('disk'), 'root') |
2098 | |
2099 | if FLAGS.libvirt_type == 'uml': |
2100 | _disk_prefix = 'ubd' |
2101 | @@ -1208,10 +1168,6 @@ |
2102 | |
2103 | return domain |
2104 | |
2105 | - def get_diagnostics(self, instance_name): |
2106 | - raise exception.ApiError(_("diagnostics are not supported " |
2107 | - "for libvirt")) |
2108 | - |
2109 | def get_disks(self, instance_name): |
2110 | """ |
2111 | Note that this function takes an instance name. |
2112 | @@ -1335,8 +1291,8 @@ |
2113 | |
2114 | """ |
2115 | |
2116 | - hddinfo = os.statvfs(FLAGS.instances_path) |
2117 | - return hddinfo.f_frsize * hddinfo.f_blocks / 1024 / 1024 / 1024 |
2118 | + stats = libvirt_utils.get_fs_info(FLAGS.instances_path) |
2119 | + return stats['total'] / (1024 ** 3) |
2120 | |
2121 | def get_vcpu_used(self): |
2122 | """ Get vcpu usage number of physical computer. |
2123 | @@ -1378,9 +1334,8 @@ |
2124 | |
2125 | """ |
2126 | |
2127 | - hddinfo = os.statvfs(FLAGS.instances_path) |
2128 | - avail = hddinfo.f_frsize * hddinfo.f_bavail / 1024 / 1024 / 1024 |
2129 | - return self.get_local_gb_total() - avail |
2130 | + stats = libvirt_utils.get_fs_info(FLAGS.instances_path) |
2131 | + return stats['used'] / (1024 ** 3) |
2132 | |
2133 | def get_hypervisor_type(self): |
2134 | """Get hypervisor type. |
2135 | @@ -1714,8 +1669,8 @@ |
2136 | base = os.path.basename(info['path']) |
2137 | # Get image type and create empty disk image. |
2138 | instance_disk = os.path.join(instance_dir, base) |
2139 | - utils.execute('qemu-img', 'create', '-f', info['type'], |
2140 | - instance_disk, info['local_gb']) |
2141 | + libvirt_utils.create_image(info['type'], instance_disk, |
2142 | + info['local_gb']) |
2143 | |
2144 | # if image has kernel and ramdisk, just download |
2145 | # following normal way. |
2146 | @@ -1723,13 +1678,13 @@ |
2147 | user = manager.AuthManager().get_user(instance_ref['user_id']) |
2148 | project = manager.AuthManager().get_project( |
2149 | instance_ref['project_id']) |
2150 | - self._fetch_image(nova_context.get_admin_context(), |
2151 | + libvirt_utils.fetch_image(nova_context.get_admin_context(), |
2152 | os.path.join(instance_dir, 'kernel'), |
2153 | instance_ref['kernel_id'], |
2154 | user, |
2155 | project) |
2156 | if instance_ref['ramdisk_id']: |
2157 | - self._fetch_image(nova_context.get_admin_context(), |
2158 | + libvirt_utils.fetch_image(nova_context.get_admin_context(), |
2159 | os.path.join(instance_dir, 'ramdisk'), |
2160 | instance_ref['ramdisk_id'], |
2161 | user, |
2162 | @@ -1806,10 +1761,7 @@ |
2163 | if disk_type == 'raw': |
2164 | size = int(os.path.getsize(path)) |
2165 | else: |
2166 | - out, err = utils.execute('qemu-img', 'info', path) |
2167 | - size = [i.split('(')[1].split()[0] for i in out.split('\n') |
2168 | - if i.strip().find('virtual size') >= 0] |
2169 | - size = int(size[0]) |
2170 | + size = libvirt_utils.get_disk_size(path) |
2171 | |
2172 | # block migration needs same/larger size of empty image on the |
2173 | # destination host. since qemu-img creates bit smaller size image |
2174 | @@ -1835,18 +1787,10 @@ |
2175 | self.firewall_driver.unfilter_instance(instance_ref, |
2176 | network_info=network_info) |
2177 | |
2178 | - def update_host_status(self): |
2179 | - """See xenapi_conn.py implementation.""" |
2180 | - pass |
2181 | - |
2182 | - def get_host_stats(self, refresh=False): |
2183 | - """See xenapi_conn.py implementation.""" |
2184 | - pass |
2185 | - |
2186 | def host_power_action(self, host, action): |
2187 | """Reboots, shuts down or powers up the host.""" |
2188 | - pass |
2189 | + raise NotImplementedError() |
2190 | |
2191 | def set_host_enabled(self, host, enabled): |
2192 | """Sets the specified host's ability to accept new instances.""" |
2193 | - pass |
2194 | + raise NotImplementedError() |
2195 | |
2196 | === added file 'nova/virt/libvirt/utils.py' |
2197 | --- nova/virt/libvirt/utils.py 1970-01-01 00:00:00 +0000 |
2198 | +++ nova/virt/libvirt/utils.py 2011-09-16 15:21:29 +0000 |
2199 | @@ -0,0 +1,229 @@ |
2200 | +# vim: tabstop=4 shiftwidth=4 softtabstop=4 |
2201 | + |
2202 | +# Copyright 2010 United States Government as represented by the |
2203 | +# Administrator of the National Aeronautics and Space Administration. |
2204 | +# All Rights Reserved. |
2205 | +# Copyright (c) 2010 Citrix Systems, Inc. |
2206 | +# Copyright (c) 2011 Piston Cloud Computing, Inc |
2207 | +# Copyright (c) 2011 OpenStack LLC |
2208 | +# |
2209 | +# Licensed under the Apache License, Version 2.0 (the "License"); you may |
2210 | +# not use this file except in compliance with the License. You may obtain |
2211 | +# a copy of the License at |
2212 | +# |
2213 | +# http://www.apache.org/licenses/LICENSE-2.0 |
2214 | +# |
2215 | +# Unless required by applicable law or agreed to in writing, software |
2216 | +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
2217 | +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
2218 | +# License for the specific language governing permissions and limitations |
2219 | +# under the License. |
2220 | + |
2221 | +import os |
2222 | +import random |
2223 | + |
2224 | +from nova import exception |
2225 | +from nova import flags |
2226 | +from nova import utils |
2227 | +from nova.virt import disk |
2228 | +from nova.virt import images |
2229 | + |
2230 | +FLAGS = flags.FLAGS |
2231 | + |
2232 | +flags.DEFINE_string('qemu_img', 'qemu-img', |
2233 | + 'binary to use for qemu-img commands') |
2234 | + |
2235 | + |
2236 | +def execute(*args, **kwargs): |
2237 | + return utils.execute(*args, **kwargs) |
2238 | + |
2239 | + |
2240 | +def create_image(disk_format, path, size): |
2241 | + """Create a disk image |
2242 | + |
2243 | + :param disk_format: Disk image format (as known by qemu-img) |
2244 | + :param path: Desired location of the disk image |
2245 | + :param size: Desired size of disk image. May be given as an int or |
2246 | + a string. If given as an int, it will be interpreted |
2247 | + as bytes. If it's a string, it should consist of a number |
2248 | + followed by an optional prefix ('k' for kilobytes, 'm' |
2249 | + for megabytes, 'g' for gigabytes, 't' for terabytes). If no |
2250 | + prefix is given, it will be interpreted as bytes. |
2251 | + """ |
2252 | + execute(FLAGS.qemu_img, 'create', '-f', disk_format, path, size) |
2253 | + |
2254 | + |
2255 | +def create_cow_image(backing_file, path): |
2256 | + """Create COW image |
2257 | + |
2258 | + Creates a COW image with the given backing file |
2259 | + |
2260 | + :param backing_file: Existing image on which to base the COW image |
2261 | + :param path: Desired location of the COW image |
2262 | + """ |
2263 | + execute(FLAGS.qemu_img, 'create', '-f', 'qcow2', '-o', |
2264 | + 'cluster_size=2M,backing_file=%s' % backing_file, path) |
2265 | + |
2266 | + |
2267 | +def get_disk_size(path): |
2268 | + """Get the (virtual) size of a disk image |
2269 | + |
2270 | + :param path: Path to the disk image |
2271 | + :returns: Size (in bytes) of the given disk image as it would be seen |
2272 | + by a virtual machine. |
2273 | + """ |
2274 | + out, err = execute(FLAGS.qemu_img, 'info', path) |
2275 | + size = [i.split('(')[1].split()[0] for i in out.split('\n') |
2276 | + if i.strip().find('virtual size') >= 0] |
2277 | + return int(size[0]) |
2278 | + |
2279 | + |
2280 | +def copy_image(src, dest): |
2281 | + """Copy a disk image |
2282 | + |
2283 | + :param src: Source image |
2284 | + :param dest: Destination path |
2285 | + """ |
2286 | + execute('cp', src, dest) |
2287 | + |
2288 | + |
2289 | +def mkfs(fs, path): |
2290 | + """Format a file or block device |
2291 | + |
2292 | + :param fs: Filesystem type (examples include 'swap', 'ext3', 'ext4' |
2293 | + 'btrfs', etc.) |
2294 | + :param path: Path to file or block device to format |
2295 | + """ |
2296 | + if fs == 'swap': |
2297 | + execute('mkswap', path) |
2298 | + else: |
2299 | + execute('mkfs', '-t', fs, path) |
2300 | + |
2301 | + |
2302 | +def ensure_tree(path): |
2303 | + """Create a directory (and any ancestor directories required) |
2304 | + |
2305 | + :param path: Directory to create |
2306 | + """ |
2307 | + execute('mkdir', '-p', path) |
2308 | + |
2309 | + |
2310 | +def write_to_file(path, contents, umask=None): |
2311 | + """Write the given contents to a file |
2312 | + |
2313 | + :param path: Destination file |
2314 | + :param contents: Desired contents of the file |
2315 | + :param umask: Umask to set when creating this file (will be reset) |
2316 | + """ |
2317 | + if umask: |
2318 | + saved_umask = os.umask(umask) |
2319 | + |
2320 | + try: |
2321 | + with open(path, 'w') as f: |
2322 | + f.write(contents) |
2323 | + finally: |
2324 | + if umask: |
2325 | + os.umask(saved_umask) |
2326 | + |
2327 | + |
2328 | +def chown(path, owner): |
2329 | + """Change ownership of file or directory |
2330 | + |
2331 | + :param path: File or directory whose ownership to change |
2332 | + :param owner: Desired new owner (given as uid or username) |
2333 | + """ |
2334 | + utils.execute('chown', owner, path, run_as_root=True) |
2335 | + |
2336 | + |
2337 | +def extract_snapshot(disk_path, snapshot_name, out_path): |
2338 | + """Extract a named snapshot from a disk image |
2339 | + |
2340 | + :param disk_path: Path to disk image |
2341 | + :param snapshot_name: Name of snapshot in disk image |
2342 | + :param out_path: Desired path of extracted snapshot |
2343 | + """ |
2344 | + qemu_img_cmd = (FLAGS.qemu_img, |
2345 | + 'convert', |
2346 | + '-f', |
2347 | + 'qcow2', |
2348 | + '-O', |
2349 | + 'raw', |
2350 | + '-s', |
2351 | + snapshot_name, |
2352 | + disk_path, |
2353 | + out_path) |
2354 | + execute(*qemu_img_cmd) |
2355 | + |
2356 | + |
2357 | +def load_file(path): |
2358 | + """Read contents of file |
2359 | + |
2360 | + :param path: File to read |
2361 | + """ |
2362 | + with open(path, 'r+') as fp: |
2363 | + return fp.read() |
2364 | + |
2365 | + |
2366 | +def file_open(*args, **kwargs): |
2367 | + """Open file |
2368 | + |
2369 | + see built-in file() documentation for more details |
2370 | + """ |
2371 | + return file(*args, **kwargs) |
2372 | + |
2373 | + |
2374 | +def get_open_port(start_port, end_port): |
2375 | + """Find an available port |
2376 | + |
2377 | + :param start_port: Start of acceptable port range |
2378 | + :param end_port: End of acceptable port range |
2379 | + """ |
2380 | + for i in xrange(0, 100): # don't loop forever |
2381 | + port = random.randint(start_port, end_port) |
2382 | + # netcat will exit with 0 only if the port is in use, |
2383 | + # so a nonzero return value implies it is unused |
2384 | + cmd = 'netcat', '0.0.0.0', port, '-w', '1' |
2385 | + try: |
2386 | + stdout, stderr = execute(*cmd, process_input='') |
2387 | + except exception.ProcessExecutionError: |
2388 | + return port |
2389 | + raise Exception(_('Unable to find an open port')) |
2390 | + |
2391 | + |
2392 | +def run_ajaxterm(cmd, token, port): |
2393 | + """Run ajaxterm |
2394 | + |
2395 | + :param cmd: Command to connect to |
2396 | + :param token: Token to require for authentication |
2397 | + :param port: Port to run on |
2398 | + """ |
2399 | + cmd = ['%s/tools/ajaxterm/ajaxterm.py' % utils.novadir(), |
2400 | + '--command', cmd, '-t', token, '-p', port] |
2401 | + execute(*cmd) |
2402 | + |
2403 | + |
2404 | +def get_fs_info(path): |
2405 | + """Get free/used/total space info for a filesystem |
2406 | + |
2407 | + :param path: Any dirent on the filesystem |
2408 | + :returns: A dict containing: |
2409 | + |
2410 | + :free: How much space is free (in bytes) |
2411 | + :used: How much space is used (in bytes) |
2412 | + :total: How big the filesystem is (in bytes) |
2413 | + """ |
2414 | + hddinfo = os.statvfs(path) |
2415 | + total = hddinfo.f_frsize * hddinfo.f_blocks |
2416 | + free = hddinfo.f_frsize * hddinfo.f_bavail |
2417 | + used = hddinfo.f_frsize * (hddinfo.f_blocks - hddinfo.f_bfree) |
2418 | + return {'total': total, |
2419 | + 'free': free, |
2420 | + 'used': used} |
2421 | + |
2422 | + |
2423 | +def fetch_image(context, target, image_id, user_id, project_id, |
2424 | + size=None): |
2425 | + """Grab image and optionally attempt to resize it""" |
2426 | + images.fetch(context, image_id, target, user_id, project_id) |
2427 | + if size: |
2428 | + disk.extend(target, size) |
Whoa, hang on. Found a bug.