From f5bcf9d3553bbd04ddd560e036f7a93d5b5f7b70 Mon Sep 17 00:00:00 2001 From: licanwei Date: Mon, 30 Oct 2017 20:30:19 -0700 Subject: [PATCH] Add storage capacity balance Strategy This patch adds Storage Capacity Balance Strategy to balance the storage capacity through volume migration. Change-Id: I52ea7ce00deb609a2f668db330f1fbc1c9932613 Implements: blueprint storage-workload-balance --- ...age-workload-balance-0ecabbc1791e6894.yaml | 4 + setup.cfg | 1 + watcher/common/cinder_helper.py | 4 + .../strategy/strategies/__init__.py | 5 +- .../strategies/storage_capacity_balance.py | 409 ++++++++++++++++++ .../test_storage_capacity_balance.py | 245 +++++++++++ 6 files changed, 667 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/storage-workload-balance-0ecabbc1791e6894.yaml create mode 100644 watcher/decision_engine/strategy/strategies/storage_capacity_balance.py create mode 100644 watcher/tests/decision_engine/strategy/strategies/test_storage_capacity_balance.py diff --git a/releasenotes/notes/storage-workload-balance-0ecabbc1791e6894.yaml b/releasenotes/notes/storage-workload-balance-0ecabbc1791e6894.yaml new file mode 100644 index 000000000..4dfe28ba5 --- /dev/null +++ b/releasenotes/notes/storage-workload-balance-0ecabbc1791e6894.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Added storage capacity balance strategy. diff --git a/setup.cfg b/setup.cfg index 588557179..37c1b64b8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -77,6 +77,7 @@ watcher_strategies = workload_balance = watcher.decision_engine.strategy.strategies.workload_balance:WorkloadBalance uniform_airflow = watcher.decision_engine.strategy.strategies.uniform_airflow:UniformAirflow noisy_neighbor = watcher.decision_engine.strategy.strategies.noisy_neighbor:NoisyNeighbor + storage_capacity_balance = watcher.decision_engine.strategy.strategies.storage_capacity_balance:StorageCapacityBalance watcher_actions = migrate = watcher.applier.actions.migration:Migrate diff --git a/watcher/common/cinder_helper.py b/watcher/common/cinder_helper.py index ef1b39070..b1daa264a 100644 --- a/watcher/common/cinder_helper.py +++ b/watcher/common/cinder_helper.py @@ -70,6 +70,10 @@ class CinderHelper(object): def get_volume_type_list(self): return self.cinder.volume_types.list() + def get_volume_snapshots_list(self): + return self.cinder.volume_snapshots.list( + search_opts={'all_tenants': True}) + def get_volume_type_by_backendname(self, backendname): """Retrun a list of volume type""" volume_type_list = self.get_volume_type_list() diff --git a/watcher/decision_engine/strategy/strategies/__init__.py b/watcher/decision_engine/strategy/strategies/__init__.py index fbb739ccc..30b5d2466 100644 --- a/watcher/decision_engine/strategy/strategies/__init__.py +++ b/watcher/decision_engine/strategy/strategies/__init__.py @@ -21,6 +21,8 @@ from watcher.decision_engine.strategy.strategies import dummy_with_scorer from watcher.decision_engine.strategy.strategies import noisy_neighbor from watcher.decision_engine.strategy.strategies import outlet_temp_control from watcher.decision_engine.strategy.strategies import saving_energy +from watcher.decision_engine.strategy.strategies import \ + storage_capacity_balance from watcher.decision_engine.strategy.strategies import uniform_airflow from watcher.decision_engine.strategy.strategies import \ vm_workload_consolidation @@ -33,6 +35,7 @@ OutletTempControl = outlet_temp_control.OutletTempControl DummyStrategy = dummy_strategy.DummyStrategy DummyWithScorer = dummy_with_scorer.DummyWithScorer SavingEnergy = saving_energy.SavingEnergy +StorageCapacityBalance = storage_capacity_balance.StorageCapacityBalance VMWorkloadConsolidation = vm_workload_consolidation.VMWorkloadConsolidation WorkloadBalance = workload_balance.WorkloadBalance WorkloadStabilization = workload_stabilization.WorkloadStabilization @@ -42,4 +45,4 @@ NoisyNeighbor = noisy_neighbor.NoisyNeighbor __all__ = ("Actuator", "BasicConsolidation", "OutletTempControl", "DummyStrategy", "DummyWithScorer", "VMWorkloadConsolidation", "WorkloadBalance", "WorkloadStabilization", "UniformAirflow", - "NoisyNeighbor", "SavingEnergy") + "NoisyNeighbor", "SavingEnergy", "StorageCapacityBalance") diff --git a/watcher/decision_engine/strategy/strategies/storage_capacity_balance.py b/watcher/decision_engine/strategy/strategies/storage_capacity_balance.py new file mode 100644 index 000000000..52b271c17 --- /dev/null +++ b/watcher/decision_engine/strategy/strategies/storage_capacity_balance.py @@ -0,0 +1,409 @@ +# -*- encoding: utf-8 -*- +# Copyright (c) 2017 ZTE Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +""" +*Workload balance using cinder volume migration* + +*Description* + +This strategy migrates volumes based on the workload of the +cinder pools. +It makes decision to migrate a volume whenever a pool's used +utilization % is higher than the specified threshold. The volume +to be moved should make the pool close to average workload of all +cinder pools. + +*Requirements* + +* You must have at least 2 cinder volume pools to run + this strategy. + +""" + +from oslo_config import cfg +from oslo_log import log + +from watcher._i18n import _ +from watcher.common import cinder_helper +from watcher.decision_engine.strategy.strategies import base + +LOG = log.getLogger(__name__) + + +class StorageCapacityBalance(base.WorkloadStabilizationBaseStrategy): + """Storage capacity balance using cinder volume migration + + *Description* + + This strategy migrates volumes based on the workload of the + cinder pools. + It makes decision to migrate a volume whenever a pool's used + utilization % is higher than the specified threshold. The volume + to be moved should make the pool close to average workload of all + cinder pools. + + *Requirements* + + * You must have at least 2 cinder volume pools to run + this strategy. + """ + + def __init__(self, config, osc=None): + """VolumeMigrate using cinder volume migration + + :param config: A mapping containing the configuration of this strategy + :type config: :py:class:`~.Struct` instance + :param osc: :py:class:`~.OpenStackClients` instance + """ + super(StorageCapacityBalance, self).__init__(config, osc) + self._cinder = None + self.volume_threshold = 80.0 + self.pool_type_cache = dict() + self.source_pools = [] + self.dest_pools = [] + + @property + def cinder(self): + if not self._cinder: + self._cinder = cinder_helper.CinderHelper(osc=self.osc) + return self._cinder + + @classmethod + def get_name(cls): + return "storage_capacity_balance" + + @classmethod + def get_display_name(cls): + return _("Storage Capacity Balance Strategy") + + @classmethod + def get_translatable_display_name(cls): + return "Storage Capacity Balance Strategy" + + @classmethod + def get_schema(cls): + # Mandatory default setting for each element + return { + "properties": { + "volume_threshold": { + "description": "volume threshold for capacity balance", + "type": "number", + "default": 80.0 + }, + }, + } + + @classmethod + def get_config_opts(cls): + return [ + cfg.ListOpt( + "ex_pools", + help="exclude pools", + default=['local_vstorage']), + ] + + def get_pools(self, cinder): + """Get all volume pools excepting ex_pools. + + :param cinder: cinder client + :return: volume pools + """ + ex_pools = self.config.ex_pools + pools = cinder.get_storage_pool_list() + filtered_pools = [p for p in pools + if p.pool_name not in ex_pools] + return filtered_pools + + def get_volumes(self, cinder): + """Get all volumes with staus in available or in-use and no snapshot. + + :param cinder: cinder client + :return: all volumes + """ + all_volumes = cinder.get_volume_list() + valid_status = ['in-use', 'available'] + + volume_snapshots = cinder.get_volume_snapshots_list() + snapshot_volume_ids = [] + for snapshot in volume_snapshots: + snapshot_volume_ids.append(snapshot.volume_id) + + nosnap_volumes = list(filter(lambda v: v.id not in snapshot_volume_ids, + all_volumes)) + LOG.info("volumes in snap: %s", snapshot_volume_ids) + status_volumes = list( + filter(lambda v: v.status in valid_status, nosnap_volumes)) + valid_volumes = [v for v in status_volumes + if getattr(v, 'migration_status') == 'success' + or getattr(v, 'migration_status') is None] + LOG.info("valid volumes: %s", valid_volumes) + + return valid_volumes + + def group_pools(self, pools, threshold): + """group volume pools by threshold. + + :param pools: all volume pools + :param threshold: volume threshold + :return: under and over threshold pools + """ + under_pools = list( + filter(lambda p: float(p.total_capacity_gb) - + float(p.free_capacity_gb) < + float(p.total_capacity_gb) * threshold, pools)) + + over_pools = list( + filter(lambda p: float(p.total_capacity_gb) - + float(p.free_capacity_gb) >= + float(p.total_capacity_gb) * threshold, pools)) + + return over_pools, under_pools + + def get_volume_type_by_name(self, cinder, backendname): + # return list of pool type + if backendname in self.pool_type_cache.keys(): + return self.pool_type_cache.get(backendname) + + volume_type_list = cinder.get_volume_type_list() + volume_type = list(filter( + lambda volume_type: + volume_type.extra_specs.get( + 'volume_backend_name') == backendname, volume_type_list)) + if volume_type: + self.pool_type_cache[backendname] = volume_type + return self.pool_type_cache.get(backendname) + else: + return [] + + def migrate_fit(self, volume, threshold): + target_pool_name = None + if volume.volume_type: + LOG.info("volume %s type %s", volume.id, volume.volume_type) + return target_pool_name + self.dest_pools.sort( + key=lambda p: float(p.free_capacity_gb) / + float(p.total_capacity_gb)) + for pool in reversed(self.dest_pools): + total_cap = float(pool.total_capacity_gb) + allocated = float(pool.allocated_capacity_gb) + ratio = pool.max_over_subscription_ratio + if total_cap*ratio < allocated+float(volume.size): + LOG.info("pool %s allocated over", pool.name) + continue + free_cap = float(pool.free_capacity_gb)-float(volume.size) + if free_cap > (1-threshold)*total_cap: + target_pool_name = pool.name + index = self.dest_pools.index(pool) + setattr(self.dest_pools[index], 'free_capacity_gb', + str(free_cap)) + LOG.info("volume: get pool %s for vol %s", target_pool_name, + volume.name) + break + return target_pool_name + + def check_pool_type(self, volume, dest_pool): + target_type = None + # check type feature + if not volume.volume_type: + return target_type + volume_type_list = self.cinder.get_volume_type_list() + volume_type = list(filter( + lambda volume_type: + volume_type.name == volume.volume_type, volume_type_list)) + if volume_type: + src_extra_specs = volume_type[0].extra_specs + src_extra_specs.pop('volume_backend_name', None) + + backendname = getattr(dest_pool, 'volume_backend_name') + dst_pool_type = self.get_volume_type_by_name(self.cinder, backendname) + + for src_key in src_extra_specs.keys(): + dst_pool_type = [pt for pt in dst_pool_type + if pt.extra_specs.get(src_key) + == src_extra_specs.get(src_key)] + if dst_pool_type: + if volume.volume_type: + if dst_pool_type[0].name != volume.volume_type: + target_type = dst_pool_type[0].name + else: + target_type = dst_pool_type[0].name + return target_type + + def retype_fit(self, volume, threshold): + target_type = None + self.dest_pools.sort( + key=lambda p: float(p.free_capacity_gb) / + float(p.total_capacity_gb)) + for pool in reversed(self.dest_pools): + backendname = getattr(pool, 'volume_backend_name') + pool_type = self.get_volume_type_by_name(self.cinder, backendname) + LOG.info("volume: pool %s, type %s", pool.name, pool_type) + if pool_type is None: + continue + total_cap = float(pool.total_capacity_gb) + allocated = float(pool.allocated_capacity_gb) + ratio = pool.max_over_subscription_ratio + if total_cap*ratio < allocated+float(volume.size): + LOG.info("pool %s allocated over", pool.name) + continue + free_cap = float(pool.free_capacity_gb)-float(volume.size) + if free_cap > (1-threshold)*total_cap: + target_type = self.check_pool_type(volume, pool) + if target_type is None: + continue + index = self.dest_pools.index(pool) + setattr(self.dest_pools[index], 'free_capacity_gb', + str(free_cap)) + LOG.info("volume: get type %s for vol %s", target_type, + volume.name) + break + return target_type + + def get_actions(self, pool, volumes, threshold): + """get volume, pool key-value action + + return: retype, migrate dict + """ + retype_dicts = dict() + migrate_dicts = dict() + total_cap = float(pool.total_capacity_gb) + used_cap = float(pool.total_capacity_gb)-float(pool.free_capacity_gb) + seek_flag = True + + volumes_in_pool = list( + filter(lambda v: getattr(v, 'os-vol-host-attr:host') == pool.name, + volumes)) + LOG.info("volumes in pool: %s", str(volumes_in_pool)) + if not volumes_in_pool: + return retype_dicts, migrate_dicts + ava_volumes = list(filter(lambda v: v.status == 'available', + volumes_in_pool)) + ava_volumes.sort(key=lambda v: float(v.size)) + LOG.info("available volumes in pool: %s ", str(ava_volumes)) + for vol in ava_volumes: + vol_flag = False + migrate_pool = self.migrate_fit(vol, threshold) + if migrate_pool: + migrate_dicts[vol.id] = migrate_pool + vol_flag = True + else: + target_type = self.retype_fit(vol, threshold) + if target_type: + retype_dicts[vol.id] = target_type + vol_flag = True + if vol_flag: + used_cap -= float(vol.size) + if used_cap < threshold*total_cap: + seek_flag = False + break + if seek_flag: + noboot_volumes = list( + filter(lambda v: v.bootable.lower() == 'false' + and v.status == 'in-use', volumes_in_pool)) + noboot_volumes.sort(key=lambda v: float(v.size)) + LOG.info("noboot volumes: %s ", str(noboot_volumes)) + for vol in noboot_volumes: + vol_flag = False + migrate_pool = self.migrate_fit(vol, threshold) + if migrate_pool: + migrate_dicts[vol.id] = migrate_pool + vol_flag = True + else: + target_type = self.retype_fit(vol, threshold) + if target_type: + retype_dicts[vol.id] = target_type + vol_flag = True + if vol_flag: + used_cap -= float(vol.size) + if used_cap < threshold*total_cap: + seek_flag = False + break + + if seek_flag: + boot_volumes = list(filter(lambda v: v.bootable.lower() == 'true' + and v.status == 'in-use', volumes_in_pool)) + boot_volumes.sort(key=lambda v: float(v.size)) + LOG.info("boot volumes: %s ", str(boot_volumes)) + for vol in boot_volumes: + vol_flag = False + migrate_pool = self.migrate_fit(vol, threshold) + if migrate_pool: + migrate_dicts[vol.id] = migrate_pool + vol_flag = True + else: + target_type = self.retype_fit(vol, threshold) + if target_type: + retype_dicts[vol.id] = target_type + vol_flag = True + if vol_flag: + used_cap -= float(vol.size) + if used_cap < threshold*total_cap: + seek_flag = False + break + return retype_dicts, migrate_dicts + + def pre_execute(self): + """Pre-execution phase + + This can be used to fetch some pre-requisites or data. + """ + LOG.info("Initializing Storage Capacity Balance Strategy") + self.volume_threshold = self.input_parameters.volume_threshold + + def do_execute(self, audit=None): + """Strategy execution phase + + This phase is where you should put the main logic of your strategy. + """ + all_pools = self.get_pools(self.cinder) + all_volumes = self.get_volumes(self.cinder) + threshold = float(self.volume_threshold)/100 + self.source_pools, self.dest_pools = self.group_pools( + all_pools, threshold) + LOG.info(" source pools: %s dest pools:%s", + self.source_pools, self.dest_pools) + if not self.source_pools: + LOG.info("No pools require optimization") + return + + if not self.dest_pools: + LOG.info("No enough pools for optimization") + return + for source_pool in self.source_pools: + retype_actions, migrate_actions = self.get_actions( + source_pool, all_volumes, threshold) + for vol_id, pool_type in retype_actions.items(): + vol = [v for v in all_volumes if v.id == vol_id] + parameters = {'migration_type': 'retype', + 'destination_type': pool_type, + 'resource_name': vol[0].name} + self.solution.add_action(action_type='volume_migrate', + resource_id=vol_id, + input_parameters=parameters) + for vol_id, pool_name in migrate_actions.items(): + vol = [v for v in all_volumes if v.id == vol_id] + parameters = {'migration_type': 'migrate', + 'destination_node': pool_name, + 'resource_name': vol[0].name} + self.solution.add_action(action_type='volume_migrate', + resource_id=vol_id, + input_parameters=parameters) + + def post_execute(self): + """Post-execution phase + + """ + pass diff --git a/watcher/tests/decision_engine/strategy/strategies/test_storage_capacity_balance.py b/watcher/tests/decision_engine/strategy/strategies/test_storage_capacity_balance.py new file mode 100644 index 000000000..cc19185ce --- /dev/null +++ b/watcher/tests/decision_engine/strategy/strategies/test_storage_capacity_balance.py @@ -0,0 +1,245 @@ +# -*- encoding: utf-8 -*- +# Copyright (c) 2017 ZTE +# +# Authors: Canwei Li +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import mock + + +from watcher.common import cinder_helper +from watcher.common import clients +from watcher.common import utils +from watcher.decision_engine.strategy import strategies +from watcher.tests import base + + +class TestStorageCapacityBalance(base.TestCase): + + def setUp(self): + super(TestStorageCapacityBalance, self).setUp() + + def test_fake_pool(name, free, total, allocated): + fake_pool = mock.MagicMock() + fake_pool.name = name + fake_pool.pool_name = name.split('#')[1] + fake_pool.volume_backend_name = name.split('#')[1] + fake_pool.free_capacity_gb = free + fake_pool.total_capacity_gb = total + fake_pool.allocated_capacity_gb = allocated + fake_pool.max_over_subscription_ratio = 1.0 + + return fake_pool + + self.fake_pool1 = test_fake_pool('host1@IPSAN-1#pool1', + '60', '100', '90') + + self.fake_pool2 = test_fake_pool('host1@IPSAN-1#pool2', + '20', '100', '80') + + self.fake_pool3 = test_fake_pool('host1@IPSAN-1#local_vstorage', + '20', '100', '80') + self.fake_pools = [self.fake_pool1, self.fake_pool2, + self.fake_pool3] + + def test_fake_vol(id, name, size, status, bootable, + migration_status=None, + volume_type=None): + fake_vol = mock.MagicMock() + fake_vol.id = id + fake_vol.name = name + fake_vol.size = size + fake_vol.status = status + fake_vol.bootable = bootable + fake_vol.migration_status = migration_status + fake_vol.volume_type = volume_type + setattr(fake_vol, 'os-vol-host-attr:host', 'host1@IPSAN-1#pool2') + + return fake_vol + + self.fake_vol1 = test_fake_vol('922d4762-0bc5-4b30-9cb9-48ab644dd861', + 'test_volume1', 4, + 'available', 'true', 'success', + volume_type='type2') + self.fake_vol2 = test_fake_vol('922d4762-0bc5-4b30-9cb9-48ab644dd862', + 'test_volume2', 10, + 'in-use', 'false') + self.fake_vol3 = test_fake_vol('922d4762-0bc5-4b30-9cb9-48ab644dd863', + 'test_volume3', 4, + 'in-use', 'true', volume_type='type2') + self.fake_vol4 = test_fake_vol('922d4762-0bc5-4b30-9cb9-48ab644dd864', + 'test_volume4', 10, + 'error', 'true') + self.fake_vol5 = test_fake_vol('922d4762-0bc5-4b30-9cb9-48ab644dd865', + 'test_volume5', 15, + 'in-use', 'true') + + self.fake_volumes = [self.fake_vol1, + self.fake_vol2, + self.fake_vol3, + self.fake_vol4, + self.fake_vol5] + + def test_fake_snap(vol_id): + fake_snap = mock.MagicMock() + fake_snap.volume_id = vol_id + + return fake_snap + + self.fake_snap = [test_fake_snap( + '922d4762-0bc5-4b30-9cb9-48ab644dd865')] + + def test_fake_volume_type(type_name, extra_specs): + fake_type = mock.MagicMock() + fake_type.name = type_name + fake_type.extra_specs = extra_specs + + return fake_type + + self.fake_types = [test_fake_volume_type( + 'type1', {'volume_backend_name': 'pool1'}), + test_fake_volume_type( + 'type2', {'volume_backend_name': 'pool2'}) + ] + + osc = clients.OpenStackClients() + + p_cinder = mock.patch.object(osc, 'cinder') + p_cinder.start() + self.addCleanup(p_cinder.stop) + self.m_cinder = cinder_helper.CinderHelper(osc=osc) + + p_model = mock.patch.object( + strategies.StorageCapacityBalance, "compute_model", + new_callable=mock.PropertyMock) + self.m_model = p_model.start() + self.addCleanup(p_model.stop) + + p_audit_scope = mock.patch.object( + strategies.StorageCapacityBalance, "audit_scope", + new_callable=mock.PropertyMock + ) + self.m_audit_scope = p_audit_scope.start() + self.addCleanup(p_audit_scope.stop) + + self.m_audit_scope.return_value = mock.Mock() + self.m_cinder.get_storage_pool_list = mock.Mock( + return_value=self.fake_pools) + self.m_cinder.get_volume_list = mock.Mock( + return_value=self.fake_volumes) + self.m_cinder.get_volume_snapshots_list = mock.Mock( + return_value=self.fake_snap) + self.m_cinder.get_volume_type_list = mock.Mock( + return_value=self.fake_types) + self.strategy = strategies.StorageCapacityBalance( + config=mock.Mock(), osc=osc) + self.strategy._cinder = self.m_cinder + self.strategy.input_parameters = utils.Struct() + self.strategy.input_parameters.update( + {'volume_threshold': 80.0}) + self.strategy.volume_threshold = 80.0 + + def test_get_pools(self): + self.strategy.config.ex_pools = "local_vstorage" + pools = self.strategy.get_pools(self.m_cinder) + self.assertEqual(len(pools), 2) + + def test_get_volumes(self): + volumes = self.strategy.get_volumes(self.m_cinder) + self.assertEqual(len(volumes), 3) + + def test_group_pools(self): + self.strategy.config.ex_pools = "local_vstorage" + pools = self.strategy.get_pools(self.m_cinder) + over_pools, under_pools = self.strategy.group_pools(pools, 0.50) + self.assertEqual(len(under_pools), 1) + self.assertEqual(len(over_pools), 1) + + over_pools, under_pools = self.strategy.group_pools(pools, 0.85) + self.assertEqual(len(under_pools), 2) + self.assertEqual(len(over_pools), 0) + + over_pools, under_pools = self.strategy.group_pools(pools, 0.30) + self.assertEqual(len(under_pools), 0) + self.assertEqual(len(over_pools), 2) + + def test_get_volume_type_by_name(self): + vol_type = self.strategy.get_volume_type_by_name( + self.m_cinder, 'pool1') + self.assertEqual(len(vol_type), 1) + + vol_type = self.strategy.get_volume_type_by_name( + self.m_cinder, 'ks3200') + self.assertEqual(len(vol_type), 0) + + def test_check_pool_type(self): + pool_type = self.strategy.check_pool_type( + self.fake_vol3, self.fake_pool1) + self.assertIsNotNone(pool_type) + + pool_type = self.strategy.check_pool_type( + self.fake_vol3, self.fake_pool2) + self.assertIsNone(pool_type) + + def test_migrate_fit(self): + self.strategy.config.ex_pools = "local_vstorage" + pools = self.strategy.get_pools(self.m_cinder) + self.strategy.source_pools, self.strategy.dest_pools = ( + self.strategy.group_pools(pools, 0.60)) + target_pool = self.strategy.migrate_fit(self.fake_vol2, 0.60) + self.assertIsNotNone(target_pool) + + target_pool = self.strategy.migrate_fit(self.fake_vol3, 0.50) + self.assertIsNone(target_pool) + + target_pool = self.strategy.migrate_fit(self.fake_vol5, 0.60) + self.assertIsNone(target_pool) + + def test_retype_fit(self): + self.strategy.config.ex_pools = "local_vstorage" + pools = self.strategy.get_pools(self.m_cinder) + self.strategy.source_pools, self.strategy.dest_pools = ( + self.strategy.group_pools(pools, 0.50)) + target_pool = self.strategy.retype_fit(self.fake_vol1, 0.50) + self.assertIsNotNone(target_pool) + + target_pool = self.strategy.retype_fit(self.fake_vol2, 0.50) + self.assertIsNone(target_pool) + + target_pool = self.strategy.retype_fit(self.fake_vol3, 0.50) + self.assertIsNotNone(target_pool) + + target_pool = self.strategy.retype_fit(self.fake_vol5, 0.60) + self.assertIsNone(target_pool) + + def test_execute(self): + self.strategy.input_parameters.update( + {'volume_threshold': 45.0}) + self.strategy.config.ex_pools = "local_vstorage" + solution = self.strategy.execute() + self.assertEqual(len(solution.actions), 1) + + setattr(self.fake_pool1, 'free_capacity_gb', '60') + self.strategy.input_parameters.update( + {'volume_threshold': 50.0}) + solution = self.strategy.execute() + self.assertEqual(len(solution.actions), 2) + + setattr(self.fake_pool1, 'free_capacity_gb', '60') + self.strategy.input_parameters.update( + {'volume_threshold': 60.0}) + solution = self.strategy.execute() + self.assertEqual(len(solution.actions), 3)