diff --git a/nova/tests/unit/virt/vmwareapi/test_driver_api.py b/nova/tests/unit/virt/vmwareapi/test_driver_api.py index 25891a015e3..71a290abc3f 100644 --- a/nova/tests/unit/virt/vmwareapi/test_driver_api.py +++ b/nova/tests/unit/virt/vmwareapi/test_driver_api.py @@ -2708,10 +2708,13 @@ def test_live_migration(self, volumes_to_devices, @mock.patch.object(volumeops.VMwareVolumeOps, 'map_volumes_to_devices', returns=[]) + @mock.patch.object(vmops.VMwareVMOps, + 'reconfigure_vm_device_change') @mock.patch.object(vmops.VMwareVMOps, 'live_migration', side_effect=test.TestingException) def test_live_migration_failure_rollback(self, mock_live_migration, + mock_reconfigure_vm_device_change, volumes_to_devices): self._create_instance() migrate_data = self._create_live_migrate_data() @@ -2732,6 +2735,7 @@ def test_live_migration_failure_rollback(self, migrate_data=migrate_data) post_method.assert_not_called() + mock_reconfigure_vm_device_change.assert_called() recover_method.assert_called() def test_rollback_live_migration_at_destination(self): diff --git a/nova/tests/unit/virt/vmwareapi/test_vmops.py b/nova/tests/unit/virt/vmwareapi/test_vmops.py index 0af5d40908c..87d6f6f08c6 100644 --- a/nova/tests/unit/virt/vmwareapi/test_vmops.py +++ b/nova/tests/unit/virt/vmwareapi/test_vmops.py @@ -3179,9 +3179,7 @@ def fake_invoke(module, method, *args, **kwargs): file_path) self.assertEqual('fira-host', result[0]) cookies = result[1] - self.assertEqual(1, len(cookies)) - self.assertEqual('vmware_cgi_ticket', cookies[0].name) - self.assertEqual('"fira-ticket"', cookies[0].value) + self.assertEqual('vmware_cgi_ticket="fira-ticket"', cookies) def test_fetch_vsphere_image(self): vsphere_location = 'vsphere://my?dcPath=mycenter&dsName=mystore' diff --git a/nova/virt/vmwareapi/driver.py b/nova/virt/vmwareapi/driver.py index 42a7ca0b22e..6df3228e7e7 100644 --- a/nova/virt/vmwareapi/driver.py +++ b/nova/virt/vmwareapi/driver.py @@ -19,7 +19,6 @@ A connection to the VMware vCenter platform. """ import contextlib -from operator import attrgetter import os import random import re @@ -173,9 +172,11 @@ def __init__(self, virtapi, scheme="https"): # Register the OpenStack extension self._register_openstack_extension() + client_factory = self._session.vim.client.factory + virtapi._compute.additional_endpoints.extend([ special_spawning._SpecialVmSpawningServer(self), - VmwareRpcService(self._vmops)]) + VmwareRpcService(self._vmops, client_factory)]) def _check_min_version(self): min_version = v_utils.convert_version_to_int(constants.MIN_VC_VERSION) @@ -846,6 +847,8 @@ def check_can_live_migrate_source(self, context, instance, if dest_check_data.instance_already_migrated: return dest_check_data + self._vmops.check_can_live_migrate_source(instance) + if dest_check_data.is_same_vcenter: # Drop the service-credentials, no need to check the volumes, # as we do not need to mess around with them @@ -873,52 +876,9 @@ def pre_live_migration(self, context, instance, block_device_info, if migrate_data.instance_already_migrated: return migrate_data - return self._pre_live_migration(context, instance, + return self._vmops.pre_live_migration(context, instance, block_device_info, network_info, disk_info, migrate_data) - def _pre_live_migration(self, context, instance, block_device_info, - network_info, disk_info, migrate_data): - result = self._vmops.place_vm(context, instance) - - if hasattr(result, 'drsFault'): - LOG.error("Placement Error: %s", vim_util.serialize_object( - result.drsFault, typed=True), instance=instance) - - if (not hasattr(result, 'recommendations') or - not result.recommendations): - raise exception.MigrationError( - reason="PlaceVM did not give any recommendations") - - rs = sorted([r for r in result.recommendations - if r.reason == "xvmotionPlacement" and - r.action], - key=attrgetter("rating")) - if not rs: - raise exception.MigrationError( - reason="Did not get any xvmotionPlacement") - - relocate_spec = rs[0].action[0].relocateSpec - - # Should never happen, but if it does we rather want an error - # here, than sometime down the line - if not relocate_spec.host: - raise exception.MigrationError( - reason="No host with enough resources") - - # Samere here: Should never happen - if not relocate_spec.datastore: - raise exception.MigrationError( - reason="No datastore with enough resources") - - # relocate_defaults are serialized/deserialized on put/get - defaults = migrate_data.relocate_defaults - spec = vim_util.serialize_object(relocate_spec, typed=True) - defaults["relocate_spec"] = spec - # Writing the values back - migrate_data.relocate_defaults = defaults - - return migrate_data - def _get_checked_volumes(self, context, instance, required_values, exc=exception.MigrationError): volumes = self._get_volume_mappings(context, instance) @@ -946,6 +906,7 @@ def live_migration(self, context, instance, dest, post_method(context, instance, dest, block_migration, migrate_data) return + original_cdroms = [] try: # We require the target-datastore for all volume-attachment required_volume_attributes = ["datastore_ref"] @@ -971,12 +932,16 @@ def live_migration(self, context, instance, dest, vif_model = None # Doesn't matter as we won't change the type migrate_data.vif_infos = target.get_vif_info(context, vif_model=vif_model, network_info=dest_network_info) - self._vmops.live_migration(instance, migrate_data, volumes) + + self._vmops.live_migration(context, instance, migrate_data, + volumes, original_cdroms) LOG.info("Migration operation completed", instance=instance) post_method(context, instance, dest, block_migration, migrate_data) except Exception: LOG.exception("Failed due to an exception", instance=instance) with excutils.save_and_reraise_exception(): + self._vmops.reconfigure_vm_device_change(context, instance, + original_cdroms) # We are still in the task-state migrating, so cannot # recover the DRS settings. We rely on the sync to do that LOG.debug("Calling live migration recover_method " diff --git a/nova/virt/vmwareapi/rpc.py b/nova/virt/vmwareapi/rpc.py index 45314d9184d..4ad7610cce9 100644 --- a/nova/virt/vmwareapi/rpc.py +++ b/nova/virt/vmwareapi/rpc.py @@ -75,13 +75,33 @@ def confirm_migration_destination(self, ctxt, instance=None): return self._call('confirm_migration_destination', ctxt, instance_uuid=instance.uuid) + def prepare_ds_transfer(self, ctxt, request_method=None, + ds_ref=None, path=None): + ds_ref_value = vutil.get_moref_value(ds_ref) + return self._call('prepare_ds_transfer', ctxt, + request_method=request_method, + ds_ref_value=ds_ref_value, path=path) + + def delete_config_drive_files(self, ctxt, instance=None, cdroms=None): + serialized = [vutil.serialize_object(spec, True) for spec in cdroms] + return self._call('delete_config_drive_files', ctxt, + instance_uuid=instance.uuid, + cdroms=serialized) + + def reconfigure_vm_device_change(self, ctxt, instance=None, devices=None): + serialized = [vutil.serialize_object(spec, True) for spec in devices] + return self._call('reconfigure_vm_device_change', ctxt, + instance_uuid=instance.uuid, + devices=serialized) + @profiler.trace_cls("rpc") class VmwareRpcService(object): target = messaging.Target(version=_VERSION, namespace=_NAMESPACE) - def __init__(self, vm_ops): + def __init__(self, vm_ops, client_factory): self._vm_ops = vm_ops + self._client_factory = client_factory def get_vif_info(self, ctxt, vif_model=None, network_info=None): return self._vm_ops.get_vif_info(ctxt, vif_model=vif_model, @@ -112,3 +132,33 @@ def rollback_migrate_disk(self, ctxt, instance_uuid=None, def confirm_migration_destination(self, ctxt, instance_uuid=None): instance = Instance.get_by_uuid(ctxt, instance_uuid, expected_attrs=[]) return self._vm_ops.confirm_migration_destination(ctxt, instance) + + def prepare_ds_transfer(self, ctxt, request_method=None, + ds_ref_value=None, path=None): + ds_ref = vutil.get_moref(ds_ref_value, 'Datastore') + return self._vm_ops.prepare_ds_transfer(ctxt, + request_method=request_method, + ds_ref=ds_ref, + path=path) + + def delete_config_drive_files(self, ctxt, instance_uuid=None, + cdroms=None): + instance = Instance.get_by_uuid(ctxt, instance_uuid, expected_attrs=[]) + cf = self._client_factory + deserialized = [ + vutil.deserialize_object(cf, spec, "VirtualDeviceConfigSpec") + for spec in cdroms] + + return self._vm_ops.delete_config_drive_files(ctxt, + instance=instance, cdroms=deserialized) + + def reconfigure_vm_device_change(self, ctxt, instance_uuid=None, + devices=None): + instance = Instance.get_by_uuid(ctxt, instance_uuid, expected_attrs=[]) + cf = self._client_factory + deserialized = [ + vutil.deserialize_object(cf, spec, "VirtualDeviceConfigSpec") + for spec in devices] + + return self._vm_ops.reconfigure_vm_device_change(ctxt, + instance=instance, devices=deserialized) diff --git a/nova/virt/vmwareapi/vm_util.py b/nova/virt/vmwareapi/vm_util.py index 6cd8d3cd464..37be92e0b81 100644 --- a/nova/virt/vmwareapi/vm_util.py +++ b/nova/virt/vmwareapi/vm_util.py @@ -793,7 +793,8 @@ def get_cdrom_attach_config_spec(client_factory, datastore, file_path, controller_key, - cdrom_unit_number): + cdrom_unit_number, + cdrom_key=-1): """Builds and returns the cdrom attach config spec.""" config_spec = client_factory.create('ns0:VirtualMachineConfigSpec') @@ -802,7 +803,8 @@ def get_cdrom_attach_config_spec(client_factory, datastore, controller_key, file_path, - cdrom_unit_number) + cdrom_unit_number, + cdrom_key=cdrom_key) device_config_spec.append(virtual_device_config_spec) @@ -880,7 +882,8 @@ def get_hardware_devices_by_type(session, vm_ref): nics = {} controllers = {} disks = {} - swap = [] + cdroms = {} + swap = None ephemeral = [] first_device = None @@ -902,7 +905,7 @@ def get_hardware_devices_by_type(session, vm_ref): backing_type = device.backing.__class__.__name__ if backing_type == "VirtualDiskFlatVer2BackingInfo": if "swap" in device.backing.fileName: - swap.append(device) + swap = device elif "ephemeral" in device.backing.fileName: ephemeral.append(device) else: @@ -917,6 +920,8 @@ def get_hardware_devices_by_type(session, vm_ref): disks[uuid.lower()] = device elif backing_type == "VirtualDiskRawDiskMappingVer1BackingInfo": disks[device.backing.lunUuid.lower()] = device + elif device_type == "VirtualCdrom": + cdroms[int(device.key)] = device return { "nics": nics, @@ -924,6 +929,7 @@ def get_hardware_devices_by_type(session, vm_ref): "swap": swap, "ephemeral": ephemeral, "disks": disks, + "cdroms": cdroms, "root_device": first_device } @@ -1084,28 +1090,31 @@ def create_virtual_cdrom_spec(client_factory, datastore, controller_key, file_path, - cdrom_unit_number): + cdrom_unit_number, + cdrom_key=-1, + cdrom_device_backing=None): """Builds spec for the creation of a new Virtual CDROM to the VM.""" config_spec = client_factory.create( 'ns0:VirtualDeviceConfigSpec') - config_spec.operation = "add" + config_spec.operation = "edit" if cdrom_key > -1 else "add" cdrom = client_factory.create('ns0:VirtualCdrom') - cdrom_device_backing = client_factory.create( - 'ns0:VirtualCdromIsoBackingInfo') - cdrom_device_backing.datastore = datastore - cdrom_device_backing.fileName = file_path + if not cdrom_device_backing and file_path: + cdrom_device_backing = client_factory.create( + 'ns0:VirtualCdromIsoBackingInfo') + cdrom_device_backing.datastore = datastore + cdrom_device_backing.fileName = file_path cdrom.backing = cdrom_device_backing cdrom.controllerKey = controller_key cdrom.unitNumber = cdrom_unit_number - cdrom.key = -1 + cdrom.key = cdrom_key connectable_spec = client_factory.create('ns0:VirtualDeviceConnectInfo') connectable_spec.startConnected = True connectable_spec.allowGuestControl = False - connectable_spec.connected = True + connectable_spec.connected = cdrom_device_backing is not None cdrom.connectable = connectable_spec diff --git a/nova/virt/vmwareapi/vmops.py b/nova/virt/vmwareapi/vmops.py index a03b1f5817e..12c223812e5 100644 --- a/nova/virt/vmwareapi/vmops.py +++ b/nova/virt/vmwareapi/vmops.py @@ -19,14 +19,17 @@ Class for VM tasks like spawn, snapshot, suspend, resume etc. """ -import collections +import contextlib import copy import itertools import math +from operator import attrgetter from operator import itemgetter import os import re +import shutil import sys +import threading import time import decorator @@ -34,6 +37,7 @@ from oslo_concurrency import lockutils from oslo_log import log as logging +import oslo_messaging as messaging from oslo_serialization import jsonutils from oslo_utils import excutils from oslo_utils import strutils @@ -42,6 +46,7 @@ from oslo_utils import uuidutils from oslo_vmware import exceptions as vexc from oslo_vmware.objects import datastore as ds_obj +from oslo_vmware import rw_handles from oslo_vmware import vim_util as vutil from nova.api.metadata import base as instance_metadata @@ -80,6 +85,7 @@ RESIZE_TOTAL_STEPS = 11 HOST_IP_ADDR_SEPARATOR = "|" +CONFIGDRIVE_NAME = "configdrive.iso" class VirtualMachineInstanceConfigInfo(object): @@ -472,19 +478,16 @@ def _get_storage_policy(self, flavor): CONF.vmware.pbm_default_policy) return None - def _get_esx_host_and_cookies(self, datastore, dc_path, file_path): + def _get_esx_host_and_cookies(self, datastore, dc_path, file_path, + method='PUT'): hosts = datastore.get_connected_hosts(self._session) host = ds_obj.Datastore.choose_host(hosts) host_name = self._session._call_method(vutil, 'get_object_property', host, 'name') url = ds_obj.DatastoreURL('https', host_name, file_path, dc_path, datastore.name) - cookie_header = url.get_transfer_ticket(self._session, 'PUT') - name, value = cookie_header.split('=') - # TODO(rgerganov): this is a hack to emulate cookiejar until we fix - # oslo.vmware to accept plain http headers - Cookie = collections.namedtuple('Cookie', ['name', 'value']) - return host_name, [Cookie(name, value)] + cookie_header = url.get_transfer_ticket(self._session, method) + return host_name, cookie_header def _fetch_vsphere_image(self, context, vi, image_ds_loc): """Fetch image which is located on a vSphere datastore.""" @@ -518,6 +521,48 @@ def _fetch_vsphere_image(self, context, vi, image_ds_loc): 'datastore_name': vi.datastore.name}, instance=vi.instance) + def _get_ds_file_handle(self, ds_path, + ds_ref=None, datastore=None, dc_info=None, + method="PUT", file_size=None, instance=None): + """Get a file handle to individual file to host via HTTP PUT/GET.""" + session = self._session + + LOG.debug("%(method)s to file %(ds_path)s", + {'method': method, 'ds_path': ds_path}, instance=instance) + + if not datastore: + if not ds_ref: + raise ValueError( + "Either datastore or ds_ref needs to be passed") + datastore = ds_obj.get_datastore_by_ref(self._session, + ds_ref) + + # try to get esx cookie to upload + try: + dc_path = 'ha-datacenter' + host, cookies = self._get_esx_host_and_cookies(datastore, + dc_path, ds_path.rel_path) + except Exception as e: + LOG.warning("Get esx cookies failed: %s", e, + instance=instance) + if not dc_info: + dc_info = ds_util.get_dc_info(session, ds_ref) + dc_path = vutil.get_inventory_path(session.vim, dc_info.ref) + + host = self._session._host + cookies = session.vim.client.options.transport.cookiejar + + if method == 'GET': + return rw_handles.FileReadHandle(host, session._port, + dc_path, ds_path.datastore, cookies=cookies, + file_path=ds_path.rel_path) + else: + if file_size is None: + raise ValueError("file_size needs to be set for PUT") + return rw_handles.FileWriteHandle(host, session._port, + dc_path, ds_path.datastore, cookies=cookies, + file_path=ds_path.rel_path, file_size=file_size) + def _fetch_image_as_file(self, context, vi, image_ds_loc): """Download image as an individual file to host via HTTP PUT.""" session = self._session @@ -1531,10 +1576,10 @@ def _create_config_drive(self, context, instance, injected_files, try: with configdrive.ConfigDriveBuilder(instance_md=inst_md) as cdb: with utils.tempdir() as tmp_path: - tmp_file = os.path.join(tmp_path, 'configdrive.iso') + tmp_file = os.path.join(tmp_path, CONFIGDRIVE_NAME) cdb.make_drive(tmp_file) - upload_iso_path = "%s/configdrive.iso" % ( - upload_folder) + upload_iso_path = "%s/%s" % ( + upload_folder, CONFIGDRIVE_NAME) images.upload_iso_to_datastore( tmp_file, instance, host=self._session._host, @@ -2460,6 +2505,25 @@ def _revert_migration_update_disks(self, vm_ref, instance, vmdk, self._resize_create_ephemerals_and_swap(vm_ref, instance, block_device_info) + def prepare_ds_transfer(self, ctxt, request_method=None, ds_ref=None, + path=None): + schema = 'https' + ds = ds_obj.get_datastore_by_ref(self._session, ds_ref) + dc_info = ds_util.get_dc_info(self._session, ds_ref) + dc_name = 'ha-datacenter' # When connecting directly to esxi host + hosts = ds.get_connected_hosts(self._session) + host_ref = ds.choose_host(hosts) + hostname = vim_util.get_entity_name(self._session, host_ref) + + ds_url = ds.build_url(schema, hostname, path, datacenter_name=dc_name) + + if request_method != "GET": + ds_path = ds.build_path(path) + ds_util.mkdir(self._session, ds_path.parent, dc_info.ref) + + ticket = ds_url.get_transfer_ticket(self._session, request_method) + return {'ds_url': str(ds_url), 'ticket': ticket} + def finish_revert_migration(self, context, instance, network_info, block_device_info, power_on=True): """Finish reverting a resize.""" @@ -2707,7 +2771,51 @@ def _get_network_device_change(self, vm_ref, image_meta, network_info): return device_changes - def live_migration(self, instance, migrate_data, volume_mapping): + def pre_live_migration(self, context, instance, block_device_info, + network_info, disk_info, migrate_data): + result = self.place_vm(context, instance) + + if hasattr(result, 'drsFault'): + LOG.error("Placement Error: %s", vutil.serialize_object( + result.drsFault, typed=True), instance=instance) + + if (not hasattr(result, 'recommendations') or + not result.recommendations): + raise exception.MigrationError( + reason="PlaceVM did not give any recommendations") + + rs = sorted([r for r in result.recommendations + if r.reason == "xvmotionPlacement" and + r.action], + key=attrgetter("rating")) + if not rs: + raise exception.MigrationError( + reason="Did not get any xvmotionPlacement") + + relocate_spec = rs[0].action[0].relocateSpec + + # Should never happen, but if it does we rather want an error + # here, than sometime down the line + if not relocate_spec.host: + raise exception.MigrationError( + reason="No host with enough resources") + + # Same here: Should never happen + if not relocate_spec.datastore: + raise exception.MigrationError( + reason="No datastore with enough resources") + + # relocate_defaults are serialized/deserialized on put/get + defaults = migrate_data.relocate_defaults + spec = vutil.serialize_object(relocate_spec, typed=True) + defaults["relocate_spec"] = spec + # Writing the values back + migrate_data.relocate_defaults = defaults + + return migrate_data + + def live_migration(self, context, instance, migrate_data, volume_mapping, + original_cdroms): defaults = migrate_data.relocate_defaults client_factory = self._session.vim.client.factory @@ -2730,11 +2838,15 @@ def live_migration(self, instance, migrate_data, volume_mapping): vm_ref = vm_util.get_vm_ref(self._session, instance) + migration = migrate_data.migration + target = self.api_for_migration(migration) + device_config_spec = [] relocate_spec.deviceChange = device_config_spec disks = [] relocate_spec.disk = disks + new_cdroms = [] netdevices = [] for device in vm_util.get_hardware_devices(self._session, vm_ref): class_name = device.__class__.__name__ @@ -2744,18 +2856,47 @@ def live_migration(self, instance, migrate_data, volume_mapping): locator = client_factory.create( "ns0:VirtualMachineRelocateSpecDiskLocator") locator.diskId = device.key - target = volume_mapping.get(device.key) - if not target: # Not a volume + target_mapping = volume_mapping.get(device.key) + if not target_mapping: # Not a volume locator.datastore = datastore else: - locator.datastore = target["datastore_ref"] - profile_id = target.get("profile_id") + locator.datastore = target_mapping["datastore_ref"] + profile_id = target_mapping.get("profile_id") if profile_id: profile_spec = client_factory.create( "ns0:VirtualMachineDefinedProfileSpec") profile_spec.profileId = profile_id locator.profile = [profile_spec] disks.append(locator) + elif class_name == "VirtualCdrom": + # Not really nice, but it looks like CD-ROMs are not really + # that well supported in live-migration, especially + # across VCenters. + # So we have to + # - disconnect the cdrom on the source + # - copy the iso over to the destination + # - migrate the vm over + # - reattach the iso + # And no, it can't be done in the first and the last step + # cannot be merged into the relocate_spec + + # Create a spec to recover the original state + original_cdroms.append( + vm_util.create_virtual_cdrom_spec(client_factory, None, + device.controllerKey, None, + device.unitNumber, device.key, device.backing)) + + ds_path = self._disconnect_and_copy_cdrom(context, instance, + device, target, datastore) + + if not ds_path: # Nothing to do for this CD-ROM + continue + + # Create a spec for the target config + target_spec = vm_util.create_virtual_cdrom_spec(client_factory, + datastore, device.controllerKey, str(ds_path), + device.unitNumber, device.key) + new_cdroms.append(target_spec) for vif_info in migrate_data.vif_infos: device = vmwarevif.get_network_device(netdevices, @@ -2772,7 +2913,137 @@ def live_migration(self, instance, migrate_data, volume_mapping): config_spec.device = device device_config_spec.append(config_spec) - vm_util.relocate_vm(self._session, vm_ref, spec=relocate_spec) + try: + vm_util.relocate_vm(self._session, vm_ref, spec=relocate_spec) + except vexc.VimException as e: + target.delete_config_drive_files(context, instance, new_cdroms) + raise e + + self.delete_config_drive_files(context, instance, original_cdroms) + + if new_cdroms: + # Technically a post-live-migration task, but we have no + # way of passing those changes without API changes to the + # destination compute-host + try: + target.reconfigure_vm_device_change(context, instance, + new_cdroms) + except messaging.RemoteError: + # Failing now would stop the instance reflecting + # the new host + LOG.exception("Failed to reconfiguring cdroms. " + "Swallowing error as we can't go back now", + instance=instance) + + def delete_config_drive_files(self, ctxt, instance, cdroms): + for cdrom_spec in cdroms: + try: + datastore = getattr(cdrom_spec.device.backing, 'datastore', + None) + if not datastore: + continue + file_name = getattr(cdrom_spec.device.backing, 'fileName', + None) + if not file_name: + continue + if not file_name.endswith(CONFIGDRIVE_NAME): + continue + dc_info = ds_util.get_dc_info(self._session, datastore) + ds_util.file_delete(self._session, file_name, dc_info.ref) + except vexc.VimException: + # No reason to error out the instance, as it is working + # but we need some visiblity here for the operator + LOG.exception("Cannot delete file %r", file_name, + instance=instance) + + def _disconnect_and_copy_cdrom(self, context, instance, cdrom, target, + dest_ds_ref): + backing = getattr(cdrom, "backing", None) + if not backing: + return None + + file_name = getattr(backing, "fileName", None) + if not file_name: + return None + + ds_ref = getattr(backing, "datastore", None) + if not ds_ref: + return None + + get_path = ds_obj.DatastorePath.parse(file_name) + + dest = target.prepare_ds_transfer(context, "PUT", dest_ds_ref, + get_path.rel_path) + ds_url = dest['ds_url'] + ticket = dest['ticket'] + + if cdrom.connectable.connected: + self._force_disconnect_cdrom(instance, cdrom) + + with contextlib.closing(self._get_ds_file_handle( + get_path, ds_ref, method='GET')) as get: + put = rw_handles.FileWriteHandle(ds_url, + cookies=ticket, + file_size=get.get_size()) + with contextlib.closing(put): + shutil.copyfileobj(get, put) + + ds_url = ds_obj.DatastoreURL.urlparse(ds_url) + ds_path = ds_obj.DatastorePath(ds_url.datastore_name, ds_url.path) + return ds_path + + def _force_disconnect_cdrom(self, instance, cdrom): + vm_ref = vm_util.get_vm_ref(self._session, instance) + cf = self._session.vim.client.factory + + backing = cf.create("ns0:VirtualCdromRemoteAtapiBackingInfo") + backing.deviceName = "" + backing.useAutoDetect = True + + device_change = vm_util.create_virtual_cdrom_spec(cf, None, + cdrom.controllerKey, None, + cdrom.unitNumber, cdrom.key, backing) + + config_spec = cf.create('ns0:VirtualMachineConfigSpec') + config_spec.deviceChange = [device_change] + + reconfig_task = self._session._call_method(self._session.vim, + "ReconfigVM_Task", vm_ref, + spec=config_spec) + task_completed = threading.Event() + + def set_task_completed(gt): + task_completed.set() + + wait_for_task = utils.spawn( + self._session.wait_for_task, reconfig_task) + wait_for_task.link(set_task_completed) + + while not task_completed.is_set(): + question = self._session._call_method(vutil, + "get_object_property", vm_ref, "summary.runtime.question") + if question and any(message + for message in question.message + if message.id == "msg.cdromdisconnect.locked"): + yes = next(info + for info in question.choice.choiceInfo + if info.label == "button.yes") + LOG.warning("Forcing disconnect for cdrom", instance=instance) + self._session._call_method(self._session.vim, "AnswerVM", + vm_ref, + questionId=question.id, + answerChoice=yes.key) + task_completed.wait(1) + + @compute_utils.wrap_instance_event(prefix="vmwareapi") + def reconfigure_vm_device_change(self, context, instance, devices): + if not devices: + return + serialized = [vutil.serialize_object(spec) for spec in devices] + LOG.debug("Reconfiguring devices %s", serialized, instance=instance) + vm_ref = vm_util.get_vm_ref(self._session, instance) + return vm_util.reconfigure_vm_device_change(self._session, vm_ref, + devices) def _detach_volumes(self, instance, block_device_info): disks = driver.block_device_info_get_mapping(block_device_info) @@ -4271,3 +4542,19 @@ def _get_instance_config_info(self, context, instance, image_meta=None): extra_specs = self._get_extra_specs(instance.flavor, image_meta) return self._get_vm_config_info(context, instance, image_info, extra_specs) + + def check_can_live_migrate_source(self, instance): + vm_ref = vm_util.get_vm_ref(self._session, instance) + devices = vm_util.get_hardware_devices_by_type(self._session, vm_ref) + for cdrom in six.itervalues(devices["cdroms"]): + backing = getattr(cdrom, 'backing', None) + if not backing: + continue + + file_name = getattr(backing, 'fileName', None) + if not file_name: + continue + if not file_name.endswith(CONFIGDRIVE_NAME): + raise exception.MigrationPreCheckError( + reason=("Found non-configdrive CD-ROM. " + "Can only migrate configdrive"))