Improve StackValidationFailed exception

We use StackValidationFailed in many different scenarios and
the the message is at times extremely unhelpful, specifically
when the validation error is deep in a nested stack.

Change-Id: I0183bdf81442e62325a427b4eec5c4cd9b7cb91f
Closes-Bug: #1686360
changes/02/466802/3
rabi 5 years ago
parent 30c87bae3d
commit fe74658ff0
  1. 37
      heat/common/exception.py
  2. 15
      heat/engine/resources/openstack/heat/resource_chain.py
  3. 5
      heat/engine/resources/openstack/heat/resource_group.py
  4. 16
      heat/engine/resources/stack_resource.py
  5. 20
      heat/engine/resources/template_resource.py
  6. 5
      heat/engine/stack.py
  7. 5
      heat/tests/openstack/heat/test_resource_chain.py
  8. 15
      heat/tests/test_exception.py
  9. 4
      heat/tests/test_provider_template.py
  10. 13
      heat/tests/test_stack_resource.py
  11. 4
      heat_integrationtests/functional/test_resource_group.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):

@ -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())

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

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

@ -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):

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

@ -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<nested_stack>.resources[2].'
'properties: unknown property group',
e.message.lower())
def test_validate_fake_resource_type(self):
# Setup

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

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

@ -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<file://tmp/nested.yaml>: '
'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))

@ -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<nested_stack>.resources."
"0<provider.yaml>.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)

Loading…
Cancel
Save