From: Anthony Lee Date: Tue, 24 Feb 2015 11:34:06 +0000 (-0800) Subject: HP 3par driver filter and evaluator function X-Git-Url: https://review.fuel-infra.org/gitweb?a=commitdiff_plain;h=807cf27a77eac5658642f993b4a409f6ac00bf60;p=openstack-build%2Fcinder-build.git HP 3par driver filter and evaluator function This patch implements the merged change in the scheduler https://review.openstack.org/#/c/129987/ in HP 3par driver. 1. Added a way to get filter_function and goodness_function strings from cinder.conf in the base class VolumeDriver, for example filter_function = "capabilities.total_volumes < 600 && capabilities.capacity_utilization < 80" goodness_function = "capabilities.capacity_utilization < 40 ? 80 : 60" The strings can be got from the [DEFAULT] section or a driver instance group section. If the string can not be found in the cinder.conf, it will try to find a default filter_fuction or a default goodness_function which can be specified in the driver code. HP3par drivers don't provide default values. However if other driver want to provide default values, it can be done by overwriting get_default_filter_function and get_default_goodness_function. 2. Also added goodness_function and filter_function to the _update_volume_stats in ISCSIDriver, FibreChannelDriver, FakeISCSIDriver and LVMVolumeDriver. 3. Added 2 stats total_volumes and capacity_utilization so they can be used as the part of the formula in filter_function or goodness_function string. 4. Fixed free_capacity for limited cpg to include SDUsage's usedMiB as part of the calcuation so it is consistent with unlimited cpg and capacity_utilization looks better. 5. Added total_volumes capability to the LVM driver. Implements: blueprint hp3par-driver-supplies-filtering-weighing-functions Change-Id: I4ce77c9c1b1e14de82465bb94068b2ff10c19b91 --- diff --git a/cinder/tests/test_hp3par.py b/cinder/tests/test_hp3par.py index 97241b45f..df618dde6 100644 --- a/cinder/tests/test_hp3par.py +++ b/cinder/tests/test_hp3par.py @@ -50,6 +50,10 @@ HP3PAR_SAN_IP = '2.2.2.2' HP3PAR_SAN_SSH_PORT = 999 HP3PAR_SAN_SSH_CON_TIMEOUT = 44 HP3PAR_SAN_SSH_PRIVATE = 'foobar' +GOODNESS_FUNCTION = \ + "stats.capacity_utilization < 0.6? 100:25" +FILTER_FUNCTION = \ + "stats.total_volumes < 400 && stats.capacity_utilization < 0.8" CHAP_USER_KEY = "HPQ-cinder-CHAP-name" CHAP_PASS_KEY = "HPQ-cinder-CHAP-secret" @@ -225,6 +229,7 @@ class HP3PARBaseDriver(object): 'name': HP3PAR_CPG, 'numFPVVs': 2, 'numTPVVs': 0, + 'numTDVVs': 1, 'state': 1, 'uuid': '29c214aa-62b9-41c8-b198-543f6cf24edf'}] @@ -486,6 +491,8 @@ class HP3PARBaseDriver(object): configuration.hp3par_snapshot_retention = "" configuration.hp3par_iscsi_ips = [] configuration.hp3par_iscsi_chap_enabled = False + configuration.goodness_function = GOODNESS_FUNCTION + configuration.filter_function = FILTER_FUNCTION return configuration @mock.patch( @@ -3335,9 +3342,12 @@ class TestHP3PARFCDriver(HP3PARBaseDriver, test.TestCase): self.assertNotIn('initiator_target_map', conn_info['data']) def test_get_volume_stats(self): - # setup_mock_client drive with default configuration + # setup_mock_client drive with the configuration # and return the mock HTTP 3PAR client - mock_client = self.setup_driver() + config = self.setup_configuration() + config.filter_function = FILTER_FUNCTION + config.goodness_function = GOODNESS_FUNCTION + mock_client = self.setup_driver(config=config) mock_client.getCPG.return_value = self.cpgs[0] mock_client.getStorageSystemInfo.return_value = { 'serialNumber': '1234' @@ -3362,6 +3372,12 @@ class TestHP3PARFCDriver(HP3PARBaseDriver, test.TestCase): self.assertEqual(stats['free_capacity_gb'], 0) self.assertEqual(stats['pools'][0]['total_capacity_gb'], 24.0) self.assertEqual(stats['pools'][0]['free_capacity_gb'], 3.0) + self.assertEqual(stats['pools'][0]['capacity_utilization'], 87.5) + self.assertEqual(stats['pools'][0]['total_volumes'], 3) + self.assertEqual(stats['pools'][0]['goodness_function'], + GOODNESS_FUNCTION) + self.assertEqual(stats['pools'][0]['filter_function'], + FILTER_FUNCTION) expected = [ mock.call.getStorageSystemInfo(), @@ -3380,6 +3396,12 @@ class TestHP3PARFCDriver(HP3PARBaseDriver, test.TestCase): self.assertEqual(stats['free_capacity_gb'], 0) self.assertEqual(stats['pools'][0]['total_capacity_gb'], 24.0) self.assertEqual(stats['pools'][0]['free_capacity_gb'], 3.0) + self.assertEqual(stats['pools'][0]['capacity_utilization'], 87.5) + self.assertEqual(stats['pools'][0]['total_volumes'], 3) + self.assertEqual(stats['pools'][0]['goodness_function'], + GOODNESS_FUNCTION) + self.assertEqual(stats['pools'][0]['filter_function'], + FILTER_FUNCTION) cpg2 = self.cpgs[0].copy() cpg2.update({'SDGrowth': {'limitMiB': 8192}}) @@ -3392,10 +3414,20 @@ class TestHP3PARFCDriver(HP3PARBaseDriver, test.TestCase): self.assertEqual(stats['pools'][0]['total_capacity_gb'], total_capacity_gb) free_capacity_gb = int( - (8192 - self.cpgs[0]['UsrUsage']['usedMiB']) * const) + (8192 - (self.cpgs[0]['UsrUsage']['usedMiB'] + + self.cpgs[0]['SDUsage']['usedMiB'])) * const) self.assertEqual(stats['free_capacity_gb'], 0) self.assertEqual(stats['pools'][0]['free_capacity_gb'], free_capacity_gb) + cap_util = (float(total_capacity_gb - free_capacity_gb) / + float(total_capacity_gb)) * 100 + self.assertEqual(stats['pools'][0]['capacity_utilization'], + cap_util) + self.assertEqual(stats['pools'][0]['total_volumes'], 3) + self.assertEqual(stats['pools'][0]['goodness_function'], + GOODNESS_FUNCTION) + self.assertEqual(stats['pools'][0]['filter_function'], + FILTER_FUNCTION) common.client.deleteCPG(HP3PAR_CPG) common.client.createCPG(HP3PAR_CPG, {}) @@ -3704,9 +3736,12 @@ class TestHP3PARISCSIDriver(HP3PARBaseDriver, test.TestCase): self.assertDictMatch(result, self.properties) def test_get_volume_stats(self): - # setup_mock_client drive with default configuration + # setup_mock_client drive with the configuration # and return the mock HTTP 3PAR client - mock_client = self.setup_driver() + config = self.setup_configuration() + config.filter_function = FILTER_FUNCTION + config.goodness_function = GOODNESS_FUNCTION + mock_client = self.setup_driver(config=config) mock_client.getCPG.return_value = self.cpgs[0] mock_client.getStorageSystemInfo.return_value = { 'serialNumber': '1234' @@ -3729,6 +3764,12 @@ class TestHP3PARISCSIDriver(HP3PARBaseDriver, test.TestCase): self.assertEqual(stats['free_capacity_gb'], 0) self.assertEqual(stats['pools'][0]['total_capacity_gb'], 24.0) self.assertEqual(stats['pools'][0]['free_capacity_gb'], 3.0) + self.assertEqual(stats['pools'][0]['capacity_utilization'], 87.5) + self.assertEqual(stats['pools'][0]['total_volumes'], 3) + self.assertEqual(stats['pools'][0]['goodness_function'], + GOODNESS_FUNCTION) + self.assertEqual(stats['pools'][0]['filter_function'], + FILTER_FUNCTION) expected = [ mock.call.getStorageSystemInfo(), @@ -3753,10 +3794,20 @@ class TestHP3PARISCSIDriver(HP3PARBaseDriver, test.TestCase): self.assertEqual(stats['pools'][0]['total_capacity_gb'], total_capacity_gb) free_capacity_gb = int( - (8192 - self.cpgs[0]['UsrUsage']['usedMiB']) * const) + (8192 - (self.cpgs[0]['UsrUsage']['usedMiB'] + + self.cpgs[0]['SDUsage']['usedMiB'])) * const) self.assertEqual(stats['free_capacity_gb'], 0) self.assertEqual(stats['pools'][0]['free_capacity_gb'], free_capacity_gb) + cap_util = (float(total_capacity_gb - free_capacity_gb) / + float(total_capacity_gb)) * 100 + self.assertEqual(stats['pools'][0]['capacity_utilization'], + cap_util) + self.assertEqual(stats['pools'][0]['total_volumes'], 3) + self.assertEqual(stats['pools'][0]['goodness_function'], + GOODNESS_FUNCTION) + self.assertEqual(stats['pools'][0]['filter_function'], + FILTER_FUNCTION) def test_create_host(self): # setup_mock_client drive with default configuration diff --git a/cinder/tests/test_volume.py b/cinder/tests/test_volume.py index fa21ee746..139dcc575 100644 --- a/cinder/tests/test_volume.py +++ b/cinder/tests/test_volume.py @@ -4787,6 +4787,9 @@ class ISCSITestCase(DriverTestCase): 'lv_count': '2', 'uuid': 'vR1JU3-FAKE-C4A9-PQFh-Mctm-9FwA-Xwzc1m'}] + def _fake_get_volumes(obj, lv_name=None): + return [{'vg': 'fake_vg', 'name': 'fake_vol', 'size': '1000'}] + self.stubs.Set(brick_lvm.LVM, 'get_all_volume_groups', _fake_get_all_volume_groups) @@ -4795,6 +4798,10 @@ class ISCSITestCase(DriverTestCase): 'get_all_physical_volumes', _fake_get_all_physical_volumes) + self.stubs.Set(brick_lvm.LVM, + 'get_volumes', + _fake_get_volumes) + self.volume.driver.vg = brick_lvm.LVM('cinder-volumes', 'sudo') self.volume.driver._update_volume_stats() @@ -4807,6 +4814,8 @@ class ISCSITestCase(DriverTestCase): stats['pools'][0]['free_capacity_gb'], float('0.52')) self.assertEqual( stats['pools'][0]['provisioned_capacity_gb'], float('5.0')) + self.assertEqual( + stats['pools'][0]['total_volumes'], int('1')) def test_validate_connector(self): iscsi_driver =\ diff --git a/cinder/volume/driver.py b/cinder/volume/driver.py index 16fd79caf..9c0f4aa3a 100644 --- a/cinder/volume/driver.py +++ b/cinder/volume/driver.py @@ -197,7 +197,19 @@ volume_opts = [ secret=True), cfg.StrOpt('driver_data_namespace', default=None, - help='Namespace for driver private data values to be saved in.') + help='Namespace for driver private data values to be ' + 'saved in.'), + cfg.StrOpt('filter_function', + default=None, + help='String representation for an equation that will be ' + 'used to filter hosts. Only used when the driver ' + 'filter is set to be used by the Cinder scheduler.'), + cfg.StrOpt('goodness_function', + default=None, + help='String representation for an equation that will be ' + 'used to determine the goodness of a host. Only used ' + 'when using the goodness weigher is set to be used by ' + 'the Cinder scheduler.'), ] # for backward compatibility @@ -430,6 +442,42 @@ class BaseVD(object): """ return None + def _update_pools_and_stats(self, data): + """Updates data for pools and volume stats based on provided data.""" + # provisioned_capacity_gb is set to None by default below, but + # None won't be used in calculation. It will be overridden by + # driver's provisioned_capacity_gb if reported, otherwise it + # defaults to allocated_capacity_gb in host_manager.py. + if self.pools: + for pool in self.pools: + new_pool = {} + new_pool.update(dict( + pool_name=pool, + total_capacity_gb=0, + free_capacity_gb=0, + provisioned_capacity_gb=None, + reserved_percentage=100, + QoS_support=False, + filter_function=self.get_filter_function(), + goodness_function=self.get_goodness_function() + )) + data["pools"].append(new_pool) + else: + # No pool configured, the whole backend will be treated as a pool + single_pool = {} + single_pool.update(dict( + pool_name=data["volume_backend_name"], + total_capacity_gb=0, + free_capacity_gb=0, + provisioned_capacity_gb=None, + reserved_percentage=100, + QoS_support=False, + filter_function=self.get_filter_function(), + goodness_function=self.get_goodness_function() + )) + data["pools"].append(single_pool) + self._stats = data + def copy_volume_data(self, context, src_vol, dest_vol, remote=None): """Copy data from src_vol to dest_vol.""" LOG.debug(('copy_data_between_volumes %(src)s -> %(dest)s.') @@ -529,6 +577,60 @@ class BaseVD(object): finally: self._detach_volume(context, attach_info, volume, properties) + def get_filter_function(self): + """Get filter_function string. + + Returns either the string from the driver instance or global section + in cinder.conf. If nothing is specified in cinder.conf, then try to + find the default filter_function. When None is returned the scheduler + will always pass the driver instance. + + :return a filter_function string or None + """ + ret_function = self.configuration.filter_function + if not ret_function: + ret_function = CONF.filter_function + if not ret_function: + ret_function = self.get_default_filter_function() + return ret_function + + def get_goodness_function(self): + """Get good_function string. + + Returns either the string from the driver instance or global section + in cinder.conf. If nothing is specified in cinder.conf, then try to + find the default goodness_function. When None is returned the scheduler + will give the lowest score to the driver instance. + + :return a goodness_function string or None + """ + ret_function = self.configuration.goodness_function + if not ret_function: + ret_function = CONF.goodness_function + if not ret_function: + ret_function = self.get_default_goodness_function() + return ret_function + + def get_default_filter_function(self): + """Get the default filter_function string. + + Each driver could overwrite the method to return a well-known + default string if it is available. + + :return: None + """ + return None + + def get_default_goodness_function(self): + """Get the default goodness_function string. + + Each driver could overwrite the method to return a well-known + default string if it is available. + + :return: None + """ + return None + def _attach_volume(self, context, volume, properties, remote=False): """Attach the volume.""" if remote: @@ -1487,7 +1589,7 @@ class ISCSIDriver(VolumeDriver): def _update_volume_stats(self): """Retrieve stats info from volume group.""" - LOG.debug("Updating volume stats") + LOG.debug("Updating volume stats...") data = {} backend_name = self.configuration.safe_get('volume_backend_name') data["volume_backend_name"] = backend_name or 'Generic_iSCSI' @@ -1496,35 +1598,7 @@ class ISCSIDriver(VolumeDriver): data["storage_protocol"] = 'iSCSI' data["pools"] = [] - # provisioned_capacity_gb is set to None by default below, but - # None won't be used in calculation. It will be overridden by - # driver's provisioned_capacity_gb if reported, otherwise it - # defaults to allocated_capacity_gb in host_manager.py. - if self.pools: - for pool in self.pools: - new_pool = {} - new_pool.update(dict( - pool_name=pool, - total_capacity_gb=0, - free_capacity_gb=0, - provisioned_capacity_gb=None, - reserved_percentage=100, - QoS_support=False - )) - data["pools"].append(new_pool) - else: - # No pool configured, the whole backend will be treated as a pool - single_pool = {} - single_pool.update(dict( - pool_name=data["volume_backend_name"], - total_capacity_gb=0, - free_capacity_gb=0, - provisioned_capacity_gb=None, - reserved_percentage=100, - QoS_support=False - )) - data["pools"].append(single_pool) - self._stats = data + self._update_pools_and_stats(data) class FakeISCSIDriver(ISCSIDriver): @@ -1649,7 +1723,7 @@ class ISERDriver(ISCSIDriver): def _update_volume_stats(self): """Retrieve stats info from volume group.""" - LOG.debug("Updating volume stats") + LOG.debug("Updating volume stats...") data = {} backend_name = self.configuration.safe_get('volume_backend_name') data["volume_backend_name"] = backend_name or 'Generic_iSER' @@ -1658,29 +1732,7 @@ class ISERDriver(ISCSIDriver): data["storage_protocol"] = 'iSER' data["pools"] = [] - if self.pools: - for pool in self.pools: - new_pool = {} - new_pool.update(dict( - pool_name=pool, - total_capacity_gb=0, - free_capacity_gb=0, - reserved_percentage=100, - QoS_support=False - )) - data["pools"].append(new_pool) - else: - # No pool configured, the whole backend will be treated as a pool - single_pool = {} - single_pool.update(dict( - pool_name=data["volume_backend_name"], - total_capacity_gb=0, - free_capacity_gb=0, - reserved_percentage=100, - QoS_support=False - )) - data["pools"].append(single_pool) - self._stats = data + self._update_pools_and_stats(data) class FakeISERDriver(FakeISCSIDriver): @@ -1759,3 +1811,27 @@ class FibreChannelDriver(VolumeDriver): {'setting': setting}) LOG.error(*msg) raise exception.InvalidConnectorException(missing=setting) + + def get_volume_stats(self, refresh=False): + """Get volume stats. + + If 'refresh' is True, run update the stats first. + """ + if refresh: + self._update_volume_stats() + + return self._stats + + def _update_volume_stats(self): + """Retrieve stats info from volume group.""" + + LOG.debug("Updating volume stats...") + data = {} + backend_name = self.configuration.safe_get('volume_backend_name') + data["volume_backend_name"] = backend_name or 'Generic_FC' + data["vendor_name"] = 'Open Source' + data["driver_version"] = '1.0' + data["storage_protocol"] = 'FC' + data["pools"] = [] + + self._update_pools_and_stats(data) diff --git a/cinder/volume/drivers/lvm.py b/cinder/volume/drivers/lvm.py index 00a3de36a..339da1c28 100644 --- a/cinder/volume/drivers/lvm.py +++ b/cinder/volume/drivers/lvm.py @@ -219,6 +219,10 @@ class LVMVolumeDriver(driver.VolumeDriver): thin_enabled = self.configuration.lvm_type == 'thin' + # Calculate the total volumes used by the VG group. + # This includes volumes and snapshots. + total_volumes = len(self.vg.get_volumes()) + # Skip enabled_pools setting, treat the whole backend as one pool # XXX FIXME if multipool support is added to LVM driver. single_pool = {} @@ -234,6 +238,9 @@ class LVMVolumeDriver(driver.VolumeDriver): self.configuration.max_over_subscription_ratio), thin_provisioning_support=thin_enabled, thick_provisioning_support=not thin_enabled, + total_volumes=total_volumes, + filter_function=self.get_filter_function(), + goodness_function=self.get_goodness_function() )) data["pools"].append(single_pool) diff --git a/cinder/volume/drivers/san/hp/hp_3par_common.py b/cinder/volume/drivers/san/hp/hp_3par_common.py index 00b645d0d..2e562a340 100644 --- a/cinder/volume/drivers/san/hp/hp_3par_common.py +++ b/cinder/volume/drivers/san/hp/hp_3par_common.py @@ -168,10 +168,11 @@ class HP3PARCommon(object): 2.0.35 - Fix default snapCPG for manage_existing bug #1393609 2.0.36 - Added support for dedup provisioning 2.0.37 - Added support for enabling Flash Cache + 2.0.38 - Add stats for hp3par goodness_function and filter_function """ - VERSION = "2.0.37" + VERSION = "2.0.38" stats = {} @@ -661,13 +662,20 @@ class HP3PARCommon(object): return iscsi_ports - def get_volume_stats(self, refresh=False): + def get_volume_stats(self, + refresh, + filter_function=None, + goodness_function=None): if refresh: - self._update_volume_stats() + self._update_volume_stats( + filter_function=filter_function, + goodness_function=goodness_function) return self.stats - def _update_volume_stats(self): + def _update_volume_stats(self, + filter_function=None, + goodness_function=None): # const to convert MiB to GB const = 0.0009765625 @@ -676,13 +684,23 @@ class HP3PARCommon(object): pools = [] info = self.client.getStorageSystemInfo() + for cpg_name in self.config.hp3par_cpg: try: cpg = self.client.getCPG(cpg_name) + if 'numTDVVs' in cpg: + total_volumes = int( + cpg['numFPVVs'] + cpg['numTPVVs'] + cpg['numTDVVs'] + ) + else: + total_volumes = int( + cpg['numFPVVs'] + cpg['numTPVVs'] + ) + if 'limitMiB' not in cpg['SDGrowth']: # cpg usable free space - cpg_avail_space = \ - self.client.getCPGAvailableSpace(cpg_name) + cpg_avail_space = ( + self.client.getCPGAvailableSpace(cpg_name)) free_capacity = int( cpg_avail_space['usableFreeMiB'] * const) # total_capacity is the best we can do for a limitless cpg @@ -693,7 +711,11 @@ class HP3PARCommon(object): else: total_capacity = int(cpg['SDGrowth']['limitMiB'] * const) free_capacity = int((cpg['SDGrowth']['limitMiB'] - - cpg['UsrUsage']['usedMiB']) * const) + (cpg['UsrUsage']['usedMiB'] + + cpg['SDUsage']['usedMiB'])) * const) + capacity_utilization = ( + (float(total_capacity - free_capacity) / + float(total_capacity)) * 100) except hpexceptions.HTTPNotFound: err = (_("CPG (%s) doesn't exist on array") @@ -708,8 +730,13 @@ class HP3PARCommon(object): 'reserved_percentage': 0, 'location_info': ('HP3PARDriver:%(sys_id)s:%(dest_cpg)s' % {'sys_id': info['serialNumber'], - 'dest_cpg': cpg_name}) + 'dest_cpg': cpg_name}), + 'total_volumes': total_volumes, + 'capacity_utilization': capacity_utilization, + 'filter_function': filter_function, + 'goodness_function': goodness_function } + pools.append(pool) self.stats = {'driver_version': '1.0', diff --git a/cinder/volume/drivers/san/hp/hp_3par_fc.py b/cinder/volume/drivers/san/hp/hp_3par_fc.py index b003174b7..b0ffcd529 100644 --- a/cinder/volume/drivers/san/hp/hp_3par_fc.py +++ b/cinder/volume/drivers/san/hp/hp_3par_fc.py @@ -108,7 +108,10 @@ class HP3PARFCDriver(cinder.volume.driver.FibreChannelDriver): def get_volume_stats(self, refresh=False): common = self._login() try: - stats = common.get_volume_stats(refresh) + stats = common.get_volume_stats( + refresh, + self.get_filter_function(), + self.get_goodness_function()) stats['storage_protocol'] = 'FC' stats['driver_version'] = self.VERSION backend_name = self.configuration.safe_get('volume_backend_name') diff --git a/cinder/volume/drivers/san/hp/hp_3par_iscsi.py b/cinder/volume/drivers/san/hp/hp_3par_iscsi.py index 374316340..db1eac41b 100644 --- a/cinder/volume/drivers/san/hp/hp_3par_iscsi.py +++ b/cinder/volume/drivers/san/hp/hp_3par_iscsi.py @@ -114,7 +114,10 @@ class HP3PARISCSIDriver(cinder.volume.driver.ISCSIDriver): def get_volume_stats(self, refresh=False): common = self._login() try: - stats = common.get_volume_stats(refresh) + stats = common.get_volume_stats( + refresh, + self.get_filter_function(), + self.get_goodness_function()) stats['storage_protocol'] = 'iSCSI' stats['driver_version'] = self.VERSION backend_name = self.configuration.safe_get('volume_backend_name')