From f1fe4b6c623be93ffc6f05ffe77ad69bdb626a0a Mon Sep 17 00:00:00 2001 From: licanwei Date: Fri, 30 Aug 2019 02:16:38 -0700 Subject: [PATCH] node resource consolidation This strategy is used to centralize VMs to as few nodes as possible by VM migration. User can set a input parameter to decide how to select the destination node. Implements: blueprint node-resource-consolidation Closes-Bug: #1843016 Change-Id: I104c864d532c2092f5dc6f0c8f756ebeae12f09e --- ...source-consolidation-73bc0c0abfeb0b03.yaml | 7 + setup.cfg | 1 + .../strategy/strategies/__init__.py | 6 +- .../strategies/node_resource_consolidation.py | 290 +++++++++++++++ .../model/data/scenario_10.xml | 27 ++ .../model/faker_cluster_state.py | 3 + .../test_node_resource_consolidation.py | 334 ++++++++++++++++++ 7 files changed, 667 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/node-resource-consolidation-73bc0c0abfeb0b03.yaml create mode 100644 watcher/decision_engine/strategy/strategies/node_resource_consolidation.py create mode 100644 watcher/tests/decision_engine/model/data/scenario_10.xml create mode 100644 watcher/tests/decision_engine/strategy/strategies/test_node_resource_consolidation.py diff --git a/releasenotes/notes/node-resource-consolidation-73bc0c0abfeb0b03.yaml b/releasenotes/notes/node-resource-consolidation-73bc0c0abfeb0b03.yaml new file mode 100644 index 000000000..b693e363a --- /dev/null +++ b/releasenotes/notes/node-resource-consolidation-73bc0c0abfeb0b03.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + Added strategy "node resource consolidation". This + strategy is used to centralize VMs to as few nodes + as possible by VM migration. User can set an input + parameter to decide how to select the destination node. diff --git a/setup.cfg b/setup.cfg index b744faabf..917dd0e4c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -87,6 +87,7 @@ watcher_strategies = storage_capacity_balance = watcher.decision_engine.strategy.strategies.storage_capacity_balance:StorageCapacityBalance zone_migration = watcher.decision_engine.strategy.strategies.zone_migration:ZoneMigration host_maintenance = watcher.decision_engine.strategy.strategies.host_maintenance:HostMaintenance + node_resource_consolidation = watcher.decision_engine.strategy.strategies.node_resource_consolidation:NodeResourceConsolidation watcher_actions = migrate = watcher.applier.actions.migration:Migrate diff --git a/watcher/decision_engine/strategy/strategies/__init__.py b/watcher/decision_engine/strategy/strategies/__init__.py index afb712cbd..c3287dd9c 100644 --- a/watcher/decision_engine/strategy/strategies/__init__.py +++ b/watcher/decision_engine/strategy/strategies/__init__.py @@ -20,6 +20,8 @@ from watcher.decision_engine.strategy.strategies import basic_consolidation from watcher.decision_engine.strategy.strategies import dummy_strategy from watcher.decision_engine.strategy.strategies import dummy_with_scorer from watcher.decision_engine.strategy.strategies import host_maintenance +from watcher.decision_engine.strategy.strategies import \ + node_resource_consolidation 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 @@ -45,6 +47,8 @@ VMWorkloadConsolidation = vm_workload_consolidation.VMWorkloadConsolidation WorkloadBalance = workload_balance.WorkloadBalance WorkloadStabilization = workload_stabilization.WorkloadStabilization UniformAirflow = uniform_airflow.UniformAirflow +NodeResourceConsolidation = ( + node_resource_consolidation.NodeResourceConsolidation) NoisyNeighbor = noisy_neighbor.NoisyNeighbor ZoneMigration = zone_migration.ZoneMigration HostMaintenance = host_maintenance.HostMaintenance @@ -54,4 +58,4 @@ __all__ = ("Actuator", "BaseStrategy", "BasicConsolidation", "VMWorkloadConsolidation", "WorkloadBalance", "WorkloadStabilization", "UniformAirflow", "NoisyNeighbor", "SavingEnergy", "StorageCapacityBalance", "ZoneMigration", - "HostMaintenance") + "HostMaintenance", "NodeResourceConsolidation") diff --git a/watcher/decision_engine/strategy/strategies/node_resource_consolidation.py b/watcher/decision_engine/strategy/strategies/node_resource_consolidation.py new file mode 100644 index 000000000..38a46a6d5 --- /dev/null +++ b/watcher/decision_engine/strategy/strategies/node_resource_consolidation.py @@ -0,0 +1,290 @@ +# -*- encoding: utf-8 -*- +# Copyright (c) 2019 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. +# + +from oslo_log import log + +from watcher._i18n import _ +from watcher.common import exception +from watcher.decision_engine.model import element +from watcher.decision_engine.strategy.strategies import base +from watcher import objects + +LOG = log.getLogger(__name__) + + +class NodeResourceConsolidation(base.ServerConsolidationBaseStrategy): + """consolidating resources on nodes using server migration + + *Description* + + This strategy checks the resource usages of compute nodes, if the used + resources are less than total, it will try to migrate server to + consolidate the use of resource. + + *Requirements* + + * You must have at least 2 compute nodes to run + this strategy. + * Hardware: compute nodes should use the same physical CPUs/RAMs + + *Limitations* + + * This is a proof of concept that is not meant to be used in production + * It assume that live migrations are possible + + *Spec URL* + + http://specs.openstack.org/openstack/watcher-specs/specs/train/implemented/node-resource-consolidation.html + """ + + CHANGE_NOVA_SERVICE_STATE = "change_nova_service_state" + REASON_FOR_DISABLE = 'Watcher node resource consolidation strategy' + + def __init__(self, config, osc=None): + """node resource consolidation + + :param config: A mapping containing the configuration of this strategy + :type config: :py:class:`~.Struct` instance + :param osc: :py:class:`~.OpenStackClients` instance + """ + super(NodeResourceConsolidation, self).__init__(config, osc) + self.host_choice = 'auto' + self.audit = None + self.compute_nodes_count = 0 + self.number_of_released_nodes = 0 + self.number_of_migrations = 0 + + @classmethod + def get_name(cls): + return "node_resource_consolidation" + + @classmethod + def get_display_name(cls): + return _("Node Resource Consolidation strategy") + + @classmethod + def get_translatable_display_name(cls): + return "Node Resource Consolidation strategy" + + @classmethod + def get_schema(cls): + # Mandatory default setting for each element + return { + "properties": { + "host_choice": { + "description": "the way to select the server migration " + "destination node, The value auto " + "means that Nova schedular selects " + "the destination node, and specify " + "means the strategy specifies the " + "destination.", + "type": "string", + "default": 'auto' + }, + }, + } + + def check_resources(self, servers, destination): + # check whether a node able to accommodate a VM + dest_flag = False + if not destination: + return dest_flag + free_res = self.compute_model.get_node_free_resources(destination) + for server in servers: + # just vcpu and memory, do not consider disk + if free_res['vcpu'] >= server.vcpus and ( + free_res['memory'] >= server.memory): + free_res['vcpu'] -= server.vcpus + free_res['memory'] -= server.memory + dest_flag = True + servers.remove(server) + + return dest_flag + + def select_destination(self, server, source, destinations): + dest_node = None + if not destinations: + return dest_node + sorted_nodes = sorted( + destinations, + key=lambda x: self.compute_model.get_node_free_resources( + x)['vcpu']) + for dest in sorted_nodes: + if self.check_resources([server], dest): + if self.compute_model.migrate_instance(server, source, dest): + dest_node = dest + break + + return dest_node + + def add_migrate_actions(self, sources, destinations): + if not sources or not destinations: + return + for node in sources: + servers = self.compute_model.get_node_instances(node) + sorted_servers = sorted( + servers, + key=lambda x: x.vcpus, + reverse=True) + for server in sorted_servers: + parameters = {'migration_type': 'live', + 'source_node': node.hostname, + 'resource_name': server.name} + action_flag = False + if self.host_choice != 'auto': + # specify destination host + dest = self.select_destination(server, node, destinations) + if dest: + parameters['destination_node'] = dest.hostname + action_flag = True + else: + action_flag = True + if action_flag: + self.number_of_migrations += 1 + self.solution.add_action( + action_type=self.MIGRATION, + resource_id=server.uuid, + input_parameters=parameters) + + def add_change_node_state_actions(self, nodes, status): + if status not in (element.ServiceState.DISABLED.value, + element.ServiceState.ENABLED.value): + raise exception.IllegalArgumentException( + message=_("The node status is not defined")) + changed_nodes = [] + for node in nodes: + if node.status != status: + parameters = {'state': status, + 'resource_name': node.hostname} + if status == element.ServiceState.DISABLED.value: + parameters['disabled_reason'] = self.REASON_FOR_DISABLE + self.solution.add_action( + action_type=self.CHANGE_NOVA_SERVICE_STATE, + resource_id=node.uuid, + input_parameters=parameters) + node.status = status + changed_nodes.append(node) + + return changed_nodes + + def get_nodes_migrate_failed(self): + # check if migration action ever failed + # just for continuous audit + nodes_failed = [] + if self.audit is None or ( + self.audit.audit_type == + objects.audit.AuditType.ONESHOT.value): + return nodes_failed + filters = {'audit_uuid': self.audit.uuid} + actions = objects.action.Action.list( + self.ctx, + filters=filters) + for action in actions: + if action.state == objects.action.State.FAILED and ( + action.action_type == self.MIGRATION): + server_uuid = action.input_parameters.get('resource_id') + node = self.compute_model.get_node_by_instance_uuid( + server_uuid) + if node not in nodes_failed: + nodes_failed.append(node) + + return nodes_failed + + def group_nodes(self, nodes): + free_nodes = [] + source_nodes = [] + dest_nodes = [] + nodes_failed = self.get_nodes_migrate_failed() + LOG.info("nodes: %s migration failed", nodes_failed) + sorted_nodes = sorted( + nodes, + key=lambda x: self.compute_model.get_node_used_resources( + x)['vcpu']) + for node in sorted_nodes: + if node in dest_nodes: + break + # If ever migration failed, do not migrate again + if node in nodes_failed: + # maybe can as the destination node + if node.status == element.ServiceState.ENABLED.value: + dest_nodes.append(node) + continue + used_resource = self.compute_model.get_node_used_resources(node) + if used_resource['vcpu'] > 0: + servers = self.compute_model.get_node_instances(node) + for dest in reversed(sorted_nodes): + # skip if compute node is disabled + if dest.status == element.ServiceState.DISABLED.value: + LOG.info("node %s is down", dest.hostname) + continue + if dest in dest_nodes: + continue + if node == dest: + # The last on as destination node + dest_nodes.append(dest) + break + if self.check_resources(servers, dest): + dest_nodes.append(dest) + if node not in source_nodes: + source_nodes.append(node) + if not servers: + break + else: + free_nodes.append(node) + + return free_nodes, source_nodes, dest_nodes + + def pre_execute(self): + self._pre_execute() + self.host_choice = self.input_parameters.host_choice + + def do_execute(self, audit=None): + """Strategy execution phase + + Executing strategy and creating solution. + """ + self.audit = audit + nodes = list(self.compute_model.get_all_compute_nodes().values()) + free_nodes, source_nodes, dest_nodes = self.group_nodes(nodes) + self.compute_nodes_count = len(nodes) + self.number_of_released_nodes = len(source_nodes) + LOG.info("Free nodes: %s", free_nodes) + LOG.info("Source nodes: %s", source_nodes) + LOG.info("Destination nodes: %s", dest_nodes) + if not source_nodes: + LOG.info("No compute node needs to be consolidated") + return + nodes_disabled = [] + if self.host_choice == 'auto': + # disable compute node to avoid to be select by Nova scheduler + nodes_disabled = self.add_change_node_state_actions( + free_nodes+source_nodes, element.ServiceState.DISABLED.value) + self.add_migrate_actions(source_nodes, dest_nodes) + if nodes_disabled: + # restore disabled compute node after migration + self.add_change_node_state_actions( + nodes_disabled, element.ServiceState.ENABLED.value) + + def post_execute(self): + """Post-execution phase + + """ + self.solution.set_efficacy_indicators( + compute_nodes_count=self.compute_nodes_count, + released_compute_nodes_count=self.number_of_released_nodes, + instance_migrations_count=self.number_of_migrations, + ) diff --git a/watcher/tests/decision_engine/model/data/scenario_10.xml b/watcher/tests/decision_engine/model/data/scenario_10.xml new file mode 100644 index 000000000..bcc4bb9bc --- /dev/null +++ b/watcher/tests/decision_engine/model/data/scenario_10.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/watcher/tests/decision_engine/model/faker_cluster_state.py b/watcher/tests/decision_engine/model/faker_cluster_state.py index 51ca228c5..ac1602645 100644 --- a/watcher/tests/decision_engine/model/faker_cluster_state.py +++ b/watcher/tests/decision_engine/model/faker_cluster_state.py @@ -171,6 +171,9 @@ class FakerModelCollector(base.BaseClusterDataModelCollector): return self.load_model( 'scenario_9_with_3_active_plus_1_disabled_nodes.xml') + def generate_scenario_10(self): + return self.load_model('scenario_10.xml') + class FakerStorageModelCollector(base.BaseClusterDataModelCollector): diff --git a/watcher/tests/decision_engine/strategy/strategies/test_node_resource_consolidation.py b/watcher/tests/decision_engine/strategy/strategies/test_node_resource_consolidation.py new file mode 100644 index 000000000..cd9018970 --- /dev/null +++ b/watcher/tests/decision_engine/strategy/strategies/test_node_resource_consolidation.py @@ -0,0 +1,334 @@ +# -*- encoding: utf-8 -*- +# Copyright (c) 2019 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. +# + +import mock + +from watcher.common import exception +from watcher.decision_engine.model import element +from watcher.decision_engine.strategy import strategies +from watcher import objects +from watcher.tests.decision_engine.strategy.strategies.test_base \ + import TestBaseStrategy +from watcher.tests.objects import utils as obj_utils + + +class TestNodeResourceConsolidation(TestBaseStrategy): + + def setUp(self): + super(TestNodeResourceConsolidation, self).setUp() + self.strategy = strategies.NodeResourceConsolidation( + config=mock.Mock()) + self.model = self.fake_c_cluster.generate_scenario_10() + self.m_c_model.return_value = self.model + + def test_check_resources(self): + instance = [self.model.get_instance_by_uuid( + "6ae05517-a512-462d-9d83-90c313b5a8ff")] + dest = self.model.get_node_by_uuid( + "89dce55c-8e74-4402-b23f-32aaf216c972") + # test destination is null + result = self.strategy.check_resources(instance, []) + self.assertFalse(result) + + result = self.strategy.check_resources(instance, dest) + self.assertTrue(result) + self.assertEqual([], instance) + + def test_select_destination(self): + instance0 = self.model.get_instance_by_uuid( + "6ae05517-a512-462d-9d83-90c313b5a8ff") + source = self.model.get_node_by_instance_uuid( + "6ae05517-a512-462d-9d83-90c313b5a8ff") + expected = self.model.get_node_by_uuid( + "89dce55c-8e74-4402-b23f-32aaf216c972") + # test destination is null + result = self.strategy.select_destination(instance0, source, []) + self.assertIsNone(result) + + nodes = list(self.model.get_all_compute_nodes().values()) + nodes.remove(source) + result = self.strategy.select_destination(instance0, source, nodes) + self.assertEqual(expected, result) + + def test_add_migrate_actions_with_null(self): + self.strategy.add_migrate_actions([], []) + self.assertEqual([], self.strategy.solution.actions) + self.strategy.add_migrate_actions(None, None) + self.assertEqual([], self.strategy.solution.actions) + + def test_add_migrate_actions_with_auto(self): + self.strategy.host_choice = 'auto' + source = self.model.get_node_by_instance_uuid( + "6ae05517-a512-462d-9d83-90c313b5a8ff") + nodes = list(self.model.get_all_compute_nodes().values()) + nodes.remove(source) + self.strategy.add_migrate_actions([source], nodes) + expected = [{'action_type': 'migrate', + 'input_parameters': { + 'migration_type': 'live', + 'resource_id': '6ae05517-a512-462d-9d83-90c313b5a8f1', + 'resource_name': 'INSTANCE_1', + 'source_node': 'hostname_0'}}, + {'action_type': 'migrate', + 'input_parameters': { + 'migration_type': 'live', + 'resource_id': '6ae05517-a512-462d-9d83-90c313b5a8ff', + 'resource_name': 'INSTANCE_0', + 'source_node': 'hostname_0'}}] + self.assertEqual(expected, self.strategy.solution.actions) + + def test_add_migrate_actions_with_specify(self): + self.strategy.host_choice = 'specify' + source = self.model.get_node_by_instance_uuid( + "6ae05517-a512-462d-9d83-90c313b5a8ff") + nodes = list(self.model.get_all_compute_nodes().values()) + nodes.remove(source) + self.strategy.add_migrate_actions([source], nodes) + expected = [{'action_type': 'migrate', + 'input_parameters': { + 'destination_node': 'hostname_1', + 'migration_type': 'live', + 'resource_id': '6ae05517-a512-462d-9d83-90c313b5a8f1', + 'resource_name': 'INSTANCE_1', + 'source_node': 'hostname_0'}}, + {'action_type': 'migrate', + 'input_parameters': { + 'destination_node': 'hostname_2', + 'migration_type': 'live', + 'resource_id': '6ae05517-a512-462d-9d83-90c313b5a8ff', + 'resource_name': 'INSTANCE_0', + 'source_node': 'hostname_0'}}] + self.assertEqual(expected, self.strategy.solution.actions) + + def test_add_migrate_actions_with_no_action(self): + self.strategy.host_choice = 'specify' + source = self.model.get_node_by_uuid( + "89dce55c-8e74-4402-b23f-32aaf216c971") + dest = self.model.get_node_by_uuid( + "89dce55c-8e74-4402-b23f-32aaf216c972") + self.strategy.add_migrate_actions([source], [dest]) + self.assertEqual([], self.strategy.solution.actions) + + def test_add_change_node_state_actions_with_exeception(self): + self.assertRaises(exception.IllegalArgumentException, + self.strategy.add_change_node_state_actions, + [], 'down') + + def test_add_change_node_state_actions(self): + node1 = self.model.get_node_by_uuid( + "89dce55c-8e74-4402-b23f-32aaf216c972") + node2 = self.model.get_node_by_uuid( + "89dce55c-8e74-4402-b23f-32aaf216c97f") + # disable two nodes + status = element.ServiceState.DISABLED.value + result = self.strategy.add_change_node_state_actions( + [node1, node2], status) + self.assertEqual([node1, node2], result) + expected = [{ + 'action_type': 'change_nova_service_state', + 'input_parameters': { + 'disabled_reason': 'Watcher node resource ' + 'consolidation strategy', + 'resource_id': '89dce55c-8e74-4402-b23f-32aaf216c972', + 'resource_name': 'hostname_2', + 'state': 'disabled'}}, + { + 'action_type': 'change_nova_service_state', + 'input_parameters': { + 'disabled_reason': 'Watcher node resource consolidation ' + 'strategy', + 'resource_id': '89dce55c-8e74-4402-b23f-32aaf216c97f', + 'resource_name': 'hostname_0', + 'state': 'disabled'}}] + self.assertEqual(expected, self.strategy.solution.actions) + + def test_add_change_node_state_actions_one_disabled(self): + node1 = self.model.get_node_by_uuid( + "89dce55c-8e74-4402-b23f-32aaf216c972") + node2 = self.model.get_node_by_uuid( + "89dce55c-8e74-4402-b23f-32aaf216c97f") + # disable two nodes + status = element.ServiceState.DISABLED.value + + # one enable, one disable + node1.status = element.ServiceState.DISABLED.value + result = self.strategy.add_change_node_state_actions( + [node1, node2], status) + self.assertEqual([node2], result) + expected = [{ + 'action_type': 'change_nova_service_state', + 'input_parameters': { + 'disabled_reason': 'Watcher node resource consolidation ' + 'strategy', + 'resource_id': '89dce55c-8e74-4402-b23f-32aaf216c97f', + 'resource_name': 'hostname_0', + 'state': 'disabled'}}] + self.assertEqual(expected, self.strategy.solution.actions) + + def test_get_nodes_migrate_failed_return_null(self): + self.strategy.audit = None + result = self.strategy.get_nodes_migrate_failed() + self.assertEqual([], result) + self.strategy.audit = mock.Mock( + audit_type=objects.audit.AuditType.ONESHOT.value) + result = self.strategy.get_nodes_migrate_failed() + self.assertEqual([], result) + + @mock.patch.object(objects.action.Action, 'list') + def test_get_nodes_migrate_failed(self, mock_list): + self.strategy.audit = mock.Mock( + audit_type=objects.audit.AuditType.CONTINUOUS.value) + fake_action = obj_utils.get_test_action( + self.context, + state=objects.action.State.FAILED, + action_type='migrate', + input_parameters={ + 'resource_id': '6ae05517-a512-462d-9d83-90c313b5a8f1'}) + mock_list.return_value = [fake_action] + result = self.strategy.get_nodes_migrate_failed() + expected = self.model.get_node_by_uuid( + '89dce55c-8e74-4402-b23f-32aaf216c97f') + self.assertEqual([expected], result) + + def test_group_nodes_with_ONESHOT(self): + self.strategy.audit = mock.Mock( + audit_type=objects.audit.AuditType.ONESHOT.value) + nodes = list(self.model.get_all_compute_nodes().values()) + result = self.strategy.group_nodes(nodes) + node0 = self.model.get_node_by_name('hostname_0') + node1 = self.model.get_node_by_name('hostname_1') + node2 = self.model.get_node_by_name('hostname_2') + node3 = self.model.get_node_by_name('hostname_3') + node4 = self.model.get_node_by_name('hostname_4') + node5 = self.model.get_node_by_name('hostname_5') + node6 = self.model.get_node_by_name('hostname_6') + node7 = self.model.get_node_by_name('hostname_7') + source_nodes = [node3, node4, node7] + dest_nodes = [node2, node0, node1] + self.assertIn(node5, result[0]) + self.assertIn(node6, result[0]) + self.assertEqual(source_nodes, result[1]) + self.assertEqual(dest_nodes, result[2]) + + @mock.patch.object(objects.action.Action, 'list') + def test_group_nodes_with_CONTINUOUS(self, mock_list): + self.strategy.audit = mock.Mock( + audit_type=objects.audit.AuditType.CONTINUOUS.value) + fake_action = obj_utils.get_test_action( + self.context, + state=objects.action.State.FAILED, + action_type='migrate', + input_parameters={ + 'resource_id': '6ae05517-a512-462d-9d83-90c313b5a8f6'}) + mock_list.return_value = [fake_action] + nodes = list(self.model.get_all_compute_nodes().values()) + result = self.strategy.group_nodes(nodes) + node0 = self.model.get_node_by_name('hostname_0') + node1 = self.model.get_node_by_name('hostname_1') + node2 = self.model.get_node_by_name('hostname_2') + node3 = self.model.get_node_by_name('hostname_3') + node4 = self.model.get_node_by_name('hostname_4') + node5 = self.model.get_node_by_name('hostname_5') + node6 = self.model.get_node_by_name('hostname_6') + node7 = self.model.get_node_by_name('hostname_7') + source_nodes = [node4, node7] + dest_nodes = [node3, node2, node0, node1] + self.assertIn(node5, result[0]) + self.assertIn(node6, result[0]) + self.assertEqual(source_nodes, result[1]) + self.assertEqual(dest_nodes, result[2]) + + @mock.patch.object(objects.action.Action, 'list') + def test_execute_with_auto(self, mock_list): + fake_action = obj_utils.get_test_action( + self.context, + state=objects.action.State.FAILED, + action_type='migrate', + input_parameters={ + 'resource_id': '6ae05517-a512-462d-9d83-90c313b5a8f6'}) + mock_list.return_value = [fake_action] + mock_audit = mock.Mock( + audit_type=objects.audit.AuditType.CONTINUOUS.value) + self.strategy.host_choice = 'auto' + self.strategy.do_execute(mock_audit) + expected = [ + {'action_type': 'change_nova_service_state', + 'input_parameters': { + 'disabled_reason': 'Watcher node resource consolidation ' + 'strategy', + 'resource_id': '89dce55c-8e74-4402-b23f-32aaf216c975', + 'resource_name': 'hostname_5', + 'state': 'disabled'}}, + {'action_type': 'change_nova_service_state', + 'input_parameters': { + 'disabled_reason': 'Watcher node resource consolidation ' + 'strategy', + 'resource_id': '89dce55c-8e74-4402-b23f-32aaf216c974', + 'resource_name': 'hostname_4', + 'state': 'disabled'}}, + {'action_type': 'migrate', + 'input_parameters': { + 'migration_type': 'live', + 'resource_id': '6ae05517-a512-462d-9d83-90c313b5a8f7', + 'resource_name': 'INSTANCE_7', + 'source_node': 'hostname_4'}}, + {'action_type': 'migrate', + 'input_parameters': { + 'migration_type': 'live', + 'resource_id': '6ae05517-a512-462d-9d83-90c313b5a8f8', + 'resource_name': 'INSTANCE_8', + 'source_node': 'hostname_7'}}, + {'action_type': 'change_nova_service_state', + 'input_parameters': { + 'resource_id': '89dce55c-8e74-4402-b23f-32aaf216c975', + 'resource_name': 'hostname_5', + 'state': 'enabled'}}, + {'action_type': 'change_nova_service_state', + 'input_parameters': { + 'resource_id': '89dce55c-8e74-4402-b23f-32aaf216c974', + 'resource_name': 'hostname_4', + 'state': 'enabled'}}] + self.assertEqual(expected, self.strategy.solution.actions) + + def test_execute_with_specify(self): + mock_audit = mock.Mock( + audit_type=objects.audit.AuditType.ONESHOT.value) + self.strategy.host_choice = 'specify' + self.strategy.do_execute(mock_audit) + expected = [ + {'action_type': 'migrate', + 'input_parameters': { + 'destination_node': 'hostname_2', + 'migration_type': 'live', + 'resource_id': '6ae05517-a512-462d-9d83-90c313b5a8f6', + 'resource_name': 'INSTANCE_6', + 'source_node': 'hostname_3'}}, + {'action_type': 'migrate', + 'input_parameters': { + 'destination_node': 'hostname_0', + 'migration_type': 'live', + 'resource_id': '6ae05517-a512-462d-9d83-90c313b5a8f7', + 'resource_name': 'INSTANCE_7', + 'source_node': 'hostname_4'}}, + {'action_type': 'migrate', + 'input_parameters': { + 'destination_node': 'hostname_1', + 'migration_type': 'live', + 'resource_id': '6ae05517-a512-462d-9d83-90c313b5a8f8', + 'resource_name': 'INSTANCE_8', + 'source_node': 'hostname_7'}}] + self.assertEqual(expected, self.strategy.solution.actions)