From 70c4ab3fcf921bd29f92f745aedabab14c1b6cd0 Mon Sep 17 00:00:00 2001 From: huangtianhua Date: Fri, 22 Jul 2016 15:50:20 +0800 Subject: [PATCH] Support 'conditions' section for templates This changes: 1. Support 'Conditions' for AWSTemplateFormatVersion: 2010-09-09 2. Support 'conditions' for heat_template_version: 2016-10-14 3. There is no need to add a new HeatTemplateFormatVersion template, because we decide to support conditions in AWSTemplateFormatVersion and heat_template_version: 2016-10-14, so remove the HeatTemplateFormatVersion.2016-10-14 4. Move the definition of function 'equals' to hot/functions.py 5. Mark 'equals' as condition function which supported in conditions. Change-Id: I2e7bdfa1c2052e75f35f2bd0003cdc170188d8b8 Blueprint: support-conditions-function --- doc/source/template_guide/hot_spec.rst | 69 +++++++++++++++++-- heat/common/exception.py | 4 ++ heat/engine/cfn/functions.py | 30 -------- heat/engine/cfn/template.py | 48 ++++++------- heat/engine/function.py | 16 +++++ heat/engine/hot/functions.py | 30 ++++++++ heat/engine/hot/template.py | 31 ++++++++- heat/engine/template.py | 7 ++ .../openstack/barbican/test_container.py | 2 +- heat/tests/test_hot.py | 32 ++++++--- heat/tests/test_template.py | 46 +++++++------ setup.cfg | 1 - 12 files changed, 224 insertions(+), 92 deletions(-) diff --git a/doc/source/template_guide/hot_spec.rst b/doc/source/template_guide/hot_spec.rst index 5ae935892c..8611fdc16a 100644 --- a/doc/source/template_guide/hot_spec.rst +++ b/doc/source/template_guide/hot_spec.rst @@ -43,7 +43,7 @@ HOT templates are defined in YAML and follow the structure outlined below. .. code-block:: yaml - heat_template_version: 2015-04-30 + heat_template_version: 2016-10-14 description: # a description of the template @@ -60,6 +60,9 @@ HOT templates are defined in YAML and follow the structure outlined below. outputs: # declaration of output parameters + conditions: + # declaration of conditions + heat_template_version This key with value ``2013-05-23`` (or a later date) indicates that the YAML document is a HOT template of the specified version. @@ -89,6 +92,15 @@ outputs once the template has been instantiated. This section is optional and can be omitted when no output values are required. +conditions + This optional section includes statements which can be used to restrict + when a resource is created or when a property is defined. They can be + associated with resources and resource properties in the + ``resources`` section, also can be associated with outputs in the + ``outputs`` sections of a template. + + Note: Support for this section is added in the Newton version. + .. _hot_spec_template_version: @@ -209,10 +221,9 @@ for the ``heat_template_version`` key: The key with value ``2016-10-14`` or ``newton`` indicates that the YAML document is a HOT template and it may contain features added and/or removed up until the Newton release. This version adds the ``yaql`` function which - can be used for evaluation of complex expressions, and also adds ``equals`` - function which can be used to compare whether two values are equal, and - the ``map_replace`` function that can do key/value replacements on a mapping. - The complete list of supported functions is:: + can be used for evaluation of complex expressions, and the ``map_replace`` + function that can do key/value replacements on a mapping. The complete list + of supported functions is:: digest get_attr @@ -227,7 +238,13 @@ for the ``heat_template_version`` key: str_replace str_split yaql + + This version also adds ``equals`` condition function which can be used + to compare whether two values are equal. The complete list of supported + condition functions is:: + equals + get_param .. _hot_spec_parameter_groups: @@ -756,6 +773,48 @@ be defined as an output parameter value: { get_attr: [my_instance, first_address] } +Conditions section +~~~~~~~~~~~~~~~~~~ +The ``conditions`` section defines one or more conditions which are evaluated +based on input parameter values provided when a user creates or updates a +stack. The condition can be associated with resources, resource properties and +outputs. For example, based on the result of a condition, user can +conditionally create resources, user can conditionally set different values +of properties, and user can conditionally give outputs of a stack. + +The ``conditions`` section is defined with the following syntax + +.. code-block:: yaml + + conditions: + : {expression1} + : {expression2} + ... + +condition name + The condition name, which must be unique within the ``conditions`` + section of a template. + +expression + The expression which is expected to return True or False. Usually, + the condition functions can be used as expression to define conditions:: + + equals + get_param + + Note: In condition functions, you can reference a value from an input + parameter, but you cannot reference resource or its attribute. + +An example of conditions section definition + +.. code-block:: yaml + + conditions: + cd1: True + cd2: {get_param: param1} + cd3: {equals: [{get_param: param2}, "yes"]} + + .. _hot_spec_intrinsic_functions: Intrinsic functions diff --git a/heat/common/exception.py b/heat/common/exception.py index e23b5ffe29..1869d64979 100644 --- a/heat/common/exception.py +++ b/heat/common/exception.py @@ -129,6 +129,10 @@ class InvalidTemplateSection(HeatException): msg_fmt = _("The template section is invalid: %(section)s") +class InvalidConditionFunction(HeatException): + msg_fmt = _("The function is not supported in condition: %(func)s") + + class ImmutableParameterModified(HeatException): msg_fmt = _("The following parameters are immutable and may not be " "updated: %(keys)s") diff --git a/heat/engine/cfn/functions.py b/heat/engine/cfn/functions.py index 35627f1662..6f0cc9bca4 100644 --- a/heat/engine/cfn/functions.py +++ b/heat/engine/cfn/functions.py @@ -48,36 +48,6 @@ class FindInMap(function.Function): return mapping[key][value] -class Equals(function.Function): - """A function for comparing whether two values are equal. - - Takes the form:: - - { "Fn::Equals" : ["value_1", "value_2"] } - - The value to be any type that you want to compare. Returns true - if the two values are equal or false if they aren't. - """ - - def __init__(self, stack, fn_name, args): - super(Equals, self).__init__(stack, fn_name, args) - try: - if (not self.args or - not isinstance(self.args, list)): - raise ValueError() - self.value1, self.value2 = self.args - except ValueError: - msg = _('Arguments to "%s" must be of the form: ' - '[value_1, value_2]') - raise ValueError(msg % self.fn_name) - - def result(self): - resolved_v1 = function.resolve(self.value1) - resolved_v2 = function.resolve(self.value2) - - return resolved_v1 == resolved_v2 - - class GetAZs(function.Function): """A function for retrieving the availability zones. diff --git a/heat/engine/cfn/template.py b/heat/engine/cfn/template.py index e9cbef5196..271f4537cf 100644 --- a/heat/engine/cfn/template.py +++ b/heat/engine/cfn/template.py @@ -19,20 +19,21 @@ from heat.common import exception from heat.common.i18n import _ from heat.engine.cfn import functions as cfn_funcs from heat.engine import function +from heat.engine.hot import functions as hot_funcs from heat.engine import parameters from heat.engine import rsrc_defn from heat.engine import template -class CfnTemplate(template.Template): - """A stack template.""" +class CfnTemplateBase(template.Template): + """The base implementation of cfn template.""" SECTIONS = ( VERSION, ALTERNATE_VERSION, - DESCRIPTION, MAPPINGS, PARAMETERS, RESOURCES, OUTPUTS + DESCRIPTION, MAPPINGS, PARAMETERS, RESOURCES, OUTPUTS, ) = ( 'AWSTemplateFormatVersion', 'HeatTemplateFormatVersion', - 'Description', 'Mappings', 'Parameters', 'Resources', 'Outputs' + 'Description', 'Mappings', 'Parameters', 'Resources', 'Outputs', ) OUTPUT_KEYS = ( @@ -206,7 +207,26 @@ class CfnTemplate(template.Template): self.t[self.RESOURCES][name] = cfn_tmpl -class HeatTemplate(CfnTemplate): +class CfnTemplate(CfnTemplateBase): + + CONDITIONS = 'Conditions' + SECTIONS = CfnTemplateBase.SECTIONS + (CONDITIONS,) + + condition_functions = { + 'Fn::Equals': hot_funcs.Equals, + 'Ref': cfn_funcs.ParamRef, + 'Fn::FindInMap': cfn_funcs.FindInMap, + } + + def __init__(self, tmpl, template_id=None, files=None, env=None): + super(CfnTemplate, self).__init__(tmpl, template_id, files, env) + + self._parser_condition_functions = dict( + (n, function.Invalid) for n in self.functions) + self._parser_condition_functions.update(self.condition_functions) + + +class HeatTemplate(CfnTemplateBase): functions = { 'Fn::FindInMap': cfn_funcs.FindInMap, 'Fn::GetAZs': cfn_funcs.GetAZs, @@ -220,21 +240,3 @@ class HeatTemplate(CfnTemplate): 'Fn::MemberListToMap': cfn_funcs.MemberListToMap, 'Fn::ResourceFacade': cfn_funcs.ResourceFacade, } - - -class HeatTemplate20161014(HeatTemplate): - functions = { - 'Fn::FindInMap': cfn_funcs.FindInMap, - 'Fn::GetAZs': cfn_funcs.GetAZs, - 'Ref': cfn_funcs.Ref, - 'Fn::GetAtt': cfn_funcs.GetAtt, - 'Fn::Select': cfn_funcs.Select, - 'Fn::Join': cfn_funcs.Join, - 'Fn::Split': cfn_funcs.Split, - 'Fn::Replace': cfn_funcs.Replace, - 'Fn::Base64': cfn_funcs.Base64, - 'Fn::MemberListToMap': cfn_funcs.MemberListToMap, - 'Fn::ResourceFacade': cfn_funcs.ResourceFacade, - # supports Fn::Equals in Newton - 'Fn::Equals': cfn_funcs.Equals, - } diff --git a/heat/engine/function.py b/heat/engine/function.py index 0855f2dfdc..2592c5c527 100644 --- a/heat/engine/function.py +++ b/heat/engine/function.py @@ -18,6 +18,8 @@ import weakref import six +from heat.common import exception + @six.add_metaclass(abc.ABCMeta) class Function(object): @@ -203,3 +205,17 @@ def dep_attrs(snippet, resource_name): attrs = (dep_attrs(value, resource_name) for value in snippet) return itertools.chain.from_iterable(attrs) return [] + + +class Invalid(Function): + """A function for checking condition functions and to force failures. + + This function is used to force failures for functions that are not + supported in condition definition. + """ + + def __init__(self, stack, fn_name, args): + raise exception.InvalidConditionFunction(func=fn_name) + + def result(self): + return super(Invalid, self).result() diff --git a/heat/engine/hot/functions.py b/heat/engine/hot/functions.py index 17113e563f..b689f6ee9b 100644 --- a/heat/engine/hot/functions.py +++ b/heat/engine/hot/functions.py @@ -872,3 +872,33 @@ class Yaql(function.Function): self._expression = function.resolve(self._expression) self.validate_expression(self._expression) return self.parser(self._expression).evaluate(context=self.context) + + +class Equals(function.Function): + """A function for comparing whether two values are equal. + + Takes the form:: + + { "equals" : ["value_1", "value_2"] } + + The value can be any type that you want to compare. Returns true + if the two values are equal or false if they aren't. + """ + + def __init__(self, stack, fn_name, args): + super(Equals, self).__init__(stack, fn_name, args) + try: + if (not self.args or + not isinstance(self.args, list)): + raise ValueError() + self.value1, self.value2 = self.args + except ValueError: + msg = _('Arguments to "%s" must be of the form: ' + '[value_1, value_2]') + raise ValueError(msg % self.fn_name) + + def result(self): + resolved_v1 = function.resolve(self.value1) + resolved_v2 = function.resolve(self.value2) + + return resolved_v1 == resolved_v2 diff --git a/heat/engine/hot/template.py b/heat/engine/hot/template.py index 4784a23c04..4d5827e04a 100644 --- a/heat/engine/hot/template.py +++ b/heat/engine/hot/template.py @@ -30,10 +30,10 @@ class HOTemplate20130523(template.Template): SECTIONS = ( VERSION, DESCRIPTION, PARAMETER_GROUPS, - PARAMETERS, RESOURCES, OUTPUTS, MAPPINGS + PARAMETERS, RESOURCES, OUTPUTS, MAPPINGS, ) = ( 'heat_template_version', 'description', 'parameter_groups', - 'parameters', 'resources', 'outputs', '__undefined__' + 'parameters', 'resources', 'outputs', '__undefined__', ) OUTPUT_KEYS = ( @@ -394,6 +394,15 @@ class HOTemplate20160408(HOTemplate20151015): class HOTemplate20161014(HOTemplate20160408): + + CONDITIONS = 'conditions' + + SECTIONS = HOTemplate20160408.SECTIONS + (CONDITIONS,) + + _CFN_TO_HOT_SECTIONS = HOTemplate20160408._CFN_TO_HOT_SECTIONS + _CFN_TO_HOT_SECTIONS.update({ + cfn_template.CfnTemplate.CONDITIONS: CONDITIONS}) + deletion_policies = { 'Delete': rsrc_defn.ResourceDefinition.DELETE, 'Retain': rsrc_defn.ResourceDefinition.RETAIN, @@ -426,7 +435,6 @@ class HOTemplate20161014(HOTemplate20160408): # functions added in 2016-10-14 'yaql': hot_funcs.Yaql, - 'equals': cfn_funcs.Equals, 'map_replace': hot_funcs.MapReplace, # functions removed from 2015-10-15 @@ -442,3 +450,20 @@ class HOTemplate20161014(HOTemplate20160408): 'Fn::ResourceFacade': hot_funcs.Removed, 'Ref': hot_funcs.Removed, } + + condition_functions = { + 'get_param': hot_funcs.GetParam, + 'equals': hot_funcs.Equals, + } + + def __init__(self, tmpl, template_id=None, files=None, env=None): + super(HOTemplate20161014, self).__init__( + tmpl, template_id, files, env) + + self._parser_condition_functions = {} + for n, f in six.iteritems(self.functions): + if not isinstance(f, hot_funcs.Removed): + self._parser_condition_functions[n] = function.Invalid + else: + self._parser_condition_functions[n] = f + self._parser_condition_functions.update(self.condition_functions) diff --git a/heat/engine/template.py b/heat/engine/template.py index 8a913a2a55..2ff0b68178 100644 --- a/heat/engine/template.py +++ b/heat/engine/template.py @@ -90,6 +90,10 @@ def get_template_class(template_data): class Template(collections.Mapping): """A stack template.""" + condition_functions = {} + _parser_condition_functions = {} + functions = {} + def __new__(cls, template, *args, **kwargs): """Create a new Template of the appropriate class.""" global _template_classes @@ -260,6 +264,9 @@ class Template(collections.Mapping): def parse(self, stack, snippet, path=''): return parse(self.functions, stack, snippet, path) + def parse_condition(self, stack, snippet): + return parse(self._parser_condition_functions, stack, snippet) + def validate(self): """Validate the template. diff --git a/heat/tests/openstack/barbican/test_container.py b/heat/tests/openstack/barbican/test_container.py index 124d06d6d6..70c5546710 100644 --- a/heat/tests/openstack/barbican/test_container.py +++ b/heat/tests/openstack/barbican/test_container.py @@ -110,7 +110,7 @@ class TestContainer(common.HeatTestCase): self.stack = utils.parse_stack(tmpl) else: self.stack = stack - resource_defns = self.stack.t.resource_definitions(stack) + resource_defns = self.stack.t.resource_definitions(self.stack) if snippet is None: snippet = resource_defns['container'] res_class = container.resource_mapping()[tmpl_name] diff --git a/heat/tests/test_hot.py b/heat/tests/test_hot.py index 4c7e707b6d..73a320eff7 100644 --- a/heat/tests/test_hot.py +++ b/heat/tests/test_hot.py @@ -156,6 +156,10 @@ class HOTemplateTest(common.HeatTestCase): def resolve(snippet, template, stack=None): return function.resolve(template.parse(stack, snippet)) + @staticmethod + def resolve_condition(snippet, template, stack=None): + return function.resolve(template.parse_condition(stack, snippet)) + def test_defaults(self): """Test default content behavior of HOT template.""" @@ -1073,7 +1077,7 @@ class HOTemplateTest(common.HeatTestCase): tmpl = template.Template(hot_tpl) stack = parser.Stack(utils.dummy_context(), 'test_equals_false', tmpl) - resolved = self.resolve(snippet, tmpl, stack) + resolved = self.resolve_condition(snippet, tmpl, stack) self.assertFalse(resolved) # when param 'env_type' is 'prod', equals function resolve to true tmpl = template.Template(hot_tpl, @@ -1081,7 +1085,7 @@ class HOTemplateTest(common.HeatTestCase): {'env_type': 'prod'})) stack = parser.Stack(utils.dummy_context(), 'test_equals_true', tmpl) - resolved = self.resolve(snippet, tmpl, stack) + resolved = self.resolve_condition(snippet, tmpl, stack) self.assertTrue(resolved) def test_equals_invalid_args(self): @@ -1089,15 +1093,27 @@ class HOTemplateTest(common.HeatTestCase): snippet = {'equals': ['test', 'prod', 'invalid']} exc = self.assertRaises(exception.StackValidationFailed, - self.resolve, snippet, tmpl) - self.assertIn('.equals: Arguments to "equals" must be of the form: ' - '[value_1, value_2]', six.text_type(exc)) + self.resolve_condition, snippet, tmpl) + + error_msg = ('.equals: Arguments to "equals" must be ' + 'of the form: [value_1, value_2]') + self.assertIn(error_msg, six.text_type(exc)) snippet = {'equals': "invalid condition"} exc = self.assertRaises(exception.StackValidationFailed, - self.resolve, snippet, tmpl) - self.assertIn('.equals: Arguments to "equals" must be of the form: ' - '[value_1, value_2]', six.text_type(exc)) + self.resolve_condition, snippet, tmpl) + self.assertIn(error_msg, six.text_type(exc)) + + def test_equals_with_non_supported_function(self): + + tmpl = template.Template(hot_newton_tpl_empty) + + snippet = {'equals': [{'get_attr': [None, 'att1']}, + {'get_attr': [None, 'att2']}]} + exc = self.assertRaises(exception.InvalidConditionFunction, + self.resolve_condition, snippet, tmpl) + error_msg = 'The function is not supported in condition: get_attr' + self.assertIn(error_msg, six.text_type(exc)) def test_repeat(self): """Test repeat function.""" diff --git a/heat/tests/test_template.py b/heat/tests/test_template.py index d5d0816cb1..a998dc0412 100644 --- a/heat/tests/test_template.py +++ b/heat/tests/test_template.py @@ -55,8 +55,8 @@ empty_template = template_format.parse('''{ "HeatTemplateFormatVersion" : "2012-12-12", }''') -empty_template20161014 = template_format.parse('''{ - "HeatTemplateFormatVersion" : "2016-10-14", +aws_empty_template = template_format.parse('''{ + "AWSTemplateFormatVersion" : "2010-09-09", }''') parameter_template = template_format.parse('''{ @@ -119,17 +119,20 @@ class TemplatePluginFixture(fixtures.Fixture): class TestTemplatePluginManager(common.HeatTestCase): def test_template_NEW_good(self): class NewTemplate(template.Template): - SECTIONS = (VERSION, MAPPINGS) = ('NEWTemplateFormatVersion', - '__undefined__') + SECTIONS = (VERSION, MAPPINGS, CONDITIONS) = ( + 'NEWTemplateFormatVersion', + '__undefined__', + 'conditions') RESOURCES = 'thingies' - def param_schemata(self): + def param_schemata(self, param_defaults=None): pass def get_section_name(self, section): pass - def parameters(self, stack_identifier, user_params): + def parameters(self, stack_identifier, user_params, + param_defaults=None): pass def validate_resource_definitions(self, stack): @@ -144,9 +147,6 @@ class TestTemplatePluginManager(common.HeatTestCase): def __getitem__(self, section): return {} - def functions(self): - return {} - class NewTemplatePrint(function.Function): def result(self): return 'always this' @@ -495,6 +495,10 @@ class TemplateTest(common.HeatTestCase): def resolve(snippet, template, stack=None): return function.resolve(template.parse(stack, snippet)) + @staticmethod + def resolve_condition(snippet, template, stack=None): + return function.resolve(template.parse_condition(stack, snippet)) + def test_defaults(self): empty = template.Template(empty_template) self.assertNotIn('AWSTemplateFormatVersion', empty) @@ -593,8 +597,7 @@ class TemplateTest(common.HeatTestCase): invalid_heat_version_tmp) ex_error_msg = ('The template version is invalid: ' '"HeatTemplateFormatVersion: 2010-09-09". ' - '"HeatTemplateFormatVersion" should be one of: ' - '2012-12-12, 2016-10-14') + '"HeatTemplateFormatVersion" should be: 2012-12-12') self.assertEqual(ex_error_msg, six.text_type(init_ex)) def test_invalid_version_not_in_heat_versions(self): @@ -771,7 +774,7 @@ class TemplateTest(common.HeatTestCase): def test_equals(self): tpl = template_format.parse(''' - HeatTemplateFormatVersion: 2016-10-14 + AWSTemplateFormatVersion: 2010-09-09 Parameters: env_type: Type: String @@ -782,7 +785,7 @@ class TemplateTest(common.HeatTestCase): tmpl = template.Template(tpl) stk = stack.Stack(utils.dummy_context(), 'test_equals_false', tmpl) - resolved = self.resolve(snippet, tmpl, stk) + resolved = self.resolve_condition(snippet, tmpl, stk) self.assertFalse(resolved) # when param 'env_type' is 'prod', equals function resolve to true tmpl = template.Template(tpl, @@ -790,23 +793,24 @@ class TemplateTest(common.HeatTestCase): {'env_type': 'prod'})) stk = stack.Stack(utils.dummy_context(), 'test_equals_true', tmpl) - resolved = self.resolve(snippet, tmpl, stk) + resolved = self.resolve_condition(snippet, tmpl, stk) self.assertTrue(resolved) def test_equals_invalid_args(self): - tmpl = template.Template(empty_template20161014) + tmpl = template.Template(aws_empty_template) snippet = {'Fn::Equals': ['test', 'prod', 'invalid']} exc = self.assertRaises(exception.StackValidationFailed, - self.resolve, snippet, tmpl) - self.assertIn('.Fn::Equals: Arguments to "Fn::Equals" must be of ' - 'the form: [value_1, value_2]', six.text_type(exc)) + self.resolve_condition, snippet, tmpl) + + error_msg = ('.Fn::Equals: Arguments to "Fn::Equals" must be ' + 'of the form: [value_1, value_2]') + self.assertIn(error_msg, six.text_type(exc)) # test invalid type snippet = {'Fn::Equals': {"equal": False}} exc = self.assertRaises(exception.StackValidationFailed, - self.resolve, snippet, tmpl) - self.assertIn('.Fn::Equals: Arguments to "Fn::Equals" must be of ' - 'the form: [value_1, value_2]', six.text_type(exc)) + self.resolve_condition, snippet, tmpl) + self.assertIn(error_msg, six.text_type(exc)) def test_join(self): tmpl = template.Template(empty_template) diff --git a/setup.cfg b/setup.cfg index 2e1c6c5044..4c4aabdc20 100644 --- a/setup.cfg +++ b/setup.cfg @@ -155,7 +155,6 @@ heat.templates = heat_template_version.2016-04-08 = heat.engine.hot.template:HOTemplate20160408 heat_template_version.2016-10-14 = heat.engine.hot.template:HOTemplate20161014 heat_template_version.newton = heat.engine.hot.template:HOTemplate20161014 - HeatTemplateFormatVersion.2016-10-14 = heat.engine.cfn.template:HeatTemplate20161014 [global] setup-hooks =