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
This commit is contained in:
Steven Hardy 2015-12-07 15:59:09 +00:00
parent 8b83e14d84
commit 4cfd9a10ac
5 changed files with 245 additions and 31 deletions

View File

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

View File

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

View File

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

View File

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

View File

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