diff --git a/nova/tests/unit/virt/vmwareapi/test_vm_util.py b/nova/tests/unit/virt/vmwareapi/test_vm_util.py index ea30895a4d..13383fd9f6 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 0a051d62f5..5b95ee9949 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 6452434ce7..2a42174bf7 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 6386a63d51..cea5195670 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 613dc671c9..a45e084454 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 0000000000..677ed056f2 --- /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.