diff --git a/heat/engine/function.py b/heat/engine/function.py index 8477964fba..aa3d7d4b51 100644 --- a/heat/engine/function.py +++ b/heat/engine/function.py @@ -18,6 +18,7 @@ import weakref import six +from heat.common import exception from heat.common.i18n import _ @@ -198,16 +199,29 @@ def resolve(snippet): return snippet -def validate(snippet): +def validate(snippet, path=''): if isinstance(snippet, Function): - snippet.validate() + try: + snippet.validate() + except AssertionError: + raise + except Exception as e: + path = '.'.join([path, snippet.fn_name]) + raise exception.StackValidationFailed( + path=path, message=six.text_type(e)) elif isinstance(snippet, collections.Mapping): - for v in six.itervalues(snippet): - validate(v) + def mkpath(key): + return '.'.join([path, key]) + + for k, v in six.iteritems(snippet): + validate(v, mkpath(k)) elif (not isinstance(snippet, six.string_types) and isinstance(snippet, collections.Iterable)): - for v in snippet: - validate(v) + def mkpath(indx): + return '.'.join([path, '[%d]' % indx]) + + for i, v in enumerate(snippet): + validate(v, mkpath(i)) def dependencies(snippet, path=''): diff --git a/heat/engine/output.py b/heat/engine/output.py index 3a4b71fe29..8fc7c84373 100644 --- a/heat/engine/output.py +++ b/heat/engine/output.py @@ -25,9 +25,9 @@ class OutputDefinition(object): self._resolved_value = None self._description = description - def validate(self): + def validate(self, path=''): """Validate the output value without resolving it.""" - function.validate(self._value) + function.validate(self._value, path) def dep_attrs(self, resource_name): """Iterate over attributes of a given resource that this references. diff --git a/heat/engine/resource.py b/heat/engine/resource.py index edacbf17bd..ded6178b96 100644 --- a/heat/engine/resource.py +++ b/heat/engine/resource.py @@ -1501,8 +1501,8 @@ class Resource(object): self.stack.context, self.t.resource_type ) - - function.validate(self.t) + path = '.'.join([self.stack.t.RESOURCES, self.name]) + function.validate(self.t, path) self.validate_deletion_policy(self.t.deletion_policy()) self.t.update_policy(self.update_policy_schema, self.context).validate() diff --git a/heat/engine/stack.py b/heat/engine/stack.py index 7780e8e4cd..b53a0cc316 100644 --- a/heat/engine/stack.py +++ b/heat/engine/stack.py @@ -838,15 +838,13 @@ class Stack(collections.Mapping): for op_name, output in six.iteritems(self.outputs): try: - output.validate() - except exception.StackValidationFailed as ex: + path = '.'.join([self.t.OUTPUTS, op_name, + self.t.OUTPUT_VALUE]) + output.validate(path) + except exception.StackValidationFailed: raise except AssertionError: raise - except Exception as ex: - raise exception.StackValidationFailed( - error='Validation error in output "%s"' % op_name, - message=six.text_type(ex)) def requires_deferred_auth(self): """Determine whether to perform API requests with deferred auth. diff --git a/heat/tests/test_function.py b/heat/tests/test_function.py index e34412fc2e..d4f2102b8c 100644 --- a/heat/tests/test_function.py +++ b/heat/tests/test_function.py @@ -178,8 +178,9 @@ class ValidateTest(common.HeatTestCase): def test_validate_func(self): self.assertIsNone(function.validate(self.func)) self.func = TestFunction(None, 'foo', ['bar']) - ex = self.assertRaises(TypeError, function.validate, self.func) - self.assertEqual('Need more arguments', six.text_type(ex)) + self.assertRaisesRegexp(exception.StackValidationFailed, + '.foo: Need more arguments', + function.validate, self.func) def test_validate_dict(self): snippet = {'foo': 'bar', 'blarg': self.func} @@ -187,8 +188,9 @@ class ValidateTest(common.HeatTestCase): self.func = TestFunction(None, 'foo', ['bar']) snippet = {'foo': 'bar', 'blarg': self.func} - ex = self.assertRaises(TypeError, function.validate, snippet) - self.assertEqual('Need more arguments', six.text_type(ex)) + self.assertRaisesRegexp(exception.StackValidationFailed, + '.blarg.foo: Need more arguments', + function.validate, snippet) def test_validate_list(self): snippet = ['foo', 'bar', 'baz', 'blarg', self.func] @@ -196,8 +198,9 @@ class ValidateTest(common.HeatTestCase): self.func = TestFunction(None, 'foo', ['bar']) snippet = {'foo': 'bar', 'blarg': self.func} - ex = self.assertRaises(TypeError, function.validate, snippet) - self.assertEqual('Need more arguments', six.text_type(ex)) + self.assertRaisesRegexp(exception.StackValidationFailed, + '.blarg.foo: Need more arguments', + function.validate, snippet) def test_validate_all(self): snippet = ['foo', {'bar': ['baz', {'blarg': self.func}]}] @@ -205,8 +208,9 @@ class ValidateTest(common.HeatTestCase): self.func = TestFunction(None, 'foo', ['bar']) snippet = {'foo': 'bar', 'blarg': self.func} - ex = self.assertRaises(TypeError, function.validate, snippet) - self.assertEqual('Need more arguments', six.text_type(ex)) + self.assertRaisesRegexp(exception.StackValidationFailed, + '.blarg.foo: Need more arguments', + function.validate, snippet) class DependenciesTest(common.HeatTestCase): diff --git a/heat/tests/test_hot.py b/heat/tests/test_hot.py index 811ece4621..3ef3e9b3b5 100644 --- a/heat/tests/test_hot.py +++ b/heat/tests/test_hot.py @@ -1118,7 +1118,10 @@ class HOTemplateTest(common.HeatTestCase): 'data': {'var1': [1, 2, 3, 4]}}} tmpl = template.Template(hot_newton_tpl_empty) yaql = tmpl.parse(None, snippet) - self.assertRaises(ValueError, function.validate, yaql) + regxp = ('.yaql: Bad expression Parse error: unexpected end ' + 'of statement.') + self.assertRaisesRegexp(exception.StackValidationFailed, regxp, + function.validate, yaql) def test_yaql_data_as_function(self): snippet = {'yaql': {'expression': '$.data.var1.len()', @@ -1381,7 +1384,10 @@ conditions: snippet = {'repeat': {'template': 'this is %var%', 'for_each': '%var%'}} repeat = tmpl.parse(None, snippet) - self.assertRaises(TypeError, function.validate, repeat) + regxp = ('.repeat: The "for_each" argument to "repeat" ' + 'must contain a map') + self.assertRaisesRegexp(exception.StackValidationFailed, regxp, + function.validate, repeat) def test_digest(self): snippet = {'digest': ['md5', 'foobar']} @@ -1624,10 +1630,11 @@ conditions: snippet = {'Fn::GetAZs': ''} stack = parser.Stack(utils.dummy_context(), 'test_stack', template.Template(hot_juno_tpl_empty)) - error = self.assertRaises(exception.InvalidTemplateVersion, - function.validate, - stack.t.parse(stack, snippet)) - self.assertIn(next(iter(snippet)), six.text_type(error)) + regxp = '.Fn::GetAZs: The template version is invalid' + self.assertRaisesRegexp(exception.StackValidationFailed, + regxp, + function.validate, + stack.t.parse(stack, snippet)) def test_add_resource(self): hot_tpl = template_format.parse(''' diff --git a/heat/tests/test_stack.py b/heat/tests/test_stack.py index afb09e84b9..6604ac268c 100644 --- a/heat/tests/test_stack.py +++ b/heat/tests/test_stack.py @@ -1846,13 +1846,11 @@ class StackTest(common.HeatTestCase): self.stack = stack.Stack(self.ctx, 'stack_with_correct_outputs', template.Template(tmpl)) - ex = self.assertRaises(exception.StackValidationFailed, - self.stack.validate) - - self.assertEqual('Validation error in output "Resource_attr": ' - 'The Referenced Attribute ' - '(AResource Bar) is incorrect.', - six.text_type(ex)) + self.assertRaisesRegexp( + exception.StackValidationFailed, + ('Outputs.Resource_attr.Value.Fn::GetAtt: The Referenced ' + 'Attribute \(AResource Bar\) is incorrect.'), + self.stack.validate) def test_incorrect_outputs_cfn_incorrect_reference(self): tmpl = template_format.parse(""" @@ -2212,13 +2210,11 @@ class StackTest(common.HeatTestCase): self.stack = stack.Stack(self.ctx, 'stack_with_correct_outputs', template.Template(tmpl)) - ex = self.assertRaises(exception.StackValidationFailed, - self.stack.validate) - - self.assertEqual('Validation error in output "resource_attr": ' - 'The Referenced Attribute ' - '(AResource Bar) is incorrect.', - six.text_type(ex)) + self.assertRaisesRegexp( + exception.StackValidationFailed, + ('outputs.resource_attr.value.get_attr: The Referenced Attribute ' + '\(AResource Bar\) is incorrect.'), + self.stack.validate) def test_snapshot_save_called_first(self): def snapshotting_called_first(stack, action, status, reason):