Implement volume capacity stats for VMware

Calculates the total capacity and free space for datastores meeting the
following criteria.
- accessible
- not in maintenance
- mounted to an ESX host belonging to the clusters configured for the
VMware driver.

Change-Id: Ib8b0b6b0bee37d5d2e151bf670a5e868400b2833
This commit is contained in:
Ivaylo Mitev 2018-09-26 05:52:32 -07:00
parent 5b37d2aa33
commit a742569dc9
3 changed files with 204 additions and 44 deletions

View File

@ -42,6 +42,7 @@ class VMwareVStorageObjectDriverTestCase(test.TestCase):
IP = 'localhost'
PORT = 2321
IMG_TX_TIMEOUT = 10
RESERVED_PERCENTAGE = 0
VMDK_DRIVER = vmdk.VMwareVcVmdkDriver
FCD_DRIVER = fcd.VMwareVStorageObjectDriver
@ -60,6 +61,7 @@ class VMwareVStorageObjectDriverTestCase(test.TestCase):
self._config.vmware_host_ip = self.IP
self._config.vmware_host_port = self.PORT
self._config.vmware_image_transfer_timeout_secs = self.IMG_TX_TIMEOUT
self._config.reserved_percentage = self.RESERVED_PERCENTAGE
self._driver = fcd.VMwareVStorageObjectDriver(
configuration=self._config)
self._context = context.get_admin_context()
@ -72,15 +74,39 @@ class VMwareVStorageObjectDriverTestCase(test.TestCase):
self.assertFalse(self._driver._storage_policy_enabled)
vops.set_vmx_version.assert_called_once_with('vmx-13')
def test_get_volume_stats(self):
@mock.patch.object(VMDK_DRIVER, 'volumeops')
@mock.patch.object(VMDK_DRIVER, '_get_datastore_summaries')
def test_get_volume_stats(self, _get_datastore_summaries, vops):
FREE_GB = 7
TOTAL_GB = 11
class ObjMock(object):
def __init__(self, **kwargs):
self.__dict__.update(kwargs)
_get_datastore_summaries.return_value = \
ObjMock(objects= [
ObjMock(propSet = [
ObjMock(name = "host",
val = ObjMock(DatastoreHostMount = [])),
ObjMock(name = "summary",
val = ObjMock(freeSpace = FREE_GB * units.Gi,
capacity = TOTAL_GB * units.Gi,
accessible = True))
])
])
vops._in_maintenance.return_value = False
stats = self._driver.get_volume_stats()
self.assertEqual('VMware', stats['vendor_name'])
self.assertEqual(self._driver.VERSION, stats['driver_version'])
self.assertEqual(self._driver.STORAGE_TYPE, stats['storage_protocol'])
self.assertEqual(0, stats['reserved_percentage'])
self.assertEqual('unknown', stats['total_capacity_gb'])
self.assertEqual('unknown', stats['free_capacity_gb'])
self.assertEqual(self.RESERVED_PERCENTAGE,
stats['reserved_percentage'])
self.assertEqual(TOTAL_GB, stats['total_capacity_gb'])
self.assertEqual(FREE_GB, stats['free_capacity_gb'])
def _create_volume_dict(self,
vol_id=VOL_ID,

View File

@ -34,15 +34,29 @@ from cinder import test
from cinder.tests.unit import fake_constants
from cinder.tests.unit import fake_snapshot
from cinder.tests.unit import fake_volume
from cinder.volume import configuration
from cinder.volume.drivers.vmware import datastore as hub
from cinder.volume.drivers.vmware import exceptions as vmdk_exceptions
from cinder.volume.drivers.vmware import vmdk
from cinder.volume.drivers.vmware import volumeops
class MockConfiguration(object):
def __init__(self, **kwargs):
for kw in kwargs:
setattr(self, kw, kwargs[kw])
def safe_get(self, name):
return getattr(self, name) if hasattr(self, name) else None
def append_config_values(self, opts):
for opt in opts:
if not hasattr(self, opt.name):
setattr(self, opt.name, opt.default or None)
# TODO(vbala) Split test methods handling multiple cases into multiple methods,
# each handling a specific case.
@ddt.ddt
class VMwareVcVmdkDriverTestCase(test.TestCase):
"""Unit tests for VMwareVcVmdkDriver."""
@ -80,27 +94,29 @@ class VMwareVcVmdkDriverTestCase(test.TestCase):
def setUp(self):
super(VMwareVcVmdkDriverTestCase, self).setUp()
self._config = mock.Mock(spec=configuration.Configuration)
self._config.vmware_host_ip = self.IP
self._config.vmware_host_port = self.PORT
self._config.vmware_host_username = self.USERNAME
self._config.vmware_host_password = self.PASSWORD
self._config.vmware_wsdl_location = None
self._config.vmware_volume_folder = self.VOLUME_FOLDER
self._config.vmware_api_retry_count = self.API_RETRY_COUNT
self._config.vmware_task_poll_interval = self.TASK_POLL_INTERVAL
self._config.vmware_image_transfer_timeout_secs = self.IMG_TX_TIMEOUT
self._config.vmware_max_objects_retrieval = self.MAX_OBJECTS
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._config.vmware_host_version = self.DEFAULT_VC_VERSION
self._config.vmware_connection_pool_size = self.POOL_SIZE
self._config.vmware_adapter_type = self.ADAPTER_TYPE
self._config.vmware_snapshot_format = self.SNAPSHOT_FORMAT
self._config.vmware_lazy_create = True
self._config.vmware_datastore_regex = None
self._config = MockConfiguration(
vmware_host_ip=self.IP,
vmware_host_port=self.PORT,
vmware_host_username=self.USERNAME,
vmware_host_password=self.PASSWORD,
vmware_wsdl_location=None,
vmware_volume_folder=self.VOLUME_FOLDER,
vmware_api_retry_count=self.API_RETRY_COUNT,
vmware_task_poll_interval=self.TASK_POLL_INTERVAL,
vmware_image_transfer_timeout_secs=self.IMG_TX_TIMEOUT,
vmware_max_objects_retrieval=self.MAX_OBJECTS,
vmware_tmp_dir=self.TMP_DIR,
vmware_ca_file=self.CA_FILE,
vmware_insecure=False,
vmware_cluster_name=self.CLUSTERS,
vmware_host_version=self.DEFAULT_VC_VERSION,
vmware_connection_pool_size=self.POOL_SIZE,
vmware_adapter_type=self.ADAPTER_TYPE,
vmware_snapshot_format=self.SNAPSHOT_FORMAT,
vmware_lazy_create=True,
vmware_datastore_regex=None,
reserved_percentage=0
)
self._db = mock.Mock()
self._driver = vmdk.VMwareVcVmdkDriver(configuration=self._config,
@ -108,15 +124,38 @@ class VMwareVcVmdkDriverTestCase(test.TestCase):
self._context = context.get_admin_context()
def test_get_volume_stats(self):
@mock.patch.object(VMDK_DRIVER, 'volumeops')
@mock.patch.object(VMDK_DRIVER, '_get_datastore_summaries')
def test_get_volume_stats(self, _get_datastore_summaries, vops):
FREE_GB = 7
TOTAL_GB = 11
class ObjMock(object):
def __init__(self, **kwargs):
self.__dict__.update(kwargs)
_get_datastore_summaries.return_value = (ObjMock(objects= [
ObjMock(propSet = [
ObjMock(name = "host",
val = ObjMock(DatastoreHostMount = [])),
ObjMock(name = "summary",
val = ObjMock(freeSpace = FREE_GB * units.Gi,
capacity = TOTAL_GB * units.Gi,
accessible = True))
])
]))
vops._in_maintenance.return_value = False
stats = self._driver.get_volume_stats()
self.assertEqual('VMware', stats['vendor_name'])
self.assertEqual(self._driver.VERSION, stats['driver_version'])
self.assertEqual('vmdk', stats['storage_protocol'])
self.assertEqual(0, stats['reserved_percentage'])
self.assertEqual('unknown', stats['total_capacity_gb'])
self.assertEqual('unknown', stats['free_capacity_gb'])
self.assertEqual(self._config.reserved_percentage,
stats['reserved_percentage'])
self.assertEqual(TOTAL_GB, stats['total_capacity_gb'])
self.assertEqual(FREE_GB, stats['free_capacity_gb'])
self.assertFalse(stats['shared_targets'])
def _create_volume_dict(self,

View File

@ -36,6 +36,7 @@ from oslo_vmware import exceptions
from oslo_vmware import image_transfer
from oslo_vmware import pbm
from oslo_vmware import vim_util
import six
from cinder import exception
from cinder.i18n import _
@ -130,6 +131,8 @@ vmdk_opts = [
cfg.MultiStrOpt('vmware_cluster_name',
help='Name of a vCenter compute cluster where volumes '
'should be created.'),
cfg.MultiStrOpt('vmware_storage_profile',
help='Names of storage profiles to be monitored.'),
cfg.IntOpt('vmware_connection_pool_size',
default=10,
help='Maximum number of connections in http connection pool.'),
@ -264,7 +267,8 @@ class VMwareVcVmdkDriver(driver.VolumeDriver):
# 3.2.0 - config option to disable lazy creation of backend volume
# 3.3.0 - config option to specify datastore name regex
# 3.4.0 - added NFS41 as a supported datastore type
VERSION = '3.4.0'
# 3.4.1 - volume capacity stats implemented
VERSION = '3.4.1'
# ThirdPartySystems wiki page
CI_WIKI_NAME = "VMware_CI"
@ -316,21 +320,112 @@ class VMwareVcVmdkDriver(driver.VolumeDriver):
:param refresh: Whether to get refreshed information
"""
if not self._stats:
backend_name = self.configuration.safe_get('volume_backend_name')
if not backend_name:
backend_name = self.__class__.__name__
data = {'volume_backend_name': backend_name,
'vendor_name': 'VMware',
'driver_version': self.VERSION,
'storage_protocol': 'vmdk',
'reserved_percentage': 0,
'total_capacity_gb': 'unknown',
'free_capacity_gb': 'unknown',
'shared_targets': False}
self._stats = data
if not self._stats or refresh:
self._stats = self._get_volume_stats()
return self._stats
def _get_volume_stats(self):
backend_name = self.configuration.safe_get('volume_backend_name')
if not backend_name:
backend_name = self.__class__.__name__
data = {'volume_backend_name': backend_name,
'vendor_name': 'VMware',
'driver_version': self.VERSION,
'storage_protocol': 'vmdk',
'reserved_percentage': self.configuration.reserved_percentage,
'shared_targets': False}
ds_summaries = self._get_datastore_summaries()
available_hosts = self._get_hosts(self._clusters)
global_capacity = 0
global_free = 0
while True:
for ds in ds_summaries.objects:
ds_props = self._get_object_properties(ds)
summary = ds_props['summary']
if self._is_datastore_accessible(summary,
ds_props['host'],
available_hosts):
global_capacity += summary.capacity
global_free += summary.freeSpace
if getattr(ds_summaries, 'token', None):
ds_summaries = self.volumeops.continue_retrieval(ds_summaries)
else:
break
data['total_capacity_gb'] = round(global_capacity / units.Gi)
data['free_capacity_gb'] = round(global_free / units.Gi)
return data
def _get_datastore_summaries(self):
client_factory = self.session.vim.client.factory
object_specs = []
if (self._storage_policy_enabled
and self.configuration.vmware_storage_profile):
# Get all available storage profiles on the vCenter and extract the
# IDs of those that we want to observe
profiles_ids = []
for profile in pbm.get_all_profiles(self.session):
if profile.name in self.configuration.vmware_storage_profile:
profiles_ids.append(profile.profileId)
# Get all matching Datastores for each profile
datastores = {}
for profile_id in profiles_ids:
for pbm_hub in pbm.filter_hubs_by_profile(self.session,
None,
profile_id):
if pbm_hub.hubType != "Datastore":
# We are not interested in Datastore Clusters for now
continue
if pbm_hub.hubId not in datastores:
# Reconstruct a managed object reference to datastore
datastores[pbm_hub.hubId] = vim_util.get_moref(
pbm_hub.hubId, "Datastore")
# Build property collector object specs out of them
for datastore_ref in six.itervalues(datastores):
object_specs.append(
vim_util.build_object_spec(client_factory,
datastore_ref,
[]))
else:
# Build a catch-all object spec that would reach all datastores
object_specs.append(
vim_util.build_object_spec(
client_factory,
self.session.vim.service_content.rootFolder,
[vim_util.build_recursive_traversal_spec(client_factory)]))
prop_spec = vim_util.build_property_spec(client_factory, 'Datastore',
['summary', 'host'])
filter_spec = vim_util.build_property_filter_spec(client_factory,
prop_spec,
object_specs)
options = client_factory.create('ns0:RetrieveOptions')
options.maxObjects = self.configuration.vmware_max_objects_retrieval
result = self.session.vim.RetrievePropertiesEx(
self.session.vim.service_content.propertyCollector,
specSet=[filter_spec],
options=options)
return result
def _get_object_properties(self, obj_content):
props = {}
if hasattr(obj_content, 'propSet'):
prop_set = obj_content.propSet
if prop_set:
props = {prop.name: prop.val for prop in prop_set}
return props
def _is_datastore_accessible(self, ds_summary, ds_host_mounts,
available_hosts):
# available_hosts empty => vmware_cluster_name not specified => don't
# filter by hosts
cluster_access_to_ds = not available_hosts
for host_mount in ds_host_mounts.DatastoreHostMount:
for avlbl_host in available_hosts:
if avlbl_host.value == host_mount.key.value:
cluster_access_to_ds = True
return (ds_summary.accessible
and not self.volumeops._in_maintenance(ds_summary)
and cluster_access_to_ds)
def _verify_volume_creation(self, volume):
"""Verify that the volume can be created.