diff --git a/heat/common/exception.py b/heat/common/exception.py index eb9165279c..0947f48a8c 100644 --- a/heat/common/exception.py +++ b/heat/common/exception.py @@ -227,18 +227,35 @@ class HeatExceptionWithPath(HeatException): message=self.error_message ) - def error(self): - return self.error - - def path(self): - return self.path - - def error_message(self): - return self.error_message - class StackValidationFailed(HeatExceptionWithPath): - pass + def __init__(self, error=None, path=None, message=None, + resource=None): + if path is None: + path = [] + elif isinstance(path, six.string_types): + path = [path] + + if resource is not None and not path: + path = [resource.stack.t.get_section_name( + resource.stack.t.RESOURCES), resource.name] + if isinstance(error, Exception): + if isinstance(error, StackValidationFailed): + str_error = error.error + message = error.error_message + path = path + error.path + # This is a hack to avoid the py3 (chained exception) + # json serialization circular reference error from + # oslo.messaging. + self.args = error.args + else: + str_error = six.text_type(type(error).__name__) + message = six.text_type(error) + else: + str_error = error + + super(StackValidationFailed, self).__init__(error=str_error, path=path, + message=message) class InvalidSchemaError(HeatException): diff --git a/heat/engine/resources/openstack/heat/resource_chain.py b/heat/engine/resources/openstack/heat/resource_chain.py index 497d945692..f90a649cbe 100644 --- a/heat/engine/resources/openstack/heat/resource_chain.py +++ b/heat/engine/resources/openstack/heat/resource_chain.py @@ -98,20 +98,7 @@ class ResourceChain(stack_resource.StackResource): # Valid if it's a template resource pass - # Check the nested template itself - nested_tmpl = self.child_template() - nested_stack_name = '%s-%s' % (self.stack.name, self.name) - - try: - nested_stack = self._parse_nested_stack(nested_stack_name, - nested_tmpl, - {}) - nested_stack.strict_validate = False - nested_stack.validate() - except Exception as ex: - msg = (_('Failed to validate nested template: %s') - % six.text_type(ex)) - raise exception.StackValidationFailed(message=msg) + super(ResourceChain, self).validate_nested_stack() def handle_create(self): return self.create_with_template(self.child_template()) diff --git a/heat/engine/resources/openstack/heat/resource_group.py b/heat/engine/resources/openstack/heat/resource_group.py index 894ba49338..83763df44d 100644 --- a/heat/engine/resources/openstack/heat/resource_group.py +++ b/heat/engine/resources/openstack/heat/resource_group.py @@ -295,8 +295,9 @@ class ResourceGroup(stack_resource.StackResource): nested_stack.strict_validate = False nested_stack.validate() except Exception as ex: - msg = _("Failed to validate: %s") % six.text_type(ex) - raise exception.StackValidationFailed(message=msg) + path = "%s<%s>" % (self.name, self.template_url) + raise exception.StackValidationFailed( + ex, path=[self.stack.t.RESOURCES, path]) def _current_blacklist(self): db_rsrc_names = self.data().get('name_blacklist') diff --git a/heat/engine/resources/stack_resource.py b/heat/engine/resources/stack_resource.py index c1f4ccf86a..62c1fbb7b3 100644 --- a/heat/engine/resources/stack_resource.py +++ b/heat/engine/resources/stack_resource.py @@ -76,10 +76,20 @@ class StackResource(resource.Resource): except AssertionError: raise except Exception as ex: + path = "%s<%s>" % (self.name, self.template_url) raise exception.StackValidationFailed( - error=_("Failed to validate"), - path=[self.stack.t.get_section_name('resources'), self.name], - message=six.text_type(ex)) + ex, path=[self.stack.t.RESOURCES, path]) + + @property + def template_url(self): + """Template url for the stack resource. + + When stack resource is a TemplateResource, it's the template + location. For group resources like ResourceGroup where the + template is constructed dynamically, it's just a placeholder. + """ + + return "nested_stack" def _outputs_to_attribs(self, json_snippet): outputs = json_snippet.get('Outputs') diff --git a/heat/engine/resources/template_resource.py b/heat/engine/resources/template_resource.py index 1b5b41a9f1..d55ef881d2 100644 --- a/heat/engine/resources/template_resource.py +++ b/heat/engine/resources/template_resource.py @@ -79,7 +79,7 @@ class TemplateResource(stack_resource.StackResource): 'Only Templates with an extension of .yaml or ' '.template are supported')) else: - self.template_name = tri.template_name + self._template_name = tri.template_name self.resource_type = tri.name self.resource_path = tri.path if tri.user_resource: @@ -174,24 +174,28 @@ class TemplateResource(stack_resource.StackResource): def child_template(self): if not self._parsed_nested: self._parsed_nested = template_format.parse(self.template_data(), - self.template_name) + self.template_url) return self._parsed_nested def regenerate_info_schema(self, definition): self._get_resource_info(definition) self._generate_schema() + @property + def template_url(self): + return self._template_name + def template_data(self): # we want to have the latest possible template. # 1. look in files # 2. try download # 3. look in the db reported_excp = None - t_data = self.stack.t.files.get(self.template_name) + t_data = self.stack.t.files.get(self.template_url) stored_t_data = t_data - if not t_data and self.template_name.endswith((".yaml", ".template")): + if not t_data and self.template_url.endswith((".yaml", ".template")): try: - t_data = self.get_template_file(self.template_name, + t_data = self.get_template_file(self.template_url, self.allowed_schemes) except exception.NotFound as err: if self.action == self.UPDATE: @@ -204,14 +208,14 @@ class TemplateResource(stack_resource.StackResource): if t_data is not None: if t_data != stored_t_data: - self.stack.t.files[self.template_name] = t_data + self.stack.t.files[self.template_url] = t_data self.stack.t.env.register_class(self.resource_type, - self.template_name, + self.template_url, path=self.resource_path) return t_data if reported_excp is None: reported_excp = ValueError(_('Unknown error retrieving %s') % - self.template_name) + self.template_url) raise reported_excp def _validate_against_facade(self, facade_cls): diff --git a/heat/engine/stack.py b/heat/engine/stack.py index 098f95b260..e8b57ec251 100644 --- a/heat/engine/stack.py +++ b/heat/engine/stack.py @@ -21,7 +21,6 @@ import warnings from oslo_config import cfg from oslo_log import log as logging -from oslo_utils import encodeutils from oslo_utils import excutils from oslo_utils import timeutils as oslo_timeutils from oslo_utils import uuidutils @@ -858,8 +857,8 @@ class Stack(collections.Mapping): except Exception as ex: LOG.info("Exception in stack validation", exc_info=True) - raise exception.StackValidationFailed( - message=encodeutils.safe_decode(six.text_type(ex))) + raise exception.StackValidationFailed(error=ex, + resource=res) if result: raise exception.StackValidationFailed(message=result) eventlet.sleep(0) diff --git a/heat/tests/openstack/heat/test_resource_chain.py b/heat/tests/openstack/heat/test_resource_chain.py index 63b55cff50..a95819e88e 100644 --- a/heat/tests/openstack/heat/test_resource_chain.py +++ b/heat/tests/openstack/heat/test_resource_chain.py @@ -146,7 +146,10 @@ class ResourceChainTest(common.HeatTestCase): chain.validate_nested_stack() self.fail('Exception expected') except exception.StackValidationFailed as e: - self.assertIn('unknown property group', e.message.lower()) + self.assertEqual('property error: ' + 'resources.test.resources[2].' + 'properties: unknown property group', + e.message.lower()) def test_validate_fake_resource_type(self): # Setup diff --git a/heat/tests/test_exception.py b/heat/tests/test_exception.py index 727f47108b..593c29f868 100644 --- a/heat/tests/test_exception.py +++ b/heat/tests/test_exception.py @@ -45,6 +45,17 @@ class TestHeatException(common.HeatTestCase): class TestStackValidationFailed(common.HeatTestCase): scenarios = [ + ('test_error_as_exception', dict( + kwargs=dict( + error=exception.StackValidationFailed( + error='Error', + path=['some', 'path'], + message='Some message')), + expected='Error: some.path: Some message', + called_error='Error', + called_path=['some', 'path'], + called_msg='Some message' + )), ('test_full_exception', dict( kwargs=dict( error='Error', @@ -124,8 +135,8 @@ class TestStackValidationFailed(common.HeatTestCase): try: raise exception.StackValidationFailed(**self.kwargs) except exception.StackValidationFailed as ex: - self.assertEqual(self.expected, six.text_type(ex)) - self.assertEqual(self.called_error, ex.error) + self.assertIn(self.expected, six.text_type(ex)) + self.assertIn(self.called_error, ex.error) self.assertEqual(self.called_path, ex.path) self.assertEqual(self.called_msg, ex.error_message) diff --git a/heat/tests/test_provider_template.py b/heat/tests/test_provider_template.py index 9c0c0ced87..8de20c782e 100644 --- a/heat/tests/test_provider_template.py +++ b/heat/tests/test_provider_template.py @@ -579,7 +579,7 @@ class ProviderTemplateTest(common.HeatTestCase): temp_res = template_resource.TemplateResource('test_t_res', definition, stack) self.assertEqual('test_resource.template', - temp_res.template_name) + temp_res.template_url) def test_resource_info_special(self): provider = { @@ -614,7 +614,7 @@ class ProviderTemplateTest(common.HeatTestCase): temp_res = template_resource.TemplateResource('foo', definition, stack) self.assertEqual('foo.template', - temp_res.template_name) + temp_res.template_url) def test_get_error_for_invalid_template_name(self): # assertion: if the name matches {.yaml|.template} and is valid diff --git a/heat/tests/test_stack_resource.py b/heat/tests/test_stack_resource.py index 7fb63bd2ab..4fc78001d8 100644 --- a/heat/tests/test_stack_resource.py +++ b/heat/tests/test_stack_resource.py @@ -79,7 +79,7 @@ main_template = ''' heat_template_version: 2013-05-23 resources: volume_server: - type: nested.yaml + type: file://tmp/nested.yaml ''' my_wrong_nested_template = ''' @@ -396,14 +396,15 @@ class StackResourceTest(StackResourceBaseTest): def test_validate_error_reference(self): stack_name = 'validate_error_reference' tmpl = template_format.parse(main_template) - files = {'nested.yaml': my_wrong_nested_template} + files = {'file://tmp/nested.yaml': my_wrong_nested_template} stack = parser.Stack(utils.dummy_context(), stack_name, templatem.Template(tmpl, files=files)) rsrc = stack['volume_server'] - raise_exc_msg = ('Failed to validate: resources.volume_server: ' - 'The specified reference "instance" ' - '(in volume_attachment.Properties.instance_uuid) ' - 'is incorrect.') + raise_exc_msg = ('InvalidTemplateReference: ' + 'resources.volume_server: ' + 'The specified reference "instance" (in ' + 'volume_attachment.Properties.instance_uuid) is ' + 'incorrect.') exc = self.assertRaises(exception.StackValidationFailed, rsrc.validate) self.assertEqual(raise_exc_msg, six.text_type(exc)) diff --git a/heat_integrationtests/functional/test_resource_group.py b/heat_integrationtests/functional/test_resource_group.py index 8d8cc89be0..3f47ca5a38 100644 --- a/heat_integrationtests/functional/test_resource_group.py +++ b/heat_integrationtests/functional/test_resource_group.py @@ -82,7 +82,9 @@ resources: # Prove validation works for non-zero create/update template_two_nested = self.template.replace("count: 0", "count: 2") - expected_err = "Value 'BAD' is not an integer" + expected_err = ("resources.random_group.resources." + "0.resources.random: : " + "Value 'BAD' is not an integer") ex = self.assertRaises(exc.HTTPBadRequest, self.update_stack, stack_identifier, template_two_nested, environment=env, files=files)