Browse Source

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
changes/86/345886/9
huangtianhua 6 years ago
parent
commit
70c4ab3fcf
  1. 69
      doc/source/template_guide/hot_spec.rst
  2. 4
      heat/common/exception.py
  3. 30
      heat/engine/cfn/functions.py
  4. 40
      heat/engine/cfn/template.py
  5. 16
      heat/engine/function.py
  6. 30
      heat/engine/hot/functions.py
  7. 31
      heat/engine/hot/template.py
  8. 7
      heat/engine/template.py
  9. 2
      heat/tests/openstack/barbican/test_container.py
  10. 32
      heat/tests/test_hot.py
  11. 46
      heat/tests/test_template.py
  12. 1
      setup.cfg

69
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:
<condition name1>: {expression1}
<condition name2>: {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

4
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")

30
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.

40
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,23 +207,26 @@ class CfnTemplate(template.Template):
self.t[self.RESOURCES][name] = cfn_tmpl
class HeatTemplate(CfnTemplate):
functions = {
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,
'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,
}
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 HeatTemplate20161014(HeatTemplate):
class HeatTemplate(CfnTemplateBase):
functions = {
'Fn::FindInMap': cfn_funcs.FindInMap,
'Fn::GetAZs': cfn_funcs.GetAZs,
@ -235,6 +239,4 @@ class HeatTemplate20161014(HeatTemplate):
'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,
}

16
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()

30
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

31
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)

7
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.

2
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]

32
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."""

46
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)

1
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 =

Loading…
Cancel
Save