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
This commit is contained in:
parent
30c87bae3d
commit
fe74658ff0
@ -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…
Reference in New Issue
Block a user