Provide 'and' function

Provides condition function 'and' for templates:
AWSTemplateFormatVersion.2010-09-09
heat_template_version.2016-10-14

Blueprint: support-conditions-function
Change-Id: I7a9a10b871f50a5a2885adc72d60d626f960557c
This commit is contained in:
huangtianhua 2016-06-17 09:25:38 +08:00
parent aeb15bf818
commit 6fa325e34e
7 changed files with 254 additions and 7 deletions

View File

@ -425,3 +425,26 @@ Usage
Returns false if the param 'env_type' equals to 'prod',
otherwise returns true.
-------
Fn::And
-------
Acts as an AND operator to evaluate all the specified conditions.
Returns true if all the specified conditions evaluate to true, or returns
false if any one of the conditions evaluates to false.
Parameters
~~~~~~~~~~
condition:
A condition such as Fn::Equals that evaluates to true or false.
Usage
~~~~~
.. code-block:: yaml
{'Fn::And': [{'Fn::Equals': [{'Ref': env_type}, 'prod']},
{'Fn::Not': [{'Fn::Equals': [{'Ref': zone}, 'beijing']}]}]
Returns true if the param 'env_type' equals to 'prod' and the param 'zone' is
not equal to 'beijing', otherwise returns false.

View File

@ -243,12 +243,14 @@ for the ``heat_template_version`` key:
This version adds ``equals`` condition function which can be used
to compare whether two values are equal, the ``not`` condition function
which acts as a NOT operator. The complete list of supported
condition functions is::
which acts as a NOT operator, the ``and`` condition function which acts
as an AND operator to evaluate all the specified conditions. The complete
list of supported condition functions is::
equals
get_param
not
and
.. _hot_spec_parameter_groups:
@ -833,6 +835,7 @@ expression
equals
get_param
not
and
Note: In condition functions, you can reference a value from an input
parameter, but you cannot reference resource or its attribute.
@ -854,6 +857,15 @@ An example of conditions section definition
equals:
- get_param: param3
- yes
cd5:
and:
- equals:
- get_param: env_type
- prod
- not:
equals:
- get_param: zone
- beijing
The example below shows how to associate condition with resources
@ -1594,3 +1606,37 @@ Another example
not: True
This function returns false.
and
---
The ``and`` function acts as an AND operator to evaluate all the
specified conditions.
The syntax of the ``and`` function is
.. code-block:: yaml
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.
Returns true if all the specified conditions evaluate to true, or returns
false if any one of the conditions evaluates to false.
For example
.. code-block:: yaml
and:
- equals:
- get_param: env_type
- prod
- not:
equals:
- get_param: zone
- beijing
If param 'env_type' equals to 'prod', and param 'zone' is not equal to
'beijing', this function returns true, otherwise returns false.

View File

@ -200,7 +200,8 @@ class CfnTemplate(CfnTemplateBase):
'Fn::Equals': hot_funcs.Equals,
'Ref': cfn_funcs.ParamRef,
'Fn::FindInMap': cfn_funcs.FindInMap,
'Fn::Not': cfn_funcs.Not
'Fn::Not': cfn_funcs.Not,
'Fn::And': hot_funcs.And
}
def __init__(self, tmpl, template_id=None, files=None, env=None):

View File

@ -999,3 +999,39 @@ class Not(function.Function):
'after resolved the value is: %s')
raise ValueError(msg % resolved_value)
return not resolved_value
class And(function.Function):
"""A function acts as an AND operator.
Takes the form::
{ "and" : [{condition_1}, {condition_2}, {...}, {condition_n}] }
Returns true if all the specified conditions evaluate to true, or returns
false if any one of the conditions evaluates to false. The minimum number
of conditions that you can include is 2.
"""
def __init__(self, stack, fn_name, args):
super(And, self).__init__(stack, fn_name, args)
if (not self.args or
not (isinstance(self.args, collections.Sequence) and
not isinstance(self.args, six.string_types)) or
len(self.args) < 2):
msg = _('Arguments to "%s" must be of the form: '
'[{condition_1}, {condition_2}, {...}, {condition_n}], '
'the minimum number of conditions is 2.')
raise ValueError(msg % self.fn_name)
def result(self):
for cd in self.args:
resolved_value = function.resolve(cd)
if not isinstance(resolved_value, bool):
msg = _('The condition value should be boolean, '
'after resolved the value is: %s')
raise ValueError(msg % resolved_value)
if not resolved_value:
return False
return True

