Changes for supporting fast cloning on Xenserver.

Implements blueprint fast-cloning-for-xenserver
	1. use_cow_images flag is reused for xenserver to check if copy on write images should be used.
	2. image-id is used to tag an image which has already been streamed from glance.
	3. If cow is true, when an instance of an image is created for the first time on a given xenserver, the image is streamed from glance and copy on write disk is created for the instance.
	4. For subsequent instance creation requests (of the same image), a copy on write disk is created from the base image that is already present on the host.
	5. If cow is false, when an instance of an image is created for the first time on a host, the image is streamed from glance and its copy is made to create a virtual disk for the instance.
	6. For subsequent instance creation requests, a copy of disk is made for creating the disk for the instance.
	7. Snapshot creation code was updated to handle cow=true. Now there can be upto 3 disks in the chain. The base disk needs to be uploaded too.
	8. Also added a cache_images flag. Depending on whether the flag is turned on on not, images will be cached on the host.

Change-Id: I54838a24b061c134877f3479c925c6ee78da14bc
This commit is contained in:
Devdeep Singh 2012-01-17 18:59:14 +05:30
parent d8f8bad0f2
commit 6c3bc216c1
9 changed files with 268 additions and 20 deletions

View File

@ -43,6 +43,7 @@ David Subiros <david.perez5@hp.com>
Dean Troyer <dtroyer@gmail.com>
Deepak Garg <deepak.garg@citrix.com>
Derek Higgins <higginsd@gmail.com>
Devdeep Singh <devdeep.singh@citrix.com>
Devendra Modium <dmodium@isi.edu>
Devin Carlen <devin.carlen@gmail.com>
Donal Lafferty <donal.lafferty@citrix.com>

View File

@ -445,6 +445,12 @@ global_opts = [
cfg.ListOpt('isolated_hosts',
default=[],
help='Host reserved for specific images'),
cfg.BoolOpt('cache_images',
default=True,
help='Cache glance images locally'),
cfg.BoolOpt('use_cow_images',
default=True,
help='Whether to use cow images')
]
FLAGS.register_opts(global_opts)

View File

