From 4cfd9a10ac585f3b675185acde8ab762e65ec55b Mon Sep 17 00:00:00 2001 From: Steven Hardy Date: Mon, 7 Dec 2015 15:59:09 +0000 Subject: [PATCH] Fix update preview to handle nested stacks Currently the update preview code has no support for previewing the effect of an update on nested stacks, which I assume was an oversight in the original implementation. So this adds a show_nested flag to the API which allows enabling recursive preview of the whole update including nested stacks. Closes-Bug: #1521971 Depends-On: I06f3b52d5d48dd5e6e266321e58ca8e6116d6017 Change-Id: I96af4d2f07056846aac7ae9ad9b6eb160e8bd51a --- heat/api/openstack/v1/stacks.py | 14 ++ heat/engine/service.py | 83 +++++++-- heat/engine/update.py | 4 +- heat_integrationtests/common/test.py | 6 +- .../functional/test_preview_update.py | 169 ++++++++++++++++-- 5 files changed, 245 insertions(+), 31 deletions(-) diff --git a/heat/api/openstack/v1/stacks.py b/heat/api/openstack/v1/stacks.py index 6d57dc3c1..f4de2be7c 100644 --- a/heat/api/openstack/v1/stacks.py +++ b/heat/api/openstack/v1/stacks.py @@ -486,12 +486,23 @@ class StackController(object): raise exc.HTTPAccepted() + def _param_show_nested(self, req): + whitelist = {'show_nested': 'single'} + params = util.get_allowed_params(req.params, whitelist) + + p_name = 'show_nested' + if p_name in params: + return self._extract_bool_param(p_name, params[p_name]) + @util.identified_stack def preview_update(self, req, identity, body): """Preview update for existing stack with a new template/parameters.""" data = InstantiationData(body) args = self.prepare_args(data) + show_nested = self._param_show_nested(req) + if show_nested is not None: + args[rpc_api.PARAM_SHOW_NESTED] = show_nested changes = self.rpc_client.preview_update_stack( req.context, identity, @@ -509,6 +520,9 @@ class StackController(object): data = InstantiationData(body, patch=True) args = self.prepare_args(data) + show_nested = self._param_show_nested(req) + if show_nested is not None: + args['show_nested'] = show_nested changes = self.rpc_client.preview_update_stack( req.context, identity, diff --git a/heat/engine/service.py b/heat/engine/service.py index 5f77937fe..d087fad98 100644 --- a/heat/engine/service.py +++ b/heat/engine/service.py @@ -930,20 +930,81 @@ class EngineService(service.Service): actions = update_task.preview() - def fmt_updated_res(k): - return api.format_stack_resource(updated_stack.resources.get(k)) + def fmt_action_map(current, updated, act): + def fmt_updated_res(k): + return api.format_stack_resource(updated.resources.get(k)) - def fmt_current_res(k): - return api.format_stack_resource(current_stack.resources.get(k)) + def fmt_current_res(k): + return api.format_stack_resource(current.resources.get(k)) + + return { + 'unchanged': map(fmt_updated_res, act.get('unchanged', [])), + 'updated': map(fmt_current_res, act.get('updated', [])), + 'replaced': map(fmt_updated_res, act.get('replaced', [])), + 'added': map(fmt_updated_res, act.get('added', [])), + 'deleted': map(fmt_current_res, act.get('deleted', [])), + } updated_stack.id = current_stack.id - return { - 'unchanged': map(fmt_updated_res, actions['unchanged']), - 'updated': map(fmt_current_res, actions['updated']), - 'replaced': map(fmt_updated_res, actions['replaced']), - 'added': map(fmt_updated_res, actions['added']), - 'deleted': map(fmt_current_res, actions['deleted']), - } + fmt_actions = fmt_action_map(current_stack, updated_stack, actions) + + if args.get(rpc_api.PARAM_SHOW_NESTED): + # Note preview_resources is needed here to build the tree + # of nested resources/stacks in memory, otherwise the + # nested/has_nested() tests below won't work + updated_stack.preview_resources() + + def nested_fmt_actions(current, updated, act): + updated.id = current.id + + # Recurse for resources deleted from the current stack, + # which is all those marked as deleted or replaced + def _n_deleted(stk, deleted): + for rsrc in deleted: + deleted_rsrc = stk.resources.get(rsrc) + if deleted_rsrc.has_nested(): + nested_stk = deleted_rsrc.nested() + nested_rsrc = nested_stk.resources.keys() + n_fmt = fmt_action_map( + nested_stk, None, {'deleted': nested_rsrc}) + fmt_actions['deleted'].extend(n_fmt['deleted']) + _n_deleted(nested_stk, nested_rsrc) + _n_deleted(current, act['deleted'] + act['replaced']) + + # Recurse for all resources added to the updated stack, + # which is all those marked added or replaced + def _n_added(stk, added): + for rsrc in added: + added_rsrc = stk.resources.get(rsrc) + if added_rsrc.has_nested(): + nested_stk = added_rsrc.nested() + nested_rsrc = nested_stk.resources.keys() + n_fmt = fmt_action_map( + None, nested_stk, {'added': nested_rsrc}) + fmt_actions['added'].extend(n_fmt['added']) + _n_added(nested_stk, nested_rsrc) + _n_added(updated, act['added'] + act['replaced']) + + # Recursively preview all "updated" resources + for rsrc in act['updated']: + current_rsrc = current.resources.get(rsrc) + updated_rsrc = updated.resources.get(rsrc) + if current_rsrc.has_nested() and updated_rsrc.has_nested(): + current_nested = current_rsrc.nested() + updated_nested = updated_rsrc.nested() + update_task = update.StackUpdate( + current_nested, updated_nested, None) + n_actions = update_task.preview() + n_fmt_actions = fmt_action_map( + current_nested, updated_nested, n_actions) + for k in fmt_actions: + fmt_actions[k].extend(n_fmt_actions[k]) + nested_fmt_actions(current_nested, updated_nested, + n_actions) + # Start the recursive nested_fmt_actions with the parent stack. + nested_fmt_actions(current_stack, updated_stack, actions) + + return fmt_actions @context.request_context def stack_cancel_update(self, cnxt, stack_identity, diff --git a/heat/engine/update.py b/heat/engine/update.py index 836a8c7a1..6d2af0940 100644 --- a/heat/engine/update.py +++ b/heat/engine/update.py @@ -259,6 +259,6 @@ class StackUpdate(object): set(updated_keys + replaced_keys))), 'updated': updated_keys, 'replaced': replaced_keys, - 'added': added_keys, - 'deleted': deleted_keys, + 'added': list(added_keys), + 'deleted': list(deleted_keys), } diff --git a/heat_integrationtests/common/test.py b/heat_integrationtests/common/test.py index 1ffe222b7..8fc5c602a 100644 --- a/heat_integrationtests/common/test.py +++ b/heat_integrationtests/common/test.py @@ -392,7 +392,8 @@ class HeatIntegrationTest(testscenarios.WithScenarios, def preview_update_stack(self, stack_identifier, template, environment=None, files=None, parameters=None, - tags=None, disable_rollback=True): + tags=None, disable_rollback=True, + show_nested=False): env = environment or {} env_files = files or {} parameters = parameters or {} @@ -406,7 +407,8 @@ class HeatIntegrationTest(testscenarios.WithScenarios, disable_rollback=disable_rollback, parameters=parameters, environment=env, - tags=tags + tags=tags, + show_nested=show_nested ) def assert_resource_is_a_stack(self, stack_identifier, res_name, diff --git a/heat_integrationtests/functional/test_preview_update.py b/heat_integrationtests/functional/test_preview_update.py index 0e39bc9e5..971e9c5c7 100644 --- a/heat_integrationtests/functional/test_preview_update.py +++ b/heat_integrationtests/functional/test_preview_update.py @@ -54,7 +54,14 @@ test_template_two_resource = { } -class UpdatePreviewStackTest(functional_base.FunctionalTestsBase): +class UpdatePreviewBase(functional_base.FunctionalTestsBase): + + def assert_empty_sections(self, changes, empty_sections): + for section in empty_sections: + self.assertEqual([], changes[section]) + + +class UpdatePreviewStackTest(UpdatePreviewBase): def test_add_resource(self): self.stack_identifier = self.stack_create( @@ -69,9 +76,7 @@ class UpdatePreviewStackTest(functional_base.FunctionalTestsBase): added = changes['added'][0]['resource_name'] self.assertEqual('test2', added) - empty_sections = ('updated', 'replaced', 'deleted') - for section in empty_sections: - self.assertEqual([], changes[section]) + self.assert_empty_sections(changes, ['updated', 'replaced', 'deleted']) def test_no_change(self): self.stack_identifier = self.stack_create( @@ -83,9 +88,8 @@ class UpdatePreviewStackTest(functional_base.FunctionalTestsBase): unchanged = changes['unchanged'][0]['resource_name'] self.assertEqual('test1', unchanged) - empty_sections = ('updated', 'replaced', 'deleted', 'added') - for section in empty_sections: - self.assertEqual([], changes[section]) + self.assert_empty_sections( + changes, ['updated', 'replaced', 'deleted', 'added']) def test_update_resource(self): self.stack_identifier = self.stack_create( @@ -113,9 +117,8 @@ class UpdatePreviewStackTest(functional_base.FunctionalTestsBase): updated = changes['updated'][0]['resource_name'] self.assertEqual('test1', updated) - empty_sections = ('added', 'unchanged', 'replaced', 'deleted') - for section in empty_sections: - self.assertEqual([], changes[section]) + self.assert_empty_sections( + changes, ['added', 'unchanged', 'replaced', 'deleted']) def test_replaced_resource(self): self.stack_identifier = self.stack_create( @@ -139,9 +142,8 @@ class UpdatePreviewStackTest(functional_base.FunctionalTestsBase): replaced = changes['replaced'][0]['resource_name'] self.assertEqual('test1', replaced) - empty_sections = ('added', 'unchanged', 'updated', 'deleted') - for section in empty_sections: - self.assertEqual([], changes[section]) + self.assert_empty_sections( + changes, ['added', 'unchanged', 'updated', 'deleted']) def test_delete_resource(self): self.stack_identifier = self.stack_create( @@ -156,6 +158,141 @@ class UpdatePreviewStackTest(functional_base.FunctionalTestsBase): deleted = changes['deleted'][0]['resource_name'] self.assertEqual('test2', deleted) - empty_sections = ('updated', 'replaced', 'added') - for section in empty_sections: - self.assertEqual([], changes[section]) + self.assert_empty_sections(changes, ['updated', 'replaced', 'added']) + + +class UpdatePreviewStackTestNested(UpdatePreviewBase): + template_nested_parent = ''' +heat_template_version: 2016-04-08 +resources: + nested1: + type: nested1.yaml +''' + + template_nested1 = ''' +heat_template_version: 2016-04-08 +resources: + nested2: + type: nested2.yaml +''' + + template_nested2 = ''' +heat_template_version: 2016-04-08 +resources: + random: + type: OS::Heat::RandomString +''' + + template_nested2_2 = ''' +heat_template_version: 2016-04-08 +resources: + random: + type: OS::Heat::RandomString + random2: + type: OS::Heat::RandomString +''' + + def _get_by_resource_name(self, changes, name, action): + filtered_l = [x for x in changes[action] + if x['resource_name'] == name] + self.assertEqual(1, len(filtered_l)) + return filtered_l[0] + + def test_nested_resources_nochange(self): + files = {'nested1.yaml': self.template_nested1, + 'nested2.yaml': self.template_nested2} + self.stack_identifier = self.stack_create( + template=self.template_nested_parent, files=files) + result = self.preview_update_stack( + self.stack_identifier, + template=self.template_nested_parent, + files=files, show_nested=True) + changes = result['resource_changes'] + + # The nested random resource should be unchanged, but we always + # update nested stacks even when there are no changes + self.assertEqual(1, len(changes['unchanged'])) + self.assertEqual('random', changes['unchanged'][0]['resource_name']) + self.assertEqual('nested2', changes['unchanged'][0]['parent_resource']) + + self.assertEqual(2, len(changes['updated'])) + u_nested1 = self._get_by_resource_name(changes, 'nested1', 'updated') + self.assertNotIn('parent_resource', u_nested1) + u_nested2 = self._get_by_resource_name(changes, 'nested2', 'updated') + self.assertEqual('nested1', u_nested2['parent_resource']) + + self.assert_empty_sections(changes, ['replaced', 'deleted', 'added']) + + def test_nested_resources_add(self): + files = {'nested1.yaml': self.template_nested1, + 'nested2.yaml': self.template_nested2} + self.stack_identifier = self.stack_create( + template=self.template_nested_parent, files=files) + files['nested2.yaml'] = self.template_nested2_2 + result = self.preview_update_stack( + self.stack_identifier, + template=self.template_nested_parent, + files=files, show_nested=True) + changes = result['resource_changes'] + + # The nested random resource should be unchanged, but we always + # update nested stacks even when there are no changes + self.assertEqual(1, len(changes['unchanged'])) + self.assertEqual('random', changes['unchanged'][0]['resource_name']) + self.assertEqual('nested2', changes['unchanged'][0]['parent_resource']) + + self.assertEqual(1, len(changes['added'])) + self.assertEqual('random2', changes['added'][0]['resource_name']) + self.assertEqual('nested2', changes['added'][0]['parent_resource']) + + self.assert_empty_sections(changes, ['replaced', 'deleted']) + + def test_nested_resources_delete(self): + files = {'nested1.yaml': self.template_nested1, + 'nested2.yaml': self.template_nested2_2} + self.stack_identifier = self.stack_create( + template=self.template_nested_parent, files=files) + files['nested2.yaml'] = self.template_nested2 + result = self.preview_update_stack( + self.stack_identifier, + template=self.template_nested_parent, + files=files, show_nested=True) + changes = result['resource_changes'] + + # The nested random resource should be unchanged, but we always + # update nested stacks even when there are no changes + self.assertEqual(1, len(changes['unchanged'])) + self.assertEqual('random', changes['unchanged'][0]['resource_name']) + self.assertEqual('nested2', changes['unchanged'][0]['parent_resource']) + + self.assertEqual(1, len(changes['deleted'])) + self.assertEqual('random2', changes['deleted'][0]['resource_name']) + self.assertEqual('nested2', changes['deleted'][0]['parent_resource']) + + self.assert_empty_sections(changes, ['replaced', 'added']) + + def test_nested_resources_replace(self): + files = {'nested1.yaml': self.template_nested1, + 'nested2.yaml': self.template_nested2} + self.stack_identifier = self.stack_create( + template=self.template_nested_parent, files=files) + parent_none = self.template_nested_parent.replace( + 'nested1.yaml', 'OS::Heat::None') + result = self.preview_update_stack( + self.stack_identifier, + template=parent_none, + show_nested=True) + changes = result['resource_changes'] + + # The nested random resource should be unchanged, but we always + # update nested stacks even when there are no changes + self.assertEqual(1, len(changes['replaced'])) + self.assertEqual('nested1', changes['replaced'][0]['resource_name']) + + self.assertEqual(2, len(changes['deleted'])) + d_random = self._get_by_resource_name(changes, 'random', 'deleted') + self.assertEqual('nested2', d_random['parent_resource']) + d_nested2 = self._get_by_resource_name(changes, 'nested2', 'deleted') + self.assertEqual('nested1', d_nested2['parent_resource']) + + self.assert_empty_sections(changes, ['updated', 'unchanged', 'added'])