View File

@ -465,7 +465,8 @@ class HOTemplate20161014(HOTemplate20160408):
condition_functions = {
'get_param': hot_funcs.GetParam,
'equals': hot_funcs.Equals,
'not': hot_funcs.Not
'not': hot_funcs.Not,
'and': hot_funcs.And
}
def __init__(self, tmpl, template_id=None, files=None, env=None):

View File

@ -999,6 +999,73 @@ class TemplateTest(common.HeatTestCase):
'of the form: [condition]')
self.assertIn(error_msg, six.text_type(exc))
def test_and(self):
tpl = template_format.parse('''
AWSTemplateFormatVersion: 2010-09-09
Parameters:
env_type:
Type: String
Default: 'test'
zone:
Type: String
Default: 'shanghai'
''')
snippet = {
'Fn::And': [
{'Fn::Equals': [{'Ref': 'env_type'}, 'prod']},
{'Fn::Not': [{'Fn::Equals': [{'Ref': 'zone'}, "beijing"]}]}]}
# when param 'env_type' is 'test', and function resolve to false
tmpl = template.Template(tpl)
stk = stack.Stack(utils.dummy_context(),
'test_and_false', tmpl)
resolved = self.resolve_condition(snippet, tmpl, stk)
self.assertFalse(resolved)
# when param 'env_type' is 'prod', and param 'zone' is 'shanghai',
# the 'and' function resolve to true
tmpl = template.Template(tpl,
env=environment.Environment(
{'env_type': 'prod'}))
stk = stack.Stack(utils.dummy_context(),
'test_and_true', tmpl)
resolved = self.resolve_condition(snippet, tmpl, stk)
self.assertTrue(resolved)
# when param 'env_type' is 'prod', and param 'zone' is 'shanghai',
# the 'and' function resolve to true
tmpl = template.Template(tpl,
env=environment.Environment(
{'env_type': 'prod',
'zone': 'beijing'}))
stk = stack.Stack(utils.dummy_context(),
'test_and_false', tmpl)
resolved = self.resolve_condition(snippet, tmpl, stk)
self.assertFalse(resolved)
def test_and_invalid_args(self):
tmpl = template.Template(aws_empty_template)
snippet = {'Fn::And': ['invalid_arg']}
exc = self.assertRaises(exception.StackValidationFailed,
self.resolve_condition, snippet, tmpl)
error_msg = ('.Fn::And: Arguments to "Fn::And" must be '
'of the form: [{condition_1}, {condition_2}, {...}, '
'{condition_n}]')
self.assertIn(error_msg, six.text_type(exc))
# test invalid type
snippet = {'Fn::And': 'invalid'}
exc = self.assertRaises(exception.StackValidationFailed,
self.resolve_condition, snippet, tmpl)
self.assertIn(error_msg, six.text_type(exc))
snippet = {'Fn::And': ['cd1', True]}
exc = self.assertRaises(ValueError,
self.resolve_condition, snippet, tmpl)
error_msg = ('The condition value should be boolean, '
'after resolved the value is: cd1')
self.assertIn(error_msg, six.text_type(exc))
def test_join(self):
tmpl = template.Template(empty_template)
join = {"Fn::Join": [" ", ["foo", "bar"]]}

View File

