Merge lp:~ewanmellor/nova/xapi-plugin into lp:~hudson-openstack/nova/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
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.

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()