From 4a92678f189e02963302121a814bf17797d6d684 Mon Sep 17 00:00:00 2001 From: huangtianhua Date: Fri, 9 Sep 2016 17:08:00 -0400 Subject: [PATCH] Allows condition name using boolean or function This change supports: 1. Allow boolean value or condition function as condition name of 'if' function: resources: r1: ... properties: a: {if: [true, 'value_true', 'value_false']} r2: ... properties: b: {if: [{equals: [...]}, 'value_true', 'value_false']} 2. Allow boolean value or condition function as condtiion name in resource definition: resources: r1: ... condition: false r2: ... condition: {and: [cd1, cd2]} 3. Allow boolean value or condition function as condition name in outputs: outputs: output1: value: ... condition: true output2: value: ... condition: {not: cd3} Change-Id: I2bf9bb0b608788c928d12425cbfdaf658df9e880 Co-Authored-By: Zane Bitter Blueprint: support-conditions-function --- doc/source/template_guide/hot_spec.rst | 4 +-- heat/engine/cfn/template.py | 13 ++++++---- heat/engine/conditions.py | 3 +++ heat/engine/hot/functions.py | 13 +++++++--- heat/engine/hot/template.py | 13 +++++----- heat/engine/rsrc_defn.py | 9 ++++--- heat/engine/template_common.py | 7 +++-- heat/tests/test_hot.py | 29 +++++++++++++++++++++ heat/tests/test_template.py | 36 +++++++++++++++++++++++++- 9 files changed, 102 insertions(+), 25 deletions(-) diff --git a/doc/source/template_guide/hot_spec.rst b/doc/source/template_guide/hot_spec.rst index 19ddd09bda..509f464b40 100644 --- a/doc/source/template_guide/hot_spec.rst +++ b/doc/source/template_guide/hot_spec.rst @@ -644,7 +644,7 @@ the following syntax update_policy: deletion_policy: external_id: - condition: + condition: resource ID A resource ID which must be unique within the ``resources`` section of the @@ -775,7 +775,7 @@ according to the following syntax : description: value: - condition: + condition: parameter name The output parameter name, which must be unique within the ``outputs`` diff --git a/heat/engine/cfn/template.py b/heat/engine/cfn/template.py index 0b43cdff10..afb2f55b84 100644 --- a/heat/engine/cfn/template.py +++ b/heat/engine/cfn/template.py @@ -11,11 +11,14 @@ # License for the specific language governing permissions and limitations # under the License. +import functools + 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 from heat.engine import parameters from heat.engine import rsrc_defn from heat.engine import template_common @@ -121,7 +124,7 @@ class CfnTemplateBase(template_common.CommonTemplate): raise exception.StackValidationFailed(message=msg) defn = rsrc_defn.ResourceDefinition(name, **defn_data) - cond_name = defn.condition_name() + cond_name = defn.condition() if cond_name is not None: try: @@ -205,14 +208,14 @@ class CfnTemplate(CfnTemplateBase): for arg in super(CfnTemplate, self)._rsrc_defn_args(stack, name, data): yield arg - def no_parse(field, path): - return field + parse_cond = functools.partial(self.parse_condition, stack) yield ('condition', self._parse_resource_field(self.RES_CONDITION, - (six.string_types, bool), + (six.string_types, bool, + function.Function), 'string or boolean', - name, data, no_parse)) + name, data, parse_cond)) class HeatTemplate(CfnTemplateBase): diff --git a/heat/engine/conditions.py b/heat/engine/conditions.py index 3cc31c66d3..cc18fa9b64 100644 --- a/heat/engine/conditions.py +++ b/heat/engine/conditions.py @@ -53,6 +53,9 @@ class Conditions(object): if condition_name is None: return True + if isinstance(condition_name, bool): + return condition_name + if not (isinstance(condition_name, six.string_types) and condition_name in self._conditions): raise ValueError(_('Invalid condition "%s"') % condition_name) diff --git a/heat/engine/hot/functions.py b/heat/engine/hot/functions.py index 0b9e8277fc..007750638b 100644 --- a/heat/engine/hot/functions.py +++ b/heat/engine/hot/functions.py @@ -1149,17 +1149,22 @@ class If(function.Macro): not isinstance(self.args, collections.Sequence) or isinstance(self.args, six.string_types)): raise ValueError() - cd_name, value_if_true, value_if_false = self.args + condition, value_if_true, value_if_false = self.args except ValueError: msg = _('Arguments to "%s" must be of the form: ' '[condition_name, value_if_true, value_if_false]') raise ValueError(msg % self.fn_name) - cd = self._get_condition(cd_name) + cond = self.template.parse_condition(self.stack, condition, + self.fn_name) + cd = self._get_condition(function.resolve(cond)) return parse_func(value_if_true if cd else value_if_false) - def _get_condition(self, cd_name): - return self.template.conditions(self.stack).is_enabled(cd_name) + def _get_condition(self, cond): + if isinstance(cond, bool): + return cond + + return self.template.conditions(self.stack).is_enabled(cond) class ConditionBoolean(function.Function): diff --git a/heat/engine/hot/template.py b/heat/engine/hot/template.py index af280a2e8b..f6d0e1a8db 100644 --- a/heat/engine/hot/template.py +++ b/heat/engine/hot/template.py @@ -241,7 +241,7 @@ class HOTemplate20130523(template_common.CommonTemplate): raise exception.StackValidationFailed(message=msg) defn = rsrc_defn.ResourceDefinition(name, **defn_data) - cond_name = defn.condition_name() + cond_name = defn.condition() if cond_name is not None: try: @@ -494,9 +494,7 @@ class HOTemplate20161014(HOTemplate20160408): yield arg parse = functools.partial(self.parse, stack) - - def no_parse(field, path): - return field + parse_cond = functools.partial(self.parse_condition, stack) yield ('external_id', self._parse_resource_field(self.RES_EXTERNAL_ID, @@ -507,6 +505,7 @@ class HOTemplate20161014(HOTemplate20160408): yield ('condition', self._parse_resource_field(self.RES_CONDITION, - (six.string_types, bool), - 'string or boolean', - name, data, no_parse)) + (six.string_types, bool, + function.Function), + 'string_or_boolean', + name, data, parse_cond)) diff --git a/heat/engine/rsrc_defn.py b/heat/engine/rsrc_defn.py index 49e780e2ef..4efff2fd1f 100644 --- a/heat/engine/rsrc_defn.py +++ b/heat/engine/rsrc_defn.py @@ -135,8 +135,9 @@ class ResourceDefinitionCore(object): self._deletion_policy = self.RETAIN if condition is not None: - assert isinstance(condition, six.string_types) - self._hash ^= hash(condition) + assert isinstance(condition, (six.string_types, bool, + function.Function)) + self._hash ^= _hash_data(condition) def freeze(self, **overrides): """Return a frozen resource definition, with all functions resolved. @@ -268,12 +269,12 @@ class ResourceDefinitionCore(object): """Return the external resource id.""" return function.resolve(self._external_id) - def condition_name(self): + def condition(self): """Return the name of the conditional inclusion rule, if any. Returns None if the resource is included unconditionally. """ - return self._condition + return function.resolve(self._condition) def render_hot(self): """Return a HOT snippet for the resource definition.""" diff --git a/heat/engine/template_common.py b/heat/engine/template_common.py index f421d18f90..fb5557a070 100644 --- a/heat/engine/template_common.py +++ b/heat/engine/template_common.py @@ -179,9 +179,12 @@ class CommonTemplate(template.Template): description = val.get(self.OUTPUT_DESCRIPTION) if hasattr(self, 'OUTPUT_CONDITION'): - cond_name = val.get(self.OUTPUT_CONDITION) + path = [self.OUTPUTS, key, self.OUTPUT_CONDITION] + cond = self.parse_condition(stack, + val.get(self.OUTPUT_CONDITION), + '.'.join(path)) try: - enabled = conds.is_enabled(cond_name) + enabled = conds.is_enabled(function.resolve(cond)) except ValueError as exc: path = [self.OUTPUTS, key, self.OUTPUT_CONDITION] message = six.text_type(exc) diff --git a/heat/tests/test_hot.py b/heat/tests/test_hot.py index 6be84fa609..494dfcd260 100644 --- a/heat/tests/test_hot.py +++ b/heat/tests/test_hot.py @@ -1180,6 +1180,35 @@ class HOTemplateTest(common.HeatTestCase): resolved = self.resolve(snippet, tmpl, stack) self.assertEqual('value_if_false', resolved) + def test_if_using_boolean_condition(self): + snippet = {'if': [True, 'value_if_true', 'value_if_false']} + # when condition is true, if function resolve to value_if_true + tmpl = template.Template(hot_newton_tpl_empty) + stack = parser.Stack(utils.dummy_context(), + 'test_if_using_boolean_condition', tmpl) + resolved = self.resolve(snippet, tmpl, stack) + self.assertEqual('value_if_true', resolved) + # when condition is false, if function resolve to value_if_false + snippet = {'if': [False, 'value_if_true', 'value_if_false']} + resolved = self.resolve(snippet, tmpl, stack) + self.assertEqual('value_if_false', resolved) + + def test_if_using_condition_function(self): + tmpl_with_conditions = template_format.parse(''' +heat_template_version: 2016-10-14 +conditions: + create_prod: False +''') + snippet = {'if': [{'not': 'create_prod'}, + 'value_if_true', 'value_if_false']} + + tmpl = template.Template(tmpl_with_conditions) + stack = parser.Stack(utils.dummy_context(), + 'test_if_using_condition_function', tmpl) + + resolved = self.resolve(snippet, tmpl, stack) + self.assertEqual('value_if_true', resolved) + def test_if_invalid_args(self): snippet = {'if': ['create_prod', 'one_value']} tmpl = template.Template(hot_newton_tpl_empty) diff --git a/heat/tests/test_template.py b/heat/tests/test_template.py index 9c91794fae..5c8d12b040 100644 --- a/heat/tests/test_template.py +++ b/heat/tests/test_template.py @@ -297,7 +297,7 @@ class TestTemplateConditionParser(common.HeatTestCase): 'outputs': { 'foo': { 'condition': 'prod_env', - 'value': {'get_attr': ['r1', 'foo']} + 'value': 'show me' } } } @@ -400,6 +400,15 @@ class TestTemplateConditionParser(common.HeatTestCase): ex = self.assertRaises(ValueError, conds.is_enabled, 111) self.assertIn('Invalid condition "111"', six.text_type(ex)) + def test_res_condition_using_boolean(self): + tmpl = copy.deepcopy(self.tmpl) + # test condition name is boolean + stk = stack.Stack(self.ctx, 'test_res_cd_boolean', tmpl) + + conds = tmpl.conditions(stk) + self.assertTrue(conds.is_enabled(True)) + self.assertFalse(conds.is_enabled(False)) + def test_parse_output_condition_invalid(self): stk = stack.Stack(self.ctx, 'test_output_invalid_condition', @@ -441,6 +450,31 @@ class TestTemplateConditionParser(common.HeatTestCase): self.assertIn('Circular definition for condition "first_cond"', six.text_type(ex)) + def test_parse_output_condition_boolean(self): + t = copy.deepcopy(self.tmpl.t) + t['outputs']['foo']['condition'] = True + stk = stack.Stack(self.ctx, + 'test_output_cd_boolean', + template.Template(t)) + + self.assertEqual('show me', stk.outputs['foo'].get_value()) + + t = copy.deepcopy(self.tmpl.t) + t['outputs']['foo']['condition'] = False + stk = stack.Stack(self.ctx, + 'test_output_cd_boolean', + template.Template(t)) + self.assertIsNone(stk.outputs['foo'].get_value()) + + def test_parse_output_condition_function(self): + t = copy.deepcopy(self.tmpl.t) + t['outputs']['foo']['condition'] = {'not': 'prod_env'} + stk = stack.Stack(self.ctx, + 'test_output_cd_function', + template.Template(t)) + + self.assertEqual('show me', stk.outputs['foo'].get_value()) + class TestTemplateValidate(common.HeatTestCase):