From bca8b8e80453a31f5f3a716e1f3f3f7f348cdb4b Mon Sep 17 00:00:00 2001 From: Zane Bitter Date: Thu, 8 Sep 2016 15:37:51 -0400 Subject: [PATCH] Allow referencing conditions by name This change allows reference with other conditions by name in definition of a condition, something like: conditions: cd1: {equals: [{get_param: env_type}, 'prod']} cd2: {not: cd1} cd3: {equals: [{get_param: zone}, 'fujian']} cd4: {and: [cd1, cd3]} Change-Id: I6a0a00c23aa7d559dedd6998adaa7962d607f315 Co-Authored-By: huangtianhua Blueprint: support-conditions-function Related-Bug: #1621529 --- doc/source/template_guide/hot_spec.rst | 60 +++++- heat/engine/conditions.py | 16 +- heat/engine/hot/functions.py | 4 +- heat/tests/test_template.py | 68 ++++++- .../functional/test_conditions.py | 190 +++++++++++++++++- 5 files changed, 306 insertions(+), 32 deletions(-) diff --git a/doc/source/template_guide/hot_spec.rst b/doc/source/template_guide/hot_spec.rst index 434f3e8de1..19ddd09bda 100644 --- a/doc/source/template_guide/hot_spec.rst +++ b/doc/source/template_guide/hot_spec.rst @@ -842,7 +842,8 @@ expression or Note: In condition functions, you can reference a value from an input - parameter, but you cannot reference resource or its attribute. + parameter, but you cannot reference resource or its attribute. We support + referencing other conditions (by condition name) in condition functions. An example of conditions section definition @@ -878,6 +879,12 @@ An example of conditions section definition - equals: - get_param: zone - beijing + cd7: + not: cd4 + cd8: + and: + - cd1 + - cd2 The example below shows how to associate condition with resources @@ -1592,9 +1599,9 @@ The syntax of the ``not`` function is not: condition -Note: A condition such as ``equals`` that evaluates to true or false -can be defined in ``not`` function, also we can set a boolean -value as condition. +Note: A condition can be an expression such as ``equals``, ``or`` and ``and`` +that evaluates to true or false, can be a boolean, and can be other condition +name defined in ``conditions`` section of template. Returns true for a condition that evaluates to false or returns false for a condition that evaluates to true. @@ -1611,7 +1618,7 @@ For example If param 'env_type' equals to 'prod', this function returns false, otherwise returns true. -Another example +Another example with boolean value definition .. code-block:: yaml @@ -1619,6 +1626,15 @@ Another example This function returns false. +Another example reference other condition name + +.. code-block:: yaml + + not: my_other_condition + +This function returns false if my_other_condition evaluates to true, +otherwise returns true. + and --- The ``and`` function acts as an AND operator to evaluate all the @@ -1630,9 +1646,9 @@ The syntax of the ``and`` function is and: [{condition_1}, {condition_2}, ... {condition_n}] -Note: A condition such as ``equals`` or ``not`` that evaluates to true or -false can be defined in ``and`` function, also we can set a boolean -value as condition. +Note: A condition can be an expression such as ``equals``, ``or`` and ``not`` +that evaluates to true or false, can be a boolean, and can be other condition +names defined in ``conditions`` section of template. Returns true if all the specified conditions evaluate to true, or returns false if any one of the conditions evaluates to false. @@ -1653,6 +1669,17 @@ For example If param 'env_type' equals to 'prod', and param 'zone' is not equal to 'beijing', this function returns true, otherwise returns false. +Another example reference with other conditions + +.. code-block:: yaml + + and: + - other_condition_1 + - other_condition_2 + +This function returns true if other_condition_1 and other_condition_2 +evaluate to true both, otherwise returns false. + or -- The ``or`` function acts as an OR operator to evaluate all the @@ -1664,9 +1691,9 @@ The syntax of the ``or`` function is or: [{condition_1}, {condition_2}, ... {condition_n}] -Note: A condition such as ``equals`` or ``not`` that evaluates to true or -false can be defined in ``or`` function, also we can set a boolean -value as condition. +Note: A condition can be an expression such as ``equals``, ``and`` and ``not`` +that evaluates to true or false, can be a boolean, and can be other condition +names defined in ``conditions`` section of template. Returns true if any one of the specified conditions evaluate to true, or returns false if all of the conditions evaluates to false. @@ -1686,3 +1713,14 @@ For example If param 'env_type' equals to 'prod', or the param 'zone' is not equal to 'beijing', this function returns true, otherwise returns false. + +Another example reference other conditions + +.. code-block:: yaml + + or: + - other_condition_1 + - other_condition_2 + +This function returns true if any one of other_condition_1 or +other_condition_2 evaluate to true, otherwise returns false. diff --git a/heat/engine/conditions.py b/heat/engine/conditions.py index e54269cccb..3cc31c66d3 100644 --- a/heat/engine/conditions.py +++ b/heat/engine/conditions.py @@ -21,6 +21,9 @@ from heat.common import exception from heat.engine import function +_in_progress = object() + + class Conditions(object): def __init__(self, conditions_dict): assert isinstance(conditions_dict, collections.Mapping) @@ -55,8 +58,19 @@ class Conditions(object): raise ValueError(_('Invalid condition "%s"') % condition_name) if condition_name not in self._resolved: + self._resolved[condition_name] = _in_progress self._resolved[condition_name] = self._resolve(condition_name) - return self._resolved[condition_name] + + result = self._resolved[condition_name] + + if result is _in_progress: + message = _('Circular definition for condition ' + '"%s"') % condition_name + raise exception.StackValidationFailed( + error='Condition validation error', + message=message) + + return result def __repr__(self): return 'Conditions(%r)' % self._conditions diff --git a/heat/engine/hot/functions.py b/heat/engine/hot/functions.py index b2570ec581..0b9e8277fc 100644 --- a/heat/engine/hot/functions.py +++ b/heat/engine/hot/functions.py @@ -1182,8 +1182,8 @@ class ConditionBoolean(function.Function): if isinstance(arg, bool): return arg - msg = _('The condition value must be a boolean: %s') - raise ValueError(msg % arg) + conditions = self.stack.t.conditions(self.stack) + return conditions.is_enabled(arg) class Not(ConditionBoolean): diff --git a/heat/tests/test_template.py b/heat/tests/test_template.py index ba83a7cbed..9c91794fae 100644 --- a/heat/tests/test_template.py +++ b/heat/tests/test_template.py @@ -362,6 +362,32 @@ class TestTemplateConditionParser(common.HeatTestCase): self.assertIn('The definition of condition "prod_env" is invalid', six.text_type(ex)) + def test_condition_reference_condition(self): + t = { + 'heat_template_version': '2016-10-14', + 'parameters': { + 'env_type': { + 'type': 'string', + 'default': 'test' + } + }, + 'conditions': { + 'prod_env': {'equals': [{'get_param': 'env_type'}, 'prod']}, + 'test_env': {'not': 'prod_env'}, + 'prod_or_test_env': {'or': ['prod_env', 'test_env']}, + 'prod_and_test_env': {'and': ['prod_env', 'test_env']}, + }} + + # test with get_attr in equals + tmpl = template.Template(t) + stk = stack.Stack(self.ctx, 'test_condition_reference', tmpl) + conditions = tmpl.conditions(stk) + + self.assertFalse(conditions.is_enabled('prod_env')) + self.assertTrue(conditions.is_enabled('test_env')) + self.assertTrue(conditions.is_enabled('prod_or_test_env')) + self.assertFalse(conditions.is_enabled('prod_and_test_env')) + def test_get_res_condition_invalid(self): tmpl = copy.deepcopy(self.tmpl) # test condition name is invalid @@ -392,6 +418,29 @@ class TestTemplateConditionParser(common.HeatTestCase): self.assertIn('Invalid condition "222"', six.text_type(ex)) self.assertIn('outputs.foo.condition', six.text_type(ex)) + def test_conditions_circular_ref(self): + t = { + 'heat_template_version': '2016-10-14', + 'parameters': { + 'env_type': { + 'type': 'string', + 'default': 'test' + } + }, + 'conditions': { + 'first_cond': {'not': 'second_cond'}, + 'second_cond': {'not': 'third_cond'}, + 'third_cond': {'not': 'first_cond'}, + } + } + tmpl = template.Template(t) + stk = stack.Stack(self.ctx, 'test_condition_circular_ref', tmpl) + conds = tmpl.conditions(stk) + ex = self.assertRaises(exception.StackValidationFailed, + conds.is_enabled, 'first_cond') + self.assertIn('Circular definition for condition "first_cond"', + six.text_type(ex)) + class TestTemplateValidate(common.HeatTestCase): @@ -958,12 +1007,13 @@ class TemplateTest(common.HeatTestCase): def test_not_invalid_args(self): tmpl = template.Template(aws_empty_template) + stk = stack.Stack(utils.dummy_context(), + 'test_not_invalid', tmpl) snippet = {'Fn::Not': ['invalid_arg']} exc = self.assertRaises(ValueError, - self.resolve_condition, snippet, tmpl) + self.resolve_condition, snippet, tmpl, stk) - error_msg = ('The condition value must be a boolean: ' - 'invalid_arg') + error_msg = 'Invalid condition "invalid_arg"' self.assertIn(error_msg, six.text_type(exc)) # test invalid type snippet = {'Fn::Not': 'invalid'} @@ -1036,11 +1086,11 @@ class TemplateTest(common.HeatTestCase): self.resolve_condition, snippet, tmpl) self.assertIn(error_msg, six.text_type(exc)) + stk = stack.Stack(utils.dummy_context(), 'test_and_invalid', tmpl) snippet = {'Fn::And': ['cd1', True]} exc = self.assertRaises(ValueError, - self.resolve_condition, snippet, tmpl) - error_msg = ('The condition value must be a boolean: ' - 'cd1') + self.resolve_condition, snippet, tmpl, stk) + error_msg = 'Invalid condition "cd1"' self.assertIn(error_msg, six.text_type(exc)) def test_or(self): @@ -1097,11 +1147,11 @@ class TemplateTest(common.HeatTestCase): self.resolve_condition, snippet, tmpl) self.assertIn(error_msg, six.text_type(exc)) + stk = stack.Stack(utils.dummy_context(), 'test_or_invalid', tmpl) snippet = {'Fn::Or': ['invalid_cd', True]} exc = self.assertRaises(ValueError, - self.resolve_condition, snippet, tmpl) - error_msg = ('The condition value must be a boolean: ' - 'invalid_cd') + self.resolve_condition, snippet, tmpl, stk) + error_msg = 'Invalid condition "invalid_cd"' self.assertIn(error_msg, six.text_type(exc)) def test_join(self): diff --git a/heat_integrationtests/functional/test_conditions.py b/heat_integrationtests/functional/test_conditions.py index 95b43fe23c..659d3723ad 100644 --- a/heat_integrationtests/functional/test_conditions.py +++ b/heat_integrationtests/functional/test_conditions.py @@ -38,6 +38,14 @@ Conditions: - Fn::Equals: - Ref: zone - beijing + Xian_Zone: + Fn::Equals: + - Ref: zone + - xian + Xianyang_Zone: + Fn::Equals: + - Ref: zone + - xianyang Fujian_Zone: Fn::Or: - Fn::Equals: @@ -46,6 +54,16 @@ Conditions: - Fn::Equals: - Ref: zone - xiamen + Fujian_Prod: + Fn::And: + - Fujian_Zone + - Prod + Shannxi_Provice: + Fn::Or: + - Xian_Zone + - Xianyang_Zone + Not_Shannxi: + Fn::Not: [Shannxi_Provice] Resources: test_res: Type: OS::Heat::TestResource @@ -71,6 +89,21 @@ Resources: Condition: Fujian_Zone Properties: value: fujian_res + fujian_prod_res: + Type: OS::Heat::TestResource + Condition: Fujian_Prod + Properties: + value: fujian_prod_res + shannxi_res: + Type: OS::Heat::TestResource + Condition: Shannxi_Provice + Properties: + value: shannxi_res + not_shannxi_res: + Type: OS::Heat::TestResource + Condition: Not_Shannxi + Properties: + value: not_shannxi_res Outputs: res_value: Value: {"Fn::GetAtt": [prod_res, output]} @@ -112,6 +145,14 @@ conditions: - equals: - get_param: env_type - prod + xian_zone: + equals: + - get_param: zone + - xian + xianyang_zone: + equals: + - get_param: zone + - xianyang fujian_zone: or: - equals: @@ -120,6 +161,16 @@ conditions: - equals: - get_param: zone - xiamen + fujian_prod: + and: + - fujian_zone + - prod + shannxi_provice: + or: + - xian_zone + - xianyang_zone + not_shannxi: + not: shannxi_provice resources: test_res: type: OS::Heat::TestResource @@ -145,6 +196,21 @@ resources: condition: fujian_zone properties: value: fujian_res + fujian_prod_res: + type: OS::Heat::TestResource + condition: fujian_prod + properties: + value: fujian_prod_res + shannxi_res: + type: OS::Heat::TestResource + condition: shannxi_provice + properties: + value: shannxi_res + not_shannxi_res: + type: OS::Heat::TestResource + condition: not_shannxi + properties: + value: not_shannxi_res outputs: res_value: value: {get_attr: [prod_res, output]} @@ -244,30 +310,43 @@ class CreateUpdateResConditionTest(functional_base.FunctionalTestsBase): def setUp(self): super(CreateUpdateResConditionTest, self).setUp() - def res_assert_for_prod(self, resources, bj_prod=True, fj_zone=False): + def res_assert_for_prod(self, resources, bj_prod=True, fj_zone=False, + shannxi_provice=False): res_names = [res.resource_name for res in resources] if bj_prod: - self.assertEqual(3, len(resources)) + self.assertEqual(4, len(resources)) self.assertIn('beijing_prod_res', res_names) + self.assertIn('not_shannxi_res', res_names) elif fj_zone: - self.assertEqual(3, len(resources)) + self.assertEqual(5, len(resources)) self.assertIn('fujian_res', res_names) self.assertNotIn('beijing_prod_res', res_names) + self.assertIn('not_shannxi_res', res_names) + self.assertIn('fujian_prod_res', res_names) + elif shannxi_provice: + self.assertEqual(3, len(resources)) + self.assertIn('shannxi_res', res_names) else: - self.assertEqual(2, len(resources)) + self.assertEqual(3, len(resources)) + self.assertIn('not_shannxi_res', res_names) self.assertIn('prod_res', res_names) self.assertIn('test_res', res_names) - def res_assert_for_test(self, resources, fj_zone=False): + def res_assert_for_test(self, resources, fj_zone=False, + shannxi_provice=False): res_names = [res.resource_name for res in resources] if fj_zone: - self.assertEqual(3, len(resources)) + self.assertEqual(4, len(resources)) self.assertIn('fujian_res', res_names) - else: - self.assertEqual(2, len(resources)) + self.assertIn('not_shannxi_res', res_names) + elif shannxi_provice: + self.assertEqual(3, len(resources)) self.assertNotIn('fujian_res', res_names) - + self.assertIn('shannxi_res', res_names) + else: + self.assertEqual(3, len(resources)) + self.assertIn('not_shannxi_res', res_names) self.assertIn('test_res', res_names) self.assertIn('test_res1', res_names) self.assertNotIn('prod_res', res_names) @@ -334,6 +413,15 @@ class CreateUpdateResConditionTest(functional_base.FunctionalTestsBase): self.res_assert_for_test(resources, fj_zone=True) self.output_assert_for_test(stack_identifier) + parms = {'zone': 'xianyang'} + self.update_stack(stack_identifier, + template=cfn_template, + parameters=parms) + + resources = self.client.resources.list(stack_identifier) + self.res_assert_for_test(resources, shannxi_provice=True) + self.output_assert_for_test(stack_identifier) + parms = {'env_type': 'prod'} self.update_stack(stack_identifier, template=cfn_template, @@ -363,6 +451,17 @@ class CreateUpdateResConditionTest(functional_base.FunctionalTestsBase): self.res_assert_for_prod(resources, bj_prod=False, fj_zone=True) self.output_assert_for_prod(stack_identifier, False) + parms = {'env_type': 'prod', + 'zone': 'xianyang'} + self.update_stack(stack_identifier, + template=cfn_template, + parameters=parms) + + resources = self.client.resources.list(stack_identifier) + self.res_assert_for_prod(resources, bj_prod=False, fj_zone=False, + shannxi_provice=True) + self.output_assert_for_prod(stack_identifier, False) + def test_stack_create_update_cfn_template_prod_to_test(self): parms = {'env_type': 'prod'} stack_identifier = self.stack_create(template=cfn_template, @@ -381,6 +480,28 @@ class CreateUpdateResConditionTest(functional_base.FunctionalTestsBase): self.res_assert_for_prod(resources, bj_prod=False, fj_zone=True) self.output_assert_for_prod(stack_identifier, bj_prod=False) + parms = {'zone': 'xianyang', + 'env_type': 'prod'} + self.update_stack(stack_identifier, + template=cfn_template, + parameters=parms) + + resources = self.client.resources.list(stack_identifier) + self.res_assert_for_prod(resources, bj_prod=False, fj_zone=False, + shannxi_provice=True) + self.output_assert_for_prod(stack_identifier, bj_prod=False) + + parms = {'zone': 'shanghai', + 'env_type': 'prod'} + self.update_stack(stack_identifier, + template=cfn_template, + parameters=parms) + + resources = self.client.resources.list(stack_identifier) + self.res_assert_for_prod(resources, bj_prod=False, fj_zone=False, + shannxi_provice=False) + self.output_assert_for_prod(stack_identifier, bj_prod=False) + parms = {'env_type': 'test'} self.update_stack(stack_identifier, template=cfn_template, @@ -400,12 +521,32 @@ class CreateUpdateResConditionTest(functional_base.FunctionalTestsBase): self.res_assert_for_test(resources, fj_zone=True) self.output_assert_for_test(stack_identifier) + parms = {'env_type': 'test', + 'zone': 'xianyang'} + self.update_stack(stack_identifier, + template=cfn_template, + parameters=parms) + + resources = self.client.resources.list(stack_identifier) + self.res_assert_for_test(resources, fj_zone=False, + shannxi_provice=True) + self.output_assert_for_test(stack_identifier) + def test_stack_create_update_hot_template_test_to_prod(self): stack_identifier = self.stack_create(template=hot_template) resources = self.client.resources.list(stack_identifier) self.res_assert_for_test(resources) self.output_assert_for_test(stack_identifier) + parms = {'zone': 'xianyang'} + self.update_stack(stack_identifier, + template=hot_template, + parameters=parms) + + resources = self.client.resources.list(stack_identifier) + self.res_assert_for_test(resources, shannxi_provice=True) + self.output_assert_for_test(stack_identifier) + parms = {'env_type': 'prod'} self.update_stack(stack_identifier, template=hot_template, @@ -425,6 +566,16 @@ class CreateUpdateResConditionTest(functional_base.FunctionalTestsBase): self.res_assert_for_prod(resources, False) self.output_assert_for_prod(stack_identifier, False) + parms = {'env_type': 'prod', + 'zone': 'xianyang'} + self.update_stack(stack_identifier, + template=hot_template, + parameters=parms) + + resources = self.client.resources.list(stack_identifier) + self.res_assert_for_prod(resources, False, shannxi_provice=True) + self.output_assert_for_prod(stack_identifier, False) + def test_stack_create_update_hot_template_prod_to_test(self): parms = {'env_type': 'prod'} stack_identifier = self.stack_create(template=hot_template, @@ -433,6 +584,16 @@ class CreateUpdateResConditionTest(functional_base.FunctionalTestsBase): self.res_assert_for_prod(resources) self.output_assert_for_prod(stack_identifier) + parms = {'env_type': 'prod', + 'zone': 'xianyang'} + self.update_stack(stack_identifier, + template=hot_template, + parameters=parms) + + resources = self.client.resources.list(stack_identifier) + self.res_assert_for_prod(resources, False, shannxi_provice=True) + self.output_assert_for_prod(stack_identifier, False) + parms = {'env_type': 'test'} self.update_stack(stack_identifier, template=hot_template, @@ -442,6 +603,17 @@ class CreateUpdateResConditionTest(functional_base.FunctionalTestsBase): self.res_assert_for_test(resources) self.output_assert_for_test(stack_identifier) + parms = {'env_type': 'test', + 'zone': 'xianyang'} + self.update_stack(stack_identifier, + template=hot_template, + parameters=parms) + + resources = self.client.resources.list(stack_identifier) + self.res_assert_for_test(resources, fj_zone=False, + shannxi_provice=True) + self.output_assert_for_test(stack_identifier) + def test_condition_rename(self): stack_identifier = self.stack_create(template=before_rename_tmpl) self.update_stack(stack_identifier, template=after_rename_tmpl)