VMDK:Using host mount info for datastore selection

Currently the VMDK driver picks a datastore for volume creation based
on space utilization. The shadow VM corresponding to the volume is
migrated to a different host and datastore if the volume is attached
to an instance created in a host which cannot access the volume's
current datastore. To reduce these migrations, this fix selects a
datastore which is connected to maximum number of hosts, provided
there is enough space to accommodate the volume. Ties are broken
based on space utilization; datastore with low space utilization
is preferred.

Closes-Bug: #1250816
Change-Id: I5ecf22d782857fca0889799229465c49afe188a0
This commit is contained in:
Vipin Balachandran 2013-11-13 20:33:39 +05:30
parent a99c0c14e6
commit b85cc49588
3 changed files with 141 additions and 49 deletions

View File

@ -203,7 +203,7 @@ class VMwareEsxVmdkDriverTestCase(test.TestCase):
"""Test get_volume_stats."""
stats = self._driver.get_volume_stats()
self.assertEqual(stats['vendor_name'], 'VMware')
self.assertEqual(stats['driver_version'], '1.0')
self.assertEqual(stats['driver_version'], '1.1.0')
self.assertEqual(stats['storage_protocol'], 'LSI Logic SCSI')
self.assertEqual(stats['reserved_percentage'], 0)
self.assertEqual(stats['total_capacity_gb'], 'unknown')
@ -721,32 +721,55 @@ class VMwareEsxVmdkDriverTestCase(test.TestCase):
m = self.mox
m.StubOutWithMock(self._driver.__class__, 'volumeops')
self._driver.volumeops = self._volumeops
datastore1 = FakeMor('Datastore', 'my_ds_1')
datastore2 = FakeMor('Datastore', 'my_ds_2')
datastore3 = FakeMor('Datastore', 'my_ds_3')
datastore4 = FakeMor('Datastore', 'my_ds_4')
datastores = [datastore1, datastore2, datastore3, datastore4]
m.StubOutWithMock(self._volumeops, 'get_summary')
summary1 = FakeDatastoreSummary(10, 10)
summary2 = FakeDatastoreSummary(25, 50)
summary3 = FakeDatastoreSummary(50, 50)
summary4 = FakeDatastoreSummary(100, 100)
moxd = self._volumeops.get_summary(datastore1)
moxd.MultipleTimes().AndReturn(summary1)
moxd = self._volumeops.get_summary(datastore2)
moxd.MultipleTimes().AndReturn(summary2)
moxd = self._volumeops.get_summary(datastore3)
moxd.MultipleTimes().AndReturn(summary3)
moxd = self._volumeops.get_summary(datastore4)
moxd.MultipleTimes().AndReturn(summary4)
summary1 = FakeDatastoreSummary(5, 100)
summary2 = FakeDatastoreSummary(25, 100)
summary3 = FakeDatastoreSummary(50, 100)
summary4 = FakeDatastoreSummary(75, 100)
self._volumeops.get_summary(
datastore1).MultipleTimes().AndReturn(summary1)
self._volumeops.get_summary(
datastore2).MultipleTimes().AndReturn(summary2)
self._volumeops.get_summary(
datastore3).MultipleTimes().AndReturn(summary3)
self._volumeops.get_summary(
datastore4).MultipleTimes().AndReturn(summary4)
m.StubOutWithMock(self._volumeops, 'get_connected_hosts')
host1 = FakeMor('HostSystem', 'my_host_1')
host2 = FakeMor('HostSystem', 'my_host_2')
host3 = FakeMor('HostSystem', 'my_host_3')
host4 = FakeMor('HostSystem', 'my_host_4')
self._volumeops.get_connected_hosts(
datastore1).MultipleTimes().AndReturn([host1, host2, host3, host4])
self._volumeops.get_connected_hosts(
datastore2).MultipleTimes().AndReturn([host1, host2, host3])
self._volumeops.get_connected_hosts(
datastore3).MultipleTimes().AndReturn([host1, host2])
self._volumeops.get_connected_hosts(
datastore4).MultipleTimes().AndReturn([host1, host2])
m.ReplayAll()
summary = self._driver._select_datastore_summary(1, datastores)
self.assertEqual(summary, summary1)
summary = self._driver._select_datastore_summary(10, datastores)
self.assertEqual(summary, summary3)
summary = self._driver._select_datastore_summary(50, datastores)
self.assertEqual(summary, summary2)
summary = self._driver._select_datastore_summary(40, datastores)
self.assertEqual(summary, summary4)
self.assertRaises(error_util.VimException,
self._driver._select_datastore_summary,
100, datastores)

View File

