VMware: Create volume backing in specific clusters
The VMDK driver doesn't allow specifying a set of vCenter clusters as the target for volume backing creation unlike the VMware Nova driver. This patch adds support for an optional list of vCenter cluster names in Cinder conf which will be used by the VMDK driver as targets for volume backing creation. DocImpact Added a new config option 'vmware_cluster_name' which specifies a vCenter compute cluster where volumes should be created. Change-Id: I0dcb3a8ac7c9eaa0d0697f4967873d82bf1bbddf
This commit is contained in:
parent
ab2b966ea1
commit
ec5c84d121
|
@ -383,6 +383,14 @@ class DatastoreTest(test.TestCase):
|
|||
self._vops.get_connected_hosts.reset_mock()
|
||||
self._vops.get_connected_hosts.return_value = None
|
||||
|
||||
def test_select_datastore_with_empty_host_list(self):
|
||||
size_bytes = units.Ki
|
||||
req = {self._ds_sel.SIZE_BYTES: size_bytes}
|
||||
self._vops.get_hosts.return_value = mock.Mock(objects=[])
|
||||
|
||||
self.assertEqual((), self._ds_sel.select_datastore(req, hosts=[]))
|
||||
self._vops.get_hosts.assert_called_once_with()
|
||||
|
||||
@mock.patch('oslo_vmware.pbm.get_profile_id_by_name')
|
||||
@mock.patch('cinder.volume.drivers.vmware.datastore.DatastoreSelector.'
|
||||
'_filter_by_profile')
|
||||
|
|
|
@ -149,6 +149,7 @@ class VMwareEsxVmdkDriverTestCase(test.TestCase):
|
|||
TMP_DIR = "/vmware-tmp"
|
||||
CA_FILE = "/etc/ssl/rui-ca-cert.pem"
|
||||
VMDK_DRIVER = vmdk.VMwareEsxVmdkDriver
|
||||
CLUSTERS = ["cls-1", "cls-2"]
|
||||
|
||||
def setUp(self):
|
||||
super(VMwareEsxVmdkDriverTestCase, self).setUp()
|
||||
|
@ -166,10 +167,11 @@ class VMwareEsxVmdkDriverTestCase(test.TestCase):
|
|||
self._config.vmware_tmp_dir = self.TMP_DIR
|
||||
self._config.vmware_ca_file = self.CA_FILE
|
||||
self._config.vmware_insecure = False
|
||||
self._config.vmware_cluster_name = self.CLUSTERS
|
||||
self._db = mock.Mock()
|
||||
self._driver = vmdk.VMwareEsxVmdkDriver(configuration=self._config,
|
||||
db=self._db)
|
||||
api_retry_count = self._config.vmware_api_retry_count,
|
||||
api_retry_count = self._config.vmware_api_retry_count
|
||||
task_poll_interval = self._config.vmware_task_poll_interval,
|
||||
self._session = api.VMwareAPISession(self.IP, self.USERNAME,
|
||||
self.PASSWORD, api_retry_count,
|
||||
|
@ -1773,21 +1775,37 @@ class VMwareVcVmdkDriverTestCase(VMwareEsxVmdkDriverTestCase):
|
|||
version = self._driver._get_vc_version()
|
||||
self.assertEqual(ver.LooseVersion('6.0.1'), version)
|
||||
|
||||
@mock.patch('cinder.volume.drivers.vmware.volumeops.VMwareVolumeOps')
|
||||
@mock.patch('cinder.volume.drivers.vmware.vmdk.VMwareVcVmdkDriver.'
|
||||
'_get_vc_version')
|
||||
@mock.patch('cinder.volume.drivers.vmware.vmdk.VMwareVcVmdkDriver.'
|
||||
'session', new_callable=mock.PropertyMock)
|
||||
def test_do_setup_with_pbm_disabled(self, session, get_vc_version):
|
||||
def test_do_setup_with_pbm_disabled(self, session, get_vc_version,
|
||||
vops_cls):
|
||||
session_obj = mock.Mock(name='session')
|
||||
session.return_value = session_obj
|
||||
get_vc_version.return_value = ver.LooseVersion('5.0')
|
||||
|
||||
cluster_refs = mock.Mock()
|
||||
cluster_refs.values.return_value = mock.sentinel.cluster_refs
|
||||
vops = mock.Mock()
|
||||
vops.get_cluster_refs.return_value = cluster_refs
|
||||
|
||||
def vops_side_effect(session, max_objects):
|
||||
vops._session = session
|
||||
vops._max_objects = max_objects
|
||||
return vops
|
||||
|
||||
vops_cls.side_effect = vops_side_effect
|
||||
|
||||
self._driver.do_setup(mock.ANY)
|
||||
|
||||
self.assertFalse(self._driver._storage_policy_enabled)
|
||||
get_vc_version.assert_called_once_with()
|
||||
self.assertEqual(session_obj, self._driver.volumeops._session)
|
||||
self.assertEqual(session_obj, self._driver.ds_sel._session)
|
||||
self.assertEqual(mock.sentinel.cluster_refs, self._driver._clusters)
|
||||
vops.get_cluster_refs.assert_called_once_with(self.CLUSTERS)
|
||||
|
||||
@mock.patch('oslo_vmware.pbm.get_pbm_wsdl_location')
|
||||
@mock.patch('cinder.volume.drivers.vmware.vmdk.VMwareVcVmdkDriver.'
|
||||
|
@ -1807,12 +1825,14 @@ class VMwareVcVmdkDriverTestCase(VMwareEsxVmdkDriverTestCase):
|
|||
get_pbm_wsdl_location.assert_called_once_with(
|
||||
six.text_type(vc_version))
|
||||
|
||||
@mock.patch('cinder.volume.drivers.vmware.volumeops.VMwareVolumeOps')
|
||||
@mock.patch('oslo_vmware.pbm.get_pbm_wsdl_location')
|
||||
@mock.patch('cinder.volume.drivers.vmware.vmdk.VMwareVcVmdkDriver.'
|
||||
'_get_vc_version')
|
||||
@mock.patch('cinder.volume.drivers.vmware.vmdk.VMwareVcVmdkDriver.'
|
||||
'session', new_callable=mock.PropertyMock)
|
||||
def test_do_setup(self, session, get_vc_version, get_pbm_wsdl_location):
|
||||
def test_do_setup(self, session, get_vc_version, get_pbm_wsdl_location,
|
||||
vops_cls):
|
||||
session_obj = mock.Mock(name='session')
|
||||
session.return_value = session_obj
|
||||
|
||||
|
@ -1820,6 +1840,18 @@ class VMwareVcVmdkDriverTestCase(VMwareEsxVmdkDriverTestCase):
|
|||
get_vc_version.return_value = vc_version
|
||||
get_pbm_wsdl_location.return_value = 'file:///pbm.wsdl'
|
||||
|
||||
cluster_refs = mock.Mock()
|
||||
cluster_refs.values.return_value = mock.sentinel.cluster_refs
|
||||
vops = mock.Mock()
|
||||
vops.get_cluster_refs.return_value = cluster_refs
|
||||
|
||||
def vops_side_effect(session, max_objects):
|
||||
vops._session = session
|
||||
vops._max_objects = max_objects
|
||||
return vops
|
||||
|
||||
vops_cls.side_effect = vops_side_effect
|
||||
|
||||
self._driver.do_setup(mock.ANY)
|
||||
|
||||
self.assertTrue(self._driver._storage_policy_enabled)
|
||||
|
@ -1828,6 +1860,8 @@ class VMwareVcVmdkDriverTestCase(VMwareEsxVmdkDriverTestCase):
|
|||
six.text_type(vc_version))
|
||||
self.assertEqual(session_obj, self._driver.volumeops._session)
|
||||
self.assertEqual(session_obj, self._driver.ds_sel._session)
|
||||
self.assertEqual(mock.sentinel.cluster_refs, self._driver._clusters)
|
||||
vops.get_cluster_refs.assert_called_once_with(self.CLUSTERS)
|
||||
|
||||
@mock.patch.object(VMDK_DRIVER, '_extend_volumeops_virtual_disk')
|
||||
@mock.patch.object(VMDK_DRIVER, '_create_backing')
|
||||
|
@ -2578,6 +2612,106 @@ class VMwareVcVmdkDriverTestCase(VMwareEsxVmdkDriverTestCase):
|
|||
close.assert_called_once_with(fd)
|
||||
delete_if_exists.assert_called_once_with(tmp)
|
||||
|
||||
@mock.patch.object(VMDK_DRIVER, 'volumeops')
|
||||
@mock.patch.object(VMDK_DRIVER, 'ds_sel')
|
||||
def test_select_datastore(self, ds_sel, vops):
|
||||
cls_1 = mock.sentinel.cls_1
|
||||
cls_2 = mock.sentinel.cls_2
|
||||
self._driver._clusters = [cls_1, cls_2]
|
||||
|
||||
host_1 = mock.sentinel.host_1
|
||||
host_2 = mock.sentinel.host_2
|
||||
host_3 = mock.sentinel.host_3
|
||||
vops.get_cluster_hosts.side_effect = [[host_1, host_2], [host_3]]
|
||||
|
||||
best_candidate = mock.sentinel.best_candidate
|
||||
ds_sel.select_datastore.return_value = best_candidate
|
||||
|
||||
req = mock.sentinel.req
|
||||
self.assertEqual(best_candidate, self._driver._select_datastore(req))
|
||||
|
||||
exp_calls = [mock.call(cls_1), mock.call(cls_2)]
|
||||
self.assertEqual(exp_calls, vops.get_cluster_hosts.call_args_list)
|
||||
|
||||
ds_sel.select_datastore.assert_called_once_with(
|
||||
req, hosts=[host_1, host_2, host_3])
|
||||
|
||||
@mock.patch.object(VMDK_DRIVER, 'volumeops')
|
||||
@mock.patch.object(VMDK_DRIVER, 'ds_sel')
|
||||
def test_select_datastore_with_no_best_candidate(self, ds_sel, vops):
|
||||
cls_1 = mock.sentinel.cls_1
|
||||
cls_2 = mock.sentinel.cls_2
|
||||
self._driver._clusters = [cls_1, cls_2]
|
||||
|
||||
host_1 = mock.sentinel.host_1
|
||||
host_2 = mock.sentinel.host_2
|
||||
host_3 = mock.sentinel.host_3
|
||||
vops.get_cluster_hosts.side_effect = [[host_1, host_2], [host_3]]
|
||||
|
||||
ds_sel.select_datastore.return_value = ()
|
||||
|
||||
req = mock.sentinel.req
|
||||
self.assertRaises(vmdk_exceptions.NoValidDatastoreException,
|
||||
self._driver._select_datastore,
|
||||
req)
|
||||
|
||||
exp_calls = [mock.call(cls_1), mock.call(cls_2)]
|
||||
self.assertEqual(exp_calls, vops.get_cluster_hosts.call_args_list)
|
||||
|
||||
ds_sel.select_datastore.assert_called_once_with(
|
||||
req, hosts=[host_1, host_2, host_3])
|
||||
|
||||
@mock.patch.object(VMDK_DRIVER, 'volumeops')
|
||||
@mock.patch.object(VMDK_DRIVER, 'ds_sel')
|
||||
def test_select_datastore_with_single_host(self, ds_sel, vops):
|
||||
cls_1 = mock.sentinel.cls_1
|
||||
cls_2 = mock.sentinel.cls_2
|
||||
self._driver._clusters = [cls_1, cls_2]
|
||||
|
||||
host_1 = mock.sentinel.host_1
|
||||
|
||||
best_candidate = mock.sentinel.best_candidate
|
||||
ds_sel.select_datastore.return_value = best_candidate
|
||||
|
||||
req = mock.sentinel.req
|
||||
self.assertEqual(best_candidate,
|
||||
self._driver._select_datastore(req, host_1))
|
||||
|
||||
ds_sel.select_datastore.assert_called_once_with(req, hosts=[host_1])
|
||||
self.assertFalse(vops.get_cluster_hosts.called)
|
||||
|
||||
@mock.patch.object(VMDK_DRIVER, 'volumeops')
|
||||
@mock.patch.object(VMDK_DRIVER, 'ds_sel')
|
||||
def test_select_datastore_with_empty_clusters(self, ds_sel, vops):
|
||||
self._driver._clusters = None
|
||||
|
||||
best_candidate = mock.sentinel.best_candidate
|
||||
ds_sel.select_datastore.return_value = best_candidate
|
||||
|
||||
req = mock.sentinel.req
|
||||
self.assertEqual(best_candidate, self._driver._select_datastore(req))
|
||||
|
||||
ds_sel.select_datastore.assert_called_once_with(req, hosts=None)
|
||||
self.assertFalse(vops.get_cluster_hosts.called)
|
||||
|
||||
@mock.patch.object(VMDK_DRIVER, 'volumeops')
|
||||
@mock.patch.object(VMDK_DRIVER, 'ds_sel')
|
||||
def test_select_datastore_with_no_valid_host(self, ds_sel, vops):
|
||||
cls_1 = mock.sentinel.cls_1
|
||||
cls_2 = mock.sentinel.cls_2
|
||||
self._driver._clusters = [cls_1, cls_2]
|
||||
|
||||
vops.get_cluster_hosts.side_effect = [[], []]
|
||||
|
||||
req = mock.sentinel.req
|
||||
self.assertRaises(vmdk_exceptions.NoValidHostException,
|
||||
self._driver._select_datastore, req)
|
||||
|
||||
exp_calls = [mock.call(cls_1), mock.call(cls_2)]
|
||||
self.assertEqual(exp_calls, vops.get_cluster_hosts.call_args_list)
|
||||
|
||||
self.assertFalse(ds_sel.called)
|
||||
|
||||
@mock.patch.object(VMDK_DRIVER, 'volumeops')
|
||||
@mock.patch.object(VMDK_DRIVER, 'ds_sel')
|
||||
def test_relocate_backing_nop(self, ds_sel, vops):
|
||||
|
|
|
@ -1510,6 +1510,58 @@ class VolumeOpsTestCase(test.TestCase):
|
|||
eagerZero=False)
|
||||
self.session.wait_for_task.assert_called_once_with(task)
|
||||
|
||||
@mock.patch('cinder.volume.drivers.vmware.volumeops.VMwareVolumeOps.'
|
||||
'_get_all_clusters')
|
||||
def test_get_cluster_refs(self, get_all_clusters):
|
||||
cls_1 = mock.sentinel.cls_1
|
||||
cls_2 = mock.sentinel.cls_2
|
||||
clusters = {"cls_1": cls_1, "cls_2": cls_2}
|
||||
get_all_clusters.return_value = clusters
|
||||
|
||||
self.assertEqual({"cls_2": cls_2},
|
||||
self.vops.get_cluster_refs(["cls_2"]))
|
||||
|
||||
@mock.patch('cinder.volume.drivers.vmware.volumeops.VMwareVolumeOps.'
|
||||
'_get_all_clusters')
|
||||
def test_get_cluster_refs_with_invalid_cluster(self, get_all_clusters):
|
||||
cls_1 = mock.sentinel.cls_1
|
||||
cls_2 = mock.sentinel.cls_2
|
||||
clusters = {"cls_1": cls_1, "cls_2": cls_2}
|
||||
get_all_clusters.return_value = clusters
|
||||
|
||||
self.assertRaises(vmdk_exceptions.ClusterNotFoundException,
|
||||
self.vops.get_cluster_refs,
|
||||
["cls_1", "cls_3"])
|
||||
|
||||
def test_get_cluster_hosts(self):
|
||||
host_1 = mock.sentinel.host_1
|
||||
host_2 = mock.sentinel.host_2
|
||||
hosts = mock.Mock(ManagedObjectReference=[host_1, host_2])
|
||||
self.session.invoke_api.return_value = hosts
|
||||
|
||||
cluster = mock.sentinel.cluster
|
||||
ret = self.vops.get_cluster_hosts(cluster)
|
||||
|
||||
self.assertEqual([host_1, host_2], ret)
|
||||
self.session.invoke_api.assert_called_once_with(vim_util,
|
||||
'get_object_property',
|
||||
self.session.vim,
|
||||
cluster,
|
||||
'host')
|
||||
|
||||
def test_get_cluster_hosts_with_no_host(self):
|
||||
self.session.invoke_api.return_value = None
|
||||
|
||||
cluster = mock.sentinel.cluster
|
||||
ret = self.vops.get_cluster_hosts(cluster)
|
||||
|
||||
self.assertEqual([], ret)
|
||||
self.session.invoke_api.assert_called_once_with(vim_util,
|
||||
'get_object_property',
|
||||
self.session.vim,
|
||||
cluster,
|
||||
'host')
|
||||
|
||||
|
||||
class VirtualDiskPathTest(test.TestCase):
|
||||
"""Unit tests for VirtualDiskPath."""
|
||||
|
|
|
@ -205,7 +205,7 @@ class DatastoreSelector(object):
|
|||
if profile_name is not None:
|
||||
profile_id = self.get_profile_id(profile_name)
|
||||
|
||||
if hosts is None:
|
||||
if not hosts:
|
||||
hosts = self._get_all_hosts()
|
||||
|
||||
LOG.debug("Using hosts: %(hosts)s for datastore selection based on "
|
||||
|
|
|
@ -45,3 +45,13 @@ class ProfileNotFoundException(exceptions.VMwareDriverException):
|
|||
class NoValidDatastoreException(exceptions.VMwareDriverException):
|
||||
"""Thrown when there are no valid datastores."""
|
||||
message = _("There are no valid datastores.")
|
||||
|
||||
|
||||
class ClusterNotFoundException(exceptions.VMwareDriverException):
|
||||
"""Thrown when the given cluster cannot be found."""
|
||||
message = _("Compute cluster: %(cluster)s not found.")
|
||||
|
||||
|
||||
class NoValidHostException(exceptions.VMwareDriverException):
|
||||
"""Thrown when there are no valid ESX hosts."""
|
||||
message = _("There are no valid ESX hosts.")
|
||||
|
|
|
@ -117,6 +117,10 @@ vmdk_opts = [
|
|||
'verified. If false, then the default CA truststore is '
|
||||
'used for verification. This option is ignored if '
|
||||
'"vmware_ca_file" is set.'),
|
||||
cfg.MultiStrOpt('vmware_cluster_name',
|
||||
default=None,
|
||||
help='Name of a vCenter compute cluster where volumes '
|
||||
'should be created.'),
|
||||
]
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
@ -226,6 +230,7 @@ class VMwareEsxVmdkDriver(driver.VolumeDriver):
|
|||
# directly to ESX
|
||||
self._storage_policy_enabled = False
|
||||
self._ds_sel = None
|
||||
self._clusters = None
|
||||
|
||||
@property
|
||||
def session(self):
|
||||
|
@ -461,13 +466,28 @@ class VMwareEsxVmdkDriver(driver.VolumeDriver):
|
|||
def _relocate_backing(self, volume, backing, host):
|
||||
pass
|
||||
|
||||
def _get_hosts(self, clusters):
|
||||
hosts = []
|
||||
if clusters:
|
||||
for cluster in clusters:
|
||||
hosts.extend(self.volumeops.get_cluster_hosts(cluster))
|
||||
return hosts
|
||||
|
||||
def _select_datastore(self, req, host=None):
|
||||
"""Selects datastore satisfying the given requirements.
|
||||
|
||||
:return: (host, resource_pool, summary)
|
||||
"""
|
||||
hosts = None
|
||||
if host:
|
||||
hosts = [host]
|
||||
elif self._clusters:
|
||||
hosts = self._get_hosts(self._clusters)
|
||||
if not hosts:
|
||||
LOG.error(_LE("There are no valid hosts available in "
|
||||
"configured cluster(s): %s."), self._clusters)
|
||||
raise vmdk_exceptions.NoValidHostException()
|
||||
|
||||
hosts = [host] if host else None
|
||||
best_candidate = self.ds_sel.select_datastore(req, hosts=hosts)
|
||||
if not best_candidate:
|
||||
LOG.error(_LE("There is no valid datastore satisfying "
|
||||
|
@ -1836,6 +1856,13 @@ class VMwareVcVmdkDriver(VMwareEsxVmdkDriver):
|
|||
self._volumeops = volumeops.VMwareVolumeOps(self.session, max_objects)
|
||||
self._ds_sel = hub.DatastoreSelector(self.volumeops, self.session)
|
||||
|
||||
# Get clusters to be used for backing VM creation.
|
||||
cluster_names = self.configuration.vmware_cluster_name
|
||||
if cluster_names:
|
||||
self._clusters = self.volumeops.get_cluster_refs(
|
||||
cluster_names).values()
|
||||
LOG.info(_LI("Using compute cluster(s): %s."), cluster_names)
|
||||
|
||||
LOG.info(_LI("Successfully setup driver: %(driver)s for server: "
|
||||
"%(ip)s."), {'driver': self.__class__.__name__,
|
||||
'ip': self.configuration.vmware_host_ip})
|
||||
|
@ -1889,15 +1916,7 @@ class VMwareVcVmdkDriver(VMwareEsxVmdkDriver):
|
|||
req[hub.DatastoreSelector.PROFILE_NAME] = backing_profile
|
||||
|
||||
# Select datastore satisfying the requirements.
|
||||
best_candidate = self.ds_sel.select_datastore(req, hosts=[host])
|
||||
if not best_candidate:
|
||||
# No candidate datastore to relocate.
|
||||
msg = _("There are no datastores matching volume requirements;"
|
||||
" can't relocate volume: %s.") % volume['name']
|
||||
LOG.error(msg)
|
||||
raise vmdk_exceptions.NoValidDatastoreException(msg)
|
||||
|
||||
(host, resource_pool, summary) = best_candidate
|
||||
(host, resource_pool, summary) = self._select_datastore(req, host)
|
||||
dc = self.volumeops.get_dc(resource_pool)
|
||||
folder = self._get_volume_group_folder(dc)
|
||||
|
||||
|
|
|
@ -1413,3 +1413,49 @@ class VMwareVolumeOps(object):
|
|||
profile_manager,
|
||||
profileIds=profile_ids)
|
||||
return profiles[0].name
|
||||
|
||||
def _get_all_clusters(self):
|
||||
clusters = {}
|
||||
retrieve_result = self._session.invoke_api(vim_util, 'get_objects',
|
||||
self._session.vim,
|
||||
'ClusterComputeResource',
|
||||
self._max_objects)
|
||||
while retrieve_result:
|
||||
if retrieve_result.objects:
|
||||
for cluster in retrieve_result.objects:
|
||||
name = urllib.unquote(cluster.propSet[0].val)
|
||||
clusters[name] = cluster.obj
|
||||
retrieve_result = self.continue_retrieval(retrieve_result)
|
||||
return clusters
|
||||
|
||||
def get_cluster_refs(self, names):
|
||||
"""Get references to given clusters.
|
||||
|
||||
:param names: list of cluster names
|
||||
:return: Dictionary of cluster names to references
|
||||
"""
|
||||
clusters = self._get_all_clusters()
|
||||
for name in names:
|
||||
if name not in clusters:
|
||||
LOG.error(_LE("Compute cluster: %s not found."), name)
|
||||
raise vmdk_exceptions.ClusterNotFoundException(cluster=name)
|
||||
|
||||
return {name: clusters[name] for name in names}
|
||||
|
||||
def get_cluster_hosts(self, cluster):
|
||||
"""Get hosts in the given cluster.
|
||||
|
||||
:param cluster: cluster reference
|
||||
:return: references to hosts in the cluster
|
||||
"""
|
||||
hosts = self._session.invoke_api(vim_util,
|
||||
'get_object_property',
|
||||
self._session.vim,
|
||||
cluster,
|
||||
'host')
|
||||
|
||||
host_refs = []
|
||||
if hosts and hosts.ManagedObjectReference:
|
||||
host_refs.extend(hosts.ManagedObjectReference)
|
||||
|
||||
return host_refs
|
||||
|
|
Loading…
Reference in New Issue