Merge "Eliminate nested stack loading in InstanceGroup/ASG scaling"

This commit is contained in:
Zuul 2018-04-02 04:35:47 +00:00 committed by Gerrit Code Review
commit 8643975783
8 changed files with 117 additions and 97 deletions

View File

@ -129,19 +129,12 @@ def get_members(group, include_failed=False):
key=lambda r: (r.status != r.FAILED, r.created_time, r.name)) key=lambda r: (r.status != r.FAILED, r.created_time, r.name))
def get_member_refids(group, exclude=None): def get_member_refids(group):
"""Get a list of member resources managed by the specified group. """Get a list of member resources managed by the specified group.
The list of resources is sorted first by created_time then by name. The list of resources is sorted first by created_time then by name.
""" """
members = get_members(group) return [r.FnGetRefId() for r in get_members(group)]
if len(members) == 0:
return []
if exclude is None:
exclude = []
return [r.FnGetRefId() for r in members
if r.FnGetRefId() not in exclude]
def get_member_names(group): def get_member_names(group):

View File

@ -75,6 +75,8 @@ class AutoScalingResourceGroup(aws_asg.AutoScalingGroup):
'outputs', 'outputs_list', 'current_size', 'refs', 'refs_map', 'outputs', 'outputs_list', 'current_size', 'refs', 'refs_map',
) )
(OUTPUT_MEMBER_IDS,) = (REFS_MAP,)
properties_schema = { properties_schema = {
RESOURCE: properties.Schema( RESOURCE: properties.Schema(
properties.Schema.MAP, properties.Schema.MAP,
@ -280,26 +282,23 @@ class AutoScalingResourceGroup(aws_asg.AutoScalingGroup):
# template generation time. # template generation time.
if key == self.OUTPUTS_LIST: if key == self.OUTPUTS_LIST:
key = self.OUTPUTS key = self.OUTPUTS
if key == self.REFS:
key = self.REFS_MAP
if key.startswith("resource."): if key.startswith("resource."):
keycomponents = key.split('.', 2) keycomponents = key.split('.', 2)
path = keycomponents[2:] + path path = keycomponents[2:] + path
if path: if path:
key = self.OUTPUTS key = self.OUTPUTS
else:
key = self.REFS_MAP
output_name = self._attribute_output_name(key, *path) output_name = self._attribute_output_name(key, *path)
value = None
if key == self.REFS_MAP: if key == self.OUTPUTS and path:
value = {r: get_res_fn(r) for r in resource_names}
elif key == self.OUTPUTS and path:
value = {r: get_attr_fn([r] + path) for r in resource_names} value = {r: get_attr_fn([r] + path) for r in resource_names}
if value is not None:
yield output.OutputDefinition(output_name, value) yield output.OutputDefinition(output_name, value)
# Always define an output for the member IDs, which also doubles as the
# output used by the REFS and REFS_MAP attributes.
member_ids_value = {r: get_res_fn(r) for r in resource_names}
yield output.OutputDefinition(self.OUTPUT_MEMBER_IDS,
member_ids_value)
def resource_mapping(): def resource_mapping():
return { return {

View File

@ -75,6 +75,8 @@ class InstanceGroup(stack_resource.StackResource):
'InstanceList', 'InstanceList',
) )
(OUTPUT_MEMBER_IDS,) = ('references',)
properties_schema = { properties_schema = {
AVAILABILITY_ZONES: properties.Schema( AVAILABILITY_ZONES: properties.Schema(
properties.Schema.LIST, properties.Schema.LIST,
@ -312,13 +314,15 @@ class InstanceGroup(stack_resource.StackResource):
Replace the instances in the group using updated launch configuration. Replace the instances in the group using updated launch configuration.
""" """
def changing_instances(tmpl): def changing_instances(old_tmpl, new_tmpl):
instances = grouputils.get_members(self) updated = set(new_tmpl.resource_definitions(None).items())
current = set((i.name, i.t) for i in instances) if old_tmpl is not None:
updated = set(tmpl.resource_definitions(None).items()) current = set(old_tmpl.resource_definitions(None).items())
changing = current ^ updated
else:
changing = updated
# includes instances to be updated and deleted # includes instances to be updated and deleted
affected = set(k for k, v in current ^ updated) return set(k for k, v in changing)
return set(i.FnGetRefId() for i in instances if i.name in affected)
def pause_between_batch(): def pause_between_batch():
while True: while True:
@ -327,7 +331,10 @@ class InstanceGroup(stack_resource.StackResource):
except scheduler.Timeout: except scheduler.Timeout:
return return
capacity = len(self.nested()) if self.nested() else 0 group_data = self._group_data()
old_template = group_data.template()
capacity = group_data.size(include_failed=True)
batches = list(self._get_batches(capacity, batch_size, min_in_service)) batches = list(self._get_batches(capacity, batch_size, min_in_service))
update_timeout = self._update_timeout(len(batches), pause_sec) update_timeout = self._update_timeout(len(batches), pause_sec)
@ -335,16 +342,20 @@ class InstanceGroup(stack_resource.StackResource):
try: try:
for index, (total_capacity, efft_bat_sz) in enumerate(batches): for index, (total_capacity, efft_bat_sz) in enumerate(batches):
template = self._create_template(total_capacity, efft_bat_sz) template = self._create_template(total_capacity, efft_bat_sz)
self._lb_reload(exclude=changing_instances(template)) self._lb_reload(exclude=changing_instances(old_template,
template),
refresh_data=False)
updater = self.update_with_template(template) updater = self.update_with_template(template)
checker = scheduler.TaskRunner(self._check_for_completion, checker = scheduler.TaskRunner(self._check_for_completion,
updater) updater)
checker(timeout=update_timeout) checker(timeout=update_timeout)
old_template = template
if index < (len(batches) - 1) and pause_sec > 0: if index < (len(batches) - 1) and pause_sec > 0:
self._lb_reload() self._lb_reload()
waiter = scheduler.TaskRunner(pause_between_batch) waiter = scheduler.TaskRunner(pause_between_batch)
waiter(timeout=pause_sec) waiter(timeout=pause_sec)
finally: finally:
self._group_data(refresh=True)
self._lb_reload() self._lb_reload()
@staticmethod @staticmethod
@ -387,11 +398,29 @@ class InstanceGroup(stack_resource.StackResource):
# nodes. # nodes.
self._lb_reload() self._lb_reload()
def _lb_reload(self, exclude=None): def _lb_reload(self, exclude=frozenset(), refresh_data=True):
lb_names = self.properties.get(self.LOAD_BALANCER_NAMES) or [] lb_names = self.properties.get(self.LOAD_BALANCER_NAMES) or []
if lb_names: if lb_names:
lb_dict = dict((name, self.stack[name]) for name in lb_names) if refresh_data:
lbutils.reload_loadbalancers(self, lb_dict, exclude) self._outputs = None
try:
all_refids = self.get_output(self.OUTPUT_MEMBER_IDS)
except (exception.NotFound,
exception.TemplateOutputError) as op_err:
LOG.debug('Falling back to grouputils due to %s', op_err)
if refresh_data:
self._nested = None
instances = grouputils.get_members(self)
all_refids = {i.name: i.FnGetRefId() for i in instances}
names = [i.name for i in instances]
else:
group_data = self._group_data(refresh=refresh_data)
names = group_data.member_names(include_failed=False)
id_list = [all_refids[n] for n in names
if n not in exclude and n in all_refids]
lbs = [self.stack[name] for name in lb_names]
lbutils.reconfigure_loadbalancers(lbs, id_list)
def get_reference_id(self): def get_reference_id(self):
return self.physical_resource_name_or_FnGetRefId() return self.physical_resource_name_or_FnGetRefId()
@ -441,6 +470,10 @@ class InstanceGroup(stack_resource.StackResource):
for r in resource_names} for r in resource_names}
yield output.OutputDefinition(key, value) yield output.OutputDefinition(key, value)
member_ids_value = {r: get_res_fn(r) for r in resource_names}
yield output.OutputDefinition(self.OUTPUT_MEMBER_IDS,
member_ids_value)
def child_template(self): def child_template(self):
num_instances = int(self.properties[self.SIZE]) num_instances = int(self.properties[self.SIZE])
return self._create_template(num_instances) return self._create_template(num_instances)

View File

@ -13,38 +13,29 @@
import copy import copy
import six
from heat.common import exception from heat.common import exception
from heat.common import grouputils
from heat.common.i18n import _ from heat.common.i18n import _
from heat.engine import rsrc_defn
from heat.engine import scheduler from heat.engine import scheduler
def reload_loadbalancers(group, load_balancers, exclude=None): def reconfigure_loadbalancers(load_balancers, id_list):
"""Notify the LoadBalancer to reload its config. """Notify the LoadBalancer to reload its config.
This must be done after activation (instance in ACTIVE state), otherwise This must be done after activation (instance in ACTIVE state), otherwise
the instances' IP addresses may not be available. the instances' IP addresses may not be available.
""" """
exclude = exclude or [] for lb in load_balancers:
id_list = grouputils.get_member_refids(group, exclude=exclude) existing_defn = lb.frozen_definition()
for name, lb in six.iteritems(load_balancers): props = copy.copy(existing_defn.properties(lb.properties_schema,
props = copy.copy(lb.properties.data) lb.context).data)
if 'Instances' in lb.properties_schema: if 'Instances' in lb.properties_schema:
props['Instances'] = id_list props['Instances'] = id_list
elif 'members' in lb.properties_schema: elif 'members' in lb.properties_schema:
props['members'] = id_list props['members'] = id_list
else: else:
raise exception.Error( raise exception.Error(
_("Unsupported resource '%s' in LoadBalancerNames") % name) _("Unsupported resource '%s' in LoadBalancerNames") % lb.name)
lb_defn = rsrc_defn.ResourceDefinition( lb_defn = existing_defn.freeze(properties=props)
lb.name,
lb.type(),
properties=props,
metadata=lb.t.metadata(),
deletion_policy=lb.t.deletion_policy())
scheduler.TaskRunner(lb.update, lb_defn)() scheduler.TaskRunner(lb.update, lb_defn)()

View File

@ -15,7 +15,6 @@ import mock
import six import six
from heat.common import exception from heat.common import exception
from heat.common import grouputils
from heat.common import template_format from heat.common import template_format
from heat.engine import properties from heat.engine import properties
from heat.engine import resource from heat.engine import resource
@ -78,23 +77,17 @@ class LBUtilsTest(common.HeatTestCase):
self.stack = utils.parse_stack(t) self.stack = utils.parse_stack(t)
def test_reload_aws_lb(self): def test_reload_aws_lb(self):
group = mock.Mock() id_list = ['ID1', 'ID2', 'ID3']
self.patchobject(grouputils, 'get_member_refids',
return_value=['ID1', 'ID2', 'ID3'])
lb1 = self.stack['aws_lb_1'] lb1 = self.stack['aws_lb_1']
lb2 = self.stack['aws_lb_2'] lb2 = self.stack['aws_lb_2']
lbs = {
'LB_1': lb1,
'LB_2': lb2
}
lb1.action = mock.Mock(return_value=lb1.CREATE) lb1.action = mock.Mock(return_value=lb1.CREATE)
lb2.action = mock.Mock(return_value=lb2.CREATE) lb2.action = mock.Mock(return_value=lb2.CREATE)
lb1.handle_update = mock.Mock() lb1.handle_update = mock.Mock()
lb2.handle_update = mock.Mock() lb2.handle_update = mock.Mock()
prop_diff = {'Instances': ['ID1', 'ID2', 'ID3']} prop_diff = {'Instances': id_list}
lbutils.reload_loadbalancers(group, lbs) lbutils.reconfigure_loadbalancers([lb1, lb2], id_list)
# For verification's purpose, we just check the prop_diff # For verification's purpose, we just check the prop_diff
lb1.handle_update.assert_called_with(mock.ANY, mock.ANY, lb1.handle_update.assert_called_with(mock.ANY, mock.ANY,
@ -103,25 +96,19 @@ class LBUtilsTest(common.HeatTestCase):
prop_diff) prop_diff)
def test_reload_neutron_lb(self): def test_reload_neutron_lb(self):
group = mock.Mock() id_list = ['ID1', 'ID2', 'ID3']
self.patchobject(grouputils, 'get_member_refids',
return_value=['ID1', 'ID2', 'ID3'])
lb1 = self.stack['neutron_lb_1'] lb1 = self.stack['neutron_lb_1']
lb2 = self.stack['neutron_lb_2'] lb2 = self.stack['neutron_lb_2']
lb1.action = mock.Mock(return_value=lb1.CREATE) lb1.action = mock.Mock(return_value=lb1.CREATE)
lb2.action = mock.Mock(return_value=lb2.CREATE) lb2.action = mock.Mock(return_value=lb2.CREATE)
lbs = {
'LB_1': lb1,
'LB_2': lb2
}
lb1.handle_update = mock.Mock() lb1.handle_update = mock.Mock()
lb2.handle_update = mock.Mock() lb2.handle_update = mock.Mock()
prop_diff = {'members': ['ID1', 'ID2', 'ID3']} prop_diff = {'members': id_list}
lbutils.reload_loadbalancers(group, lbs) lbutils.reconfigure_loadbalancers([lb1, lb2], id_list)
# For verification's purpose, we just check the prop_diff # For verification's purpose, we just check the prop_diff
lb1.handle_update.assert_called_with(mock.ANY, mock.ANY, lb1.handle_update.assert_called_with(mock.ANY, mock.ANY,
@ -130,16 +117,11 @@ class LBUtilsTest(common.HeatTestCase):
prop_diff) prop_diff)
def test_reload_non_lb(self): def test_reload_non_lb(self):
group = mock.Mock() id_list = ['ID1', 'ID2', 'ID3']
self.patchobject(grouputils, 'get_member_refids', non_lb = self.stack['non_lb']
return_value=['ID1', 'ID2', 'ID3'])
lbs = {
'LB_1': self.stack['non_lb'],
}
error = self.assertRaises(exception.Error, error = self.assertRaises(exception.Error,
lbutils.reload_loadbalancers, lbutils.reconfigure_loadbalancers,
group, lbs) [non_lb], id_list)
self.assertIn("Unsupported resource 'LB_1' in LoadBalancerNames", self.assertIn("Unsupported resource 'non_lb' in LoadBalancerNames",
six.text_type(error)) six.text_type(error))

View File

@ -255,8 +255,7 @@ class LoadbalancerReloadTest(common.HeatTestCase):
"LoadBalancerNames": ["ElasticLoadBalancer"]}) "LoadBalancerNames": ["ElasticLoadBalancer"]})
group = instgrp.InstanceGroup('asg', defn, stack) group = instgrp.InstanceGroup('asg', defn, stack)
mock_members = self.patchobject(grouputils, 'get_member_refids') mocks = self.setup_mocks(group, ['aaaa', 'bbb'])
mock_members.return_value = ['aaaa', 'bbb']
expected = rsrc_defn.ResourceDefinition( expected = rsrc_defn.ResourceDefinition(
'ElasticLoadBalancer', 'ElasticLoadBalancer',
'AWS::ElasticLoadBalancing::LoadBalancer', 'AWS::ElasticLoadBalancing::LoadBalancer',
@ -264,13 +263,11 @@ class LoadbalancerReloadTest(common.HeatTestCase):
'Listeners': [{'InstancePort': u'80', 'Listeners': [{'InstancePort': u'80',
'LoadBalancerPort': u'80', 'LoadBalancerPort': u'80',
'Protocol': 'HTTP'}], 'Protocol': 'HTTP'}],
'AvailabilityZones': ['nova']}, 'AvailabilityZones': ['nova']}
metadata={},
deletion_policy='Delete'
) )
group._lb_reload() group._lb_reload()
mock_members.assert_called_once_with(group, exclude=[]) self.check_mocks(group, mocks)
lb.update.assert_called_once_with(expected) lb.update.assert_called_once_with(expected)
def test_members(self): def test_members(self):
@ -297,18 +294,15 @@ class LoadbalancerReloadTest(common.HeatTestCase):
"LoadBalancerNames": ["ElasticLoadBalancer"]}) "LoadBalancerNames": ["ElasticLoadBalancer"]})
group = instgrp.InstanceGroup('asg', defn, stack) group = instgrp.InstanceGroup('asg', defn, stack)
mock_members = self.patchobject(grouputils, 'get_member_refids') mocks = self.setup_mocks(group, ['aaaa', 'bbb'])
mock_members.return_value = ['aaaa', 'bbb']
expected = rsrc_defn.ResourceDefinition( expected = rsrc_defn.ResourceDefinition(
'ElasticLoadBalancer', 'ElasticLoadBalancer',
'OS::Neutron::LoadBalancer', 'OS::Neutron::LoadBalancer',
{'protocol_port': 8080, {'protocol_port': 8080,
'members': ['aaaa', 'bbb']}, 'members': ['aaaa', 'bbb']})
metadata={},
deletion_policy='Delete')
group._lb_reload() group._lb_reload()
mock_members.assert_called_once_with(group, exclude=[]) self.check_mocks(group, mocks)
lb.update.assert_called_once_with(expected) lb.update.assert_called_once_with(expected)
def test_lb_reload_invalid_resource(self): def test_lb_reload_invalid_resource(self):
@ -332,9 +326,7 @@ class LoadbalancerReloadTest(common.HeatTestCase):
"LoadBalancerNames": ["ElasticLoadBalancer"]}) "LoadBalancerNames": ["ElasticLoadBalancer"]})
group = instgrp.InstanceGroup('asg', defn, stack) group = instgrp.InstanceGroup('asg', defn, stack)
mock_members = self.patchobject(grouputils, 'get_member_refids') self.setup_mocks(group, ['aaaa', 'bbb'])
mock_members.return_value = ['aaaa', 'bbb']
error = self.assertRaises(exception.Error, error = self.assertRaises(exception.Error,
group._lb_reload) group._lb_reload)
self.assertEqual( self.assertEqual(
@ -350,19 +342,48 @@ class LoadbalancerReloadTest(common.HeatTestCase):
self.patchobject(stk_defn.StackDefinition, 'get_availability_zones', self.patchobject(stk_defn.StackDefinition, 'get_availability_zones',
return_value=['abc', 'xyz']) return_value=['abc', 'xyz'])
mock_members = self.patchobject(grouputils, 'get_member_refids')
mock_members.return_value = ['aaaabbbbcccc']
stack = utils.parse_stack(t, params=inline_templates.as_params) stack = utils.parse_stack(t, params=inline_templates.as_params)
lb = stack['ElasticLoadBalancer'] lb = stack['ElasticLoadBalancer']
lb.state_set(lb.CREATE, lb.COMPLETE) lb.state_set(lb.CREATE, lb.COMPLETE)
lb.handle_update = mock.Mock(return_value=None) lb.handle_update = mock.Mock(return_value=None)
group = stack['WebServerGroup'] group = stack['WebServerGroup']
self.setup_mocks(group, ['aaaabbbbcccc'])
group._lb_reload() group._lb_reload()
lb.handle_update.assert_called_once_with( lb.handle_update.assert_called_once_with(
mock.ANY, mock.ANY, mock.ANY, mock.ANY,
{'Instances': ['aaaabbbbcccc']}) {'Instances': ['aaaabbbbcccc']})
def setup_mocks(self, group, member_refids):
refs = {str(i): r for i, r in enumerate(member_refids)}
group.get_output = mock.Mock(return_value=refs)
names = sorted(refs.keys())
group_data = group._group_data()
group_data.member_names = mock.Mock(return_value=names)
group._group_data = mock.Mock(return_value=group_data)
def check_mocks(self, group, unused):
pass
class LoadbalancerReloadFallbackTest(LoadbalancerReloadTest):
def setup_mocks(self, group, member_refids):
# Raise NotFound when getting output, to force fallback to old-school
# grouputils functions
group.get_output = mock.Mock(side_effect=exception.NotFound)
def make_mock_member(refid):
mem = mock.Mock()
mem.FnGetRefId = mock.Mock(return_value=refid)
return mem
members = [make_mock_member(r) for r in member_refids]
mock_members = self.patchobject(grouputils, 'get_members',
return_value=members)
return mock_members
def check_mocks(self, group, mock_members):
mock_members.assert_called_once_with(group)
class InstanceGroupWithNestedStack(common.HeatTestCase): class InstanceGroupWithNestedStack(common.HeatTestCase):
def setUp(self): def setUp(self):
@ -417,7 +438,10 @@ class ReplaceTest(InstanceGroupWithNestedStack):
def setUp(self): def setUp(self):
super(ReplaceTest, self).setUp() super(ReplaceTest, self).setUp()
self.group._nested = self.get_fake_nested_stack(2) nested = self.get_fake_nested_stack(2)
inspector = self.group._group_data()
inspector.size = mock.Mock(return_value=2)
inspector.template = mock.Mock(return_value=nested.defn._template)
def test_rolling_updates(self): def test_rolling_updates(self):
self.group._replace(self.min_in_service, self.batch_size, 0) self.group._replace(self.min_in_service, self.batch_size, 0)

View File

@ -297,6 +297,6 @@ class InstanceGroupReplaceTest(common.HeatTestCase):
# (6 - 1)*14*60 > 3600, so to raise error # (6 - 1)*14*60 > 3600, so to raise error
group = instgrp.InstanceGroup('asg', defn, stack) group = instgrp.InstanceGroup('asg', defn, stack)
group.nested = mock.MagicMock(return_value=range(12)) group._group_data().size = mock.Mock(return_value=12)
self.assertRaises(ValueError, self.assertRaises(ValueError,
group._replace, 10, 1, 14 * 60) group._replace, 10, 1, 14 * 60)

View File

@ -58,8 +58,6 @@ class GroupUtilsTest(common.HeatTestCase):
# refids # refids
actual_ids = grouputils.get_member_refids(group) actual_ids = grouputils.get_member_refids(group)
self.assertEqual(['ID-r0', 'ID-r1'], actual_ids) self.assertEqual(['ID-r0', 'ID-r1'], actual_ids)
partial_ids = grouputils.get_member_refids(group, exclude=['ID-r1'])
self.assertEqual(['ID-r0'], partial_ids)
def test_group_with_failed_members(self): def test_group_with_failed_members(self):
group = mock.Mock() group = mock.Mock()