97df8bb6ca
Rank all existing versions of a resource in a convergence stack to improve the likelihood that we find the best one to update. This allows us to roll back to the original version of a resource (or even attempt an in-place update of it) when replacing it has failed. Previously this only worked during automatic rollback; on subsequent updates we would always work on the failed replacement (which inevitably meant attempting another replacement in almost all cases). Change-Id: Ia231fae85d1ddb9fc7b7de4e82cec0c0e0fd06b7 Story: #2003579 Task: 24881
793 lines
33 KiB
Python
793 lines
33 KiB
Python
# 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 copy
|
|
import json
|
|
|
|
from heat_integrationtests.common import test
|
|
from heat_integrationtests.functional import functional_base
|
|
|
|
test_template_one_resource = {
|
|
'heat_template_version': 'pike',
|
|
'description': 'Test template to create one instance.',
|
|
'resources': {
|
|
'test1': {
|
|
'type': 'OS::Heat::TestResource',
|
|
'properties': {
|
|
'value': 'Test1',
|
|
'fail': False,
|
|
'update_replace': False,
|
|
'wait_secs': 1,
|
|
'action_wait_secs': {'create': 1},
|
|
'client_name': 'nova',
|
|
'entity_name': 'servers',
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
test_template_two_resource = {
|
|
'heat_template_version': 'pike',
|
|
'description': 'Test template to create two instance.',
|
|
'resources': {
|
|
'test1': {
|
|
'type': 'OS::Heat::TestResource',
|
|
'properties': {
|
|
'value': 'Test1',
|
|
'fail': False,
|
|
'update_replace': False,
|
|
'wait_secs': 0,
|
|
'action_wait_secs': {'update': 1}
|
|
}
|
|
},
|
|
'test2': {
|
|
'type': 'OS::Heat::TestResource',
|
|
'properties': {
|
|
'value': 'Test1',
|
|
'fail': False,
|
|
'update_replace': False,
|
|
'wait_secs': 0
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
def _change_rsrc_properties(template, rsrcs, values):
|
|
modified_template = copy.deepcopy(template)
|
|
for rsrc_name in rsrcs:
|
|
rsrc_prop = modified_template['resources'][
|
|
rsrc_name]['properties']
|
|
for prop, new_val in values.items():
|
|
rsrc_prop[prop] = new_val
|
|
return modified_template
|
|
|
|
|
|
class CreateStackTest(functional_base.FunctionalTestsBase):
|
|
def test_create_rollback(self):
|
|
values = {'fail': True, 'value': 'test_create_rollback'}
|
|
template = _change_rsrc_properties(test_template_one_resource,
|
|
['test1'], values)
|
|
|
|
self.stack_create(
|
|
template=template,
|
|
expected_status='ROLLBACK_COMPLETE',
|
|
disable_rollback=False)
|
|
|
|
|
|
class UpdateStackTest(functional_base.FunctionalTestsBase):
|
|
|
|
provider_template = {
|
|
'heat_template_version': '2013-05-23',
|
|
'description': 'foo',
|
|
'resources': {
|
|
'test1': {
|
|
'type': 'My::TestResource'
|
|
}
|
|
}
|
|
}
|
|
|
|
provider_group_template = '''
|
|
heat_template_version: 2013-05-23
|
|
parameters:
|
|
count:
|
|
type: number
|
|
default: 2
|
|
resources:
|
|
test_group:
|
|
type: OS::Heat::ResourceGroup
|
|
properties:
|
|
count: {get_param: count}
|
|
resource_def:
|
|
type: My::TestResource
|
|
'''
|
|
|
|
update_userdata_template = '''
|
|
heat_template_version: 2014-10-16
|
|
parameters:
|
|
flavor:
|
|
type: string
|
|
user_data:
|
|
type: string
|
|
image:
|
|
type: string
|
|
network:
|
|
type: string
|
|
|
|
resources:
|
|
server:
|
|
type: OS::Nova::Server
|
|
properties:
|
|
image: {get_param: image}
|
|
flavor: {get_param: flavor}
|
|
networks: [{network: {get_param: network} }]
|
|
user_data_format: SOFTWARE_CONFIG
|
|
user_data: {get_param: user_data}
|
|
'''
|
|
|
|
fail_param_template = '''
|
|
heat_template_version: 2014-10-16
|
|
parameters:
|
|
do_fail:
|
|
type: boolean
|
|
default: False
|
|
resources:
|
|
aresource:
|
|
type: OS::Heat::TestResource
|
|
properties:
|
|
value: Test
|
|
fail: {get_param: do_fail}
|
|
wait_secs: 1
|
|
'''
|
|
|
|
def test_stack_update_nochange(self):
|
|
template = _change_rsrc_properties(test_template_one_resource,
|
|
['test1'],
|
|
{'value': 'test_no_change'})
|
|
stack_identifier = self.stack_create(
|
|
template=template)
|
|
expected_resources = {'test1': 'OS::Heat::TestResource'}
|
|
self.assertEqual(expected_resources,
|
|
self.list_resources(stack_identifier))
|
|
|
|
# Update with no changes, resources should be unchanged
|
|
self.update_stack(stack_identifier, template)
|
|
self.assertEqual(expected_resources,
|
|
self.list_resources(stack_identifier))
|
|
|
|
def test_stack_in_place_update(self):
|
|
template = _change_rsrc_properties(test_template_one_resource,
|
|
['test1'],
|
|
{'value': 'test_in_place'})
|
|
stack_identifier = self.stack_create(
|
|
template=template)
|
|
expected_resources = {'test1': 'OS::Heat::TestResource'}
|
|
self.assertEqual(expected_resources,
|
|
self.list_resources(stack_identifier))
|
|
resource = self.client.resources.list(stack_identifier)
|
|
initial_phy_id = resource[0].physical_resource_id
|
|
|
|
tmpl_update = _change_rsrc_properties(
|
|
test_template_one_resource, ['test1'],
|
|
{'value': 'test_in_place_update'})
|
|
# Update the Value
|
|
self.update_stack(stack_identifier, tmpl_update)
|
|
resource = self.client.resources.list(stack_identifier)
|
|
# By default update_in_place
|
|
self.assertEqual(initial_phy_id,
|
|
resource[0].physical_resource_id)
|
|
|
|
def test_stack_update_replace(self):
|
|
template = _change_rsrc_properties(test_template_one_resource,
|
|
['test1'],
|
|
{'value': 'test_replace'})
|
|
stack_identifier = self.stack_create(
|
|
template=template)
|
|
expected_resources = {'test1': 'OS::Heat::TestResource'}
|
|
self.assertEqual(expected_resources,
|
|
self.list_resources(stack_identifier))
|
|
resource = self.client.resources.list(stack_identifier)
|
|
initial_phy_id = resource[0].physical_resource_id
|
|
|
|
# Update the value and also set update_replace prop
|
|
tmpl_update = _change_rsrc_properties(
|
|
test_template_one_resource, ['test1'],
|
|
{'value': 'test_in_place_update', 'update_replace': True})
|
|
self.update_stack(stack_identifier, tmpl_update)
|
|
resource = self.client.resources.list(stack_identifier)
|
|
# update Replace
|
|
self.assertNotEqual(initial_phy_id,
|
|
resource[0].physical_resource_id)
|
|
|
|
def test_stack_update_add_remove(self):
|
|
template = _change_rsrc_properties(test_template_one_resource,
|
|
['test1'],
|
|
{'value': 'test_add_remove'})
|
|
stack_identifier = self.stack_create(
|
|
template=template)
|
|
initial_resources = {'test1': 'OS::Heat::TestResource'}
|
|
self.assertEqual(initial_resources,
|
|
self.list_resources(stack_identifier))
|
|
|
|
tmpl_update = _change_rsrc_properties(
|
|
test_template_two_resource, ['test1', 'test2'],
|
|
{'value': 'test_add_remove_update'})
|
|
# Add one resource via a stack update
|
|
self.update_stack(stack_identifier, tmpl_update)
|
|
updated_resources = {'test1': 'OS::Heat::TestResource',
|
|
'test2': 'OS::Heat::TestResource'}
|
|
self.assertEqual(updated_resources,
|
|
self.list_resources(stack_identifier))
|
|
|
|
# Then remove it by updating with the original template
|
|
self.update_stack(stack_identifier, template)
|
|
self.assertEqual(initial_resources,
|
|
self.list_resources(stack_identifier))
|
|
|
|
def test_stack_update_rollback(self):
|
|
template = _change_rsrc_properties(test_template_one_resource,
|
|
['test1'],
|
|
{'value': 'test_update_rollback'})
|
|
stack_identifier = self.stack_create(
|
|
template=template)
|
|
initial_resources = {'test1': 'OS::Heat::TestResource'}
|
|
self.assertEqual(initial_resources,
|
|
self.list_resources(stack_identifier))
|
|
|
|
tmpl_update = _change_rsrc_properties(
|
|
test_template_two_resource, ['test1', 'test2'],
|
|
{'value': 'test_update_rollback', 'fail': True})
|
|
# stack update, also set failure
|
|
self.update_stack(stack_identifier, tmpl_update,
|
|
expected_status='ROLLBACK_COMPLETE',
|
|
disable_rollback=False)
|
|
# since stack update failed only the original resource is present
|
|
updated_resources = {'test1': 'OS::Heat::TestResource'}
|
|
self.assertEqual(updated_resources,
|
|
self.list_resources(stack_identifier))
|
|
|
|
def test_stack_update_from_failed(self):
|
|
# Prove it's possible to update from an UPDATE_FAILED state
|
|
template = _change_rsrc_properties(test_template_one_resource,
|
|
['test1'],
|
|
{'value': 'test_update_failed'})
|
|
stack_identifier = self.stack_create(
|
|
template=template)
|
|
initial_resources = {'test1': 'OS::Heat::TestResource'}
|
|
self.assertEqual(initial_resources,
|
|
self.list_resources(stack_identifier))
|
|
|
|
tmpl_update = _change_rsrc_properties(
|
|
test_template_one_resource, ['test1'], {'fail': True})
|
|
# Update with bad template, we should fail
|
|
self.update_stack(stack_identifier, tmpl_update,
|
|
expected_status='UPDATE_FAILED')
|
|
# but then passing a good template should succeed
|
|
self.update_stack(stack_identifier, test_template_two_resource)
|
|
updated_resources = {'test1': 'OS::Heat::TestResource',
|
|
'test2': 'OS::Heat::TestResource'}
|
|
self.assertEqual(updated_resources,
|
|
self.list_resources(stack_identifier))
|
|
|
|
@test.requires_convergence
|
|
def test_stack_update_replace_manual_rollback(self):
|
|
template = _change_rsrc_properties(test_template_one_resource,
|
|
['test1'],
|
|
{'update_replace_value': '1'})
|
|
stack_identifier = self.stack_create(template=template)
|
|
original_resource_id = self.get_physical_resource_id(stack_identifier,
|
|
'test1')
|
|
|
|
tmpl_update = _change_rsrc_properties(test_template_one_resource,
|
|
['test1'],
|
|
{'update_replace_value': '2',
|
|
'fail': True})
|
|
# Update with bad template, we should fail
|
|
self.update_stack(stack_identifier, tmpl_update,
|
|
expected_status='UPDATE_FAILED',
|
|
disable_rollback=True)
|
|
# Manually roll back to previous template
|
|
self.update_stack(stack_identifier, template)
|
|
final_resource_id = self.get_physical_resource_id(stack_identifier,
|
|
'test1')
|
|
# Original resource was good, and replacement was never created, so it
|
|
# should be kept.
|
|
self.assertEqual(original_resource_id, final_resource_id)
|
|
|
|
def test_stack_update_provider(self):
|
|
template = _change_rsrc_properties(
|
|
test_template_one_resource, ['test1'],
|
|
{'value': 'test_provider_template'})
|
|
files = {'provider.template': json.dumps(template)}
|
|
env = {'resource_registry':
|
|
{'My::TestResource': 'provider.template'}}
|
|
stack_identifier = self.stack_create(
|
|
template=self.provider_template,
|
|
files=files,
|
|
environment=env
|
|
)
|
|
|
|
initial_resources = {'test1': 'My::TestResource'}
|
|
self.assertEqual(initial_resources,
|
|
self.list_resources(stack_identifier))
|
|
|
|
# Prove the resource is backed by a nested stack, save the ID
|
|
nested_identifier = self.assert_resource_is_a_stack(stack_identifier,
|
|
'test1')
|
|
nested_id = nested_identifier.split('/')[-1]
|
|
|
|
# Then check the expected resources are in the nested stack
|
|
nested_resources = {'test1': 'OS::Heat::TestResource'}
|
|
self.assertEqual(nested_resources,
|
|
self.list_resources(nested_identifier))
|
|
tmpl_update = _change_rsrc_properties(
|
|
test_template_two_resource, ['test1', 'test2'],
|
|
{'value': 'test_provider_template'})
|
|
# Add one resource via a stack update by changing the nested stack
|
|
files['provider.template'] = json.dumps(tmpl_update)
|
|
self.update_stack(stack_identifier, self.provider_template,
|
|
environment=env, files=files)
|
|
|
|
# Parent resources should be unchanged and the nested stack
|
|
# should have been updated in-place without replacement
|
|
self.assertEqual(initial_resources,
|
|
self.list_resources(stack_identifier))
|
|
rsrc = self.client.resources.get(stack_identifier, 'test1')
|
|
self.assertEqual(rsrc.physical_resource_id, nested_id)
|
|
|
|
# Then check the expected resources are in the nested stack
|
|
nested_resources = {'test1': 'OS::Heat::TestResource',
|
|
'test2': 'OS::Heat::TestResource'}
|
|
self.assertEqual(nested_resources,
|
|
self.list_resources(nested_identifier))
|
|
|
|
def test_stack_update_alias_type(self):
|
|
env = {'resource_registry':
|
|
{'My::TestResource': 'OS::Heat::RandomString',
|
|
'My::TestResource2': 'OS::Heat::RandomString'}}
|
|
stack_identifier = self.stack_create(
|
|
template=self.provider_template,
|
|
environment=env
|
|
)
|
|
p_res = self.client.resources.get(stack_identifier, 'test1')
|
|
self.assertEqual('My::TestResource', p_res.resource_type)
|
|
|
|
initial_resources = {'test1': 'My::TestResource'}
|
|
self.assertEqual(initial_resources,
|
|
self.list_resources(stack_identifier))
|
|
res = self.client.resources.get(stack_identifier, 'test1')
|
|
# Modify the type of the resource alias to My::TestResource2
|
|
tmpl_update = copy.deepcopy(self.provider_template)
|
|
tmpl_update['resources']['test1']['type'] = 'My::TestResource2'
|
|
self.update_stack(stack_identifier, tmpl_update, environment=env)
|
|
res_a = self.client.resources.get(stack_identifier, 'test1')
|
|
self.assertEqual(res.physical_resource_id, res_a.physical_resource_id)
|
|
self.assertEqual(res.attributes['value'], res_a.attributes['value'])
|
|
|
|
def test_stack_update_alias_changes(self):
|
|
env = {'resource_registry':
|
|
{'My::TestResource': 'OS::Heat::RandomString'}}
|
|
stack_identifier = self.stack_create(
|
|
template=self.provider_template,
|
|
environment=env
|
|
)
|
|
p_res = self.client.resources.get(stack_identifier, 'test1')
|
|
self.assertEqual('My::TestResource', p_res.resource_type)
|
|
|
|
initial_resources = {'test1': 'My::TestResource'}
|
|
self.assertEqual(initial_resources,
|
|
self.list_resources(stack_identifier))
|
|
res = self.client.resources.get(stack_identifier, 'test1')
|
|
# Modify the resource alias to point to a different type
|
|
env = {'resource_registry':
|
|
{'My::TestResource': 'OS::Heat::TestResource'}}
|
|
self.update_stack(stack_identifier, template=self.provider_template,
|
|
environment=env)
|
|
res_a = self.client.resources.get(stack_identifier, 'test1')
|
|
self.assertNotEqual(res.physical_resource_id,
|
|
res_a.physical_resource_id)
|
|
|
|
def test_stack_update_provider_type(self):
|
|
template = _change_rsrc_properties(
|
|
test_template_one_resource, ['test1'],
|
|
{'value': 'test_provider_template'})
|
|
files = {'provider.template': json.dumps(template)}
|
|
env = {'resource_registry':
|
|
{'My::TestResource': 'provider.template',
|
|
'My::TestResource2': 'provider.template'}}
|
|
stack_identifier = self.stack_create(
|
|
template=self.provider_template,
|
|
files=files,
|
|
environment=env
|
|
)
|
|
p_res = self.client.resources.get(stack_identifier, 'test1')
|
|
self.assertEqual('My::TestResource', p_res.resource_type)
|
|
|
|
initial_resources = {'test1': 'My::TestResource'}
|
|
self.assertEqual(initial_resources,
|
|
self.list_resources(stack_identifier))
|
|
|
|
# Prove the resource is backed by a nested stack, save the ID
|
|
nested_identifier = self.assert_resource_is_a_stack(stack_identifier,
|
|
'test1')
|
|
nested_id = nested_identifier.split('/')[-1]
|
|
|
|
# Then check the expected resources are in the nested stack
|
|
nested_resources = {'test1': 'OS::Heat::TestResource'}
|
|
self.assertEqual(nested_resources,
|
|
self.list_resources(nested_identifier))
|
|
n_res = self.client.resources.get(nested_identifier, 'test1')
|
|
|
|
# Modify the type of the provider resource to My::TestResource2
|
|
tmpl_update = copy.deepcopy(self.provider_template)
|
|
tmpl_update['resources']['test1']['type'] = 'My::TestResource2'
|
|
self.update_stack(stack_identifier, tmpl_update,
|
|
environment=env, files=files)
|
|
p_res = self.client.resources.get(stack_identifier, 'test1')
|
|
self.assertEqual('My::TestResource2', p_res.resource_type)
|
|
|
|
# Parent resources should be unchanged and the nested stack
|
|
# should have been updated in-place without replacement
|
|
self.assertEqual({u'test1': u'My::TestResource2'},
|
|
self.list_resources(stack_identifier))
|
|
rsrc = self.client.resources.get(stack_identifier, 'test1')
|
|
self.assertEqual(rsrc.physical_resource_id, nested_id)
|
|
|
|
# Then check the expected resources are in the nested stack
|
|
self.assertEqual(nested_resources,
|
|
self.list_resources(nested_identifier))
|
|
n_res2 = self.client.resources.get(nested_identifier, 'test1')
|
|
self.assertEqual(n_res.physical_resource_id,
|
|
n_res2.physical_resource_id)
|
|
|
|
def test_stack_update_provider_group(self):
|
|
"""Test two-level nested update."""
|
|
|
|
# Create a ResourceGroup (which creates a nested stack),
|
|
# containing provider resources (which create a nested
|
|
# stack), thus exercising an update which traverses
|
|
# two levels of nesting.
|
|
template = _change_rsrc_properties(
|
|
test_template_one_resource, ['test1'],
|
|
{'value': 'test_provider_group_template'})
|
|
files = {'provider.template': json.dumps(template)}
|
|
env = {'resource_registry':
|
|
{'My::TestResource': 'provider.template'}}
|
|
|
|
stack_identifier = self.stack_create(
|
|
template=self.provider_group_template,
|
|
files=files,
|
|
environment=env
|
|
)
|
|
|
|
initial_resources = {'test_group': 'OS::Heat::ResourceGroup'}
|
|
self.assertEqual(initial_resources,
|
|
self.list_resources(stack_identifier))
|
|
|
|
# Prove the resource is backed by a nested stack, save the ID
|
|
nested_identifier = self.assert_resource_is_a_stack(stack_identifier,
|
|
'test_group')
|
|
|
|
# Then check the expected resources are in the nested stack
|
|
nested_resources = {'0': 'My::TestResource',
|
|
'1': 'My::TestResource'}
|
|
self.assertEqual(nested_resources,
|
|
self.list_resources(nested_identifier))
|
|
|
|
for n_rsrc in nested_resources:
|
|
rsrc = self.client.resources.get(nested_identifier, n_rsrc)
|
|
provider_stack = self.client.stacks.get(rsrc.physical_resource_id)
|
|
provider_identifier = '%s/%s' % (provider_stack.stack_name,
|
|
provider_stack.id)
|
|
provider_resources = {u'test1': u'OS::Heat::TestResource'}
|
|
self.assertEqual(provider_resources,
|
|
self.list_resources(provider_identifier))
|
|
|
|
tmpl_update = _change_rsrc_properties(
|
|
test_template_two_resource, ['test1', 'test2'],
|
|
{'value': 'test_provider_group_template'})
|
|
# Add one resource via a stack update by changing the nested stack
|
|
files['provider.template'] = json.dumps(tmpl_update)
|
|
self.update_stack(stack_identifier, self.provider_group_template,
|
|
environment=env, files=files)
|
|
|
|
# Parent resources should be unchanged and the nested stack
|
|
# should have been updated in-place without replacement
|
|
self.assertEqual(initial_resources,
|
|
self.list_resources(stack_identifier))
|
|
|
|
# Resource group stack should also be unchanged (but updated)
|
|
nested_stack = self.client.stacks.get(nested_identifier)
|
|
self.assertEqual('UPDATE_COMPLETE', nested_stack.stack_status)
|
|
self.assertEqual(nested_resources,
|
|
self.list_resources(nested_identifier))
|
|
|
|
for n_rsrc in nested_resources:
|
|
rsrc = self.client.resources.get(nested_identifier, n_rsrc)
|
|
provider_stack = self.client.stacks.get(rsrc.physical_resource_id)
|
|
provider_identifier = '%s/%s' % (provider_stack.stack_name,
|
|
provider_stack.id)
|
|
provider_resources = {'test1': 'OS::Heat::TestResource',
|
|
'test2': 'OS::Heat::TestResource'}
|
|
self.assertEqual(provider_resources,
|
|
self.list_resources(provider_identifier))
|
|
|
|
def test_stack_update_with_replacing_userdata(self):
|
|
"""Test case for updating userdata of instance.
|
|
|
|
Confirm that we can update userdata of instance during updating stack
|
|
by the user of member role.
|
|
|
|
Make sure that a resource that inherits from StackUser can be deleted
|
|
during updating stack.
|
|
"""
|
|
if not self.conf.minimal_image_ref:
|
|
raise self.skipException("No minimal image configured to test")
|
|
if not self.conf.minimal_instance_type:
|
|
raise self.skipException("No flavor configured to test")
|
|
|
|
parms = {'flavor': self.conf.minimal_instance_type,
|
|
'image': self.conf.minimal_image_ref,
|
|
'network': self.conf.fixed_network_name,
|
|
'user_data': ''}
|
|
|
|
stack_identifier = self.stack_create(
|
|
template=self.update_userdata_template,
|
|
parameters=parms
|
|
)
|
|
|
|
parms_updated = parms
|
|
parms_updated['user_data'] = 'two'
|
|
self.update_stack(
|
|
stack_identifier,
|
|
template=self.update_userdata_template,
|
|
parameters=parms_updated)
|
|
|
|
def test_stack_update_provider_group_patch(self):
|
|
'''Test two-level nested update with PATCH'''
|
|
template = _change_rsrc_properties(
|
|
test_template_one_resource, ['test1'],
|
|
{'value': 'test_provider_group_template'})
|
|
files = {'provider.template': json.dumps(template)}
|
|
env = {'resource_registry':
|
|
{'My::TestResource': 'provider.template'}}
|
|
|
|
stack_identifier = self.stack_create(
|
|
template=self.provider_group_template,
|
|
files=files,
|
|
environment=env
|
|
)
|
|
|
|
initial_resources = {'test_group': 'OS::Heat::ResourceGroup'}
|
|
self.assertEqual(initial_resources,
|
|
self.list_resources(stack_identifier))
|
|
|
|
# Prove the resource is backed by a nested stack, save the ID
|
|
nested_identifier = self.assert_resource_is_a_stack(stack_identifier,
|
|
'test_group')
|
|
|
|
# Then check the expected resources are in the nested stack
|
|
nested_resources = {'0': 'My::TestResource',
|
|
'1': 'My::TestResource'}
|
|
self.assertEqual(nested_resources,
|
|
self.list_resources(nested_identifier))
|
|
|
|
# increase the count, pass only the paramter, no env or template
|
|
params = {'count': 3}
|
|
self.update_stack(stack_identifier, parameters=params, existing=True)
|
|
|
|
# Parent resources should be unchanged and the nested stack
|
|
# should have been updated in-place without replacement
|
|
self.assertEqual(initial_resources,
|
|
self.list_resources(stack_identifier))
|
|
|
|
# Resource group stack should also be unchanged (but updated)
|
|
nested_stack = self.client.stacks.get(nested_identifier)
|
|
self.assertEqual('UPDATE_COMPLETE', nested_stack.stack_status)
|
|
# Add a resource, as we should have added one
|
|
nested_resources['2'] = 'My::TestResource'
|
|
self.assertEqual(nested_resources,
|
|
self.list_resources(nested_identifier))
|
|
|
|
def test_stack_update_from_failed_patch(self):
|
|
'''Test PATCH update from a failed state.'''
|
|
|
|
# Start with empty template
|
|
stack_identifier = self.stack_create(
|
|
template='heat_template_version: 2014-10-16')
|
|
|
|
# Update with a good template, but bad parameter
|
|
self.update_stack(stack_identifier,
|
|
template=self.fail_param_template,
|
|
parameters={'do_fail': True},
|
|
expected_status='UPDATE_FAILED')
|
|
|
|
# PATCH update, only providing the parameter
|
|
self.update_stack(stack_identifier,
|
|
parameters={'do_fail': False},
|
|
existing=True)
|
|
self.assertEqual({u'aresource': u'OS::Heat::TestResource'},
|
|
self.list_resources(stack_identifier))
|
|
|
|
def test_stack_update_with_new_env(self):
|
|
"""Update handles new resource types in the environment.
|
|
|
|
If a resource type appears during an update and the update fails,
|
|
retrying the update is able to find the type properly in the
|
|
environment.
|
|
"""
|
|
stack_identifier = self.stack_create(
|
|
template=test_template_one_resource)
|
|
|
|
# Update with a new resource and make the update fails
|
|
template = _change_rsrc_properties(test_template_one_resource,
|
|
['test1'], {'fail': True})
|
|
template['resources']['test2'] = {'type': 'My::TestResource'}
|
|
template['resources']['test1']['depends_on'] = 'test2'
|
|
env = {'resource_registry':
|
|
{'My::TestResource': 'OS::Heat::TestResource'}}
|
|
self.update_stack(stack_identifier,
|
|
template=template,
|
|
environment=env,
|
|
expected_status='UPDATE_FAILED')
|
|
|
|
# Fixing the template should fix the stack
|
|
template = _change_rsrc_properties(template,
|
|
['test1'], {'fail': False})
|
|
template['resources']['test2'][
|
|
'properties'] = {'action_wait_secs': {'update': 1}}
|
|
self.update_stack(stack_identifier,
|
|
template=template,
|
|
environment=env)
|
|
self.assertEqual({'test1': 'OS::Heat::TestResource',
|
|
'test2': 'My::TestResource'},
|
|
self.list_resources(stack_identifier))
|
|
|
|
def test_stack_update_with_new_version(self):
|
|
"""Update handles new template version in failure.
|
|
|
|
If a stack update fails while changing the template version, update is
|
|
able to handle the new version fine.
|
|
"""
|
|
stack_identifier = self.stack_create(
|
|
template=test_template_one_resource)
|
|
|
|
# Update with a new function and make the update fails
|
|
template = _change_rsrc_properties(test_template_two_resource,
|
|
['test1'], {'fail': True})
|
|
|
|
template['heat_template_version'] = '2015-10-15'
|
|
template['resources']['test2']['properties']['value'] = {
|
|
'list_join': [',', ['a'], ['b']]}
|
|
self.update_stack(stack_identifier,
|
|
template=template,
|
|
expected_status='UPDATE_FAILED')
|
|
|
|
template = _change_rsrc_properties(template,
|
|
['test2'], {'value': 'Test2'})
|
|
template['resources']['test1'][
|
|
'properties']['action_wait_secs'] = {'create': 1}
|
|
self.update_stack(stack_identifier,
|
|
template=template,
|
|
expected_status='UPDATE_FAILED')
|
|
self._stack_delete(stack_identifier)
|
|
|
|
def test_stack_update_with_old_version(self):
|
|
"""Update handles old template version in failure.
|
|
|
|
If a stack update fails while changing the template version, update is
|
|
able to handle the old version fine.
|
|
"""
|
|
template = _change_rsrc_properties(
|
|
test_template_one_resource,
|
|
['test1'], {'value': {'list_join': [',', ['a'], ['b']]}})
|
|
template['heat_template_version'] = '2015-10-15'
|
|
stack_identifier = self.stack_create(
|
|
template=template)
|
|
|
|
# Update with a new function and make the update fails
|
|
template = _change_rsrc_properties(test_template_one_resource,
|
|
['test1'], {'fail': True})
|
|
self.update_stack(stack_identifier,
|
|
template=template,
|
|
expected_status='UPDATE_FAILED')
|
|
self._stack_delete(stack_identifier)
|
|
|
|
def _test_conditional(self, test3_resource):
|
|
"""Update manages new conditions added.
|
|
|
|
When a new resource is added during updates, the stacks handles the new
|
|
conditions correctly, and doesn't fail to load them while the update is
|
|
still in progress.
|
|
"""
|
|
stack_identifier = self.stack_create(
|
|
template=test_template_one_resource)
|
|
|
|
updated_template = copy.deepcopy(test_template_two_resource)
|
|
updated_template['conditions'] = {'cond1': True}
|
|
updated_template['resources']['test3'] = test3_resource
|
|
test2_props = updated_template['resources']['test2']['properties']
|
|
test2_props['action_wait_secs'] = {'create': 30}
|
|
|
|
self.update_stack(stack_identifier,
|
|
template=updated_template,
|
|
expected_status='UPDATE_IN_PROGRESS')
|
|
|
|
def check_resources():
|
|
def is_complete(r):
|
|
return r.resource_status in {'CREATE_COMPLETE',
|
|
'UPDATE_COMPLETE'}
|
|
|
|
resources = self.list_resources(stack_identifier, is_complete)
|
|
if len(resources) < 2:
|
|
return False
|
|
self.assertIn('test3', resources)
|
|
return True
|
|
|
|
self.assertTrue(test.call_until_true(20, 2, check_resources))
|
|
|
|
def test_stack_update_with_if_conditions(self):
|
|
test3 = {
|
|
'type': 'OS::Heat::TestResource',
|
|
'properties': {
|
|
'value': {'if': ['cond1', 'val3', 'val4']}
|
|
}
|
|
}
|
|
self._test_conditional(test3)
|
|
|
|
def test_stack_update_with_conditions(self):
|
|
test3 = {
|
|
'type': 'OS::Heat::TestResource',
|
|
'condition': 'cond1',
|
|
'properties': {
|
|
'value': 'foo',
|
|
}
|
|
}
|
|
self._test_conditional(test3)
|
|
|
|
def test_inplace_update_old_ref_deleted_failed_stack(self):
|
|
template = '''
|
|
heat_template_version: rocky
|
|
resources:
|
|
test1:
|
|
type: OS::Heat::TestResource
|
|
properties:
|
|
value: test
|
|
test2:
|
|
type: OS::Heat::TestResource
|
|
properties:
|
|
value: {get_attr: [test1, output]}
|
|
test3:
|
|
type: OS::Heat::TestResource
|
|
properties:
|
|
value: test3
|
|
fail: false
|
|
action_wait_secs:
|
|
update: 5
|
|
'''
|
|
stack_identifier = self.stack_create(
|
|
template=template)
|
|
|
|
_template = template.replace('test1:',
|
|
'test-1:').replace('fail: false',
|
|
'fail: true')
|
|
updated_template = _template.replace(
|
|
'{get_attr: [test1',
|
|
'{get_attr: [test-1').replace('value: test3',
|
|
'value: test-3')
|
|
self.update_stack(stack_identifier,
|
|
template=updated_template,
|
|
expected_status='UPDATE_FAILED')
|
|
self.update_stack(stack_identifier, template=template,
|
|
expected_status='UPDATE_COMPLETE')
|