Further rework lb_policy

This patch further reworks lb_policy to make it support cases
where no scaling_policy and/or deletion_policy is attached to
cluster.

Change-Id: I529f74f59dec9e4f1ed590b230bce4b075749dd4
Closes-Bug: #1535508
CLoses-Bug: #1536481
This commit is contained in:
yanyanhu 2016-01-21 03:39:36 -05:00
parent c4675804f7
commit 92887f5312
3 changed files with 178 additions and 16 deletions

View File

@ -161,7 +161,11 @@ class ClusterAction(base.Action):
# Wait for cluster creation to complete
res, reason = self._wait_for_dependents()
if res == self.RES_OK:
self.outputs['nodes_added'] = [n.id for n in nodes]
nodes_added = [n.id for n in nodes]
self.outputs['nodes_added'] = nodes_added
creation = self.data.get('creation', {})
creation['nodes'] = nodes_added
self.data['creation'] = creation
for node in nodes:
self.cluster.add_node(node)
@ -395,7 +399,11 @@ class ClusterAction(base.Action):
if result != self.RES_OK:
reason = new_reason
else:
self.outputs['nodes_added'] = [node.id for node in nodes]
nodes_added = [n.id for n in nodes]
self.outputs['nodes_added'] = nodes_added
creation = self.data.get('creation', {})
creation['nodes'] = nodes_added
self.data['creation'] = creation
for node in nodes:
self.cluster.add_node(node)
@ -585,7 +593,7 @@ class ClusterAction(base.Action):
return result, reason
count, desired, candidates = self._get_action_data(current_size)
elif 'deletion' in self.data:
grace_period = self.data['deletion']['grace_period']
grace_period = self.data['deletion'].get('grace_period', None)
if candidates is not None and len(candidates) == 0:
# Choose victims randomly
candidates = scaleutils.nodes_by_random(self.cluster.nodes, count)

View File