@ -399,7 +399,14 @@ class XenAPIVMTestCase(test.TestCase):
def _check_vdis(self, start_list, end_list):
for vdi_ref in end_list:
if not vdi_ref in start_list:
self.fail('Found unexpected VDI:%s' % vdi_ref)
vdi_rec = xenapi_fake.get_record('VDI', vdi_ref)
# If the cache is turned on then the base disk will be
# there even after the cleanup
if 'other_config' in vdi_rec:
if vdi_rec['other_config']['image-id'] is None:
self.fail('Found unexpected VDI:%s' % vdi_ref)
else:
self.fail('Found unexpected VDI:%s' % vdi_ref)
def _test_spawn(self, image_ref, kernel_id, ramdisk_id,
instance_type_id="3", os_type="linux",

View File

@ -46,7 +46,7 @@ def stubout_instance_snapshot(stubs):
def fake_wait_for_vhd_coalesce(*args):
#TODO(sirp): Should we actually fake out the data here
return "fakeparent"
return "fakeparent", "fakebase"
stubs.Set(vm_utils, '_wait_for_vhd_coalesce', fake_wait_for_vhd_coalesce)

View File

@ -99,9 +99,6 @@ libvirt_opts = [
default='',
help='Override the default libvirt URI '
'(which is dependent on libvirt_type)'),
cfg.BoolOpt('use_cow_images',
default=True,
help='Whether to use cow images'),
cfg.StrOpt('cpuinfo_xml_template',
default=utils.abspath('virt/cpuinfo.xml.template'),
help='CpuInfo XML Template (Used only live migration now)'),

View File

@ -446,6 +446,35 @@ class SessionBase(object):
db_ref['xenstore_data'] = {}
db_ref['xenstore_data'][key] = value
def VDI_remove_from_other_config(self, _1, vdi_ref, key):
db_ref = _db_content['VDI'][vdi_ref]
if not 'other_config' in db_ref:
return
db_ref['other_config'][key] = None
def VDI_add_to_other_config(self, _1, vdi_ref, key, value):
db_ref = _db_content['VDI'][vdi_ref]
if not 'other_config' in db_ref:
db_ref['other_config'] = {}
db_ref['other_config'][key] = value
def VDI_copy(self, _1, vdi_to_copy_ref, sr_ref):
db_ref = _db_content['VDI'][vdi_to_copy_ref]
name_label = db_ref['name_label']
read_only = db_ref['read_only']
sharable = db_ref['sharable']
vdi_ref = create_vdi(name_label, read_only, sr_ref, sharable)
return vdi_ref
def VDI_clone(self, _1, vdi_to_clone_ref):
db_ref = _db_content['VDI'][vdi_to_clone_ref]
name_label = db_ref['name_label']
read_only = db_ref['read_only']
sr_ref = db_ref['SR']
sharable = db_ref['sharable']
vdi_ref = create_vdi(name_label, read_only, sr_ref, sharable)
return vdi_ref
def host_compute_free_memory(self, _1, ref):
#Always return 12GB available
return 12 * 1024 * 1024 * 1024
@ -457,6 +486,8 @@ class SessionBase(object):
return ''
elif (plugin, method) == ('glance', 'upload_vhd'):
return ''
elif (plugin, method) == ('glance', 'create_kernel_ramdisk'):
return ''
elif (plugin, method) == ('migration', 'move_vhds_into_sr'):
return ''
elif (plugin, method) == ('migration', 'transfer_vhd'):

View File

@ -314,6 +314,22 @@ class VMHelper(HelperBase):
% locals())
return vdi_ref
@classmethod
def copy_vdi(cls, session, sr_ref, vdi_to_copy_ref):
"""Copy a VDI and return the new VDIs reference."""
vdi_ref = session.call_xenapi('VDI.copy', vdi_to_copy_ref, sr_ref)
LOG.debug(_('Copied VDI %(vdi_ref)s from VDI '
'%(vdi_to_copy_ref)s on %(sr_ref)s.') % locals())
return vdi_ref
@classmethod
def clone_vdi(cls, session, vdi_to_clone_ref):
"""Clones a VDI and return the new VDIs reference."""
vdi_ref = session.call_xenapi('VDI.clone', vdi_to_clone_ref)
LOG.debug(_('Cloned VDI %(vdi_ref)s from VDI '
'%(vdi_to_clone_ref)s') % locals())
return vdi_ref
@classmethod
def set_vdi_name_label(cls, session, vdi_uuid, name_label):
vdi_ref = session.call_xenapi("VDI.get_by_uuid", vdi_uuid)
@ -353,11 +369,11 @@ class VMHelper(HelperBase):
LOG.debug(_('Created snapshot %(template_vm_ref)s from'
' VM %(vm_ref)s.') % locals())
parent_uuid = _wait_for_vhd_coalesce(
parent_uuid, base_uuid = _wait_for_vhd_coalesce(
session, instance, sr_ref, vm_vdi_ref, original_parent_uuid)
#TODO(sirp): we need to assert only one parent, not parents two deep
template_vdi_uuids = {'image': parent_uuid,
template_vdi_uuids = {'base': base_uuid,
'image': parent_uuid,
'snap': template_vdi_uuid}
return template_vm_ref, template_vdi_uuids
@ -373,6 +389,15 @@ class VMHelper(HelperBase):
sr_uuid = sr_rec["uuid"]
return os.path.join(FLAGS.xenapi_sr_base_path, sr_uuid)
@classmethod
def find_cached_image(cls, session, image_id, sr_ref):
"""Returns the vdi-ref of the cached image."""
for vdi_ref, vdi_rec in _get_all_vdis_in_sr(session, sr_ref):
if ('image-id' in vdi_rec['other_config'] and
vdi_rec['other_config']['image-id'] == image_id):
return vdi_ref
return None
@classmethod
def upload_image(cls, context, session, instance, vdi_uuids, image_id):
""" Requests that the Glance plugin bundle the specified VDIs and
@ -544,6 +569,108 @@ class VMHelper(HelperBase):
vdi_ref = cls.create_vdi(session, sr_ref, 'blank HD', vdi_size, False)
return vdi_ref
@classmethod
def create_kernel_image(cls, context, session, instance, image, user_id,
project_id, image_type):
"""Creates kernel/ramdisk file from the image stored in the cache.
If the image is not present in the cache, it streams it from glance.
Returns: A list of dictionaries that describe VDIs
"""
filename = ""
if FLAGS.cache_images:
args = {}
args['cached-image'] = image
args['new-image-uuid'] = str(uuid.uuid4())
task = session.async_call_plugin('glance', "create_kernel_ramdisk",
args)
filename = session.wait_for_task(task, instance.id)
if filename == "":
return cls.fetch_image(context, session, instance, image,
user_id, project_id, image_type)
else:
return [dict(vdi_type=ImageType.to_string(image_type),
vdi_uuid=None,
file=filename)]
@classmethod
def create_image(cls, context, session, instance, image, user_id,
project_id, image_type):
"""Creates VDI from the image stored in the local cache. If the image
is not present in the cache, it streams it from glance.
Returns: A list of dictionaries that describe VDIs
"""
if FLAGS.cache_images == False or image_type == ImageType.DISK_ISO:
# If caching is disabled, we do not have to keep a copy of the
# image. Fetch the image from glance.
return cls.fetch_image(context, session,
instance, instance.image_ref,
instance.user_id, instance.project_id,
image_type)
sr_ref = cls.safe_find_sr(session)
sr_type = session.call_xenapi('SR.get_record', sr_ref)["type"]
vdi_return_list = []
if FLAGS.use_cow_images and sr_type != "ext":
LOG.warning(_("Fast cloning is only supported on default local SR "
"of type ext. SR on this system was found to be of "
"type %(sr_type)s. Ignoring the cow flag.")
% locals())
vdi_ref = cls.find_cached_image(session, image, sr_ref)
if vdi_ref is None:
vdis = cls.fetch_image(context, session, instance, image, user_id,
project_id, image_type)
vdi_ref = session.call_xenapi('VDI.get_by_uuid',
vdis[0]['vdi_uuid'])
session.call_xenapi('VDI.add_to_other_config',
vdi_ref, "image-id", str(image))
session.call_xenapi('VDI.set_name_label',
vdi_ref, "Cached glance image")
for vdi in vdis:
if vdi["vdi_type"] == "swap":
session.call_xenapi('VDI.add_to_other_config',
vdi_ref, "swap-disk",
str(vdi['vdi_uuid']))
if FLAGS.use_cow_images and sr_type == 'ext':
new_vdi_ref = cls.clone_vdi(session, vdi_ref)
else:
new_vdi_ref = cls.copy_vdi(session, sr_ref, vdi_ref)
# Set the name label for the image we just created and remove image id
# field from other-config.
session.call_xenapi('VDI.set_name_label', new_vdi_ref, instance.name)
session.call_xenapi('VDI.remove_from_other_config',
new_vdi_ref, "image-id")
vdi_return_list.append(dict(
vdi_type=("os" if image_type == ImageType.DISK_VHD
else ImageType.to_string(image_type)),
vdi_uuid=session.call_xenapi('VDI.get_uuid', new_vdi_ref),
file=None))
# Create a swap disk if the glance image had one associated with it.
vdi_rec = session.call_xenapi('VDI.get_record', vdi_ref)
if 'swap-disk' in vdi_rec['other_config']:
swap_disk_uuid = vdi_rec['other_config']['swap-disk']
swap_vdi_ref = session.call_xenapi('VDI.get_by_uuid',
swap_disk_uuid)
new_swap_vdi_ref = cls.copy_vdi(session, sr_ref, swap_vdi_ref)
new_swap_vdi_uuid = session.call_xenapi('VDI.get_uuid',
new_swap_vdi_ref)
session.call_xenapi('VDI.set_name_label', new_swap_vdi_ref,
instance.name + "-swap")
vdi_return_list.append(dict(vdi_type="swap",
vdi_uuid=new_swap_vdi_uuid,
file=None))
return vdi_return_list
@classmethod
def fetch_image(cls, context, session, instance, image, user_id,
project_id, image_type):
@ -575,7 +702,7 @@ class VMHelper(HelperBase):
# which does not have the `uuid` module. To work around this,
# we generate the uuids here (under Python 2.6+) and
# pass them as arguments
uuid_stack = [str(uuid.uuid4()) for i in xrange(2)]
uuid_stack = [str(uuid.uuid4()) for i in xrange(3)]
glance_host, glance_port = glance.pick_glance_api_server()
params = {'image_id': image,
@ -712,6 +839,8 @@ class VMHelper(HelperBase):
args['vdi-ref'] = vdi_ref
# Let the plugin copy the correct number of bytes.
args['image-size'] = str(vdi_size)
if FLAGS.cache_images:
args['cached-image'] = image
task = session.async_call_plugin('glance', fn, args)
filename = session.wait_for_task(task, instance['uuid'])
# Remove the VDI as it is not needed anymore.
@ -1156,6 +1285,15 @@ def integrate_series(data, col, start, until=None):
return total.quantize(Decimal('1.0000'))
def _get_all_vdis_in_sr(session, sr_ref):
for vdi_ref in session.call_xenapi('SR.get_VDIs', sr_ref):
try:
vdi_rec = session.call_xenapi('VDI.get_record', vdi_ref)
yield vdi_ref, vdi_rec
except VMHelper.XenAPI.Failure:
continue
#TODO(sirp): This code comes from XS5.6 pluginlib.py, we should refactor to
# use that implmenetation
def get_vhd_parent(session, vdi_rec):
@ -1208,10 +1346,35 @@ def _wait_for_vhd_coalesce(session, instance, sr_ref, vdi_ref,
* parent_vhd
snapshot
Atter coalesce:
After coalesce:
* parent_vhd
snapshot
"""
def _another_child_vhd():
if not original_parent_uuid:
return False
# Search for any other vdi which parents to original parent and is not
# in the active vm/instance vdi chain.
vdi_uuid = session.call_xenapi('VDI.get_record', vdi_ref)['uuid']
parent_vdi_uuid = get_vhd_parent_uuid(session, vdi_ref)
for ref, rec in _get_all_vdis_in_sr(session, sr_ref):
if ((rec['uuid'] != vdi_uuid) and
(rec['uuid'] != parent_vdi_uuid) and
(rec['sm_config'].get('vhd-parent') == original_parent_uuid)):
# Found another vhd which too parents to original parent.
return True
# Found no other vdi with the same parent.
return False
# Check if original parent has any other child. If so, coalesce will
# not take place.
if _another_child_vhd():
parent_uuid = get_vhd_parent_uuid(session, vdi_ref)
parent_ref = session.call_xenapi("VDI.get_by_uuid", parent_uuid)
base_uuid = get_vhd_parent_uuid(session, parent_ref)
return parent_uuid, base_uuid
max_attempts = FLAGS.xenapi_vhd_coalesce_max_attempts
for i in xrange(max_attempts):
VMHelper.scan_sr(session, instance, sr_ref)
@ -1221,7 +1384,9 @@ def _wait_for_vhd_coalesce(session, instance, sr_ref, vdi_ref,
" %(original_parent_uuid)s, waiting for coalesce...")
% locals())
else:
return parent_uuid
parent_ref = session.call_xenapi("VDI.get_by_uuid", parent_uuid)
base_uuid = get_vhd_parent_uuid(session, parent_ref)
return parent_uuid, base_uuid
greenthread.sleep(FLAGS.xenapi_vhd_coalesce_poll_interval)

View File

@ -186,7 +186,7 @@ class VMOps(object):
def _create_disks(self, context, instance, image_meta):
disk_image_type = VMHelper.determine_disk_image_type(image_meta)
vdis = VMHelper.fetch_image(context, self._session,
vdis = VMHelper.create_image(context, self._session,
instance, instance.image_ref,
instance.user_id, instance.project_id,
disk_image_type)
@ -279,11 +279,11 @@ class VMOps(object):
ramdisk = None
try:
if instance.kernel_id:
kernel = VMHelper.fetch_image(context, self._session,
kernel = VMHelper.create_kernel_image(context, self._session,
instance, instance.kernel_id, instance.user_id,
instance.project_id, vm_utils.ImageType.KERNEL)[0]
if instance.ramdisk_id:
ramdisk = VMHelper.fetch_image(context, self._session,
ramdisk = VMHelper.create_kernel_image(context, self._session,
instance, instance.ramdisk_id, instance.user_id,
instance.project_id, vm_utils.ImageType.RAMDISK)[0]

View File

@ -54,6 +54,7 @@ class RetryException(Exception):
def _copy_kernel_vdi(dest, copy_args):
vdi_uuid = copy_args['vdi_uuid']
vdi_size = copy_args['vdi_size']
cached_image = copy_args['cached-image']
logging.debug("copying kernel/ramdisk file from %s to /boot/guest/%s",
dest, vdi_uuid)
filename = KERNEL_DIR + '/' + vdi_uuid
@ -67,6 +68,17 @@ def _copy_kernel_vdi(dest, copy_args):
#copy only vdi_size bytes
data = f.read(vdi_size)
of.write(data)
if cached_image:
#create a cache file. If caching is enabled, kernel images do not have
#to be fetched from glance.
cached_image = KERNEL_DIR + '/' + cached_image
logging.debug("copying kernel/ramdisk file from %s to /boot/guest/%s",
dest, cached_image)
cache_file = open(cached_image, 'wb')
cache_file.write(data)
cache_file.close()
logging.debug("Done. Filename: %s", cached_image)
f.close()
of.close()
logging.debug("Done. Filename: %s", filename)
@ -264,11 +276,17 @@ def _import_vhds(sr_path, staging_path, uuid_stack):
vdi_return_list = []
paths_to_move = []
image_info = prepare_if_exists(staging_path, 'image.vhd')
image_parent = None
base_info = prepare_if_exists(staging_path, 'base.vhd')
if base_info:
paths_to_move.append(base_info[0])
image_parent = base_info[0]
image_info = prepare_if_exists(staging_path, 'image.vhd', image_parent)
if not image_info:
raise Exception("Invalid image: image.vhd not present")
paths_to_move.append(image_info[0])
paths_to_move.insert(0, image_info[0])
snap_info = prepare_if_exists(staging_path, 'snap.vhd',
image_info[0])
@ -302,9 +320,10 @@ def _prepare_staging_area_for_upload(sr_path, staging_path, vdi_uuids):
('snap' or 'image.vhd')
"""
for name, uuid in vdi_uuids.items():
source = os.path.join(sr_path, "%s.vhd" % uuid)
link_name = os.path.join(staging_path, "%s.vhd" % name)
os.link(source, link_name)
if uuid:
source = os.path.join(sr_path, "%s.vhd" % uuid)
link_name = os.path.join(staging_path, "%s.vhd" % name)
os.link(source, link_name)
def _upload_tarball(staging_path, image_id, glance_host, glance_port,
@ -439,6 +458,24 @@ def _finish_subprocess(proc, cmdline):
return out, err
def create_kernel_ramdisk(session, args):
"""Creates a copy of the kernel/ramdisk image if it is present in the
cache. If the image is not present in the cache, it does nothing.
"""
cached_image = exists(args, 'cached-image')
image_uuid = exists(args, 'new-image-uuid')
cached_image_filename = KERNEL_DIR + '/' + cached_image
filename = KERNEL_DIR + '/' + image_uuid
if os.path.isfile(cached_image_filename):
shutil.copyfile(cached_image_filename, filename)
logging.debug("Done. Filename: %s", filename)
else:
filename = ""
logging.debug("Cached kernel/ramdisk image not found")
return filename
def download_vhd(session, args):
"""Download an image from Glance, unbundle it, and then deposit the VHDs
into the storage repository
@ -491,9 +528,12 @@ def upload_vhd(session, args):
def copy_kernel_vdi(session, args):
vdi = exists(args, 'vdi-ref')
size = exists(args, 'image-size')
cached_image = optional(args, 'cached-image')
#Use the uuid as a filename
vdi_uuid = session.xenapi.VDI.get_uuid(vdi)
copy_args = {'vdi_uuid': vdi_uuid, 'vdi_size': int(size)}
copy_args = {'vdi_uuid': vdi_uuid,
'vdi_size': int(size),
'cached-image': cached_image}
filename = with_vdi_in_dom0(session, vdi, False,
lambda dev:
_copy_kernel_vdi('/dev/%s' % dev, copy_args))
@ -515,4 +555,5 @@ if __name__ == '__main__':
XenAPIPlugin.dispatch({'upload_vhd': upload_vhd,
'download_vhd': download_vhd,
'copy_kernel_vdi': copy_kernel_vdi,
'create_kernel_ramdisk': create_kernel_ramdisk,
'remove_kernel_ramdisk': remove_kernel_ramdisk})