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
This commit is contained in:
licanwei
2019-08-30 02:16:38 -07:00
parent 845a9187e3
commit f1fe4b6c62
7 changed files with 667 additions and 1 deletions

View File

@@ -0,0 +1,27 @@
<ModelRoot>
<ComputeNode uuid="89dce55c-8e74-4402-b23f-32aaf216c97f" status="enabled" state="up" id="0" hostname="hostname_0" vcpus="40" vcpu_reserved="0" vcpu_ratio="1" disk="250" disk_gb_reserved="0" disk_ratio="1" disk_capacity="250" memory="132" memory_mb_reserved="0" memory_ratio="1">
<Instance watcher_exclude="False" state="active" name="INSTANCE_0" uuid="6ae05517-a512-462d-9d83-90c313b5a8ff" vcpus="10" disk="20" disk_capacity="20" memory="20" metadata='{"optimize": true,"top": "floor", "nested": {"x": "y"}}' project_id="91FFFE30-78A0-4152-ACD2-8310FF274DC9"/>
<Instance watcher_exclude="False" state="active" name="INSTANCE_1" uuid="6ae05517-a512-462d-9d83-90c313b5a8f1" vcpus="15" disk="20" disk_capacity="20" memory="20" metadata='{"optimize": true,"top": "floor", "nested": {"x": "y"}}' project_id="26F03131-32CB-4697-9D61-9123F87A8147"/>
</ComputeNode>
<ComputeNode uuid="89dce55c-8e74-4402-b23f-32aaf216c971" status="enabled" state="up" id="1" hostname="hostname_1" vcpus="40" vcpu_reserved="0" vcpu_ratio="1" disk="250" disk_gb_reserved="0" disk_ratio="1" disk_capacity="250" memory="132" memory_mb_reserved="0" memory_ratio="1">
<Instance watcher_exclude="False" state="active" name="INSTANCE_2" uuid="6ae05517-a512-462d-9d83-90c313b5a8f2" vcpus="14" disk="20" disk_capacity="20" memory="20" metadata='{"optimize": true,"top": "floor", "nested": {"x": "y"}}' project_id="109F7909-0607-4712-B32C-5CC6D49D2F15"/>
</ComputeNode>
<ComputeNode uuid="89dce55c-8e74-4402-b23f-32aaf216c972" status="enabled" state="up" id="2" hostname="hostname_2" vcpus="40" vcpu_reserved="0" vcpu_ratio="1" disk="250" disk_gb_reserved="0" disk_ratio="1" disk_capacity="250" memory="132" memory_mb_reserved="0" memory_ratio="1">
<Instance watcher_exclude="False" state="active" name="INSTANCE_3" uuid="6ae05517-a512-462d-9d83-90c313b5a8f3" vcpus="10" disk="20" disk_capacity="20" memory="20" metadata='{"optimize": true,"top": "floor", "nested": {"x": "y"}}' project_id="91FFFE30-78A0-4152-ACD2-8310FF274DC9"/>
<Instance watcher_exclude="False" state="active" name="INSTANCE_4" uuid="6ae05517-a512-462d-9d83-90c313b5a8f4" vcpus="10" disk="20" disk_capacity="20" memory="20" metadata='{"optimize": true,"top": "floor", "nested": {"x": "y"}}' project_id="91FFFE30-78A0-4152-ACD2-8310FF274DC9"/>
<Instance watcher_exclude="False" state="active" name="INSTANCE_5" uuid="6ae05517-a512-462d-9d83-90c313b5a8f5" vcpus="10" disk="20" disk_capacity="20" memory="20" metadata='{"optimize": true,"top": "floor", "nested": {"x": "y"}}' project_id="91FFFE30-78A0-4152-ACD2-8310FF274DC9"/>
</ComputeNode>
<ComputeNode uuid="89dce55c-8e74-4402-b23f-32aaf216c973" status="enabled" state="up" id="3" hostname="hostname_3" vcpus="40" vcpu_reserved="0" vcpu_ratio="1" disk="250" disk_gb_reserved="0" disk_ratio="1" disk_capacity="250" memory="132" memory_mb_reserved="0" memory_ratio="1">
<Instance watcher_exclude="False" state="active" name="INSTANCE_6" uuid="6ae05517-a512-462d-9d83-90c313b5a8f6" vcpus="8" disk="20" disk_capacity="20" memory="20" metadata='{"optimize": true,"top": "floor", "nested": {"x": "y"}}' project_id="91FFFE30-78A0-4152-ACD2-8310FF274DC9"/>
</ComputeNode>
<ComputeNode uuid="89dce55c-8e74-4402-b23f-32aaf216c974" status="enabled" state="up" id="4" hostname="hostname_4" vcpus="40" vcpu_reserved="0" vcpu_ratio="1" disk="250" disk_gb_reserved="0" disk_ratio="1" disk_capacity="250" memory="132" memory_mb_reserved="0" memory_ratio="1">
<Instance watcher_exclude="False" state="active" name="INSTANCE_7" uuid="6ae05517-a512-462d-9d83-90c313b5a8f7" vcpus="10" disk="20" disk_capacity="20" memory="20" metadata='{"optimize": true,"top": "floor", "nested": {"x": "y"}}' project_id="91FFFE30-78A0-4152-ACD2-8310FF274DC9"/>
</ComputeNode>
<ComputeNode uuid="89dce55c-8e74-4402-b23f-32aaf216c975" status="enabled" state="up" id="5" hostname="hostname_5" vcpus="40" vcpu_reserved="0" vcpu_ratio="1" disk="250" disk_gb_reserved="0" disk_ratio="1" disk_capacity="250" memory="132" memory_mb_reserved="0" memory_ratio="1">
</ComputeNode>
<ComputeNode uuid="89dce55c-8e74-4402-b23f-32aaf216c976" status="disabled" state="up" id="6" hostname="hostname_6" vcpus="40" vcpu_reserved="0" vcpu_ratio="1" disk="250" disk_gb_reserved="0" disk_ratio="1" disk_capacity="250" memory="132" memory_mb_reserved="0" memory_ratio="1">
</ComputeNode>
<ComputeNode uuid="89dce55c-8e74-4402-b23f-32aaf216c977" status="disabled" state="up" id="4" hostname="hostname_7" vcpus="40" vcpu_reserved="0" vcpu_ratio="1" disk="250" disk_gb_reserved="0" disk_ratio="1" disk_capacity="250" memory="132" memory_mb_reserved="0" memory_ratio="1">
<Instance watcher_exclude="False" state="active" name="INSTANCE_8" uuid="6ae05517-a512-462d-9d83-90c313b5a8f8" vcpus="12" disk="20" disk_capacity="20" memory="20" metadata='{"optimize": true,"top": "floor", "nested": {"x": "y"}}' project_id="91FFFE30-78A0-4152-ACD2-8310FF274DC9"/>
</ComputeNode>
</ModelRoot>

View File

@@ -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):

View File

@@ -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)