@ -17,6 +17,7 @@ from senlin.common import constraints
from senlin.common import consts
from senlin.common.i18n import _
from senlin.common.i18n import _LW
from senlin.common import scaleutils
from senlin.common import schema
from senlin.db import api as db_api
from senlin.drivers import base as driver_base
@ -271,6 +272,40 @@ class LoadBalancingPolicy(base.Policy):
return True, reason
def _get_delete_candidates(self, cluster_id, action):
deletion = action.data.get('deletion', None)
# No deletion field in action.data which means no scaling
# policy or deletion policy is attached.
if deletion is None:
candidates = None
if action.action == consts.CLUSTER_DEL_NODES:
# Get candidates from action.input
candidates = action.inputs.get('candidates', [])
count = len(candidates)
elif action.action == consts.CLUSTER_RESIZE:
# Calculate deletion count based on action input
db_cluster = db_api.cluster_get(action.context, cluster_id)
scaleutils.parse_resize_params(action, db_cluster)
if 'deletion' not in action.data:
return []
else:
count = action.data['deletion']['count']
else: # action.action == consts.CLUSTER_SCALE_IN
count = 1
else:
count = deletion.get('count', 0)
candidates = deletion.get('candidates', None)
# Still no candidates available, pick count of nodes randomly
if candidates is None:
nodes = db_api.node_get_all_by_cluster(action.context,
cluster_id=cluster_id)
if count > len(nodes):
count = len(nodes)
candidates = scaleutils.nodes_by_random(nodes, count)
return candidates
def pre_op(self, cluster_id, action):
"""Routine to be called before an action has been executed.
@ -282,9 +317,9 @@ class LoadBalancingPolicy(base.Policy):
:param action: The action object that triggered this operation.
:returns: Nothing.
"""
# TODO(YanyanHu): get deleted nodes list from action.data
nodes_removed = action.outputs.get('nodes_removed', [])
if len(nodes_removed) == 0:
candidates = self._get_delete_candidates(cluster_id, action)
if len(candidates) == 0:
return
db_cluster = db_api.cluster_get(action.context, cluster_id)
@ -297,7 +332,7 @@ class LoadBalancingPolicy(base.Policy):
pool_id = policy_data['pool']
# Remove nodes that will be deleted from lb pool
for node_id in nodes_removed:
for node_id in candidates:
node = node_mod.Node.load(action.context, node_id=node_id)
member_id = node.data.get('lb_member', None)
if member_id is None:
@ -312,6 +347,10 @@ class LoadBalancingPolicy(base.Policy):
'node(s) from lb pool.')
return
deletion = action.data.get('deletion', {})
deletion.update({'count': len(candidates), 'candidates': candidates})
action.data.update({'deletion': deletion})
return
def post_op(self, cluster_id, action):
@ -325,8 +364,11 @@ class LoadBalancingPolicy(base.Policy):
:param action: The action object that triggered this operation.
:returns: Nothing.
"""
# TODO(YanyanHu): get added nodes list from action.data
nodes_added = action.outputs.get('nodes_added', [])
# TODO(Yanyanhu): Need special handling for cross-az scenario
# which is supported by Neutron lbaas.
creation = action.data.get('creation', None)
nodes_added = creation.get('nodes', []) if creation else []
if len(nodes_added) == 0:
return

View File

@ -14,6 +14,7 @@ import mock
from oslo_context import context as oslo_context
from senlin.common import consts
from senlin.common import scaleutils
from senlin.db import api as db_api
from senlin.drivers import base as driver_base
from senlin.engine import cluster_policy
@ -203,6 +204,113 @@ class TestLoadBalancingPolicy(base.SenlinTestCase):
self.assertEqual((False, 'Failed in adding node into lb pool'), res)
self.lb_driver.lb_delete.assert_called_once_with(**lb_data)
def test_get_delete_candidates_no_deletion_data_del_nodes(self):
action = mock.Mock()
action.data = {}
action.action = consts.CLUSTER_DEL_NODES
action.inputs = {'candidates': ['node1', 'node2']}
policy = lb_policy.LoadBalancingPolicy('test-policy', self.spec)
res = policy._get_delete_candidates('CLUSTERID', action)
self.assertEqual(['node1', 'node2'], res)
@mock.patch.object(db_api, 'node_get_all_by_cluster')
@mock.patch.object(scaleutils, 'nodes_by_random')
def test_get_delete_candidates_no_deletion_data_scale_in(self,
m_nodes_random,
m_node_get):
action = mock.Mock()
self.context = utils.dummy_context()
action.data = {}
action.action = consts.CLUSTER_SCALE_IN
m_node_get.return_value = ['node1', 'node2', 'node3']
m_nodes_random.return_value = ['node1', 'node3']
policy = lb_policy.LoadBalancingPolicy('test-policy', self.spec)
res = policy._get_delete_candidates('CLUSTERID', action)
m_node_get.assert_called_once_with(action.context,
cluster_id='CLUSTERID')
m_nodes_random.assert_called_once_with(['node1', 'node2', 'node3'], 1)
self.assertEqual(['node1', 'node3'], res)
@mock.patch.object(db_api, 'node_get_all_by_cluster')
@mock.patch.object(db_api, 'cluster_get')
@mock.patch.object(scaleutils, 'parse_resize_params')
@mock.patch.object(scaleutils, 'nodes_by_random')
def test_get_delete_candidates_no_deletion_data_resize(self,
m_nodes_random,
m_parse_param,
m_cluster_get,
m_node_get):
def _parse_param(action, cluster):
action.data = {'deletion': {'count': 2}}
action = mock.Mock()
self.context = utils.dummy_context()
action.data = {}
action.action = consts.CLUSTER_RESIZE
m_parse_param.side_effect = _parse_param
m_node_get.return_value = ['node1', 'node2', 'node3']
m_cluster_get.return_value = 'cluster1'
m_nodes_random.return_value = ['node1', 'node3']
policy = lb_policy.LoadBalancingPolicy('test-policy', self.spec)
res = policy._get_delete_candidates('CLUSTERID', action)
m_cluster_get.assert_called_once_with(action.context,
'CLUSTERID')
m_parse_param.assert_called_once_with(action, 'cluster1')
m_node_get.assert_called_once_with(action.context,
cluster_id='CLUSTERID')
m_nodes_random.assert_called_once_with(['node1', 'node2', 'node3'], 2)
self.assertEqual(['node1', 'node3'], res)
@mock.patch.object(db_api, 'node_get_all_by_cluster')
@mock.patch.object(scaleutils, 'nodes_by_random')
def test_get_delete_candidates_deletion_no_candidates(self,
m_nodes_random,
m_node_get):
action = mock.Mock()
self.context = utils.dummy_context()
action.data = {'deletion': {'count': 1}}
m_node_get.return_value = ['node1', 'node2', 'node3']
m_nodes_random.return_value = ['node2']
policy = lb_policy.LoadBalancingPolicy('test-policy', self.spec)
res = policy._get_delete_candidates('CLUSTERID', action)
m_node_get.assert_called_once_with(action.context,
cluster_id='CLUSTERID')
m_nodes_random.assert_called_once_with(['node1', 'node2', 'node3'], 1)
self.assertEqual(['node2'], res)
@mock.patch.object(db_api, 'node_get_all_by_cluster')
@mock.patch.object(scaleutils, 'nodes_by_random')
def test_get_delete_candidates_deletion_count_over_size(self,
m_nodes_random,
m_node_get):
action = mock.Mock()
self.context = utils.dummy_context()
action.data = {'deletion': {'count': 4}}
m_node_get.return_value = ['node1', 'node2', 'node3']
policy = lb_policy.LoadBalancingPolicy('test-policy', self.spec)
policy._get_delete_candidates('CLUSTERID', action)
m_node_get.assert_called_once_with(action.context,
cluster_id='CLUSTERID')
m_nodes_random.assert_called_once_with(['node1', 'node2', 'node3'], 3)
def test_get_delete_candidates_deletion_with_candidates(self):
action = mock.Mock()
self.context = utils.dummy_context()
action.data = {'deletion': {'count': 1, 'candidates': ['node3']}}
policy = lb_policy.LoadBalancingPolicy('test-policy', self.spec)
res = policy._get_delete_candidates('CLUSTERID', action)
self.assertEqual(['node3'], res)
@mock.patch.object(lb_policy.LoadBalancingPolicy, '_build_conn_params')
@mock.patch.object(cluster_policy.ClusterPolicy, 'load')
@ -294,7 +402,7 @@ class TestLoadBalancingPolicyOperations(base.SenlinTestCase):
def test_post_op_no_nodes(self, m_extract, m_load, m_conn):
action = mock.Mock()
action.outputs = {}
action.data = {}
policy = lb_policy.LoadBalancingPolicy('test-policy', self.spec)
@ -313,7 +421,7 @@ class TestLoadBalancingPolicyOperations(base.SenlinTestCase):
node1.data = {}
node2.data = {}
action = mock.Mock()
action.outputs = {'nodes_added': ['NODE1_ID', 'NODE2_ID']}
action.data = {'creation': {'nodes': ['NODE1_ID', 'NODE2_ID']}}
action.context = 'action_context'
action.action = consts.CLUSTER_RESIZE
cp = mock.Mock()
@ -367,7 +475,7 @@ class TestLoadBalancingPolicyOperations(base.SenlinTestCase):
node1.data = {'lb_member': 'MEMBER1_ID'}
node2.data = {}
action = mock.Mock()
action.outputs = {'nodes_added': ['NODE1_ID', 'NODE2_ID']}
action.data = {'creation': {'nodes': ['NODE1_ID', 'NODE2_ID']}}
action.context = 'action_context'
action.action = consts.CLUSTER_RESIZE
policy_data = {
@ -395,7 +503,7 @@ class TestLoadBalancingPolicyOperations(base.SenlinTestCase):
node1.data = {}
action = mock.Mock()
action.data = {}
action.outputs = {'nodes_added': ['NODE1_ID']}
action.data = {'creation': {'nodes': ['NODE1_ID']}}
action.context = 'action_context'
action.action = consts.CLUSTER_RESIZE
self.lb_driver.member_add.return_value = None
@ -430,7 +538,7 @@ class TestLoadBalancingPolicyOperations(base.SenlinTestCase):
node2.data = {'lb_member': 'MEMBER2_ID'}
action = mock.Mock()
action.data = {}
action.outputs = {'nodes_removed': ['NODE1_ID', 'NODE2_ID']}
action.data = {'deletion': {'candidates': ['NODE1_ID', 'NODE2_ID']}}
action.context = 'action_context'
action.action = consts.CLUSTER_DEL_NODES
cp = mock.Mock()
@ -472,6 +580,10 @@ class TestLoadBalancingPolicyOperations(base.SenlinTestCase):
]
self.lb_driver.member_remove.assert_has_calls(calls_member_del)
expected_data = {'deletion': {'candidates': ['NODE1_ID', 'NODE2_ID'],
'count': 2}}
self.assertEqual(expected_data, action.data)
@mock.patch.object(node_mod.Node, 'load')
@mock.patch.object(db_api, 'cluster_get')
def test_pre_op_del_nodes_not_in_pool(self, m_cluster_get, m_node_load,
@ -482,7 +594,7 @@ class TestLoadBalancingPolicyOperations(base.SenlinTestCase):
node1.data = {}
node2.data = {'lb_member': 'MEMBER2_ID'}
action = mock.Mock()
action.outputs = {'nodes_removed': ['NODE1_ID', 'NODE2_ID']}
action.data = {'deletion': {'candidates': ['NODE1_ID', 'NODE2_ID']}}
action.context = 'action_context'
action.action = consts.CLUSTER_RESIZE
self.lb_driver.member_remove.return_value = True
@ -509,7 +621,7 @@ class TestLoadBalancingPolicyOperations(base.SenlinTestCase):
node1.data = {'lb_member': 'MEMBER1_ID'}
action = mock.Mock()
action.data = {}
action.outputs = {'nodes_removed': ['NODE1_ID']}
action.data = {'deletion': {'candidates': ['NODE1_ID']}}
action.context = 'action_context'
action.action = consts.CLUSTER_RESIZE
self.lb_driver.member_remove.return_value = False