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
|
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):
|
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):
|
class InvalidSchemaError(HeatException):
|
||||||
|
@ -98,20 +98,7 @@ class ResourceChain(stack_resource.StackResource):
|
|||||||
# Valid if it's a template resource
|
# Valid if it's a template resource
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Check the nested template itself
|
super(ResourceChain, self).validate_nested_stack()
|
||||||
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)
|
|
||||||
|
|
||||||
def handle_create(self):
|
def handle_create(self):
|
||||||
return self.create_with_template(self.child_template())
|
return self.create_with_template(self.child_template())
|
||||||
|
@ -295,8 +295,9 @@ class ResourceGroup(stack_resource.StackResource):
|
|||||||
nested_stack.strict_validate = False
|
nested_stack.strict_validate = False
|
||||||
nested_stack.validate()
|
nested_stack.validate()
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
msg = _("Failed to validate: %s") % six.text_type(ex)
|
path = "%s<%s>" % (self.name, self.template_url)
|
||||||
raise exception.StackValidationFailed(message=msg)
|
raise exception.StackValidationFailed(
|
||||||
|
ex, path=[self.stack.t.RESOURCES, path])
|
||||||
|
|
||||||
def _current_blacklist(self):
|
def _current_blacklist(self):
|
||||||
db_rsrc_names = self.data().get('name_blacklist')
|
db_rsrc_names = self.data().get('name_blacklist')
|
||||||
|
@ -76,10 +76,20 @@ class StackResource(resource.Resource):
|
|||||||
except AssertionError:
|
except AssertionError:
|
||||||
raise
|
raise
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
|
path = "%s<%s>" % (self.name, self.template_url)
|
||||||
raise exception.StackValidationFailed(
|
raise exception.StackValidationFailed(
|
||||||
error=_("Failed to validate"),
|
ex, path=[self.stack.t.RESOURCES, path])
|
||||||
path=[self.stack.t.get_section_name('resources'), self.name],
|
|
||||||
message=six.text_type(ex))
|
@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):
|
def _outputs_to_attribs(self, json_snippet):
|
||||||
outputs = json_snippet.get('Outputs')
|
outputs = json_snippet.get('Outputs')
|
||||||
|
@ -79,7 +79,7 @@ class TemplateResource(stack_resource.StackResource):
|
|||||||
'Only Templates with an extension of .yaml or '
|
'Only Templates with an extension of .yaml or '
|
||||||
'.template are supported'))
|
'.template are supported'))
|
||||||
else:
|
else:
|
||||||
self.template_name = tri.template_name
|
self._template_name = tri.template_name
|
||||||
self.resource_type = tri.name
|
self.resource_type = tri.name
|
||||||
self.resource_path = tri.path
|
self.resource_path = tri.path
|
||||||
if tri.user_resource:
|
if tri.user_resource:
|
||||||
@ -174,24 +174,28 @@ class TemplateResource(stack_resource.StackResource):
|
|||||||
def child_template(self):
|
def child_template(self):
|
||||||
if not self._parsed_nested:
|
if not self._parsed_nested:
|
||||||
self._parsed_nested = template_format.parse(self.template_data(),
|
self._parsed_nested = template_format.parse(self.template_data(),
|
||||||
self.template_name)
|
self.template_url)
|
||||||
return self._parsed_nested
|
return self._parsed_nested
|
||||||
|
|
||||||
def regenerate_info_schema(self, definition):
|
def regenerate_info_schema(self, definition):
|
||||||
self._get_resource_info(definition)
|
self._get_resource_info(definition)
|
||||||
self._generate_schema()
|
self._generate_schema()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def template_url(self):
|
||||||
|
return self._template_name
|
||||||
|
|
||||||
def template_data(self):
|
def template_data(self):
|
||||||
# we want to have the latest possible template.
|
# we want to have the latest possible template.
|
||||||
# 1. look in files
|
# 1. look in files
|
||||||
# 2. try download
|
# 2. try download
|
||||||
# 3. look in the db
|
# 3. look in the db
|
||||||
reported_excp = None
|
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
|
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:
|
try:
|
||||||
t_data = self.get_template_file(self.template_name,
|
t_data = self.get_template_file(self.template_url,
|
||||||
self.allowed_schemes)
|
self.allowed_schemes)
|
||||||
except exception.NotFound as err:
|
except exception.NotFound as err:
|
||||||
if self.action == self.UPDATE:
|
if self.action == self.UPDATE:
|
||||||
@ -204,14 +208,14 @@ class TemplateResource(stack_resource.StackResource):
|
|||||||
|
|
||||||
if t_data is not None:
|
if t_data is not None:
|
||||||
if t_data != stored_t_data:
|
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.stack.t.env.register_class(self.resource_type,
|
||||||
self.template_name,
|
self.template_url,
|
||||||
path=self.resource_path)
|
path=self.resource_path)
|
||||||
return t_data
|
return t_data
|
||||||
if reported_excp is None:
|
if reported_excp is None:
|
||||||
reported_excp = ValueError(_('Unknown error retrieving %s') %
|
reported_excp = ValueError(_('Unknown error retrieving %s') %
|
||||||
self.template_name)
|
self.template_url)
|
||||||
raise reported_excp
|
raise reported_excp
|
||||||
|
|
||||||
def _validate_against_facade(self, facade_cls):
|
def _validate_against_facade(self, facade_cls):
|
||||||
|
@ -21,7 +21,6 @@ import warnings
|
|||||||
|
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
from oslo_utils import encodeutils
|
|
||||||
from oslo_utils import excutils
|
from oslo_utils import excutils
|
||||||
from oslo_utils import timeutils as oslo_timeutils
|
from oslo_utils import timeutils as oslo_timeutils
|
||||||
from oslo_utils import uuidutils
|
from oslo_utils import uuidutils
|
||||||
@ -858,8 +857,8 @@ class Stack(collections.Mapping):
|
|||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
LOG.info("Exception in stack validation",
|
LOG.info("Exception in stack validation",
|
||||||
exc_info=True)
|
exc_info=True)
|
||||||
raise exception.StackValidationFailed(
|
raise exception.StackValidationFailed(error=ex,
|
||||||
message=encodeutils.safe_decode(six.text_type(ex)))
|
resource=res)
|
||||||
if result:
|
if result:
|
||||||
raise exception.StackValidationFailed(message=result)
|
raise exception.StackValidationFailed(message=result)
|
||||||
eventlet.sleep(0)
|
eventlet.sleep(0)
|
||||||
|
@ -146,7 +146,10 @@ class ResourceChainTest(common.HeatTestCase):
|
|||||||
chain.validate_nested_stack()
|
chain.validate_nested_stack()
|
||||||
self.fail('Exception expected')
|
self.fail('Exception expected')
|
||||||
except exception.StackValidationFailed as e:
|
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):
|
def test_validate_fake_resource_type(self):
|
||||||
# Setup
|
# Setup
|
||||||
|
@ -45,6 +45,17 @@ class TestHeatException(common.HeatTestCase):
|
|||||||
class TestStackValidationFailed(common.HeatTestCase):
|
class TestStackValidationFailed(common.HeatTestCase):
|
||||||
|
|
||||||
scenarios = [
|
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(
|
('test_full_exception', dict(
|
||||||
kwargs=dict(
|
kwargs=dict(
|
||||||
error='Error',
|
error='Error',
|
||||||
@ -124,8 +135,8 @@ class TestStackValidationFailed(common.HeatTestCase):
|
|||||||
try:
|
try:
|
||||||
raise exception.StackValidationFailed(**self.kwargs)
|
raise exception.StackValidationFailed(**self.kwargs)
|
||||||
except exception.StackValidationFailed as ex:
|
except exception.StackValidationFailed as ex:
|
||||||
self.assertEqual(self.expected, six.text_type(ex))
|
self.assertIn(self.expected, six.text_type(ex))
|
||||||
self.assertEqual(self.called_error, ex.error)
|
self.assertIn(self.called_error, ex.error)
|
||||||
self.assertEqual(self.called_path, ex.path)
|
self.assertEqual(self.called_path, ex.path)
|
||||||
self.assertEqual(self.called_msg, ex.error_message)
|
self.assertEqual(self.called_msg, ex.error_message)
|
||||||
|
|
||||||
|
@ -579,7 +579,7 @@ class ProviderTemplateTest(common.HeatTestCase):
|
|||||||
temp_res = template_resource.TemplateResource('test_t_res',
|
temp_res = template_resource.TemplateResource('test_t_res',
|
||||||
definition, stack)
|
definition, stack)
|
||||||
self.assertEqual('test_resource.template',
|
self.assertEqual('test_resource.template',
|
||||||
temp_res.template_name)
|
temp_res.template_url)
|
||||||
|
|
||||||
def test_resource_info_special(self):
|
def test_resource_info_special(self):
|
||||||
provider = {
|
provider = {
|
||||||
@ -614,7 +614,7 @@ class ProviderTemplateTest(common.HeatTestCase):
|
|||||||
temp_res = template_resource.TemplateResource('foo',
|
temp_res = template_resource.TemplateResource('foo',
|
||||||
definition, stack)
|
definition, stack)
|
||||||
self.assertEqual('foo.template',
|
self.assertEqual('foo.template',
|
||||||
temp_res.template_name)
|
temp_res.template_url)
|
||||||
|
|
||||||
def test_get_error_for_invalid_template_name(self):
|
def test_get_error_for_invalid_template_name(self):
|
||||||
# assertion: if the name matches {.yaml|.template} and is valid
|
# assertion: if the name matches {.yaml|.template} and is valid
|
||||||
|
@ -79,7 +79,7 @@ main_template = '''
|
|||||||
heat_template_version: 2013-05-23
|
heat_template_version: 2013-05-23
|
||||||
resources:
|
resources:
|
||||||
volume_server:
|
volume_server:
|
||||||
type: nested.yaml
|
type: file://tmp/nested.yaml
|
||||||
'''
|
'''
|
||||||
|
|
||||||
my_wrong_nested_template = '''
|
my_wrong_nested_template = '''
|
||||||
@ -396,14 +396,15 @@ class StackResourceTest(StackResourceBaseTest):
|
|||||||
def test_validate_error_reference(self):
|
def test_validate_error_reference(self):
|
||||||
stack_name = 'validate_error_reference'
|
stack_name = 'validate_error_reference'
|
||||||
tmpl = template_format.parse(main_template)
|
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,
|
stack = parser.Stack(utils.dummy_context(), stack_name,
|
||||||
templatem.Template(tmpl, files=files))
|
templatem.Template(tmpl, files=files))
|
||||||
rsrc = stack['volume_server']
|
rsrc = stack['volume_server']
|
||||||
raise_exc_msg = ('Failed to validate: resources.volume_server: '
|
raise_exc_msg = ('InvalidTemplateReference: '
|
||||||
'The specified reference "instance" '
|
'resources.volume_server<file://tmp/nested.yaml>: '
|
||||||
'(in volume_attachment.Properties.instance_uuid) '
|
'The specified reference "instance" (in '
|
||||||
'is incorrect.')
|
'volume_attachment.Properties.instance_uuid) is '
|
||||||
|
'incorrect.')
|
||||||
exc = self.assertRaises(exception.StackValidationFailed,
|
exc = self.assertRaises(exception.StackValidationFailed,
|
||||||
rsrc.validate)
|
rsrc.validate)
|
||||||
self.assertEqual(raise_exc_msg, six.text_type(exc))
|
self.assertEqual(raise_exc_msg, six.text_type(exc))
|
||||||
|
@ -82,7 +82,9 @@ resources:
|
|||||||
|
|
||||||
# Prove validation works for non-zero create/update
|
# Prove validation works for non-zero create/update
|
||||||
template_two_nested = self.template.replace("count: 0", "count: 2")
|
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,
|
ex = self.assertRaises(exc.HTTPBadRequest, self.update_stack,
|
||||||
stack_identifier, template_two_nested,
|
stack_identifier, template_two_nested,
|
||||||
environment=env, files=files)
|
environment=env, files=files)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user