From b67a416bb94bc0f2e64fc896e1c04581956f777d Mon Sep 17 00:00:00 2001 From: XinXiaohui Date: Wed, 16 Mar 2016 09:16:40 +0800 Subject: [PATCH] Calculate virtual free capacity and notify It calculates virtual free capacity for pool and backend which reports definite values. It will also notify various capacity info to Ceilometer service. The notification only occurs when volume status is updated. It will give users a bit knowledge about the current storage usage. As there are multiple schedulers and volumes, the patch contains the logic only to send changed capacity info to ceilometer as possible as. It adds a new rpc call named notify_service_capabilities() which fanout=false in volume manager. This means that only one scheduler will receive it and send the notification. Co-Authored-By: Gorka Eguileor Co-Authored-By: lisali DocImpact Implements: blueprint capacity-headroom Change-Id: Iff033d1b591fb3d9c0a5b9732c0c17d96ddbf712 --- cinder/manager.py | 5 + cinder/scheduler/driver.py | 6 + cinder/scheduler/host_manager.py | 176 +++++++ cinder/scheduler/manager.py | 9 + cinder/scheduler/rpcapi.py | 10 +- .../tests/unit/scheduler/test_host_manager.py | 487 +++++++++++++++++- cinder/tests/unit/scheduler/test_rpcapi.py | 10 + cinder/tests/unit/test_volume_utils.py | 64 +++ cinder/volume/utils.py | 31 ++ .../capacity-headroom-4b07701f1df9e5c4.yaml | 5 + 10 files changed, 801 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/capacity-headroom-4b07701f1df9e5c4.yaml diff --git a/cinder/manager.py b/cinder/manager.py index 3ecf30609c5..ac1499328df 100644 --- a/cinder/manager.py +++ b/cinder/manager.py @@ -174,6 +174,11 @@ def _publish_service_capabilities(self, context): self.service_name, self.host, self.last_capabilities) + self.scheduler_rpcapi.notify_service_capabilities( + context, + self.service_name, + self.host, + self.last_capabilities) def _add_to_threadpool(self, func, *args, **kwargs): self._tp.spawn_n(func, *args, **kwargs) diff --git a/cinder/scheduler/driver.py b/cinder/scheduler/driver.py index ac7bf61f19a..2fc3a8da587 100644 --- a/cinder/scheduler/driver.py +++ b/cinder/scheduler/driver.py @@ -103,6 +103,12 @@ def update_service_capabilities(self, service_name, host, capabilities): host, capabilities) + def notify_service_capabilities(self, service_name, host, capabilities): + """Notify capability update from a service node.""" + self.host_manager.notify_service_capabilities(service_name, + host, + capabilities) + def host_passes_filters(self, context, host, request_spec, filter_properties): """Check if the specified host passes the filters.""" diff --git a/cinder/scheduler/host_manager.py b/cinder/scheduler/host_manager.py index 5baeb2d8938..6c3203af97d 100644 --- a/cinder/scheduler/host_manager.py +++ b/cinder/scheduler/host_manager.py @@ -28,6 +28,7 @@ from cinder import context as cinder_context from cinder import exception from cinder import objects +from cinder import utils from cinder.i18n import _LI, _LW from cinder.scheduler import filters from cinder.volume import utils as vol_utils @@ -345,6 +346,17 @@ class HostManager(object): host_state_cls = HostState + REQUIRED_KEYS = frozenset([ + 'pool_name', + 'total_capacity_gb', + 'free_capacity_gb', + 'allocated_capacity_gb', + 'provisioned_capacity_gb', + 'thin_provisioning_support', + 'thick_provisioning_support', + 'max_over_subscription_ratio', + 'reserved_percentage']) + def __init__(self): self.service_states = {} # { : {: {cap k : v}}} self.host_state_map = {} @@ -358,6 +370,7 @@ def __init__(self): self._no_capabilities_hosts = set() # Hosts having no capabilities self._update_host_state_map(cinder_context.get_admin_context()) + self.service_states_last_update = {} def _choose_host_filters(self, filter_cls_names): """Return a list of available filter names. @@ -441,6 +454,24 @@ def update_service_capabilities(self, service_name, host, capabilities): # Copy the capabilities, so we don't modify the original dict capab_copy = dict(capabilities) capab_copy["timestamp"] = timeutils.utcnow() # Reported time + + # Set the default capabilities in case None is set. + capab_old = self.service_states.get(host, {"timestamp": 0}) + capab_last_update = self.service_states_last_update.get( + host, {"timestamp": 0}) + + # If the capabilites are not changed and the timestamp is older, + # record the capabilities. + + # There are cases: capab_old has the capabilities set, + # but the timestamp may be None in it. So does capab_last_update. + + if (not self._get_updated_pools(capab_old, capab_copy)) and ( + (not capab_old.get("timestamp")) or + (not capab_last_update.get("timestamp")) or + (capab_last_update["timestamp"] < capab_old["timestamp"])): + self.service_states_last_update[host] = capab_old + self.service_states[host] = capab_copy LOG.debug("Received %(service_name)s service update from " @@ -450,6 +481,34 @@ def update_service_capabilities(self, service_name, host, capabilities): self._no_capabilities_hosts.discard(host) + def notify_service_capabilities(self, service_name, host, capabilities): + """Notify the ceilometer with updated volume stats""" + if service_name != 'volume': + return + + updated = [] + capa_new = self.service_states.get(host, {}) + timestamp = timeutils.utcnow() + + # Compare the capabilities and timestamps to decide notifying + if not capa_new: + updated = self._get_updated_pools(capa_new, capabilities) + else: + if timestamp > self.service_states[host]["timestamp"]: + updated = self._get_updated_pools(self.service_states[host], + capabilities) + if not updated: + updated = self._get_updated_pools( + self.service_states_last_update.get(host, {}), + self.service_states.get(host, {})) + + if updated: + capab_copy = dict(capabilities) + capab_copy["timestamp"] = timestamp + # If capabilities changes, notify and record the capabilities. + self.service_states_last_update[host] = capab_copy + self.get_usage_and_notify(capabilities, updated, host, timestamp) + def has_all_capabilities(self): return len(self._no_capabilities_hosts) == 0 @@ -533,3 +592,120 @@ def get_pools(self, context): all_pools.append(new_pool) return all_pools + + def get_usage_and_notify(self, capa_new, updated_pools, host, timestamp): + context = cinder_context.get_admin_context() + usage = self._get_usage(capa_new, updated_pools, host, timestamp) + + self._notify_capacity_usage(context, usage) + + def _get_usage(self, capa_new, updated_pools, host, timestamp): + pools = capa_new.get('pools') + usage = [] + if pools and isinstance(pools, list): + backend_usage = dict(type='backend', + name_to_id=host, + total=0, + free=0, + allocated=0, + provisioned=0, + virtual_free=0, + reported_at=timestamp) + + # Process the usage. + for pool in pools: + pool_usage = self._get_pool_usage(pool, host, timestamp) + if pool_usage: + backend_usage["total"] += pool_usage["total"] + backend_usage["free"] += pool_usage["free"] + backend_usage["allocated"] += pool_usage["allocated"] + backend_usage["provisioned"] += pool_usage["provisioned"] + backend_usage["virtual_free"] += pool_usage["virtual_free"] + # Only the updated pool is reported. + if pool in updated_pools: + usage.append(pool_usage) + usage.append(backend_usage) + return usage + + def _get_pool_usage(self, pool, host, timestamp): + total = pool["total_capacity_gb"] + free = pool["free_capacity_gb"] + + unknowns = ["unknown", "infinite", None] + if (total in unknowns) or (free in unknowns): + return {} + + allocated = pool["allocated_capacity_gb"] + provisioned = pool["provisioned_capacity_gb"] + reserved = pool["reserved_percentage"] + ratio = pool["max_over_subscription_ratio"] + support = pool["thin_provisioning_support"] + + virtual_free = utils.calculate_virtual_free_capacity( + total, + free, + provisioned, + support, + ratio, + reserved, + support) + + pool_usage = dict( + type='pool', + name_to_id='#'.join([host, pool['pool_name']]), + total=float(total), + free=float(free), + allocated=float(allocated), + provisioned=float(provisioned), + virtual_free=float(virtual_free), + reported_at=timestamp) + + return pool_usage + + def _get_updated_pools(self, old_capa, new_capa): + # Judge if the capabilities should be reported. + + new_pools = new_capa.get('pools', []) + if not new_pools: + return [] + + if isinstance(new_pools, list): + # If the volume_stats is not well prepared, don't notify. + if not all( + self.REQUIRED_KEYS.issubset(pool) for pool in new_pools): + return [] + else: + LOG.debug("The reported capabilities are not well structured...") + return [] + + old_pools = old_capa.get('pools', []) + if not old_pools: + return new_pools + + updated_pools = [] + + newpools = {} + oldpools = {} + for new_pool in new_pools: + newpools[new_pool['pool_name']] = new_pool + + for old_pool in old_pools: + oldpools[old_pool['pool_name']] = old_pool + + for key in newpools.keys(): + if key in oldpools.keys(): + for k in self.REQUIRED_KEYS: + if newpools[key][k] != oldpools[key][k]: + updated_pools.append(newpools[key]) + break + else: + updated_pools.append(newpools[key]) + + return updated_pools + + def _notify_capacity_usage(self, context, usage): + if usage: + for u in usage: + vol_utils.notify_about_capacity_usage( + context, u, u['type'], None, None) + LOG.debug("Publish storage capacity: %s.", usage) diff --git a/cinder/scheduler/manager.py b/cinder/scheduler/manager.py index 7b39d8c9e67..4059cb56d86 100644 --- a/cinder/scheduler/manager.py +++ b/cinder/scheduler/manager.py @@ -88,6 +88,15 @@ def update_service_capabilities(self, context, service_name=None, host, capabilities) + def notify_service_capabilities(self, context, service_name, + host, capabilities): + """Process a capability update from a service node.""" + if capabilities is None: + capabilities = {} + self.driver.notify_service_capabilities(service_name, + host, + capabilities) + def _wait_for_scheduler(self): # NOTE(dulek): We're waiting for scheduler to announce that it's ready # or CONF.periodic_interval seconds from service startup has passed. diff --git a/cinder/scheduler/rpcapi.py b/cinder/scheduler/rpcapi.py index 461a8273556..363c7495648 100644 --- a/cinder/scheduler/rpcapi.py +++ b/cinder/scheduler/rpcapi.py @@ -58,9 +58,10 @@ class SchedulerAPI(rpc.RPCAPI): set to 2.3. 3.0 - Remove 2.x compatibility + 3.1 - Adds notify_service_capabilities() """ - RPC_API_VERSION = '3.0' + RPC_API_VERSION = '3.1' RPC_DEFAULT_VERSION = '3.0' TOPIC = constants.SCHEDULER_TOPIC BINARY = 'cinder-scheduler' @@ -139,3 +140,10 @@ def update_service_capabilities(self, ctxt, service_name, host, cctxt.cast(ctxt, 'update_service_capabilities', service_name=service_name, host=host, capabilities=capabilities) + + def notify_service_capabilities(self, ctxt, service_name, + host, capabilities): + cctxt = self._get_cctxt(version='3.1') + cctxt.cast(ctxt, 'notify_service_capabilities', + service_name=service_name, host=host, + capabilities=capabilities) diff --git a/cinder/tests/unit/scheduler/test_host_manager.py b/cinder/tests/unit/scheduler/test_host_manager.py index cdea584dbf6..ad5490bbc69 100644 --- a/cinder/tests/unit/scheduler/test_host_manager.py +++ b/cinder/tests/unit/scheduler/test_host_manager.py @@ -48,6 +48,8 @@ def setUp(self): self.host_manager = host_manager.HostManager() self.fake_hosts = [host_manager.HostState('fake_host%s' % x) for x in range(1, 5)] + # For a second scheduler service. + self.host_manager_1 = host_manager.HostManager() def test_choose_host_filters_not_found(self): self.flags(scheduler_default_filters='FakeFilterClass3') @@ -85,12 +87,15 @@ def test_get_filtered_hosts(self, _mock_choose_host_filters): self.assertEqual(expected, mock_func.call_args_list) self.assertEqual(set(self.fake_hosts), set(result)) + @mock.patch('cinder.scheduler.host_manager.HostManager._get_updated_pools') @mock.patch('oslo_utils.timeutils.utcnow') - def test_update_service_capabilities(self, _mock_utcnow): + def test_update_service_capabilities(self, _mock_utcnow, + _mock_get_updated_pools): service_states = self.host_manager.service_states self.assertDictMatch({}, service_states) _mock_utcnow.side_effect = [31337, 31338, 31339] + _mock_get_updated_pools.return_value = [] host1_volume_capabs = dict(free_capacity_gb=4321, timestamp=1) host2_volume_capabs = dict(free_capacity_gb=5432, timestamp=1) host3_volume_capabs = dict(free_capacity_gb=6543, timestamp=1) @@ -117,6 +122,355 @@ def test_update_service_capabilities(self, _mock_utcnow): 'host3': host3_volume_capabs} self.assertDictMatch(expected, service_states) + @mock.patch( + 'cinder.scheduler.host_manager.HostManager.get_usage_and_notify') + @mock.patch('oslo_utils.timeutils.utcnow') + def test_update_and_notify_service_capabilities_case1( + self, _mock_utcnow, + _mock_get_usage_and_notify): + + _mock_utcnow.side_effect = [31337, 31338, 31339] + service_name = 'volume' + + capab1 = {'pools': [{ + 'pool_name': 'pool1', 'thick_provisioning_support': True, + 'thin_provisioning_support': False, 'total_capacity_gb': 10, + 'free_capacity_gb': 10, 'max_over_subscription_ratio': 1, + 'provisioned_capacity_gb': 0, 'allocated_capacity_gb': 0, + 'reserved_percentage': 0}]} + + # Run 1: + # capa: capa1 + # S0: update_service_capabilities() + # S0: notify_service_capabilities() + # S1: update_service_capabilities() + # + # notify capab1 to ceilometer by S0 + # + + # S0: update_service_capabilities() + self.host_manager.update_service_capabilities(service_name, 'host1', + capab1) + self.assertDictMatch(dict(dict(timestamp=31337), **capab1), + self.host_manager.service_states['host1']) + + # S0: notify_service_capabilities() + self.host_manager.notify_service_capabilities(service_name, 'host1', + capab1) + self.assertDictMatch(dict(dict(timestamp=31337), **capab1), + self.host_manager.service_states['host1']) + self.assertDictMatch( + dict(dict(timestamp=31338), **capab1), + self.host_manager.service_states_last_update['host1']) + + # notify capab1 to ceilometer by S0 + self.assertTrue(1, _mock_get_usage_and_notify.call_count) + + # S1: update_service_capabilities() + self.host_manager_1.update_service_capabilities(service_name, 'host1', + capab1) + + self.assertDictMatch(dict(dict(timestamp=31339), **capab1), + self.host_manager_1.service_states['host1']) + + @mock.patch( + 'cinder.scheduler.host_manager.HostManager.get_usage_and_notify') + @mock.patch('oslo_utils.timeutils.utcnow') + def test_update_and_notify_service_capabilities_case2( + self, _mock_utcnow, + _mock_get_usage_and_notify): + + _mock_utcnow.side_effect = [31340, 31341, 31342] + + service_name = 'volume' + + capab1 = {'pools': [{ + 'pool_name': 'pool1', 'thick_provisioning_support': True, + 'thin_provisioning_support': False, 'total_capacity_gb': 10, + 'free_capacity_gb': 10, 'max_over_subscription_ratio': 1, + 'provisioned_capacity_gb': 0, 'allocated_capacity_gb': 0, + 'reserved_percentage': 0}]} + + self.host_manager.service_states['host1'] = ( + dict(dict(timestamp=31337), **capab1)) + self.host_manager.service_states_last_update['host1'] = ( + dict(dict(timestamp=31338), **capab1)) + self.host_manager_1.service_states['host1'] = ( + dict(dict(timestamp=31339), **capab1)) + + # Run 2: + # capa: capa1 + # S0: update_service_capabilities() + # S1: update_service_capabilities() + # S1: notify_service_capabilities() + # + # Don't notify capab1 to ceilometer. + + # S0: update_service_capabilities() + self.host_manager.update_service_capabilities(service_name, 'host1', + capab1) + + self.assertDictMatch(dict(dict(timestamp=31340), **capab1), + self.host_manager.service_states['host1']) + + self.assertDictMatch( + dict(dict(timestamp=31338), **capab1), + self.host_manager.service_states_last_update['host1']) + + # S1: update_service_capabilities() + self.host_manager_1.update_service_capabilities(service_name, 'host1', + capab1) + + self.assertDictMatch(dict(dict(timestamp=31341), **capab1), + self.host_manager_1.service_states['host1']) + + self.assertDictMatch( + dict(dict(timestamp=31339), **capab1), + self.host_manager_1.service_states_last_update['host1']) + + # S1: notify_service_capabilities() + self.host_manager_1.notify_service_capabilities(service_name, 'host1', + capab1) + + self.assertDictMatch(dict(dict(timestamp=31341), **capab1), + self.host_manager_1.service_states['host1']) + + self.assertDictMatch( + self.host_manager_1.service_states_last_update['host1'], + dict(dict(timestamp=31339), **capab1)) + + # Don't notify capab1 to ceilometer. + self.assertTrue(1, _mock_get_usage_and_notify.call_count) + + @mock.patch( + 'cinder.scheduler.host_manager.HostManager.get_usage_and_notify') + @mock.patch('oslo_utils.timeutils.utcnow') + def test_update_and_notify_service_capabilities_case3( + self, _mock_utcnow, + _mock_get_usage_and_notify): + + _mock_utcnow.side_effect = [31343, 31344, 31345] + + service_name = 'volume' + + capab1 = {'pools': [{ + 'pool_name': 'pool1', 'thick_provisioning_support': True, + 'thin_provisioning_support': False, 'total_capacity_gb': 10, + 'free_capacity_gb': 10, 'max_over_subscription_ratio': 1, + 'provisioned_capacity_gb': 0, 'allocated_capacity_gb': 0, + 'reserved_percentage': 0}]} + + self.host_manager.service_states['host1'] = ( + dict(dict(timestamp=31340), **capab1)) + self.host_manager.service_states_last_update['host1'] = ( + dict(dict(timestamp=31338), **capab1)) + self.host_manager_1.service_states['host1'] = ( + dict(dict(timestamp=31341), **capab1)) + self.host_manager_1.service_states_last_update['host1'] = ( + dict(dict(timestamp=31339), **capab1)) + + # Run 3: + # capa: capab1 + # S0: notify_service_capabilities() + # S0: update_service_capabilities() + # S1: update_service_capabilities() + # + # Don't notify capab1 to ceilometer. + + # S0: notify_service_capabilities() + self.host_manager.notify_service_capabilities(service_name, 'host1', + capab1) + self.assertDictMatch( + dict(dict(timestamp=31338), **capab1), + self.host_manager.service_states_last_update['host1']) + + self.assertDictMatch(dict(dict(timestamp=31340), **capab1), + self.host_manager.service_states['host1']) + + # Don't notify capab1 to ceilometer. + self.assertTrue(1, _mock_get_usage_and_notify.call_count) + + # S0: update_service_capabilities() + self.host_manager.update_service_capabilities(service_name, 'host1', + capab1) + + self.assertDictMatch( + dict(dict(timestamp=31340), **capab1), + self.host_manager.service_states_last_update['host1']) + + self.assertDictMatch(dict(dict(timestamp=31344), **capab1), + self.host_manager.service_states['host1']) + + # S1: update_service_capabilities() + self.host_manager_1.update_service_capabilities(service_name, 'host1', + capab1) + self.assertDictMatch(dict(dict(timestamp=31345), **capab1), + self.host_manager_1.service_states['host1']) + + self.assertDictMatch( + dict(dict(timestamp=31341), **capab1), + self.host_manager_1.service_states_last_update['host1']) + + @mock.patch( + 'cinder.scheduler.host_manager.HostManager.get_usage_and_notify') + @mock.patch('oslo_utils.timeutils.utcnow') + def test_update_and_notify_service_capabilities_case4( + self, _mock_utcnow, + _mock_get_usage_and_notify): + + _mock_utcnow.side_effect = [31346, 31347, 31348] + + service_name = 'volume' + + capab1 = {'pools': [{ + 'pool_name': 'pool1', 'thick_provisioning_support': True, + 'thin_provisioning_support': False, 'total_capacity_gb': 10, + 'free_capacity_gb': 10, 'max_over_subscription_ratio': 1, + 'provisioned_capacity_gb': 0, 'allocated_capacity_gb': 0, + 'reserved_percentage': 0}]} + + self.host_manager.service_states['host1'] = ( + dict(dict(timestamp=31344), **capab1)) + self.host_manager.service_states_last_update['host1'] = ( + dict(dict(timestamp=31340), **capab1)) + self.host_manager_1.service_states['host1'] = ( + dict(dict(timestamp=31345), **capab1)) + self.host_manager_1.service_states_last_update['host1'] = ( + dict(dict(timestamp=31341), **capab1)) + + capab2 = {'pools': [{ + 'pool_name': 'pool1', 'thick_provisioning_support': True, + 'thin_provisioning_support': False, 'total_capacity_gb': 10, + 'free_capacity_gb': 9, 'max_over_subscription_ratio': 1, + 'provisioned_capacity_gb': 1, 'allocated_capacity_gb': 1, + 'reserved_percentage': 0}]} + + # Run 4: + # capa: capab2 + # S0: update_service_capabilities() + # S1: notify_service_capabilities() + # S1: update_service_capabilities() + # + # notify capab2 to ceilometer. + + # S0: update_service_capabilities() + self.host_manager.update_service_capabilities(service_name, 'host1', + capab2) + self.assertDictMatch( + dict(dict(timestamp=31340), **capab1), + self.host_manager.service_states_last_update['host1']) + + self.assertDictMatch(dict(dict(timestamp=31346), **capab2), + self.host_manager.service_states['host1']) + + # S1: notify_service_capabilities() + self.host_manager_1.notify_service_capabilities(service_name, 'host1', + capab2) + self.assertDictMatch(dict(dict(timestamp=31345), **capab1), + self.host_manager_1.service_states['host1']) + + self.assertDictMatch( + dict(dict(timestamp=31347), **capab2), + self.host_manager_1.service_states_last_update['host1']) + + # notify capab2 to ceilometer. + self.assertTrue(2, _mock_get_usage_and_notify.call_count) + + # S1: update_service_capabilities() + self.host_manager_1.update_service_capabilities(service_name, 'host1', + capab2) + self.assertDictMatch(dict(dict(timestamp=31348), **capab2), + self.host_manager_1.service_states['host1']) + + self.assertDictMatch( + dict(dict(timestamp=31347), **capab2), + self.host_manager_1.service_states_last_update['host1']) + + @mock.patch( + 'cinder.scheduler.host_manager.HostManager.get_usage_and_notify') + @mock.patch('oslo_utils.timeutils.utcnow') + def test_update_and_notify_service_capabilities_case5( + self, _mock_utcnow, + _mock_get_usage_and_notify): + + _mock_utcnow.side_effect = [31349, 31350, 31351] + + service_name = 'volume' + + capab1 = {'pools': [{ + 'pool_name': 'pool1', 'thick_provisioning_support': True, + 'thin_provisioning_support': False, 'total_capacity_gb': 10, + 'free_capacity_gb': 10, 'max_over_subscription_ratio': 1, + 'provisioned_capacity_gb': 0, 'allocated_capacity_gb': 0, + 'reserved_percentage': 0}]} + + capab2 = {'pools': [{ + 'pool_name': 'pool1', 'thick_provisioning_support': True, + 'thin_provisioning_support': False, 'total_capacity_gb': 10, + 'free_capacity_gb': 9, 'max_over_subscription_ratio': 1, + 'provisioned_capacity_gb': 1, 'allocated_capacity_gb': 1, + 'reserved_percentage': 0}]} + + self.host_manager.service_states['host1'] = ( + dict(dict(timestamp=31346), **capab2)) + self.host_manager.service_states_last_update['host1'] = ( + dict(dict(timestamp=31340), **capab1)) + self.host_manager_1.service_states['host1'] = ( + dict(dict(timestamp=31348), **capab2)) + self.host_manager_1.service_states_last_update['host1'] = ( + dict(dict(timestamp=31347), **capab2)) + + # Run 5: + # capa: capa2 + # S0: notify_service_capabilities() + # S0: update_service_capabilities() + # S1: update_service_capabilities() + # + # This is the special case not handled. + # 1) capab is changed (from capab1 to capab2) + # 2) S1 has already notify the capab2 in Run 4. + # 3) S0 just got update_service_capabilities() in Run 4. + # 4) S0 got notify_service_capabilities() immediately in next run, + # here is Run 5. + # S0 has no ways to know whether other scheduler (here is S1) who + # has noitified the changed capab2 or not. S0 just thinks it's his + # own turn to notify the changed capab2. + # In this case, we have notified the same capabilities twice. + # + # S0: notify_service_capabilities() + self.host_manager.notify_service_capabilities(service_name, 'host1', + capab2) + self.assertDictMatch( + dict(dict(timestamp=31349), **capab2), + self.host_manager.service_states_last_update['host1']) + + self.assertDictMatch(dict(dict(timestamp=31346), **capab2), + self.host_manager.service_states['host1']) + + # S0 notify capab2 to ceilometer. + self.assertTrue(3, _mock_get_usage_and_notify.call_count) + + # S0: update_service_capabilities() + self.host_manager.update_service_capabilities(service_name, 'host1', + capab2) + self.assertDictMatch( + dict(dict(timestamp=31349), **capab2), + self.host_manager.service_states_last_update['host1']) + + self.assertDictMatch(dict(dict(timestamp=31350), **capab2), + self.host_manager.service_states['host1']) + + # S1: update_service_capabilities() + self.host_manager_1.update_service_capabilities(service_name, 'host1', + capab2) + + self.assertDictMatch( + dict(dict(timestamp=31348), **capab2), + self.host_manager_1.service_states_last_update['host1']) + + self.assertDictMatch(dict(dict(timestamp=31351), **capab2), + self.host_manager_1.service_states['host1']) + @mock.patch('cinder.objects.service.Service.is_up', new_callable=mock.PropertyMock) @mock.patch('cinder.db.service_get_all') @@ -397,6 +751,137 @@ def sort_func(data): self.assertEqual(sorted(expected, key=sort_func), sorted(res, key=sort_func)) + def test_get_usage(self): + host = "host1@backend1" + timestamp = 40000 + volume_stats1 = {'pools': [ + {'pool_name': 'pool1', + 'total_capacity_gb': 30.01, + 'free_capacity_gb': 28.01, + 'allocated_capacity_gb': 2.0, + 'provisioned_capacity_gb': 2.0, + 'max_over_subscription_ratio': 1.0, + 'thin_provisioning_support': False, + 'thick_provisioning_support': True, + 'reserved_percentage': 5}, + {'pool_name': 'pool2', + 'total_capacity_gb': 20.01, + 'free_capacity_gb': 18.01, + 'allocated_capacity_gb': 2.0, + 'provisioned_capacity_gb': 2.0, + 'max_over_subscription_ratio': 2.0, + 'thin_provisioning_support': True, + 'thick_provisioning_support': False, + 'reserved_percentage': 5}]} + + updated_pools1 = [{'pool_name': 'pool1', + 'total_capacity_gb': 30.01, + 'free_capacity_gb': 28.01, + 'allocated_capacity_gb': 2.0, + 'provisioned_capacity_gb': 2.0, + 'max_over_subscription_ratio': 1.0, + 'thin_provisioning_support': False, + 'thick_provisioning_support': True, + 'reserved_percentage': 5}, + {'pool_name': 'pool2', + 'total_capacity_gb': 20.01, + 'free_capacity_gb': 18.01, + 'allocated_capacity_gb': 2.0, + 'provisioned_capacity_gb': 2.0, + 'max_over_subscription_ratio': 2.0, + 'thin_provisioning_support': True, + 'thick_provisioning_support': False, + 'reserved_percentage': 5}] + + volume_stats2 = {'pools': [ + {'pool_name': 'pool1', + 'total_capacity_gb': 30.01, + 'free_capacity_gb': 28.01, + 'allocated_capacity_gb': 2.0, + 'provisioned_capacity_gb': 2.0, + 'max_over_subscription_ratio': 2.0, + 'thin_provisioning_support': True, + 'thick_provisioning_support': False, + 'reserved_percentage': 0}, + {'pool_name': 'pool2', + 'total_capacity_gb': 20.01, + 'free_capacity_gb': 18.01, + 'allocated_capacity_gb': 2.0, + 'provisioned_capacity_gb': 2.0, + 'max_over_subscription_ratio': 2.0, + 'thin_provisioning_support': True, + 'thick_provisioning_support': False, + 'reserved_percentage': 5}]} + + updated_pools2 = [{'pool_name': 'pool1', + 'total_capacity_gb': 30.01, + 'free_capacity_gb': 28.01, + 'allocated_capacity_gb': 2.0, + 'provisioned_capacity_gb': 2.0, + 'max_over_subscription_ratio': 2.0, + 'thin_provisioning_support': True, + 'thick_provisioning_support': False, + 'reserved_percentage': 0}] + + expected1 = [ + {"name_to_id": 'host1@backend1#pool1', + "type": "pool", + "total": 30.01, + "free": 28.01, + "allocated": 2.0, + "provisioned": 2.0, + "virtual_free": 27.01, + "reported_at": 40000}, + {"name_to_id": 'host1@backend1#pool2', + "type": "pool", + "total": 20.01, + "free": 18.01, + "allocated": 2.0, + "provisioned": 2.0, + "virtual_free": 37.02, + "reported_at": 40000}, + {"name_to_id": 'host1@backend1', + "type": "backend", + "total": 50.02, + "free": 46.02, + "allocated": 4.0, + "provisioned": 4.0, + "virtual_free": 64.03, + "reported_at": 40000}] + + expected2 = [ + {"name_to_id": 'host1@backend1#pool1', + "type": "pool", + "total": 30.01, + "free": 28.01, + "allocated": 2.0, + "provisioned": 2.0, + "virtual_free": 58.02, + "reported_at": 40000}, + {"name_to_id": 'host1@backend1', + "type": "backend", + "total": 50.02, + "free": 46.02, + "allocated": 4.0, + "provisioned": 4.0, + "virtual_free": 95.04, + "reported_at": 40000}] + + def sort_func(data): + return data['name_to_id'] + + res1 = self.host_manager._get_usage(volume_stats1, + updated_pools1, host, timestamp) + self.assertEqual(len(expected1), len(res1)) + self.assertEqual(sorted(expected1, key=sort_func), + sorted(res1, key=sort_func)) + + res2 = self.host_manager._get_usage(volume_stats2, + updated_pools2, host, timestamp) + self.assertEqual(len(expected2), len(res2)) + self.assertEqual(sorted(expected2, key=sort_func), + sorted(res2, key=sort_func)) + class HostStateTestCase(test.TestCase): """Test case for HostState class.""" diff --git a/cinder/tests/unit/scheduler/test_rpcapi.py b/cinder/tests/unit/scheduler/test_rpcapi.py index 2b2df0384d2..9b280e8cb77 100644 --- a/cinder/tests/unit/scheduler/test_rpcapi.py +++ b/cinder/tests/unit/scheduler/test_rpcapi.py @@ -96,6 +96,16 @@ def test_create_volume(self): version='3.0') create_worker_mock.assert_called_once() + def test_notify_service_capabilities(self): + capabilities = {'host': 'fake_host', + 'total': '10.01', } + self._test_scheduler_api('notify_service_capabilities', + rpc_method='cast', + service_name='fake_name', + host='fake_host', + capabilities=capabilities, + version='3.1') + def test_create_volume_serialization(self): volume = fake_volume.fake_volume_obj(self.context) create_worker_mock = self.mock_object(volume, 'create_worker') diff --git a/cinder/tests/unit/test_volume_utils.py b/cinder/tests/unit/test_volume_utils.py index bca8c71648a..915f1ee45e5 100644 --- a/cinder/tests/unit/test_volume_utils.py +++ b/cinder/tests/unit/test_volume_utils.py @@ -400,6 +400,70 @@ def test_convert_blocksize_option(self): bs = volume_utils._check_blocksize('ABM') self.assertEqual('1M', bs) + @mock.patch('cinder.volume.utils._usage_from_capacity') + @mock.patch('cinder.volume.utils.CONF') + @mock.patch('cinder.volume.utils.rpc') + def test_notify_about_capacity_usage(self, mock_rpc, + mock_conf, mock_usage): + mock_conf.host = 'host1' + output = volume_utils.notify_about_capacity_usage( + mock.sentinel.context, + mock.sentinel.capacity, + 'test_suffix') + self.assertIsNone(output) + mock_usage.assert_called_once_with(mock.sentinel.capacity) + mock_rpc.get_notifier.assert_called_once_with('capacity', 'host1') + mock_rpc.get_notifier.return_value.info.assert_called_once_with( + mock.sentinel.context, + 'capacity.test_suffix', + mock_usage.return_value) + + @mock.patch('cinder.volume.utils._usage_from_capacity') + @mock.patch('cinder.volume.utils.CONF') + @mock.patch('cinder.volume.utils.rpc') + def test_notify_about_capacity_usage_with_kwargs(self, mock_rpc, mock_conf, + mock_usage): + mock_conf.host = 'host1' + output = volume_utils.notify_about_capacity_usage( + mock.sentinel.context, + mock.sentinel.capacity, + 'test_suffix', + extra_usage_info={'a': 'b', 'c': 'd'}, + host='host2') + self.assertIsNone(output) + mock_usage.assert_called_once_with(mock.sentinel.capacity, + a='b', c='d') + mock_rpc.get_notifier.assert_called_once_with('capacity', 'host2') + mock_rpc.get_notifier.return_value.info.assert_called_once_with( + mock.sentinel.context, + 'capacity.test_suffix', + mock_usage.return_value) + + def test_usage_from_capacity(self): + test_capacity = { + 'name_to_id': 'host1@backend1#pool1', + 'type': 'pool', + 'total': '10.01', + 'free': '8.01', + 'allocated': '2', + 'provisioned': '2', + 'virtual_free': '8.01', + 'reported_at': '2014-12-11T10:10:00', + } + + usage_info = volume_utils._usage_from_capacity( + test_capacity) + expected_capacity = { + 'name_to_id': 'host1@backend1#pool1', + 'total': '10.01', + 'free': '8.01', + 'allocated': '2', + 'provisioned': '2', + 'virtual_free': '8.01', + 'reported_at': '2014-12-11T10:10:00', + } + self.assertEqual(expected_capacity, usage_info) + class OdirectSupportTestCase(test.TestCase): @mock.patch('cinder.utils.execute') diff --git a/cinder/volume/utils.py b/cinder/volume/utils.py index 824216961f5..324efc920e0 100644 --- a/cinder/volume/utils.py +++ b/cinder/volume/utils.py @@ -197,6 +197,37 @@ def notify_about_snapshot_usage(context, snapshot, event_suffix, usage_info) +def _usage_from_capacity(capacity, **extra_usage_info): + + capacity_info = { + 'name_to_id': capacity['name_to_id'], + 'total': capacity['total'], + 'free': capacity['free'], + 'allocated': capacity['allocated'], + 'provisioned': capacity['provisioned'], + 'virtual_free': capacity['virtual_free'], + 'reported_at': capacity['reported_at'] + } + + capacity_info.update(extra_usage_info) + return capacity_info + + +def notify_about_capacity_usage(context, capacity, suffix, + extra_usage_info=None, host=None): + if not host: + host = CONF.host + + if not extra_usage_info: + extra_usage_info = {} + + usage_info = _usage_from_capacity(capacity, **extra_usage_info) + + rpc.get_notifier('capacity', host).info(context, + 'capacity.%s' % suffix, + usage_info) + + def notify_about_replication_usage(context, volume, suffix, extra_usage_info=None, host=None): if not host: diff --git a/releasenotes/notes/capacity-headroom-4b07701f1df9e5c4.yaml b/releasenotes/notes/capacity-headroom-4b07701f1df9e5c4.yaml new file mode 100644 index 00000000000..c9fc5bc3400 --- /dev/null +++ b/releasenotes/notes/capacity-headroom-4b07701f1df9e5c4.yaml @@ -0,0 +1,5 @@ +--- +features: + - Cinder is now collecting capacity data, including + virtual free capacity etc from the backends. A notification + which includes that data is periodically emitted.