From 4552e08767a1ead02a888d19ef13b8f683b0e146 Mon Sep 17 00:00:00 2001 From: huangtianhua Date: Tue, 26 Jul 2016 14:32:40 +0800 Subject: [PATCH] Support condition for output Provides output condition for cfn/hot templates, if the condition of output evaluates to false, will set output value to None. Change-Id: I0398e39541a4176ef5699331c10536c59f1cb3e7 Blueprint: support-conditions-function --- doc/source/template_guide/hot_spec.rst | 22 +++++++++++++++ heat/engine/cfn/template.py | 4 +++ heat/engine/hot/template.py | 3 +++ heat/engine/service.py | 6 ++--- heat/engine/stack.py | 13 +++++++-- heat/engine/template.py | 4 +++ heat/engine/template_common.py | 17 ++++++++++++ heat/tests/test_stack.py | 2 +- heat/tests/test_template.py | 27 +++++++++++++++++++ .../functional/test_conditions.py | 26 ++++++++++++++++++ 10 files changed, 118 insertions(+), 6 deletions(-) diff --git a/doc/source/template_guide/hot_spec.rst b/doc/source/template_guide/hot_spec.rst index e45a0a3a05..fa034c46c2 100644 --- a/doc/source/template_guide/hot_spec.rst +++ b/doc/source/template_guide/hot_spec.rst @@ -766,6 +766,7 @@ according to the following syntax : description: value: + condition: parameter name The output parameter name, which must be unique within the ``outputs`` @@ -781,6 +782,13 @@ parameter value the functions. This attribute is required. +condition + To conditionally define an output value. None value will be shown if the + condition is False. + This attribute is optional. + + Note: Support ``condition`` for output is added in the Newton version. + The example below shows how the IP address of a compute resource can be defined as an output parameter @@ -855,6 +863,20 @@ parameter is equal to 'prod'. In the above sample template, the 'volume' resource is associated with the 'create_prod_res' condition. Therefore, the 'volume' resource is created only if the 'env_type' is equal to 'prod'. +The example below shows how to conditionally define an output + +.. code-block:: yaml + + outputs: + vol_size: + value: {get_attr: [my_volume, size]} + condition: create_prod_res + +In the above sample template, the 'vol_size' output is associated with +the 'create_prod_res' condition. Therefore, the 'vol_size' output is +given corresponding value only if the 'env_type' is equal to 'prod', +otherwise the value of the output is None. + .. _hot_spec_intrinsic_functions: diff --git a/heat/engine/cfn/template.py b/heat/engine/cfn/template.py index 3ec72b35c8..7ec82c342b 100644 --- a/heat/engine/cfn/template.py +++ b/heat/engine/cfn/template.py @@ -177,6 +177,10 @@ class CfnTemplate(CfnTemplateBase): HOT_TO_CFN_RES_ATTRS.update({'condition': RES_CONDITION}) extra_rsrc_defn = CfnTemplateBase.extra_rsrc_defn + (RES_CONDITION,) + + OUTPUT_CONDITION = CONDITION + OUTPUT_KEYS = CfnTemplateBase.OUTPUT_KEYS + (OUTPUT_CONDITION,) + condition_functions = { 'Fn::Equals': hot_funcs.Equals, 'Ref': cfn_funcs.ParamRef, diff --git a/heat/engine/hot/template.py b/heat/engine/hot/template.py index a028511ebc..5d59a5729d 100644 --- a/heat/engine/hot/template.py +++ b/heat/engine/hot/template.py @@ -410,6 +410,9 @@ class HOTemplate20161014(HOTemplate20160408): extra_rsrc_defn = HOTemplate20160408.extra_rsrc_defn + ( RES_EXTERNAL_ID, RES_CONDITION,) + OUTPUT_CONDITION = CONDITION + OUTPUT_KEYS = HOTemplate20160408.OUTPUT_KEYS + (OUTPUT_CONDITION,) + deletion_policies = { 'Delete': rsrc_defn.ResourceDefinition.DELETE, 'Retain': rsrc_defn.ResourceDefinition.RETAIN, diff --git a/heat/engine/service.py b/heat/engine/service.py index ff493d6a40..b2f110c275 100644 --- a/heat/engine/service.py +++ b/heat/engine/service.py @@ -1314,12 +1314,12 @@ class EngineService(service.Service): if output_key not in outputs: raise exception.NotFound(_('Specified output key %s not ' 'found.') % output_key) - output = stack.resolve_static_data(outputs[output_key]) + output = stack.resolve_outputs_data({output_key: outputs[output_key]}) if not stack.outputs: - stack.outputs.update({output_key: output}) + stack.outputs.update(output) - return api.format_stack_output(stack, {output_key: output}, output_key) + return api.format_stack_output(stack, output, output_key) def _remote_call(self, cnxt, lock_engine_id, call, **kwargs): timeout = cfg.CONF.engine_life_check_timeout diff --git a/heat/engine/stack.py b/heat/engine/stack.py index 8c6085b083..aaff77e6da 100644 --- a/heat/engine/stack.py +++ b/heat/engine/stack.py @@ -228,7 +228,7 @@ class Stack(collections.Mapping): self._set_param_stackid() if resolve_data: - self.outputs = self.resolve_static_data( + self.outputs = self.resolve_outputs_data( self.t[self.t.OUTPUTS], path=self.t.OUTPUTS) else: self.outputs = {} @@ -1503,7 +1503,7 @@ class Stack(collections.Mapping): previous_template_id = self.t.id self.t = newstack.t template_outputs = self.t[self.t.OUTPUTS] - self.outputs = self.resolve_static_data( + self.outputs = self.resolve_outputs_data( template_outputs, path=self.t.OUTPUTS) finally: if should_rollback: @@ -1970,8 +1970,17 @@ class Stack(collections.Mapping): } def resolve_static_data(self, snippet, path=''): + warnings.warn('Stack.resolve_static_data() is deprecated and ' + 'will be removed in the Ocata release. Use the ' + 'Stack.resolve_outputs_data() instead.', + DeprecationWarning) + return self.t.parse(self, snippet, path=path) + def resolve_outputs_data(self, outputs, path=''): + resolve_outputs = self.t.parse_outputs_conditions(outputs, self) + return self.t.parse(self, resolve_outputs, path=path) + def reset_resource_attributes(self): # nothing is cached if no resources exist if not self._resources: diff --git a/heat/engine/template.py b/heat/engine/template.py index b245e7e719..fca7e0acf2 100644 --- a/heat/engine/template.py +++ b/heat/engine/template.py @@ -135,6 +135,10 @@ class Template(collections.Mapping): self.t[s] = {} self.t[s].update(other.t[s]) + def parse_outputs_conditions(self, outputs, stack): + """Return a dictionary of outputs data which resolved conditions.""" + return outputs + @classmethod def load(cls, context, template_id, t=None): """Retrieve a Template with the given ID from the database.""" diff --git a/heat/engine/template_common.py b/heat/engine/template_common.py index add12346bc..1c63172a34 100644 --- a/heat/engine/template_common.py +++ b/heat/engine/template_common.py @@ -12,6 +12,7 @@ # under the License. import collections +import copy import six @@ -118,6 +119,11 @@ class CommonTemplate(template.Template): return self.get_condition(res_data, stack, path) + def get_output_condition(self, stack, o_data, o_key): + path = '.'.join([self.OUTPUTS, o_key, self.OUTPUT_CONDITION]) + + return self.get_condition(o_data, stack, path) + def get_condition(self, snippet, stack, path=''): # if specify condition return the resolved condition value, # true or false if don't specify condition, return true @@ -137,3 +143,14 @@ class CommonTemplate(template.Template): self._conditions = self.resolve_conditions(stack) return self._conditions + + def parse_outputs_conditions(self, outputs, stack): + copy_outputs = copy.deepcopy(outputs) + for key, snippet in six.iteritems(copy_outputs): + if self.has_condition_section(snippet): + cd = self.get_output_condition(stack, snippet, key) + snippet[self.OUTPUT_CONDITION] = cd + if not cd: + snippet[self.OUTPUT_VALUE] = None + + return copy_outputs diff --git a/heat/tests/test_stack.py b/heat/tests/test_stack.py index 56bdc6c86c..54084d34d7 100644 --- a/heat/tests/test_stack.py +++ b/heat/tests/test_stack.py @@ -2730,7 +2730,7 @@ class StackTest(common.HeatTestCase): stc = stack.Stack(self.ctx, utils.random_name(), tmpl, resolve_data=False) expected_exception = self.assertRaises(AssertionError, - stc.resolve_static_data, + stc.resolve_outputs_data, None) self.assertEqual(expected_message, six.text_type(expected_exception)) diff --git a/heat/tests/test_template.py b/heat/tests/test_template.py index 9f61523b96..d6f12b6800 100644 --- a/heat/tests/test_template.py +++ b/heat/tests/test_template.py @@ -299,6 +299,12 @@ class TestTemplateConditionParser(common.HeatTestCase): 'type': 'GenericResourceType', 'condition': 'prod_env' } + }, + 'outputs': { + 'foo': { + 'condition': 'prod_env', + 'value': {'get_attr': ['r1', 'foo']} + } } } @@ -381,6 +387,27 @@ class TestTemplateConditionParser(common.HeatTestCase): self.assertIn('Invalid condition "111" (in r1.condition)', six.text_type(ex)) + def test_parse_output_condition_invalid(self): + stk = stack.Stack(self.ctx, + 'test_output_invalid_condition', + self.tmpl) + + # test condition name is invalid + stk.outputs['foo']['condition'] = 'invalid_cd' + ex = self.assertRaises(exception.InvalidConditionReference, + self.tmpl.parse_outputs_conditions, + stk.outputs, stk) + self.assertIn('Invalid condition "invalid_cd" ' + '(in outputs.foo.condition)', + six.text_type(ex)) + # test condition name is not string + stk.outputs['foo']['condition'] = 222 + ex = self.assertRaises(exception.InvalidConditionReference, + self.tmpl.parse_outputs_conditions, + stk.outputs, stk) + self.assertIn('Invalid condition "222" (in outputs.foo.condition)', + six.text_type(ex)) + class TestTemplateValidate(common.HeatTestCase): diff --git a/heat_integrationtests/functional/test_conditions.py b/heat_integrationtests/functional/test_conditions.py index d0f49a8d8c..8850c499ce 100644 --- a/heat_integrationtests/functional/test_conditions.py +++ b/heat_integrationtests/functional/test_conditions.py @@ -32,6 +32,10 @@ Resources: Properties: value: prod_res Condition: Prod +Outputs: + res_value: + Value: {"Fn::GetAtt": [prod_res, output]} + Condition: Prod ''' hot_template = ''' @@ -54,6 +58,10 @@ resources: properties: value: prod_res condition: prod +outputs: + res_value: + value: {get_attr: [prod_res, output]} + condition: prod ''' @@ -74,10 +82,21 @@ class CreateUpdateResConditionTest(functional_base.FunctionalTestsBase): self.assertIn('test_res', res_names) self.assertNotIn('prod_res', res_names) + def output_assert_for_prod(self, stack_id): + output = self.client.stacks.output_show(stack_id, + 'res_value')['output'] + self.assertEqual('prod_res', output['output_value']) + + def output_assert_for_test(self, stack_id): + output = self.client.stacks.output_show(stack_id, + 'res_value')['output'] + self.assertIsNone(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) self.res_assert_for_test(resources) + self.output_assert_for_test(stack_identifier) parms = {'env_type': 'prod'} self.update_stack(stack_identifier, @@ -86,6 +105,7 @@ class CreateUpdateResConditionTest(functional_base.FunctionalTestsBase): resources = self.client.resources.list(stack_identifier) self.res_assert_for_prod(resources) + self.output_assert_for_prod(stack_identifier) def test_stack_create_update_cfn_template_prod_to_test(self): parms = {'env_type': 'prod'} @@ -93,6 +113,7 @@ class CreateUpdateResConditionTest(functional_base.FunctionalTestsBase): parameters=parms) resources = self.client.resources.list(stack_identifier) self.res_assert_for_prod(resources) + self.output_assert_for_prod(stack_identifier) parms = {'env_type': 'test'} self.update_stack(stack_identifier, @@ -101,11 +122,13 @@ class CreateUpdateResConditionTest(functional_base.FunctionalTestsBase): resources = self.client.resources.list(stack_identifier) self.res_assert_for_test(resources) + 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 = {'env_type': 'prod'} self.update_stack(stack_identifier, @@ -114,6 +137,7 @@ class CreateUpdateResConditionTest(functional_base.FunctionalTestsBase): resources = self.client.resources.list(stack_identifier) self.res_assert_for_prod(resources) + self.output_assert_for_prod(stack_identifier) def test_stack_create_update_hot_template_prod_to_test(self): parms = {'env_type': 'prod'} @@ -121,6 +145,7 @@ class CreateUpdateResConditionTest(functional_base.FunctionalTestsBase): parameters=parms) resources = self.client.resources.list(stack_identifier) self.res_assert_for_prod(resources) + self.output_assert_for_prod(stack_identifier) parms = {'env_type': 'test'} self.update_stack(stack_identifier, @@ -129,3 +154,4 @@ class CreateUpdateResConditionTest(functional_base.FunctionalTestsBase): resources = self.client.resources.list(stack_identifier) self.res_assert_for_test(resources) + self.output_assert_for_test(stack_identifier)