From d5faf45e9df00528e6e3aa55cd2edd184181a249 Mon Sep 17 00:00:00 2001 From: alecorps Date: Mon, 13 Sep 2021 17:01:43 +0200 Subject: [PATCH] VMware: Support volumes backed by VStorageObject vSphere 6.5 introduced APIs to manage virtual disks (volumes) as first class objects. The new managed disk entity is called VStorageObject aka First Class Disk (FCD). Adding support for volumes backed by VStorageObject. Change-Id: I4a5a9d3537dc175508f0a0fd82507c498737d1a5 --- .../tests/unit/virt/vmwareapi/test_vm_util.py | 61 ++++++ .../unit/virt/vmwareapi/test_volumeops.py | 173 ++++++++++++++++++ nova/virt/vmwareapi/constants.py | 3 +- nova/virt/vmwareapi/vm_util.py | 33 ++++ nova/virt/vmwareapi/volumeops.py | 65 +++++++ ...d-vmware-fcd-support-822edccb0e38bc37.yaml | 5 + 6 files changed, 339 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/add-vmware-fcd-support-822edccb0e38bc37.yaml diff --git a/nova/tests/unit/virt/vmwareapi/test_vm_util.py b/nova/tests/unit/virt/vmwareapi/test_vm_util.py index ea30895a4df4..13383fd9f63e 100644 --- a/nova/tests/unit/virt/vmwareapi/test_vm_util.py +++ b/nova/tests/unit/virt/vmwareapi/test_vm_util.py @@ -23,6 +23,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 pbm +from oslo_vmware import vim_util as vutil from nova import exception from nova.network import model as network_model @@ -1987,6 +1988,66 @@ class VMwareVMUtilTestCase(test.NoDBTestCase): mock_get_name.assert_called_once_with(self._instance.display_name, self._instance.uuid) + def test_create_fcd_id_obj(self): + fcd_id_obj = mock.Mock() + client_factory = mock.Mock() + client_factory.create.return_value = fcd_id_obj + fcd_id = mock.sentinel.fcd_id + ret = vm_util._create_fcd_id_obj(client_factory, fcd_id) + + self.assertEqual(fcd_id_obj, ret) + self.assertEqual(fcd_id, ret.id) + client_factory.create.assert_called_once_with('ns0:ID') + + @mock.patch.object(vm_util, '_create_fcd_id_obj') + @mock.patch.object(vutil, 'get_moref') + def test_attach_fcd(self, get_moref, create_fcd_id_obj): + disk_id = mock.sentinel.disk_id + create_fcd_id_obj.return_value = disk_id + + ds_ref = mock.sentinel.ds_ref + get_moref.return_value = ds_ref + + task = mock.sentinel.task + session = mock.Mock() + session._call_method.return_value = task + + vm_ref = mock.sentinel.vm_ref + fcd_id = mock.sentinel.fcd_id + ds_ref_val = mock.sentinel.ds_ref_val + controller_key = mock.sentinel.controller_key + unit_number = mock.sentinel.unit_number + vm_util.attach_fcd( + session, vm_ref, fcd_id, ds_ref_val, controller_key, unit_number) + + create_fcd_id_obj.assert_called_once_with( + session.vim.client.factory, fcd_id) + get_moref.assert_called_once_with(ds_ref_val, 'Datastore') + session._call_method.assert_called_once_with( + session.vim, "AttachDisk_Task", vm_ref, diskId=disk_id, + datastore=ds_ref, controllerKey=controller_key, + unitNumber=unit_number) + session._wait_for_task.assert_called_once_with(task) + + @mock.patch.object(vm_util, '_create_fcd_id_obj') + def test_detach_fcd(self, create_fcd_id_obj): + disk_id = mock.sentinel.disk_id + create_fcd_id_obj.return_value = disk_id + + task = mock.sentinel.task + session = mock.Mock() + session._call_method.return_value = task + + vm_ref = mock.sentinel.vm_ref + fcd_id = mock.sentinel.fcd_id + vm_util.detach_fcd(session, vm_ref, fcd_id) + + create_fcd_id_obj.assert_called_once_with( + session.vim.client.factory, fcd_id) + session._call_method.assert_called_once_with( + session.vim, "DetachDisk_Task", vm_ref, diskId=disk_id) + session._wait_for_task.assert_called_once_with(task) + @mock.patch.object(driver.VMwareAPISession, 'vim', stubs.fake_vim_prop) class VMwareVMUtilGetHostRefTestCase(test.NoDBTestCase): diff --git a/nova/tests/unit/virt/vmwareapi/test_volumeops.py b/nova/tests/unit/virt/vmwareapi/test_volumeops.py index 0a051d62f548..5b95ee99496d 100644 --- a/nova/tests/unit/virt/vmwareapi/test_volumeops.py +++ b/nova/tests/unit/virt/vmwareapi/test_volumeops.py @@ -12,6 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. +import ddt import mock from oslo_utils.fixture import uuidsentinel as uuids from oslo_vmware import exceptions as oslo_vmw_exceptions @@ -31,6 +32,7 @@ from nova.virt.vmwareapi import vm_util from nova.virt.vmwareapi import volumeops +@ddt.ddt class VMwareVolumeOpsTestCase(test.NoDBTestCase): def setUp(self): @@ -406,6 +408,57 @@ class VMwareVolumeOpsTestCase(test.NoDBTestCase): get_rdm_disk.assert_called_once_with(hardware_devices, disk_uuid) self.assertFalse(detach_disk_from_vm.called) + @mock.patch.object(vm_util, 'get_vm_ref') + @mock.patch.object(vm_util, 'get_vm_state') + @mock.patch.object(vm_util, 'detach_fcd') + def _test__detach_volume_fcd( + self, detach_fcd, get_vm_state, get_vm_ref, + adapter_type=constants.ADAPTER_TYPE_IDE, powered_off=True): + vm_ref = mock.sentinel.vm_ref + get_vm_ref.return_value = vm_ref + + if adapter_type == constants.ADAPTER_TYPE_IDE: + get_vm_state.return_value = ( + power_state.SHUTDOWN if powered_off else power_state.RUNNING) + + fcd_id = mock.sentinel.fcd_id + ds_ref_val = mock.sentinel.ds_ref_val + connection_info = {'data': {'id': fcd_id, + 'ds_ref_val': ds_ref_val, + 'adapter_type': adapter_type}} + instance = mock.sentinel.instance + + if adapter_type == constants.ADAPTER_TYPE_IDE and not powered_off: + self.assertRaises(exception.Invalid, + self._volumeops._detach_volume_fcd, + connection_info, + instance) + detach_fcd.assert_not_called() + else: + self._volumeops._detach_volume_fcd(connection_info, instance) + detach_fcd.assert_called_once_with( + self._volumeops._session, vm_ref, fcd_id) + + @ddt.data( + constants.ADAPTER_TYPE_BUSLOGIC, constants.ADAPTER_TYPE_IDE, + constants.ADAPTER_TYPE_LSILOGICSAS, constants.ADAPTER_TYPE_PARAVIRTUAL) + def test_detach_volume_fcd_powered_off_instance(self, adapter_type): + self._test__detach_volume_fcd(adapter_type=adapter_type) + + @ddt.data( + constants.ADAPTER_TYPE_BUSLOGIC, constants.ADAPTER_TYPE_IDE, + constants.ADAPTER_TYPE_LSILOGICSAS, constants.ADAPTER_TYPE_PARAVIRTUAL) + def test_detach_volume_fcd_powered_on_instance(self, adapter_type): + self._test__detach_volume_fcd(adapter_type=adapter_type, + powered_off=False) + + @mock.patch.object(volumeops.VMwareVolumeOps, '_detach_volume_fcd') + def test_detach_volume_fcd(self, detach_volume_fcd): + connection_info = {'driver_volume_type': constants.DISK_FORMAT_FCD} + instance = mock.sentinel.instance + self._volumeops.detach_volume(connection_info, instance) + detach_volume_fcd.assert_called_once_with(connection_info, instance) + def _test_attach_volume_vmdk(self, adapter_type=None): connection_info = {'driver_volume_type': constants.DISK_FORMAT_VMDK, 'serial': 'volume-fake-id', @@ -498,6 +551,126 @@ class VMwareVolumeOpsTestCase(test.NoDBTestCase): constants.ADAPTER_TYPE_PARAVIRTUAL): self._test_attach_volume_vmdk(adapter_type) + @mock.patch.object(vm_util, 'allocate_controller_key_and_unit_number') + def test_get_controller_key_and_unit( + self, allocate_controller_key_and_unit_number): + key = mock.sentinel.key + unit = mock.sentinel.unit + allocate_controller_key_and_unit_number.return_value = ( + key, unit, None) + + with mock.patch.object(self._volumeops, '_session') as session: + devices = mock.sentinel.devices + session._call_method.return_value = devices + + vm_ref = mock.sentinel.vm_ref + adapter_type = mock.sentinel.adapter_type + ret = self._volumeops._get_controller_key_and_unit( + vm_ref, adapter_type) + self.assertEqual((key, unit, None), ret) + session._call_method.assert_called_once_with( + vutil, 'get_object_property', vm_ref, 'config.hardware.device') + allocate_controller_key_and_unit_number.assert_called_once_with( + session.vim.client.factory, devices, adapter_type) + + @mock.patch.object(volumeops.VMwareVolumeOps, + '_get_controller_key_and_unit') + @mock.patch.object(vm_util, 'reconfigure_vm') + @mock.patch.object(vm_util, 'attach_fcd') + def _test_attach_fcd( + self, attach_fcd, reconfigure_vm, get_controller_key_and_unit, + existing_controller=True): + key = mock.sentinel.key + unit = mock.sentinel.unit + spec = mock.sentinel.spec + if existing_controller: + get_controller_key_and_unit.return_value = (key, unit, None) + else: + get_controller_key_and_unit.side_effect = [(None, None, spec), + (key, unit, None)] + + with mock.patch.object(self._volumeops, '_session') as session: + config_spec = mock.Mock() + session.vim.client.factory.create.return_value = config_spec + + vm_ref = mock.sentinel.vm_ref + adapter_type = mock.sentinel.adapter_type + fcd_id = mock.sentinel.fcd_id + ds_ref_val = mock.sentinel.ds_ref_val + self._volumeops._attach_fcd( + vm_ref, adapter_type, fcd_id, ds_ref_val) + + attach_fcd.assert_called_once_with( + session, vm_ref, fcd_id, ds_ref_val, key, unit) + if existing_controller: + get_controller_key_and_unit.assert_called_once_with( + vm_ref, adapter_type) + reconfigure_vm.assert_not_called() + else: + exp_calls = [mock.call(vm_ref, adapter_type), + mock.call(vm_ref, adapter_type)] + get_controller_key_and_unit.assert_has_calls(exp_calls) + self.assertEqual([spec], config_spec.deviceChange) + reconfigure_vm.assert_called_once_with( + session, vm_ref, config_spec) + + def test_attach_fcd_using_existing_controller(self): + self._test_attach_fcd() + + def test_attach_fcd_using_new_controller(self): + self._test_attach_fcd(existing_controller=False) + + @mock.patch.object(vm_util, 'get_vm_ref') + @mock.patch.object(vm_util, 'get_vm_state') + @mock.patch.object(volumeops.VMwareVolumeOps, '_attach_fcd') + def _test__attach_volume_fcd( + self, attach_fcd, get_vm_state, get_vm_ref, + adapter_type=constants.ADAPTER_TYPE_IDE, powered_off=True): + vm_ref = mock.sentinel.vm_ref + get_vm_ref.return_value = vm_ref + + if adapter_type == constants.ADAPTER_TYPE_IDE: + get_vm_state.return_value = ( + power_state.SHUTDOWN if powered_off else power_state.RUNNING) + + fcd_id = mock.sentinel.fcd_id + ds_ref_val = mock.sentinel.ds_ref_val + connection_info = {'data': {'id': fcd_id, + 'ds_ref_val': ds_ref_val, + 'adapter_type': adapter_type}} + instance = mock.sentinel.instance + + if adapter_type == constants.ADAPTER_TYPE_IDE and not powered_off: + self.assertRaises(exception.Invalid, + self._volumeops._attach_volume_fcd, + connection_info, + instance) + attach_fcd.assert_not_called() + else: + self._volumeops._attach_volume_fcd(connection_info, instance) + attach_fcd.assert_called_once_with( + vm_ref, adapter_type, fcd_id, ds_ref_val) + + @ddt.data( + constants.ADAPTER_TYPE_BUSLOGIC, constants.ADAPTER_TYPE_IDE, + constants.ADAPTER_TYPE_LSILOGICSAS, constants.ADAPTER_TYPE_PARAVIRTUAL) + def test_attach_volume_fcd_powered_off_instance(self, adapter_type): + self._test__attach_volume_fcd(adapter_type=adapter_type) + + @ddt.data( + constants.ADAPTER_TYPE_BUSLOGIC, constants.ADAPTER_TYPE_IDE, + constants.ADAPTER_TYPE_LSILOGICSAS, constants.ADAPTER_TYPE_PARAVIRTUAL) + def test_attach_volume_fcd_powered_on_instance(self, adapter_type): + self._test__attach_volume_fcd(adapter_type=adapter_type, + powered_off=False) + + @mock.patch.object(volumeops.VMwareVolumeOps, '_attach_volume_fcd') + def test_attach_volume_fcd(self, attach_volume_fcd): + connection_info = {'driver_volume_type': constants.DISK_FORMAT_FCD} + instance = mock.sentinel.instance + self._volumeops.attach_volume(connection_info, instance) + attach_volume_fcd.assert_called_once_with(connection_info, instance) + def test_attach_volume_iscsi(self): for adapter_type in (None, constants.DEFAULT_ADAPTER_TYPE, constants.ADAPTER_TYPE_BUSLOGIC, diff --git a/nova/virt/vmwareapi/constants.py b/nova/virt/vmwareapi/constants.py index 6452434ce732..2a42174bf7b0 100644 --- a/nova/virt/vmwareapi/constants.py +++ b/nova/virt/vmwareapi/constants.py @@ -27,7 +27,8 @@ MIN_VC_OVS_VERSION = '5.5.0' DISK_FORMAT_ISO = 'iso' DISK_FORMAT_VMDK = 'vmdk' DISK_FORMAT_ISCSI = 'iscsi' -DISK_FORMATS_ALL = [DISK_FORMAT_ISO, DISK_FORMAT_VMDK] +DISK_FORMAT_FCD = 'vstorageobject' +DISK_FORMATS_ALL = [DISK_FORMAT_ISO, DISK_FORMAT_VMDK, DISK_FORMAT_FCD] DISK_TYPE_THIN = 'thin' CONTAINER_FORMAT_BARE = 'bare' diff --git a/nova/virt/vmwareapi/vm_util.py b/nova/virt/vmwareapi/vm_util.py index 01a2e18c8d62..32b2ebd7e528 100644 --- a/nova/virt/vmwareapi/vm_util.py +++ b/nova/virt/vmwareapi/vm_util.py @@ -1631,3 +1631,36 @@ def rename_vm(session, vm_ref, instance): rename_task = session._call_method(session.vim, "Rename_Task", vm_ref, newName=vm_name) session._wait_for_task(rename_task) + + +def _create_fcd_id_obj(client_factory, fcd_id): + id_obj = client_factory.create('ns0:ID') + id_obj.id = fcd_id + return id_obj + + +def attach_fcd( + session, vm_ref, fcd_id, ds_ref_val, controller_key, unit_number + ): + client_factory = session.vim.client.factory + disk_id = _create_fcd_id_obj(client_factory, fcd_id) + ds_ref = vutil.get_moref(ds_ref_val, 'Datastore') + LOG.debug("Attaching fcd (id: %(fcd_id)s, datastore: %(ds_ref_val)s) to " + "vm: %(vm_ref)s.", + {'fcd_id': fcd_id, + 'ds_ref_val': ds_ref_val, + 'vm_ref': vm_ref}) + task = session._call_method( + session.vim, "AttachDisk_Task", vm_ref, diskId=disk_id, + datastore=ds_ref, controllerKey=controller_key, unitNumber=unit_number) + session._wait_for_task(task) + + +def detach_fcd(session, vm_ref, fcd_id): + client_factory = session.vim.client.factory + disk_id = _create_fcd_id_obj(client_factory, fcd_id) + LOG.debug("Detaching fcd (id: %(fcd_id)s) from vm: %(vm_ref)s.", + {'fcd_id': fcd_id, 'vm_ref': vm_ref}) + task = session._call_method( + session.vim, "DetachDisk_Task", vm_ref, diskId=disk_id) + session._wait_for_task(task) diff --git a/nova/virt/vmwareapi/volumeops.py b/nova/virt/vmwareapi/volumeops.py index 613dc671c963..a45e08445413 100644 --- a/nova/virt/vmwareapi/volumeops.py +++ b/nova/virt/vmwareapi/volumeops.py @@ -367,6 +367,53 @@ class VMwareVolumeOps(object): device_name=device_name) LOG.debug("Attached ISCSI: %s", connection_info, instance=instance) + def _get_controller_key_and_unit(self, vm_ref, adapter_type): + LOG.debug("_get_controller_key_and_unit vm: %(vm_ref)s, adapter: " + "%(adapter)s.", + {'vm_ref': vm_ref, 'adapter': adapter_type}) + client_factory = self._session.vim.client.factory + devices = self._session._call_method(vutil, + "get_object_property", + vm_ref, + "config.hardware.device") + return vm_util.allocate_controller_key_and_unit_number( + client_factory, devices, adapter_type) + + def _attach_fcd(self, vm_ref, adapter_type, fcd_id, ds_ref_val): + (controller_key, unit_number, + controller_spec) = self._get_controller_key_and_unit( + vm_ref, adapter_type) + + if controller_spec: + # No controller available to attach, create one first. + config_spec = self._session.vim.client.factory.create( + 'ns0:VirtualMachineConfigSpec') + config_spec.deviceChange = [controller_spec] + vm_util.reconfigure_vm(self._session, vm_ref, config_spec) + (controller_key, unit_number, + controller_spec) = self._get_controller_key_and_unit( + vm_ref, adapter_type) + + vm_util.attach_fcd( + self._session, vm_ref, fcd_id, ds_ref_val, controller_key, + unit_number) + + def _attach_volume_fcd(self, connection_info, instance): + """Attach fcd volume storage to VM instance.""" + LOG.debug("_attach_volume_fcd: %s", connection_info, instance=instance) + vm_ref = vm_util.get_vm_ref(self._session, instance) + data = connection_info['data'] + adapter_type = data['adapter_type'] + + if adapter_type == constants.ADAPTER_TYPE_IDE: + state = vm_util.get_vm_state(self._session, instance) + if state != power_state.SHUTDOWN: + raise exception.Invalid(_('%s does not support disk ' + 'hotplug.') % adapter_type) + + self._attach_fcd(vm_ref, adapter_type, data['id'], data['ds_ref_val']) + LOG.debug("Attached fcd: %s", connection_info, instance=instance) + def attach_volume(self, connection_info, instance, adapter_type=None): """Attach volume storage to VM instance.""" driver_type = connection_info['driver_volume_type'] @@ -376,6 +423,8 @@ class VMwareVolumeOps(object): self._attach_volume_vmdk(connection_info, instance, adapter_type) elif driver_type == constants.DISK_FORMAT_ISCSI: self._attach_volume_iscsi(connection_info, instance, adapter_type) + elif driver_type == constants.DISK_FORMAT_FCD: + self._attach_volume_fcd(connection_info, instance) else: raise exception.VolumeDriverNotFound(driver_type=driver_type) @@ -558,6 +607,20 @@ class VMwareVolumeOps(object): self.detach_disk_from_vm(vm_ref, instance, device, destroy_disk=True) LOG.debug("Detached ISCSI: %s", connection_info, instance=instance) + def _detach_volume_fcd(self, connection_info, instance): + """Detach fcd volume storage to VM instance.""" + vm_ref = vm_util.get_vm_ref(self._session, instance) + data = connection_info['data'] + adapter_type = data['adapter_type'] + + if adapter_type == constants.ADAPTER_TYPE_IDE: + state = vm_util.get_vm_state(self._session, instance) + if state != power_state.SHUTDOWN: + raise exception.Invalid(_('%s does not support disk ' + 'hotplug.') % adapter_type) + + vm_util.detach_fcd(self._session, vm_ref, data['id']) + def detach_volume(self, connection_info, instance): """Detach volume storage to VM instance.""" driver_type = connection_info['driver_volume_type'] @@ -567,6 +630,8 @@ class VMwareVolumeOps(object): self._detach_volume_vmdk(connection_info, instance) elif driver_type == constants.DISK_FORMAT_ISCSI: self._detach_volume_iscsi(connection_info, instance) + elif driver_type == constants.DISK_FORMAT_FCD: + self._detach_volume_fcd(connection_info, instance) else: raise exception.VolumeDriverNotFound(driver_type=driver_type) diff --git a/releasenotes/notes/add-vmware-fcd-support-822edccb0e38bc37.yaml b/releasenotes/notes/add-vmware-fcd-support-822edccb0e38bc37.yaml new file mode 100644 index 000000000000..677ed056f242 --- /dev/null +++ b/releasenotes/notes/add-vmware-fcd-support-822edccb0e38bc37.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Added support for VMware VStorageObject based volumes in + VMware vCenter driver. vSphere version 6.5 is required.