@ -20,6 +20,9 @@ Parameters:
Default: test
Type: String
AllowedValues: [prod, test]
zone:
Type: String
Default: beijing
Conditions:
Prod: {"Fn::Equals" : [{Ref: env_type}, "prod"]}
Test:
@ -27,6 +30,14 @@ Conditions:
- Fn::Equals:
- Ref: env_type
- prod
Beijing_Prod:
Fn::And:
- Fn::Equals:
- Ref: env_type
- prod
- Fn::Equals:
- Ref: zone
- beijing
Resources:
test_res:
Type: OS::Heat::TestResource
@ -42,6 +53,11 @@ Resources:
Properties:
value: just in test env
Condition: Test
beijing_prod_res:
Type: OS::Heat::TestResource
Properties:
value: beijing_prod_res
Condition: Beijing_Prod
Outputs:
res_value:
Value: {"Fn::GetAtt": [prod_res, output]}
@ -53,6 +69,8 @@ Outputs:
test_res1_value:
Value: {"Fn::If": [Test, {"Fn::GetAtt": [test_res1, output]},
'no_test_res1']}
beijing_prod_res:
Value: {"Fn::If": [Beijing_Prod, {Ref: beijing_prod_res}, 'no_prod_res']}
'''
hot_template = '''
@ -63,6 +81,9 @@ parameters:
type: string
constraints:
- allowed_values: [prod, test]
zone:
type: string
default: beijing
conditions:
prod: {equals : [{get_param: env_type}, "prod"]}
test:
@ -70,6 +91,14 @@ conditions:
equals:
- get_param: env_type
- prod
beijing_prod:
and:
- equals:
- get_param: zone
- beijing
- equals:
- get_param: env_type
- prod
resources:
test_res:
type: OS::Heat::TestResource
@ -85,6 +114,11 @@ resources:
properties:
value: just in test env
condition: test
beijing_prod_res:
type: OS::Heat::TestResource
properties:
value: beijing_prod_res
condition: beijing_prod
outputs:
res_value:
value: {get_attr: [prod_res, output]}
@ -95,6 +129,9 @@ outputs:
value: {if: [prod, {get_resource: prod_res}, 'no_prod_res']}
test_res1_value:
value: {if: [test, {get_attr: [test_res1, output]}, 'no_test_res1']}
beijing_prod_res:
value: {if: [beijing_prod, {get_resource: beijing_prod_res},
'no_prod_res']}
'''
@ -103,9 +140,13 @@ class CreateUpdateResConditionTest(functional_base.FunctionalTestsBase):
def setUp(self):
super(CreateUpdateResConditionTest, self).setUp()
def res_assert_for_prod(self, resources):
self.assertEqual(2, len(resources))
def res_assert_for_prod(self, resources, bj_prod=True):
res_names = [res.resource_name for res in resources]
if bj_prod:
self.assertEqual(3, len(resources))
self.assertIn('beijing_prod_res', res_names)
else:
self.assertEqual(2, len(resources))
self.assertIn('prod_res', res_names)
self.assertIn('test_res', res_names)
@ -116,7 +157,7 @@ class CreateUpdateResConditionTest(functional_base.FunctionalTestsBase):
self.assertIn('test_res1', res_names)
self.assertNotIn('prod_res', res_names)
def output_assert_for_prod(self, stack_id):
def output_assert_for_prod(self, stack_id, bj_prod=True):
output = self.client.stacks.output_show(stack_id,
'res_value')['output']
self.assertEqual('prod_res', output['output_value'])
@ -133,6 +174,14 @@ class CreateUpdateResConditionTest(functional_base.FunctionalTestsBase):
stack_id, 'test_res1_value')['output']
self.assertEqual('no_test_res1', test_res_output['output_value'])
beijing_prod_res = self.client.stacks.output_show(
stack_id, 'beijing_prod_res')['output']
if bj_prod:
self.assertNotEqual('no_prod_res',
beijing_prod_res['output_value'])
else:
self.assertEqual('no_prod_res', beijing_prod_res['output_value'])
def output_assert_for_test(self, stack_id):
output = self.client.stacks.output_show(stack_id,
'res_value')['output']
@ -151,6 +200,10 @@ class CreateUpdateResConditionTest(functional_base.FunctionalTestsBase):
self.assertEqual('just in test env',
test_res_output['output_value'])
beijing_prod_res = self.client.stacks.output_show(
stack_id, 'beijing_prod_res')['output']
self.assertEqual('no_prod_res', beijing_prod_res['output_value'])
def test_stack_create_update_cfn_template_test_to_prod(self):
stack_identifier = self.stack_create(template=cfn_template)
resources = self.client.resources.list(stack_identifier)
@ -166,6 +219,16 @@ class CreateUpdateResConditionTest(functional_base.FunctionalTestsBase):
self.res_assert_for_prod(resources)
self.output_assert_for_prod(stack_identifier)
parms = {'env_type': 'prod',
'zone': 'shanghai'}
self.update_stack(stack_identifier,
template=cfn_template,
parameters=parms)
resources = self.client.resources.list(stack_identifier)
self.res_assert_for_prod(resources, False)
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,
@ -198,6 +261,16 @@ class CreateUpdateResConditionTest(functional_base.FunctionalTestsBase):
self.res_assert_for_prod(resources)
self.output_assert_for_prod(stack_identifier)
parms = {'env_type': 'prod',
'zone': 'shanghai'}
self.update_stack(stack_identifier,
template=hot_template,
parameters=parms)
resources = self.client.resources.list(stack_identifier)
self.res_assert_for_prod(resources, False)
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,