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 <huangtianhua@huawei.com>
Blueprint: support-conditions-function
Related-Bug: #1621529
This commit is contained in:
Zane Bitter 2016-09-08 15:37:51 -04:00
parent 4090dfe926
commit bca8b8e804
5 changed files with 306 additions and 32 deletions

View File

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

View File

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

View File

@ -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):

View File

@ -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):

View File

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