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
This commit is contained in:
alecorps 2021-09-13 17:01:43 +02:00 committed by alecorps
parent bdfab8851f
commit d5faf45e9d
6 changed files with 339 additions and 1 deletions

View File

@ -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):

View File

@ -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,

View File

@ -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'

View File

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

View File

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

View File

@ -0,0 +1,5 @@
---
features:
- |
Added support for VMware VStorageObject based volumes in
VMware vCenter driver. vSphere version 6.5 is required.