Merge lp:~ewanmellor/nova/xapi-plugin into lp:~hudson-openstack/nova/trunk
- xapi-plugin
- Merge into trunk
Proposed by
Ewan Mellor
Status: | Superseded |
---|---|
Proposed branch: | lp:~ewanmellor/nova/xapi-plugin |
Merge into: | lp:~hudson-openstack/nova/trunk |
Diff against target: |
670 lines (+554/-12) 5 files modified
nova/virt/images.py (+7/-5) nova/virt/xenapi.py (+98/-7) plugins/xenapi/README (+2/-0) plugins/xenapi/etc/xapi.d/plugins/objectstore (+231/-0) plugins/xenapi/etc/xapi.d/plugins/pluginlib_nova.py (+216/-0) |
To merge this branch: | bzr merge lp:~ewanmellor/nova/xapi-plugin |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Nova Core security contacts | Pending | ||
Review via email: mp+32084@code.launchpad.net |
This proposal supersedes a proposal from 2010-08-09.
This proposal has been superseded by a proposal from 2010-08-09.
Commit message
Description of the change
Added a xapi plugin that can pull images from nova-objectstore, and use that
to get a disk, kernel, and ramdisk for the VM.
To post a comment you must log in.
lp:~ewanmellor/nova/xapi-plugin
updated
- 167. By Ewan Mellor
-
Move the xenapi top level directory under plugins, as suggested by Jay Pipes.
Unmerged revisions
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'nova/virt/images.py' |
2 | --- nova/virt/images.py 2010-08-05 23:26:39 +0000 |
3 | +++ nova/virt/images.py 2010-08-09 12:24:53 +0000 |
4 | @@ -23,6 +23,7 @@ |
5 | |
6 | import os.path |
7 | import time |
8 | +import urlparse |
9 | |
10 | from nova import flags |
11 | from nova import process |
12 | @@ -43,7 +44,7 @@ |
13 | return f(image, path, user, project) |
14 | |
15 | def _fetch_s3_image(image, path, user, project): |
16 | - url = _image_url('%s/image' % image) |
17 | + url = image_url(image) |
18 | |
19 | # This should probably move somewhere else, like e.g. a download_as |
20 | # method on User objects and at the same time get rewritten to use |
21 | @@ -51,11 +52,11 @@ |
22 | headers = {} |
23 | headers['Date'] = time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime()) |
24 | |
25 | - uri = '/' + url.partition('/')[2] |
26 | + (_, _, url_path, _, _, _) = urlparse.urlparse(url) |
27 | access = manager.AuthManager().get_access_key(user, project) |
28 | signature = signer.Signer(user.secret.encode()).s3_authorization(headers, |
29 | 'GET', |
30 | - uri) |
31 | + url_path) |
32 | headers['Authorization'] = 'AWS %s:%s' % (access, signature) |
33 | |
34 | cmd = ['/usr/bin/curl', '--silent', url] |
35 | @@ -72,5 +73,6 @@ |
36 | def _image_path(path): |
37 | return os.path.join(FLAGS.images_path, path) |
38 | |
39 | -def _image_url(path): |
40 | - return "%s:%s/_images/%s" % (FLAGS.s3_host, FLAGS.s3_port, path) |
41 | +def image_url(image): |
42 | + return "http://%s:%s/_images/%s/image" % (FLAGS.s3_host, FLAGS.s3_port, |
43 | + image) |
44 | |
45 | === modified file 'nova/virt/xenapi.py' |
46 | --- nova/virt/xenapi.py 2010-07-25 19:32:33 +0000 |
47 | +++ nova/virt/xenapi.py 2010-08-09 12:24:53 +0000 |
48 | @@ -19,6 +19,7 @@ |
49 | """ |
50 | |
51 | import logging |
52 | +import xmlrpclib |
53 | |
54 | from twisted.internet import defer |
55 | from twisted.internet import task |
56 | @@ -26,7 +27,9 @@ |
57 | from nova import exception |
58 | from nova import flags |
59 | from nova import process |
60 | +from nova.auth.manager import AuthManager |
61 | from nova.compute import power_state |
62 | +from nova.virt import images |
63 | |
64 | XenAPI = None |
65 | |
66 | @@ -71,10 +74,26 @@ |
67 | @defer.inlineCallbacks |
68 | @exception.wrap_exception |
69 | def spawn(self, instance): |
70 | - vm = self.lookup(instance.name) |
71 | + vm = yield self.lookup(instance.name) |
72 | if vm is not None: |
73 | raise Exception('Attempted to create non-unique name %s' % |
74 | instance.name) |
75 | + |
76 | + user = AuthManager().get_user(instance.datamodel['user_id']) |
77 | + vdi_uuid = yield self.fetch_image( |
78 | + instance.datamodel['image_id'], user, True) |
79 | + kernel = yield self.fetch_image( |
80 | + instance.datamodel['kernel_id'], user, False) |
81 | + ramdisk = yield self.fetch_image( |
82 | + instance.datamodel['ramdisk_id'], user, False) |
83 | + vdi_ref = yield self._conn.xenapi.VDI.get_by_uuid(vdi_uuid) |
84 | + |
85 | + vm_ref = yield self.create_vm(instance, kernel, ramdisk) |
86 | + yield self.create_vbd(vm_ref, vdi_ref, 0, True) |
87 | + yield self._conn.xenapi.VM.start(vm_ref, False, False) |
88 | + |
89 | + |
90 | + def create_vm(self, instance, kernel, ramdisk): |
91 | mem = str(long(instance.datamodel['memory_kb']) * 1024) |
92 | vcpus = str(instance.datamodel['vcpus']) |
93 | rec = { |
94 | @@ -92,9 +111,9 @@ |
95 | 'actions_after_reboot': 'restart', |
96 | 'actions_after_crash': 'destroy', |
97 | 'PV_bootloader': '', |
98 | - 'PV_kernel': instance.datamodel['kernel_id'], |
99 | - 'PV_ramdisk': instance.datamodel['ramdisk_id'], |
100 | - 'PV_args': '', |
101 | + 'PV_kernel': kernel, |
102 | + 'PV_ramdisk': ramdisk, |
103 | + 'PV_args': 'root=/dev/xvda1', |
104 | 'PV_bootloader_args': '', |
105 | 'PV_legacy_args': '', |
106 | 'HVM_boot_policy': '', |
107 | @@ -106,8 +125,48 @@ |
108 | 'user_version': '0', |
109 | 'other_config': {}, |
110 | } |
111 | - vm = yield self._conn.xenapi.VM.create(rec) |
112 | - #yield self._conn.xenapi.VM.start(vm, False, False) |
113 | + logging.debug('Created VM %s...', instance.name) |
114 | + vm_ref = self._conn.xenapi.VM.create(rec) |
115 | + logging.debug('Created VM %s as %s.', instance.name, vm_ref) |
116 | + return vm_ref |
117 | + |
118 | + |
119 | + def create_vbd(self, vm_ref, vdi_ref, userdevice, bootable): |
120 | + vbd_rec = {} |
121 | + vbd_rec['VM'] = vm_ref |
122 | + vbd_rec['VDI'] = vdi_ref |
123 | + vbd_rec['userdevice'] = str(userdevice) |
124 | + vbd_rec['bootable'] = bootable |
125 | + vbd_rec['mode'] = 'RW' |
126 | + vbd_rec['type'] = 'disk' |
127 | + vbd_rec['unpluggable'] = True |
128 | + vbd_rec['empty'] = False |
129 | + vbd_rec['other_config'] = {} |
130 | + vbd_rec['qos_algorithm_type'] = '' |
131 | + vbd_rec['qos_algorithm_params'] = {} |
132 | + vbd_rec['qos_supported_algorithms'] = [] |
133 | + logging.debug('Creating VBD for VM %s, VDI %s ... ', vm_ref, vdi_ref) |
134 | + vbd_ref = self._conn.xenapi.VBD.create(vbd_rec) |
135 | + logging.debug('Created VBD %s for VM %s, VDI %s.', vbd_ref, vm_ref, |
136 | + vdi_ref) |
137 | + return vbd_ref |
138 | + |
139 | + |
140 | + def fetch_image(self, image, user, use_sr): |
141 | + """use_sr: True to put the image as a VDI in an SR, False to place |
142 | + it on dom0's filesystem. The former is for VM disks, the latter for |
143 | + its kernel and ramdisk (if external kernels are being used).""" |
144 | + |
145 | + url = images.image_url(image) |
146 | + logging.debug("Asking xapi to fetch %s as %s" % (url, user.access)) |
147 | + fn = use_sr and 'get_vdi' or 'get_kernel' |
148 | + args = {} |
149 | + args['src_url'] = url |
150 | + args['username'] = user.access |
151 | + args['password'] = user.secret |
152 | + if use_sr: |
153 | + args['add_partition'] = 'true' |
154 | + return self._call_plugin('objectstore', fn, args) |
155 | |
156 | |
157 | def reboot(self, instance): |
158 | @@ -143,10 +202,42 @@ |
159 | else: |
160 | return vms[0] |
161 | |
162 | + |
163 | + def _call_plugin(self, plugin, fn, args): |
164 | + return _unwrap_plugin_exceptions( |
165 | + self._conn.xenapi.host.call_plugin, |
166 | + self._get_xenapi_host(), plugin, fn, args) |
167 | + |
168 | + |
169 | + def _get_xenapi_host(self): |
170 | + return self._conn.xenapi.session.get_this_host(self._conn.handle) |
171 | + |
172 | + |
173 | power_state_from_xenapi = { |
174 | - 'Halted' : power_state.RUNNING, #FIXME |
175 | + 'Halted' : power_state.SHUTDOWN, |
176 | 'Running' : power_state.RUNNING, |
177 | 'Paused' : power_state.PAUSED, |
178 | 'Suspended': power_state.SHUTDOWN, # FIXME |
179 | 'Crashed' : power_state.CRASHED |
180 | } |
181 | + |
182 | + |
183 | +def _unwrap_plugin_exceptions(func, *args, **kwargs): |
184 | + try: |
185 | + return func(*args, **kwargs) |
186 | + except XenAPI.Failure, exn: |
187 | + logging.debug("Got exception: %s", exn) |
188 | + if (len(exn.details) == 4 and |
189 | + exn.details[0] == 'XENAPI_PLUGIN_EXCEPTION' and |
190 | + exn.details[2] == 'Failure'): |
191 | + params = None |
192 | + try: |
193 | + params = eval(exn.details[3]) |
194 | + except: |
195 | + raise exn |
196 | + raise XenAPI.Failure(params) |
197 | + else: |
198 | + raise |
199 | + except xmlrpclib.ProtocolError, exn: |
200 | + logging.debug("Got exception: %s", exn) |
201 | + raise |
202 | |
203 | === added directory 'plugins' |
204 | === added directory 'plugins/xenapi' |
205 | === added file 'plugins/xenapi/README' |
206 | --- plugins/xenapi/README 1970-01-01 00:00:00 +0000 |
207 | +++ plugins/xenapi/README 2010-08-09 12:24:53 +0000 |
208 | @@ -0,0 +1,2 @@ |
209 | +This directory contains files that are required for the XenAPI support. They |
210 | +should be installed in the XenServer / Xen Cloud Platform domain 0. |
211 | |
212 | === added directory 'plugins/xenapi/etc' |
213 | === added directory 'plugins/xenapi/etc/xapi.d' |
214 | === added directory 'plugins/xenapi/etc/xapi.d/plugins' |
215 | === added file 'plugins/xenapi/etc/xapi.d/plugins/objectstore' |
216 | --- plugins/xenapi/etc/xapi.d/plugins/objectstore 1970-01-01 00:00:00 +0000 |
217 | +++ plugins/xenapi/etc/xapi.d/plugins/objectstore 2010-08-09 12:24:53 +0000 |
218 | @@ -0,0 +1,231 @@ |
219 | +#!/usr/bin/env python |
220 | + |
221 | +# Copyright (c) 2010 Citrix Systems, Inc. |
222 | +# Copyright 2010 United States Government as represented by the |
223 | +# Administrator of the National Aeronautics and Space Administration. |
224 | +# All Rights Reserved. |
225 | +# |
226 | +# Licensed under the Apache License, Version 2.0 (the "License"); you may |
227 | +# not use this file except in compliance with the License. You may obtain |
228 | +# a copy of the License at |
229 | +# |
230 | +# http://www.apache.org/licenses/LICENSE-2.0 |
231 | +# |
232 | +# Unless required by applicable law or agreed to in writing, software |
233 | +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
234 | +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
235 | +# License for the specific language governing permissions and limitations |
236 | +# under the License. |
237 | + |
238 | +# |
239 | +# XenAPI plugin for fetching images from nova-objectstore. |
240 | +# |
241 | + |
242 | +import base64 |
243 | +import errno |
244 | +import hmac |
245 | +import os |
246 | +import os.path |
247 | +import sha |
248 | +import time |
249 | +import urlparse |
250 | + |
251 | +import XenAPIPlugin |
252 | + |
253 | +from pluginlib_nova import * |
254 | +configure_logging('objectstore') |
255 | + |
256 | + |
257 | +KERNEL_DIR = '/boot/guest' |
258 | + |
259 | +DOWNLOAD_CHUNK_SIZE = 2 * 1024 * 1024 |
260 | +SECTOR_SIZE = 512 |
261 | +MBR_SIZE_SECTORS = 63 |
262 | +MBR_SIZE_BYTES = MBR_SIZE_SECTORS * SECTOR_SIZE |
263 | + |
264 | + |
265 | +def get_vdi(session, args): |
266 | + src_url = exists(args, 'src_url') |
267 | + username = exists(args, 'username') |
268 | + password = exists(args, 'password') |
269 | + add_partition = validate_bool(args, 'add_partition', 'false') |
270 | + |
271 | + (proto, netloc, url_path, _, _, _) = urlparse.urlparse(src_url) |
272 | + |
273 | + sr = find_sr(session) |
274 | + if sr is None: |
275 | + raise Exception('Cannot find SR to write VDI to') |
276 | + |
277 | + virtual_size = \ |
278 | + get_content_length(proto, netloc, url_path, username, password) |
279 | + if virtual_size < 0: |
280 | + raise Exception('Cannot get VDI size') |
281 | + |
282 | + vdi_size = virtual_size |
283 | + if add_partition: |
284 | + # Make room for MBR. |
285 | + vdi_size += MBR_SIZE_BYTES |
286 | + |
287 | + vdi = create_vdi(session, sr, src_url, vdi_size, False) |
288 | + with_vdi_in_dom0(session, vdi, False, |
289 | + lambda dev: get_vdi_(proto, netloc, url_path, |
290 | + username, password, add_partition, |
291 | + virtual_size, '/dev/%s' % dev)) |
292 | + return session.xenapi.VDI.get_uuid(vdi) |
293 | + |
294 | + |
295 | +def get_vdi_(proto, netloc, url_path, username, password, add_partition, |
296 | + virtual_size, dest): |
297 | + |
298 | + if add_partition: |
299 | + write_partition(virtual_size, dest) |
300 | + |
301 | + offset = add_partition and MBR_SIZE_BYTES or 0 |
302 | + get(proto, netloc, url_path, username, password, dest, offset) |
303 | + |
304 | + |
305 | +def write_partition(virtual_size, dest): |
306 | + mbr_last = MBR_SIZE_SECTORS - 1 |
307 | + primary_first = MBR_SIZE_SECTORS |
308 | + primary_last = MBR_SIZE_SECTORS + (virtual_size / SECTOR_SIZE) - 1 |
309 | + |
310 | + logging.debug('Writing partition table %d %d to %s...', |
311 | + primary_first, primary_last, dest) |
312 | + |
313 | + result = os.system('parted --script %s mklabel msdos' % dest) |
314 | + if result != 0: |
315 | + raise Exception('Failed to mklabel') |
316 | + result = os.system('parted --script %s mkpart primary %ds %ds' % |
317 | + (dest, primary_first, primary_last)) |
318 | + if result != 0: |
319 | + raise Exception('Failed to mkpart') |
320 | + |
321 | + logging.debug('Writing partition table %s done.', dest) |
322 | + |
323 | + |
324 | +def find_sr(session): |
325 | + host = get_this_host(session) |
326 | + srs = session.xenapi.SR.get_all() |
327 | + for sr in srs: |
328 | + sr_rec = session.xenapi.SR.get_record(sr) |
329 | + if not ('i18n-key' in sr_rec['other_config'] and |
330 | + sr_rec['other_config']['i18n-key'] == 'local-storage'): |
331 | + continue |
332 | + for pbd in sr_rec['PBDs']: |
333 | + pbd_rec = session.xenapi.PBD.get_record(pbd) |
334 | + if pbd_rec['host'] == host: |
335 | + return sr |
336 | + return None |
337 | + |
338 | + |
339 | +def get_kernel(session, args): |
340 | + src_url = exists(args, 'src_url') |
341 | + username = exists(args, 'username') |
342 | + password = exists(args, 'password') |
343 | + |
344 | + (proto, netloc, url_path, _, _, _) = urlparse.urlparse(src_url) |
345 | + |
346 | + dest = os.path.join(KERNEL_DIR, url_path[1:]) |
347 | + |
348 | + # Paranoid check against people using ../ to do rude things. |
349 | + if os.path.commonprefix([KERNEL_DIR, dest]) != KERNEL_DIR: |
350 | + raise Exception('Illegal destination %s %s', (url_path, dest)) |
351 | + |
352 | + dirname = os.path.dirname(dest) |
353 | + try: |
354 | + os.makedirs(dirname) |
355 | + except os.error, e: |
356 | + if e.errno != errno.EEXIST: |
357 | + raise |
358 | + if not os.path.isdir(dirname): |
359 | + raise Exception('Cannot make directory %s', dirname) |
360 | + |
361 | + try: |
362 | + os.remove(dest) |
363 | + except: |
364 | + pass |
365 | + |
366 | + get(proto, netloc, url_path, username, password, dest, 0) |
367 | + |
368 | + return dest |
369 | + |
370 | + |
371 | +def get_content_length(proto, netloc, url_path, username, password): |
372 | + headers = make_headers('HEAD', url_path, username, password) |
373 | + return with_http_connection( |
374 | + proto, netloc, |
375 | + lambda conn: get_content_length_(url_path, headers, conn)) |
376 | + |
377 | + |
378 | +def get_content_length_(url_path, headers, conn): |
379 | + conn.request('HEAD', url_path, None, headers) |
380 | + response = conn.getresponse() |
381 | + if response.status != 200: |
382 | + raise Exception('%d %s' % (response.status, response.reason)) |
383 | + |
384 | + return long(response.getheader('Content-Length', -1)) |
385 | + |
386 | + |
387 | +def get(proto, netloc, url_path, username, password, dest, offset): |
388 | + headers = make_headers('GET', url_path, username, password) |
389 | + download(proto, netloc, url_path, headers, dest, offset) |
390 | + |
391 | + |
392 | +def make_headers(verb, url_path, username, password): |
393 | + headers = {} |
394 | + headers['Date'] = \ |
395 | + time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime()) |
396 | + headers['Authorization'] = \ |
397 | + 'AWS %s:%s' % (username, |
398 | + s3_authorization(verb, url_path, password, headers)) |
399 | + return headers |
400 | + |
401 | + |
402 | +def s3_authorization(verb, path, password, headers): |
403 | + sha1 = hmac.new(password, digestmod=sha) |
404 | + sha1.update(plaintext(verb, path, headers)) |
405 | + return base64.encodestring(sha1.digest()).strip() |
406 | + |
407 | + |
408 | +def plaintext(verb, path, headers): |
409 | + return '%s\n\n\n%s\n%s' % (verb, |
410 | + "\n".join([headers[h] for h in headers]), |
411 | + path) |
412 | + |
413 | + |
414 | +def download(proto, netloc, url_path, headers, dest, offset): |
415 | + with_http_connection( |
416 | + proto, netloc, |
417 | + lambda conn: download_(url_path, dest, offset, headers, conn)) |
418 | + |
419 | + |
420 | +def download_(url_path, dest, offset, headers, conn): |
421 | + conn.request('GET', url_path, None, headers) |
422 | + response = conn.getresponse() |
423 | + if response.status != 200: |
424 | + raise Exception('%d %s' % (response.status, response.reason)) |
425 | + |
426 | + length = response.getheader('Content-Length', -1) |
427 | + |
428 | + with_file( |
429 | + dest, 'a', |
430 | + lambda dest_file: download_all(response, length, dest_file, offset)) |
431 | + |
432 | + |
433 | +def download_all(response, length, dest_file, offset): |
434 | + dest_file.seek(offset) |
435 | + i = 0 |
436 | + while True: |
437 | + buf = response.read(DOWNLOAD_CHUNK_SIZE) |
438 | + if buf: |
439 | + dest_file.write(buf) |
440 | + else: |
441 | + return |
442 | + i += len(buf) |
443 | + if length != -1 and i >= length: |
444 | + return |
445 | + |
446 | + |
447 | +if __name__ == '__main__': |
448 | + XenAPIPlugin.dispatch({'get_vdi': get_vdi, |
449 | + 'get_kernel': get_kernel}) |
450 | |
451 | === added file 'plugins/xenapi/etc/xapi.d/plugins/pluginlib_nova.py' |
452 | --- plugins/xenapi/etc/xapi.d/plugins/pluginlib_nova.py 1970-01-01 00:00:00 +0000 |
453 | +++ plugins/xenapi/etc/xapi.d/plugins/pluginlib_nova.py 2010-08-09 12:24:53 +0000 |
454 | @@ -0,0 +1,216 @@ |
455 | +# Copyright (c) 2010 Citrix Systems, Inc. |
456 | +# |
457 | +# Licensed under the Apache License, Version 2.0 (the "License"); you may |
458 | +# not use this file except in compliance with the License. You may obtain |
459 | +# a copy of the License at |
460 | +# |
461 | +# http://www.apache.org/licenses/LICENSE-2.0 |
462 | +# |
463 | +# Unless required by applicable law or agreed to in writing, software |
464 | +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
465 | +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
466 | +# License for the specific language governing permissions and limitations |
467 | +# under the License. |
468 | + |
469 | +# |
470 | +# Helper functions for the Nova xapi plugins. In time, this will merge |
471 | +# with the pluginlib.py shipped with xapi, but for now, that file is not |
472 | +# very stable, so it's easiest just to have a copy of all the functions |
473 | +# that we need. |
474 | +# |
475 | + |
476 | +import httplib |
477 | +import logging |
478 | +import logging.handlers |
479 | +import re |
480 | +import time |
481 | + |
482 | + |
483 | +##### Logging setup |
484 | + |
485 | +def configure_logging(name): |
486 | + log = logging.getLogger() |
487 | + log.setLevel(logging.DEBUG) |
488 | + sysh = logging.handlers.SysLogHandler('/dev/log') |
489 | + sysh.setLevel(logging.DEBUG) |
490 | + formatter = logging.Formatter('%s: %%(levelname)-8s %%(message)s' % name) |
491 | + sysh.setFormatter(formatter) |
492 | + log.addHandler(sysh) |
493 | + |
494 | + |
495 | +##### Exceptions |
496 | + |
497 | +class PluginError(Exception): |
498 | + """Base Exception class for all plugin errors.""" |
499 | + def __init__(self, *args): |
500 | + Exception.__init__(self, *args) |
501 | + |
502 | +class ArgumentError(PluginError): |
503 | + """Raised when required arguments are missing, argument values are invalid, |
504 | + or incompatible arguments are given. |
505 | + """ |
506 | + def __init__(self, *args): |
507 | + PluginError.__init__(self, *args) |
508 | + |
509 | + |
510 | +##### Helpers |
511 | + |
512 | +def ignore_failure(func, *args, **kwargs): |
513 | + try: |
514 | + return func(*args, **kwargs) |
515 | + except XenAPI.Failure, e: |
516 | + logging.error('Ignoring XenAPI.Failure %s', e) |
517 | + return None |
518 | + |
519 | + |
520 | +##### Argument validation |
521 | + |
522 | +ARGUMENT_PATTERN = re.compile(r'^[a-zA-Z0-9_:\.\-,]+$') |
523 | + |
524 | +def validate_exists(args, key, default=None): |
525 | + """Validates that a string argument to a RPC method call is given, and |
526 | + matches the shell-safe regex, with an optional default value in case it |
527 | + does not exist. |
528 | + |
529 | + Returns the string. |
530 | + """ |
531 | + if key in args: |
532 | + if len(args[key]) == 0: |
533 | + raise ArgumentError('Argument %r value %r is too short.' % (key, args[key])) |
534 | + if not ARGUMENT_PATTERN.match(args[key]): |
535 | + raise ArgumentError('Argument %r value %r contains invalid characters.' % (key, args[key])) |
536 | + if args[key][0] == '-': |
537 | + raise ArgumentError('Argument %r value %r starts with a hyphen.' % (key, args[key])) |
538 | + return args[key] |
539 | + elif default is not None: |
540 | + return default |
541 | + else: |
542 | + raise ArgumentError('Argument %s is required.' % key) |
543 | + |
544 | +def validate_bool(args, key, default=None): |
545 | + """Validates that a string argument to a RPC method call is a boolean string, |
546 | + with an optional default value in case it does not exist. |
547 | + |
548 | + Returns the python boolean value. |
549 | + """ |
550 | + value = validate_exists(args, key, default) |
551 | + if value.lower() == 'true': |
552 | + return True |
553 | + elif value.lower() == 'false': |
554 | + return False |
555 | + else: |
556 | + raise ArgumentError("Argument %s may not take value %r. Valid values are ['true', 'false']." % (key, value)) |
557 | + |
558 | +def exists(args, key): |
559 | + """Validates that a freeform string argument to a RPC method call is given. |
560 | + Returns the string. |
561 | + """ |
562 | + if key in args: |
563 | + return args[key] |
564 | + else: |
565 | + raise ArgumentError('Argument %s is required.' % key) |
566 | + |
567 | +def optional(args, key): |
568 | + """If the given key is in args, return the corresponding value, otherwise |
569 | + return None""" |
570 | + return key in args and args[key] or None |
571 | + |
572 | + |
573 | +def get_this_host(session): |
574 | + return session.xenapi.session.get_this_host(session.handle) |
575 | + |
576 | + |
577 | +def get_domain_0(session): |
578 | + this_host_ref = get_this_host(session) |
579 | + expr = 'field "is_control_domain" = "true" and field "resident_on" = "%s"' % this_host_ref |
580 | + return session.xenapi.VM.get_all_records_where(expr).keys()[0] |
581 | + |
582 | + |
583 | +def create_vdi(session, sr_ref, name_label, virtual_size, read_only): |
584 | + vdi_ref = session.xenapi.VDI.create( |
585 | + { 'name_label': name_label, |
586 | + 'name_description': '', |
587 | + 'SR': sr_ref, |
588 | + 'virtual_size': str(virtual_size), |
589 | + 'type': 'User', |
590 | + 'sharable': False, |
591 | + 'read_only': read_only, |
592 | + 'xenstore_data': {}, |
593 | + 'other_config': {}, |
594 | + 'sm_config': {}, |
595 | + 'tags': [] }) |
596 | + logging.debug('Created VDI %s (%s, %s, %s) on %s.', vdi_ref, name_label, |
597 | + virtual_size, read_only, sr_ref) |
598 | + return vdi_ref |
599 | + |
600 | + |
601 | +def with_vdi_in_dom0(session, vdi, read_only, f): |
602 | + dom0 = get_domain_0(session) |
603 | + vbd_rec = {} |
604 | + vbd_rec['VM'] = dom0 |
605 | + vbd_rec['VDI'] = vdi |
606 | + vbd_rec['userdevice'] = 'autodetect' |
607 | + vbd_rec['bootable'] = False |
608 | + vbd_rec['mode'] = read_only and 'RO' or 'RW' |
609 | + vbd_rec['type'] = 'disk' |
610 | + vbd_rec['unpluggable'] = True |
611 | + vbd_rec['empty'] = False |
612 | + vbd_rec['other_config'] = {} |
613 | + vbd_rec['qos_algorithm_type'] = '' |
614 | + vbd_rec['qos_algorithm_params'] = {} |
615 | + vbd_rec['qos_supported_algorithms'] = [] |
616 | + logging.debug('Creating VBD for VDI %s ... ', vdi) |
617 | + vbd = session.xenapi.VBD.create(vbd_rec) |
618 | + logging.debug('Creating VBD for VDI %s done.', vdi) |
619 | + try: |
620 | + logging.debug('Plugging VBD %s ... ', vbd) |
621 | + session.xenapi.VBD.plug(vbd) |
622 | + logging.debug('Plugging VBD %s done.', vbd) |
623 | + return f(session.xenapi.VBD.get_device(vbd)) |
624 | + finally: |
625 | + logging.debug('Destroying VBD for VDI %s ... ', vdi) |
626 | + vbd_unplug_with_retry(session, vbd) |
627 | + ignore_failure(session.xenapi.VBD.destroy, vbd) |
628 | + logging.debug('Destroying VBD for VDI %s done.', vdi) |
629 | + |
630 | + |
631 | +def vbd_unplug_with_retry(session, vbd): |
632 | + """Call VBD.unplug on the given VBD, with a retry if we get |
633 | + DEVICE_DETACH_REJECTED. For reasons which I don't understand, we're |
634 | + seeing the device still in use, even when all processes using the device |
635 | + should be dead.""" |
636 | + while True: |
637 | + try: |
638 | + session.xenapi.VBD.unplug(vbd) |
639 | + logging.debug('VBD.unplug successful first time.') |
640 | + return |
641 | + except XenAPI.Failure, e: |
642 | + if (len(e.details) > 0 and |
643 | + e.details[0] == 'DEVICE_DETACH_REJECTED'): |
644 | + logging.debug('VBD.unplug rejected: retrying...') |
645 | + time.sleep(1) |
646 | + elif (len(e.details) > 0 and |
647 | + e.details[0] == 'DEVICE_ALREADY_DETACHED'): |
648 | + logging.debug('VBD.unplug successful eventually.') |
649 | + return |
650 | + else: |
651 | + logging.error('Ignoring XenAPI.Failure in VBD.unplug: %s', e) |
652 | + return |
653 | + |
654 | + |
655 | +def with_http_connection(proto, netloc, f): |
656 | + conn = (proto == 'https' and |
657 | + httplib.HTTPSConnection(netloc) or |
658 | + httplib.HTTPConnection(netloc)) |
659 | + try: |
660 | + return f(conn) |
661 | + finally: |
662 | + conn.close() |
663 | + |
664 | + |
665 | +def with_file(dest_path, mode, f): |
666 | + dest = open(dest_path, mode) |
667 | + try: |
668 | + return f(dest) |
669 | + finally: |
670 | + dest.close() |