diff --git a/heat/engine/cfn/template.py b/heat/engine/cfn/template.py index 7e3354f4f0..cbde6d7448 100644 --- a/heat/engine/cfn/template.py +++ b/heat/engine/cfn/template.py @@ -15,6 +15,7 @@ import collections import six +from heat.common import exception from heat.common.i18n import _ from heat.engine.cfn import functions as cfn_funcs from heat.engine import function @@ -87,72 +88,77 @@ class CfnTemplate(template.Template): user_params=user_params, param_defaults=param_defaults) + def validate_resource_definitions(self, stack): + resources = self.t.get(self.RESOURCES) or {} + allowed_keys = set(_RESOURCE_KEYS) + + try: + + for name, snippet in resources.items(): + data = self.parse(stack, snippet) + + if not self.validate_resource_key_type(RES_TYPE, + six.string_types, + 'string', + allowed_keys, + name, data): + args = {'name': name, 'type_key': RES_TYPE} + msg = _('Resource %(name)s is missing ' + '"%(type_key)s"') % args + raise KeyError(msg) + + self.validate_resource_key_type( + RES_PROPERTIES, + (collections.Mapping, function.Function), + 'object', allowed_keys, name, data) + self.validate_resource_key_type( + RES_METADATA, + (collections.Mapping, function.Function), + 'object', allowed_keys, name, data) + self.validate_resource_key_type( + RES_DEPENDS_ON, + collections.Sequence, + 'list or string', allowed_keys, name, data) + self.validate_resource_key_type( + RES_DELETION_POLICY, + six.string_types, + 'string', allowed_keys, name, data) + self.validate_resource_key_type( + RES_UPDATE_POLICY, + (collections.Mapping, function.Function), + 'object', allowed_keys, name, data) + self.validate_resource_key_type( + RES_DESCRIPTION, + six.string_types, + 'string', allowed_keys, name, data) + except TypeError as ex: + raise exception.StackValidationFailed(message=six.text_type(ex)) + def resource_definitions(self, stack): + resources = self.t.get(self.RESOURCES) or {} + def rsrc_defn_item(name, snippet): data = self.parse(stack, snippet) - def get_check_type(key, valid_types, typename, default=None): - if key in data: - field = data[key] - if not isinstance(field, valid_types): - args = {'name': name, 'key': key, 'typename': typename} - msg = _('Resource %(name)s %(key)s type ' - 'must be %(typename)s') % args - raise TypeError(msg) - return field - else: - return default - - resource_type = get_check_type(RES_TYPE, - six.string_types, - 'string') - if resource_type is None: - args = {'name': name, 'type_key': RES_TYPE} - msg = _('Resource %(name)s is missing "%(type_key)s"') % args - raise KeyError(msg) - - properties = get_check_type(RES_PROPERTIES, - (collections.Mapping, - function.Function), - 'object') - - metadata = get_check_type(RES_METADATA, - (collections.Mapping, - function.Function), - 'object') - - depends = get_check_type(RES_DEPENDS_ON, - collections.Sequence, - 'list or string', - default=[]) - if isinstance(depends, six.string_types): + depends = data.get(RES_DEPENDS_ON) + if not depends: + depends = [] + elif isinstance(depends, six.string_types): depends = [depends] - deletion_policy = get_check_type(RES_DELETION_POLICY, - six.string_types, - 'string') + kwargs = { + 'resource_type': data.get(RES_TYPE), + 'properties': data.get(RES_PROPERTIES), + 'metadata': data.get(RES_METADATA), + 'depends': depends, + 'deletion_policy': data.get(RES_DELETION_POLICY), + 'update_policy': data.get(RES_UPDATE_POLICY), + 'description': data.get(RES_DESCRIPTION) or '' + } - update_policy = get_check_type(RES_UPDATE_POLICY, - (collections.Mapping, - function.Function), - 'object') - - description = get_check_type(RES_DESCRIPTION, - six.string_types, - 'string', - default='') - - defn = rsrc_defn.ResourceDefinition( - name, resource_type, - properties=properties, - metadata=metadata, - depends=depends, - deletion_policy=deletion_policy, - update_policy=update_policy, - description=description) + defn = rsrc_defn.ResourceDefinition(name, **kwargs) return name, defn - resources = self.t.get(self.RESOURCES) or {} return dict(rsrc_defn_item(name, data) for name, data in resources.items()) diff --git a/heat/engine/hot/template.py b/heat/engine/hot/template.py index a1ba06f003..8099c5d251 100644 --- a/heat/engine/hot/template.py +++ b/heat/engine/hot/template.py @@ -191,74 +191,72 @@ class HOTemplate20130523(template.Template): user_params=user_params, param_defaults=param_defaults) - def resource_definitions(self, stack): + def validate_resource_definitions(self, stack): + resources = self.t.get(self.RESOURCES) or {} allowed_keys = set(_RESOURCE_KEYS) + try: + for name, snippet in resources.items(): + data = self.parse(stack, snippet) + + if not self.validate_resource_key_type(RES_TYPE, + six.string_types, + 'string', + allowed_keys, + name, data): + args = {'name': name, 'type_key': RES_TYPE} + msg = _('Resource %(name)s is missing ' + '"%(type_key)s"') % args + raise KeyError(msg) + + self.validate_resource_key_type( + RES_PROPERTIES, + (collections.Mapping, function.Function), + 'object', allowed_keys, name, data) + self.validate_resource_key_type( + RES_METADATA, + (collections.Mapping, function.Function), + 'object', allowed_keys, name, data) + self.validate_resource_key_type( + RES_DEPENDS_ON, + collections.Sequence, + 'list or string', allowed_keys, name, data) + self.validate_resource_key_type( + RES_DELETION_POLICY, + six.string_types, + 'string', allowed_keys, name, data) + self.validate_resource_key_type( + RES_UPDATE_POLICY, + (collections.Mapping, function.Function), + 'object', allowed_keys, name, data) + except (TypeError, ValueError) as ex: + raise exception.StackValidationFailed(message=six.text_type(ex)) + + def resource_definitions(self, stack): + resources = self.t.get(self.RESOURCES) or {} + def rsrc_defn_item(name, snippet): data = self.parse(stack, snippet) - def get_check_type(key, valid_types, typename, default=None): - if key in data: - field = data[key] - if not isinstance(field, valid_types): - args = {'name': name, 'key': key, 'typename': typename} - msg = _('Resource %(name)s %(key)s type ' - 'must be %(typename)s') % args - raise TypeError(msg) - return field - else: - return default - - resource_type = get_check_type(RES_TYPE, - six.string_types, - 'string') - if resource_type is None: - args = {'name': name, 'type_key': RES_TYPE} - msg = _('Resource %(name)s is missing "%(type_key)s"') % args - raise KeyError(msg) - - properties = get_check_type(RES_PROPERTIES, - (collections.Mapping, - function.Function), - 'object') - - metadata = get_check_type(RES_METADATA, - (collections.Mapping, - function.Function), - 'object') - - depends = get_check_type(RES_DEPENDS_ON, - collections.Sequence, - 'list or string', - default=[]) - if isinstance(depends, six.string_types): + depends = data.get(RES_DEPENDS_ON) + if not depends: + depends = [] + elif isinstance(depends, six.string_types): depends = [depends] - deletion_policy = get_check_type(RES_DELETION_POLICY, - six.string_types, - 'string') + kwargs = { + 'resource_type': data.get(RES_TYPE), + 'properties': data.get(RES_PROPERTIES), + 'metadata': data.get(RES_METADATA), + 'depends': depends, + 'deletion_policy': data.get(RES_DELETION_POLICY), + 'update_policy': data.get(RES_UPDATE_POLICY), + 'description': None + } - update_policy = get_check_type(RES_UPDATE_POLICY, - (collections.Mapping, - function.Function), - 'object') - - for key in data: - if key not in allowed_keys: - raise ValueError(_('"%s" is not a valid keyword ' - 'inside a resource definition') % key) - - defn = rsrc_defn.ResourceDefinition( - name, resource_type, - properties=properties, - metadata=metadata, - depends=depends, - deletion_policy=deletion_policy, - update_policy=update_policy, - description=None) + defn = rsrc_defn.ResourceDefinition(name, **kwargs) return name, defn - resources = self.t.get(self.RESOURCES) or {} return dict(rsrc_defn_item(name, data) for name, data in resources.items()) diff --git a/heat/engine/stack.py b/heat/engine/stack.py index f47aff09f9..b1492be68d 100644 --- a/heat/engine/stack.py +++ b/heat/engine/stack.py @@ -469,6 +469,9 @@ class Stack(collections.Mapping): parameter_groups = param_groups.ParameterGroups(self.t) parameter_groups.validate() + # Validate types of sections in ResourceDefinitions + self.t.validate_resource_definitions(self) + # Check duplicate names between parameters and resources dup_names = set(self.parameters.keys()) & set(self.keys()) diff --git a/heat/engine/template.py b/heat/engine/template.py index c72f6d81c6..6932e02ea6 100644 --- a/heat/engine/template.py +++ b/heat/engine/template.py @@ -158,6 +158,34 @@ class Template(collections.Mapping): '''Return a parameters.Parameters object for the stack.''' pass + @classmethod + def validate_resource_key_type(cls, key, valid_types, typename, + allowed_keys, rsrc_name, rsrc_data): + """Validation type of the specific resource key. + + Used in validate_resource_definition and check correctness of + key's type. + """ + if key not in allowed_keys: + raise ValueError(_('"%s" is not a valid ' + 'keyword inside a resource ' + 'definition') % key) + if key in rsrc_data: + if not isinstance(rsrc_data.get(key), valid_types): + args = {'name': rsrc_name, 'key': key, + 'typename': typename} + message = _('Resource %(name)s %(key)s type ' + 'must be %(typename)s') % args + raise TypeError(message) + return True + else: + return False + + @abc.abstractmethod + def validate_resource_definitions(self, stack): + """Check section's type of ResourceDefinitions.""" + pass + @abc.abstractmethod def resource_definitions(self, stack): '''Return a dictionary of ResourceDefinition objects.''' diff --git a/heat/tests/test_template.py b/heat/tests/test_template.py index 0504a9c2ef..93a7f22b09 100644 --- a/heat/tests/test_template.py +++ b/heat/tests/test_template.py @@ -59,6 +59,9 @@ class TestTemplatePluginManager(common.HeatTestCase): def parameters(self, stack_identifier, user_params): pass + def validate_resource_definitions(self, stack): + pass + def resource_definitions(self, stack): pass diff --git a/heat/tests/test_validate.py b/heat/tests/test_validate.py index 91b9573f34..3599e5a7ac 100644 --- a/heat/tests/test_validate.py +++ b/heat/tests/test_validate.py @@ -1468,3 +1468,28 @@ class validateTest(common.HeatTestCase): error_message = ('Arguments to "get_attr" must be of the form ' '[resource_name, attribute, (path), ...]') self.assertEqual(error_message, six.text_type(err)) + + def test_validate_resource_attr_invalid_type(self): + t = template_format.parse(""" + heat_template_version: 2013-05-23 + resources: + resource: + type: 123 + """) + template = parser.Template(t) + stack = parser.Stack(self.ctx, 'test_stack', template) + ex = self.assertRaises(exception.StackValidationFailed, stack.validate) + self.assertEqual('Resource resource type type must be string', + six.text_type(ex)) + + def test_validate_resource_attr_invalid_type_cfn(self): + t = template_format.parse(""" + HeatTemplateFormatVersion: '2012-12-12' + Resources: + Resource: + Type: [Wrong, Type] + """) + stack = parser.Stack(self.ctx, 'test_stack', parser.Template(t)) + ex = self.assertRaises(exception.StackValidationFailed, stack.validate) + self.assertEqual('Resource Resource Type type must be string', + six.text_type(ex))