diff --git a/doc/source/template_guide/functions.rst b/doc/source/template_guide/functions.rst index a6cf14ecb8..d7d7815f29 100644 --- a/doc/source/template_guide/functions.rst +++ b/doc/source/template_guide/functions.rst @@ -393,3 +393,35 @@ Usage Returns 'value_true' if the condition 'create_prod' evaluates to true, otherwise returns 'value_false'. + +------- +Fn::Not +------- +Acts as a NOT operator. + +The syntax of the ``Fn::Not`` function is + +.. code-block:: yaml + + {'Fn::Not': [condition]} + +Returns true for a condition that evaluates to false or returns false +for a condition that evaluates to true. + +Parameters +~~~~~~~~~~ +condition: + A condition such as ``Fn::Equals`` that evaluates to true or false + can be defined in this function, also we can set a boolean value + as a condition. + +Usage +~~~~~ + +.. code-block:: yaml + + {'Fn::Not': [{'Fn::Equals': [{'Ref': env_type'}, 'prod']}]} + + +Returns false if the param 'env_type' equals to 'prod', +otherwise returns true. diff --git a/doc/source/template_guide/hot_spec.rst b/doc/source/template_guide/hot_spec.rst index 7ebe0fbd6a..16e132d176 100644 --- a/doc/source/template_guide/hot_spec.rst +++ b/doc/source/template_guide/hot_spec.rst @@ -242,11 +242,13 @@ for the ``heat_template_version`` key: if This version adds ``equals`` condition function which can be used - to compare whether two values are equal. The complete list of supported + 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:: equals get_param + not .. _hot_spec_parameter_groups: @@ -830,6 +832,7 @@ expression equals get_param + not Note: In condition functions, you can reference a value from an input parameter, but you cannot reference resource or its attribute. @@ -840,8 +843,17 @@ An example of conditions section definition conditions: cd1: True - cd2: {get_param: param1} - cd3: {equals: [{get_param: param2}, "yes"]} + cd2: + get_param: param1 + cd3: + equals: + - get_param: param2 + - yes + cd4: + not: + equals: + - get_param: param3 + - yes The example below shows how to associate condition with resources @@ -1545,3 +1557,40 @@ Note: You define all conditions in the ``conditions`` section of a template except for ``if`` conditions. You can use the ``if`` condition in the property values in the ``resources`` section and ``outputs`` sections of a template. + +not +--- +The ``not`` function acts as a NOT operator. + +The syntax of the ``not`` function is + +.. code-block:: yaml + + 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. + +Returns true for a condition that evaluates to false or +returns false for a condition that evaluates to true. + +For example + +.. code-block:: yaml + + not: + equals: + - get_param: env_type + - prod + +If param 'env_type' equals to 'prod', this function returns false, +otherwise returns true. + +Another example + +.. code-block:: yaml + + not: True + +This function returns false. diff --git a/heat/engine/cfn/functions.py b/heat/engine/cfn/functions.py index 5751acc563..1e236f50f4 100644 --- a/heat/engine/cfn/functions.py +++ b/heat/engine/cfn/functions.py @@ -574,3 +574,38 @@ class ResourceFacade(function.Function): return function.resolve(up) elif attr == self.DELETION_POLICY: return self.stack.parent_resource.t.deletion_policy() + + +class Not(function.Function): + """A function acts as a NOT operator. + + Takes the form:: + + { "Fn::Not" : [condition] } + + Returns true for a condition that evaluates to false or + returns false for a condition that evaluates to true. + """ + + def __init__(self, stack, fn_name, args): + super(Not, self).__init__(stack, fn_name, args) + try: + if (not self.args or + not isinstance(self.args, collections.Sequence) or + isinstance(self.args, six.string_types)): + raise ValueError() + if len(self.args) != 1: + raise ValueError() + self.condition = self.args[0] + except ValueError: + msg = _('Arguments to "%s" must be of the form: ' + '[condition]') + raise ValueError(msg % self.fn_name) + + def result(self): + resolved_value = function.resolve(self.condition) + if not isinstance(resolved_value, bool): + msg = _('The condition value should be boolean, ' + 'after resolved the value is: %s') + raise ValueError(msg % resolved_value) + return not resolved_value diff --git a/heat/engine/cfn/template.py b/heat/engine/cfn/template.py index 5e1a984395..e7754a7848 100644 --- a/heat/engine/cfn/template.py +++ b/heat/engine/cfn/template.py @@ -200,6 +200,7 @@ class CfnTemplate(CfnTemplateBase): 'Fn::Equals': hot_funcs.Equals, 'Ref': cfn_funcs.ParamRef, 'Fn::FindInMap': cfn_funcs.FindInMap, + 'Fn::Not': cfn_funcs.Not } def __init__(self, tmpl, template_id=None, files=None, env=None): diff --git a/heat/engine/hot/functions.py b/heat/engine/hot/functions.py index 2c91919786..1348ea2c5c 100644 --- a/heat/engine/hot/functions.py +++ b/heat/engine/hot/functions.py @@ -968,3 +968,34 @@ class If(function.Macro): raise KeyError(_('Invalid condition name "%s"') % cd_name) return conditions[cd_name] + + +class Not(function.Function): + """A function acts as a NOT operator. + + Takes the form:: + + { "not" : condition } + + Returns true for a condition that evaluates to false or + returns false for a condition that evaluates to true. + """ + + def __init__(self, stack, fn_name, args): + super(Not, self).__init__(stack, fn_name, args) + try: + if not self.args: + raise ValueError() + self.condition = self.args + except ValueError: + msg = _('Arguments to "%s" must be of the form: ' + 'condition') + raise ValueError(msg % self.fn_name) + + def result(self): + resolved_value = function.resolve(self.condition) + if not isinstance(resolved_value, bool): + msg = _('The condition value should be boolean, ' + 'after resolved the value is: %s') + raise ValueError(msg % resolved_value) + return not resolved_value diff --git a/heat/engine/hot/template.py b/heat/engine/hot/template.py index b6e3d9dc58..fd3ecbab6a 100644 --- a/heat/engine/hot/template.py +++ b/heat/engine/hot/template.py @@ -465,6 +465,7 @@ class HOTemplate20161014(HOTemplate20160408): condition_functions = { 'get_param': hot_funcs.GetParam, 'equals': hot_funcs.Equals, + 'not': hot_funcs.Not } def __init__(self, tmpl, template_id=None, files=None, env=None): diff --git a/heat/tests/test_template.py b/heat/tests/test_template.py index c3af2076e8..c2ab400385 100644 --- a/heat/tests/test_template.py +++ b/heat/tests/test_template.py @@ -950,6 +950,55 @@ class TemplateTest(common.HeatTestCase): self.resolve_condition, snippet, tmpl) self.assertIn(error_msg, six.text_type(exc)) + def test_not(self): + tpl = template_format.parse(''' + AWSTemplateFormatVersion: 2010-09-09 + Parameters: + env_type: + Type: String + Default: 'test' + ''') + snippet = {'Fn::Not': [{'Fn::Equals': [{'Ref': 'env_type'}, 'prod']}]} + # when param 'env_type' is 'test', not function resolve to true + tmpl = template.Template(tpl) + stk = stack.Stack(utils.dummy_context(), + 'test_not_true', tmpl) + resolved = self.resolve_condition(snippet, tmpl, stk) + self.assertTrue(resolved) + # when param 'env_type' is 'prod', not function resolve to false + tmpl = template.Template(tpl, + env=environment.Environment( + {'env_type': 'prod'})) + stk = stack.Stack(utils.dummy_context(), + 'test_not_false', tmpl) + resolved = self.resolve_condition(snippet, tmpl, stk) + self.assertFalse(resolved) + + def test_not_invalid_args(self): + tmpl = template.Template(aws_empty_template) + + snippet = {'Fn::Not': ['invalid_arg']} + exc = self.assertRaises(ValueError, + self.resolve_condition, snippet, tmpl) + + error_msg = ('The condition value should be boolean, ' + 'after resolved the value is: invalid_arg') + self.assertIn(error_msg, six.text_type(exc)) + # test invalid type + snippet = {'Fn::Not': 'invalid'} + exc = self.assertRaises(exception.StackValidationFailed, + self.resolve_condition, snippet, tmpl) + error_msg = ('.Fn::Not: Arguments to "Fn::Not" must be ' + 'of the form: [condition]') + self.assertIn(error_msg, six.text_type(exc)) + + snippet = {'Fn::Not': ['cd1', 'cd2']} + exc = self.assertRaises(exception.StackValidationFailed, + self.resolve_condition, snippet, tmpl) + error_msg = ('.Fn::Not: Arguments to "Fn::Not" must be ' + 'of the form: [condition]') + self.assertIn(error_msg, six.text_type(exc)) + def test_join(self): tmpl = template.Template(empty_template) join = {"Fn::Join": [" ", ["foo", "bar"]]} diff --git a/heat_integrationtests/functional/test_conditions.py b/heat_integrationtests/functional/test_conditions.py index 577a645188..aefd9e8413 100644 --- a/heat_integrationtests/functional/test_conditions.py +++ b/heat_integrationtests/functional/test_conditions.py @@ -22,6 +22,11 @@ Parameters: AllowedValues: [prod, test] Conditions: Prod: {"Fn::Equals" : [{Ref: env_type}, "prod"]} + Test: + Fn::Not: + - Fn::Equals: + - Ref: env_type + - prod Resources: test_res: Type: OS::Heat::TestResource @@ -32,6 +37,11 @@ Resources: Properties: value: prod_res Condition: Prod + test_res1: + Type: OS::Heat::TestResource + Properties: + value: just in test env + Condition: Test Outputs: res_value: Value: {"Fn::GetAtt": [prod_res, output]} @@ -40,6 +50,9 @@ Outputs: Value: {"Fn::GetAtt": [test_res, output]} prod_resource: Value: {"Fn::If": [Prod, {Ref: prod_res}, 'no_prod_res']} + test_res1_value: + Value: {"Fn::If": [Test, {"Fn::GetAtt": [test_res1, output]}, + 'no_test_res1']} ''' hot_template = ''' @@ -52,6 +65,11 @@ parameters: - allowed_values: [prod, test] conditions: prod: {equals : [{get_param: env_type}, "prod"]} + test: + not: + equals: + - get_param: env_type + - prod resources: test_res: type: OS::Heat::TestResource @@ -62,6 +80,11 @@ resources: properties: value: prod_res condition: prod + test_res1: + type: OS::Heat::TestResource + properties: + value: just in test env + condition: test outputs: res_value: value: {get_attr: [prod_res, output]} @@ -70,6 +93,8 @@ outputs: value: {get_attr: [test_res, output]} prod_resource: value: {if: [prod, {get_resource: prod_res}, 'no_prod_res']} + test_res1_value: + value: {if: [test, {get_attr: [test_res1, output]}, 'no_test_res1']} ''' @@ -85,9 +110,10 @@ class CreateUpdateResConditionTest(functional_base.FunctionalTestsBase): self.assertIn('test_res', res_names) def res_assert_for_test(self, resources): - self.assertEqual(1, len(resources)) + self.assertEqual(2, len(resources)) res_names = [res.resource_name for res in resources] self.assertIn('test_res', res_names) + self.assertIn('test_res1', res_names) self.assertNotIn('prod_res', res_names) def output_assert_for_prod(self, stack_id): @@ -103,6 +129,10 @@ class CreateUpdateResConditionTest(functional_base.FunctionalTestsBase): stack_id, 'prod_resource')['output'] self.assertNotEqual('no_prod_res', prod_resource['output_value']) + test_res_output = self.client.stacks.output_show( + stack_id, 'test_res1_value')['output'] + self.assertEqual('no_test_res1', test_res_output['output_value']) + def output_assert_for_test(self, stack_id): output = self.client.stacks.output_show(stack_id, 'res_value')['output'] @@ -116,6 +146,11 @@ class CreateUpdateResConditionTest(functional_base.FunctionalTestsBase): stack_id, 'prod_resource')['output'] self.assertEqual('no_prod_res', prod_resource['output_value']) + test_res_output = self.client.stacks.output_show( + stack_id, 'test_res1_value')['output'] + self.assertEqual('just in test env', + test_res_output['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)