@ -16,7 +16,12 @@
# under the License.
"""
Driver for virtual machines running on VMware supported datastores.
Volume driver for VMware vCenter/ESX managed datastores.
The volumes created by this driver are backed by VMDK (Virtual Machine
Disk) files stored in datastores. For ease of managing the VMDKs, the
driver creates a virtual machine for each of the volumes. This virtual
machine is never powered on and is often referred as the shadow VM.
"""
from oslo.config import cfg
@ -33,6 +38,7 @@ from cinder.volume.drivers.vmware import volumeops
from cinder.volume import volume_types
LOG = logging.getLogger(__name__)
THIN_VMDK_TYPE = 'thin'
THICK_VMDK_TYPE = 'thick'
EAGER_ZEROED_THICK_VMDK_TYPE = 'eagerZeroedThick'
@ -118,7 +124,7 @@ def _get_volume_type_extra_spec(type_id, spec_key, possible_values,
class VMwareEsxVmdkDriver(driver.VolumeDriver):
"""Manage volumes on VMware ESX server."""
VERSION = '1.0'
VERSION = '1.1.0'
def __init__(self, *args, **kwargs):
super(VMwareEsxVmdkDriver, self).__init__(*args, **kwargs)
@ -231,37 +237,64 @@ class VMwareEsxVmdkDriver(driver.VolumeDriver):
"""
return self.volumeops.get_vmfolder(datacenter)
def _select_datastore_summary(self, size_bytes, datastores):
"""Get best summary from datastore list that can accommodate volume.
def _compute_space_utilization(self, datastore_summary):
"""Compute the space utilization of the given datastore.
The implementation selects datastore based on maximum relative
free space, which is (free_space/total_space) and has free space to
store the volume backing.
:param datastore_summary: Summary of the datastore for which
space utilization is to be computed
:return: space utilization in the range [0..1]
"""
return (
1.0 -
datastore_summary.freeSpace / float(datastore_summary.capacity)
)
def _select_datastore_summary(self, size_bytes, datastores):
"""Get the best datastore summary from the given datastore list.
The implementation selects a datastore which is connected to maximum
number of hosts, provided there is enough space to accommodate the
volume. Ties are broken based on space utilization; datastore with
low space utilization is preferred.
:param size_bytes: Size in bytes of the volume
:param datastores: Datastores from which a choice is to be made
for the volume
:return: Best datastore summary to be picked for the volume
:return: Summary of the best datastore selected for volume
"""
best_summary = None
best_ratio = 0
max_host_count = 0
best_space_utilization = 1.0
for datastore in datastores:
summary = self.volumeops.get_summary(datastore)
if summary.freeSpace > size_bytes:
ratio = float(summary.freeSpace) / summary.capacity
if ratio > best_ratio:
best_ratio = ratio
host_count = len(self.volumeops.get_connected_hosts(datastore))
if host_count > max_host_count:
max_host_count = host_count
best_space_utilization = self._compute_space_utilization(
summary
)
best_summary = summary
elif host_count == max_host_count:
# break the tie based on space utilization
space_utilization = self._compute_space_utilization(
summary
)
if space_utilization < best_space_utilization:
best_space_utilization = space_utilization
best_summary = summary
if not best_summary:
msg = _("Unable to pick datastore to accommodate %(size)s bytes "
"from the datastores: %(dss)s.")
LOG.error(msg % {'size': size_bytes, 'dss': datastores})
raise error_util.VimException(msg %
{'size': size_bytes,
'dss': datastores})
"from the datastores: %(dss)s.") % {'size': size_bytes,
'dss': datastores}
LOG.error(msg)
raise error_util.VimException(msg)
LOG.debug(_("Selected datastore: %s for the volume.") % best_summary)
LOG.debug(_("Selected datastore: %(datastore)s with %(host_count)d "
"connected host(s) for the volume.") %
{'datastore': best_summary, 'host_count': max_host_count})
return best_summary
def _get_folder_ds_summary(self, size_gb, resource_pool, datastores):

View File

@ -139,6 +139,54 @@ class VMwareVolumeOps(object):
self._session.invoke_api(vim_util, 'cancel_retrieval',
self._session.vim, retrieve_result)
def _is_usable(self, datastore, mount_info):
"""Check if the given datastore is usable as per the given mount info.
The datastore is considered to be usable for a host only if it is
writable, mounted and accessible.
:param datastore: Reference to the datastore entity
:param mount_info: host mount information
:return: True if datastore is usable
"""
writable = mount_info.accessMode == "readWrite"
# If mounted attribute is not set, then default is True
mounted = True
if hasattr(mount_info, "mounted"):
mounted = mount_info.mounted
if hasattr(mount_info, "accessible"):
accessible = mount_info.accessible
else:
# If accessible attribute is not set, we look at summary
summary = self.get_summary(datastore)
accessible = summary.accessible
return writable and mounted and accessible
def get_connected_hosts(self, datastore):
"""Get all the hosts to which the datastore is connected and usable.
The datastore is considered to be usable for a host only if it is
writable, mounted and accessible.
:param datastore: Reference to the datastore entity
:return: List of managed object references of all connected
hosts
"""
host_mounts = self._session.invoke_api(vim_util, 'get_object_property',
self._session.vim, datastore,
'host')
connected_hosts = []
for host_mount in host_mounts.DatastoreHostMount:
if self._is_usable(datastore, host_mount.mountInfo):
connected_hosts.append(host_mount.key.value)
return connected_hosts
def _is_valid(self, datastore, host):
"""Check if host's datastore is accessible, mounted and writable.
@ -152,19 +200,7 @@ class VMwareVolumeOps(object):
'host')
for host_mount in host_mounts.DatastoreHostMount:
if host_mount.key.value == host.value:
mntInfo = host_mount.mountInfo
writable = mntInfo.accessMode == "readWrite"
# If mounted attribute is not set, then default is True
mounted = True
if hasattr(mntInfo, "mounted"):
mounted = mntInfo.mounted
if hasattr(mntInfo, "accessible"):
accessible = mntInfo.accessible
else:
# If accessible attribute is not set, we look at summary
summary = self.get_summary(datastore)
accessible = summary.accessible
return (accessible and mounted and writable)
return self._is_usable(datastore, host_mount.mountInfo)
return False
def get_dss_rp(self, host):
@ -200,9 +236,9 @@ class VMwareVolumeOps(object):
compute_resource,
'resourcePool')
if not valid_dss:
msg = _("There are no valid datastores present under %s.")
LOG.error(msg % host)
raise error_util.VimException(msg % host)
msg = _("There are no valid datastores attached to %s.") % host
LOG.error(msg)
raise error_util.VimException(msg)
return (valid_dss, resource_pool)
def _get_parent(self, child, parent_type):