diff --git a/heat/engine/resources/stack_resource.py b/heat/engine/resources/stack_resource.py index 649139d7b7..0fd572e27d 100644 --- a/heat/engine/resources/stack_resource.py +++ b/heat/engine/resources/stack_resource.py @@ -31,6 +31,7 @@ from heat.engine import resource from heat.engine import scheduler from heat.engine import stack as parser from heat.engine import template +from heat.objects import raw_template from heat.objects import stack as stack_object from heat.objects import stack_lock from heat.rpc import api as rpc_api @@ -266,7 +267,7 @@ class StackResource(resource.Resource): timeout_mins = self.stack.timeout_mins stack_user_project_id = self.stack.stack_user_project_id - kwargs = self._stack_kwargs(user_params, child_template) + kwargs = self._stack_kwargs(user_params, child_template, adopt_data) adopt_data_str = None if adopt_data is not None: @@ -293,11 +294,18 @@ class StackResource(resource.Resource): 'parent_resource_name': self.name }) with self.translate_remote_exceptions: - result = self.rpc_client()._create_stack(self.context, **kwargs) + result = None + try: + result = self.rpc_client()._create_stack(self.context, + **kwargs) + finally: + if adopt_data is None and not result: + raw_template.RawTemplate.delete(self.context, + kwargs['template_id']) self.resource_id_set(result['stack_id']) - def _stack_kwargs(self, user_params, child_template): + def _stack_kwargs(self, user_params, child_template, adopt_data=None): if user_params is None: user_params = self.child_params() @@ -311,11 +319,20 @@ class StackResource(resource.Resource): parsed_template = self._child_parsed_template(child_template, child_env) - return { - 'template': parsed_template.t, - 'params': child_env.user_env_as_dict(), - 'files': parsed_template.files - } + if adopt_data is None: + template_id = parsed_template.store(self.context) + return { + 'template_id': template_id, + 'template': None, + 'params': None, + 'files': None, + } + else: + return { + 'template': parsed_template.t, + 'params': child_env.user_env_as_dict(), + 'files': parsed_template.files, + } def raise_local_exception(self, ex): warnings.warn('raise_local_exception() is deprecated. Use the ' @@ -439,7 +456,14 @@ class StackResource(resource.Resource): 'args': {rpc_api.PARAM_TIMEOUT: timeout_mins} }) with self.translate_remote_exceptions: - self.rpc_client().update_stack(self.context, **kwargs) + result = None + try: + result = self.rpc_client()._update_stack(self.context, + **kwargs) + finally: + if not result: + raw_template.RawTemplate.delete(self.context, + kwargs['template_id']) return cookie def check_update_complete(self, cookie=None): diff --git a/heat/tests/test_stack_resource.py b/heat/tests/test_stack_resource.py index cd5927c1b2..277aa95946 100644 --- a/heat/tests/test_stack_resource.py +++ b/heat/tests/test_stack_resource.py @@ -25,6 +25,7 @@ from heat.common import template_format from heat.engine.resources import stack_resource from heat.engine import stack as parser from heat.engine import template as templatem +from heat.objects import raw_template from heat.objects import stack as stack_object from heat.objects import stack_lock from heat.tests import common @@ -826,6 +827,16 @@ class WithTemplateTest(StackResourceBaseTest): adopt_data={'template': 'foo', 'environment': 'eee'})), ] + class IntegerMatch(object): + def __eq__(self, other): + if getattr(self, 'match', None) is not None: + return other == self.match + if not isinstance(other, six.integer_types): + return False + + self.match = other + return True + def test_create_with_template(self): child_env = {'parameter_defaults': {}, 'event_sinks': [], @@ -841,15 +852,24 @@ class WithTemplateTest(StackResourceBaseTest): self.parent_resource.create_with_template( self.empty_temp, user_params=self.params, timeout_mins=self.timeout_mins, adopt_data=self.adopt_data) - adopt_data_str = None - if self.adopt_data: + if self.adopt_data is not None: adopt_data_str = json.dumps(self.adopt_data) + tmpl_args = { + 'template': self.empty_temp.t, + 'params': child_env, + 'files': {}, + } + else: + adopt_data_str = None + tmpl_args = { + 'template_id': self.IntegerMatch(), + 'template': None, + 'params': None, + 'files': None, + } rpcc.return_value._create_stack.assert_called_once_with( self.ctx, stack_name=res_name, - template=self.empty_temp.t, - params=child_env, - files={}, args={'disable_rollback': True, 'adopt_stack_data': adopt_data_str, 'timeout_mins': self.timeout_mins}, @@ -858,16 +878,12 @@ class WithTemplateTest(StackResourceBaseTest): parent_resource_name='test', user_creds_id='uc123', owner_id=self.parent_stack.id, - nested_depth=1) + nested_depth=1, + **tmpl_args) - def test_update_with_template(self): - nested = mock.MagicMock() - nested.updated_time = 'now_time' - nested.state = ('CREATE', 'COMPLETE') - nested.identifier.return_value = {'stack_identifier': - 'stack-identifier'} - self.parent_resource.nested = mock.MagicMock(return_value=nested) - self.parent_resource._nested = nested + def test_create_with_template_failure(self): + class StackValidationFailed_Remote(exception.StackValidationFailed): + pass child_env = {'parameter_defaults': {}, 'event_sinks': [], @@ -876,20 +892,117 @@ class WithTemplateTest(StackResourceBaseTest): 'encrypted_param_names': []} self.parent_resource.child_params = mock.Mock( return_value=self.params) + res_name = self.parent_resource.physical_resource_name() rpcc = mock.Mock() self.parent_resource.rpc_client = rpcc - rpcc.return_value._create_stack.return_value = {'stack_id': 'pancakes'} + remote_exc = StackValidationFailed_Remote(message='oops') + rpcc.return_value._create_stack.side_effect = remote_exc + self.assertRaises(exception.ResourceFailure, + self.parent_resource.create_with_template, + self.empty_temp, user_params=self.params, + timeout_mins=self.timeout_mins, + adopt_data=self.adopt_data) + if self.adopt_data is not None: + adopt_data_str = json.dumps(self.adopt_data) + tmpl_args = { + 'template': self.empty_temp.t, + 'params': child_env, + 'files': {}, + } + else: + adopt_data_str = None + tmpl_args = { + 'template_id': self.IntegerMatch(), + 'template': None, + 'params': None, + 'files': None, + } + rpcc.return_value._create_stack.assert_called_once_with( + self.ctx, + stack_name=res_name, + args={'disable_rollback': True, + 'adopt_stack_data': adopt_data_str, + 'timeout_mins': self.timeout_mins}, + environment_files=None, + stack_user_project_id='aprojectid', + parent_resource_name='test', + user_creds_id='uc123', + owner_id=self.parent_stack.id, + nested_depth=1, + **tmpl_args) + if self.adopt_data is None: + stored_tmpl_id = tmpl_args['template_id'].match + self.assertIsNotNone(stored_tmpl_id) + self.assertRaises(exception.NotFound, + raw_template.RawTemplate.get_by_id, + self.ctx, stored_tmpl_id) + + def test_update_with_template(self): + if self.adopt_data is not None: + return + nested = mock.MagicMock() + nested.updated_time = 'now_time' + nested.state = ('CREATE', 'COMPLETE') + nested.identifier.return_value = {'stack_identifier': + 'stack-identifier'} + self.parent_resource.nested = mock.MagicMock(return_value=nested) + self.parent_resource._nested = nested + + self.parent_resource.child_params = mock.Mock( + return_value=self.params) + rpcc = mock.Mock() + self.parent_resource.rpc_client = rpcc + rpcc.return_value._update_stack.return_value = {'stack_id': 'pancakes'} self.parent_resource.update_with_template( self.empty_temp, user_params=self.params, timeout_mins=self.timeout_mins) - rpcc.return_value.update_stack.assert_called_once_with( + rpcc.return_value._update_stack.assert_called_once_with( self.ctx, stack_identity={'stack_identifier': 'stack-identifier'}, - template=self.empty_temp.t, - params=child_env, - files={}, + template_id=self.IntegerMatch(), + template=None, + params=None, + files=None, args={'timeout_mins': self.timeout_mins}) + def test_update_with_template_failure(self): + class StackValidationFailed_Remote(exception.StackValidationFailed): + pass + + if self.adopt_data is not None: + return + nested = mock.MagicMock() + nested.updated_time = 'now_time' + nested.state = ('CREATE', 'COMPLETE') + nested.identifier.return_value = {'stack_identifier': + 'stack-identifier'} + self.parent_resource.nested = mock.MagicMock(return_value=nested) + self.parent_resource._nested = nested + + self.parent_resource.child_params = mock.Mock( + return_value=self.params) + rpcc = mock.Mock() + self.parent_resource.rpc_client = rpcc + remote_exc = StackValidationFailed_Remote(message='oops') + rpcc.return_value._update_stack.side_effect = remote_exc + self.assertRaises(exception.ResourceFailure, + self.parent_resource.update_with_template, + self.empty_temp, user_params=self.params, + timeout_mins=self.timeout_mins) + template_id = self.IntegerMatch() + rpcc.return_value._update_stack.assert_called_once_with( + self.ctx, + stack_identity={'stack_identifier': 'stack-identifier'}, + template_id=template_id, + template=None, + params=None, + files=None, + args={'timeout_mins': self.timeout_mins}) + self.assertIsNotNone(template_id.match) + self.assertRaises(exception.NotFound, + raw_template.RawTemplate.get_by_id, + self.ctx, template_id.match) + class RaiseLocalException(StackResourceBaseTest):