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
This commit is contained in:
huangtianhua 2016-07-26 14:32:40 +08:00
parent 74506411d5
commit 4552e08767
10 changed files with 118 additions and 6 deletions

View File

@ -766,6 +766,7 @@ according to the following syntax
<parameter name>: <parameter name>:
description: <description> description: <description>
value: <parameter value> value: <parameter value>
condition: <condition name>
parameter name parameter name
The output parameter name, which must be unique within the ``outputs`` The output parameter name, which must be unique within the ``outputs``
@ -781,6 +782,13 @@ parameter value
the functions. the functions.
This attribute is required. 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 The example below shows how the IP address of a compute resource can
be defined as an output parameter 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, 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 '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: .. _hot_spec_intrinsic_functions:

View File

@ -177,6 +177,10 @@ class CfnTemplate(CfnTemplateBase):
HOT_TO_CFN_RES_ATTRS.update({'condition': RES_CONDITION}) HOT_TO_CFN_RES_ATTRS.update({'condition': RES_CONDITION})
extra_rsrc_defn = CfnTemplateBase.extra_rsrc_defn + (RES_CONDITION,) extra_rsrc_defn = CfnTemplateBase.extra_rsrc_defn + (RES_CONDITION,)
OUTPUT_CONDITION = CONDITION
OUTPUT_KEYS = CfnTemplateBase.OUTPUT_KEYS + (OUTPUT_CONDITION,)
condition_functions = { condition_functions = {
'Fn::Equals': hot_funcs.Equals, 'Fn::Equals': hot_funcs.Equals,
'Ref': cfn_funcs.ParamRef, 'Ref': cfn_funcs.ParamRef,

View File

@ -410,6 +410,9 @@ class HOTemplate20161014(HOTemplate20160408):
extra_rsrc_defn = HOTemplate20160408.extra_rsrc_defn + ( extra_rsrc_defn = HOTemplate20160408.extra_rsrc_defn + (
RES_EXTERNAL_ID, RES_CONDITION,) RES_EXTERNAL_ID, RES_CONDITION,)
OUTPUT_CONDITION = CONDITION
OUTPUT_KEYS = HOTemplate20160408.OUTPUT_KEYS + (OUTPUT_CONDITION,)
deletion_policies = { deletion_policies = {
'Delete': rsrc_defn.ResourceDefinition.DELETE, 'Delete': rsrc_defn.ResourceDefinition.DELETE,
'Retain': rsrc_defn.ResourceDefinition.RETAIN, 'Retain': rsrc_defn.ResourceDefinition.RETAIN,

View File

@ -1314,12 +1314,12 @@ class EngineService(service.Service):
if output_key not in outputs: if output_key not in outputs:
raise exception.NotFound(_('Specified output key %s not ' raise exception.NotFound(_('Specified output key %s not '
'found.') % output_key) '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: 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): def _remote_call(self, cnxt, lock_engine_id, call, **kwargs):
timeout = cfg.CONF.engine_life_check_timeout timeout = cfg.CONF.engine_life_check_timeout

View File

@ -228,7 +228,7 @@ class Stack(collections.Mapping):
self._set_param_stackid() self._set_param_stackid()
if resolve_data: if resolve_data:
self.outputs = self.resolve_static_data( self.outputs = self.resolve_outputs_data(
self.t[self.t.OUTPUTS], path=self.t.OUTPUTS) self.t[self.t.OUTPUTS], path=self.t.OUTPUTS)
else: else:
self.outputs = {} self.outputs = {}
@ -1503,7 +1503,7 @@ class Stack(collections.Mapping):
previous_template_id = self.t.id previous_template_id = self.t.id
self.t = newstack.t self.t = newstack.t
template_outputs = self.t[self.t.OUTPUTS] 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) template_outputs, path=self.t.OUTPUTS)
finally: finally:
if should_rollback: if should_rollback:
@ -1970,8 +1970,17 @@ class Stack(collections.Mapping):
} }
def resolve_static_data(self, snippet, path=''): 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) 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): def reset_resource_attributes(self):
# nothing is cached if no resources exist # nothing is cached if no resources exist
if not self._resources: if not self._resources:

View File

@ -135,6 +135,10 @@ class Template(collections.Mapping):
self.t[s] = {} self.t[s] = {}
self.t[s].update(other.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 @classmethod
def load(cls, context, template_id, t=None): def load(cls, context, template_id, t=None):
"""Retrieve a Template with the given ID from the database.""" """Retrieve a Template with the given ID from the database."""

View File

@ -12,6 +12,7 @@
# under the License. # under the License.
import collections import collections
import copy
import six import six
@ -118,6 +119,11 @@ class CommonTemplate(template.Template):
return self.get_condition(res_data, stack, path) 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=''): def get_condition(self, snippet, stack, path=''):
# if specify condition return the resolved condition value, # if specify condition return the resolved condition value,
# true or false if don't specify condition, return true # true or false if don't specify condition, return true
@ -137,3 +143,14 @@ class CommonTemplate(template.Template):
self._conditions = self.resolve_conditions(stack) self._conditions = self.resolve_conditions(stack)
return self._conditions 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

View File

@ -2730,7 +2730,7 @@ class StackTest(common.HeatTestCase):
stc = stack.Stack(self.ctx, utils.random_name(), stc = stack.Stack(self.ctx, utils.random_name(),
tmpl, resolve_data=False) tmpl, resolve_data=False)
expected_exception = self.assertRaises(AssertionError, expected_exception = self.assertRaises(AssertionError,
stc.resolve_static_data, stc.resolve_outputs_data,
None) None)
self.assertEqual(expected_message, six.text_type(expected_exception)) self.assertEqual(expected_message, six.text_type(expected_exception))

View File

@ -299,6 +299,12 @@ class TestTemplateConditionParser(common.HeatTestCase):
'type': 'GenericResourceType', 'type': 'GenericResourceType',
'condition': 'prod_env' '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)', self.assertIn('Invalid condition "111" (in r1.condition)',
six.text_type(ex)) 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): class TestTemplateValidate(common.HeatTestCase):

View File

@ -32,6 +32,10 @@ Resources:
Properties: Properties:
value: prod_res value: prod_res
Condition: Prod Condition: Prod
Outputs:
res_value:
Value: {"Fn::GetAtt": [prod_res, output]}
Condition: Prod
''' '''
hot_template = ''' hot_template = '''
@ -54,6 +58,10 @@ resources:
properties: properties:
value: prod_res value: prod_res
condition: prod 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.assertIn('test_res', res_names)
self.assertNotIn('prod_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): def test_stack_create_update_cfn_template_test_to_prod(self):
stack_identifier = self.stack_create(template=cfn_template) stack_identifier = self.stack_create(template=cfn_template)
resources = self.client.resources.list(stack_identifier) resources = self.client.resources.list(stack_identifier)
self.res_assert_for_test(resources) self.res_assert_for_test(resources)
self.output_assert_for_test(stack_identifier)
parms = {'env_type': 'prod'} parms = {'env_type': 'prod'}
self.update_stack(stack_identifier, self.update_stack(stack_identifier,
@ -86,6 +105,7 @@ class CreateUpdateResConditionTest(functional_base.FunctionalTestsBase):
resources = self.client.resources.list(stack_identifier) resources = self.client.resources.list(stack_identifier)
self.res_assert_for_prod(resources) self.res_assert_for_prod(resources)
self.output_assert_for_prod(stack_identifier)
def test_stack_create_update_cfn_template_prod_to_test(self): def test_stack_create_update_cfn_template_prod_to_test(self):
parms = {'env_type': 'prod'} parms = {'env_type': 'prod'}
@ -93,6 +113,7 @@ class CreateUpdateResConditionTest(functional_base.FunctionalTestsBase):
parameters=parms) parameters=parms)
resources = self.client.resources.list(stack_identifier) resources = self.client.resources.list(stack_identifier)
self.res_assert_for_prod(resources) self.res_assert_for_prod(resources)
self.output_assert_for_prod(stack_identifier)
parms = {'env_type': 'test'} parms = {'env_type': 'test'}
self.update_stack(stack_identifier, self.update_stack(stack_identifier,
@ -101,11 +122,13 @@ class CreateUpdateResConditionTest(functional_base.FunctionalTestsBase):
resources = self.client.resources.list(stack_identifier) resources = self.client.resources.list(stack_identifier)
self.res_assert_for_test(resources) self.res_assert_for_test(resources)
self.output_assert_for_test(stack_identifier)
def test_stack_create_update_hot_template_test_to_prod(self): def test_stack_create_update_hot_template_test_to_prod(self):
stack_identifier = self.stack_create(template=hot_template) stack_identifier = self.stack_create(template=hot_template)
resources = self.client.resources.list(stack_identifier) resources = self.client.resources.list(stack_identifier)
self.res_assert_for_test(resources) self.res_assert_for_test(resources)
self.output_assert_for_test(stack_identifier)
parms = {'env_type': 'prod'} parms = {'env_type': 'prod'}
self.update_stack(stack_identifier, self.update_stack(stack_identifier,
@ -114,6 +137,7 @@ class CreateUpdateResConditionTest(functional_base.FunctionalTestsBase):
resources = self.client.resources.list(stack_identifier) resources = self.client.resources.list(stack_identifier)
self.res_assert_for_prod(resources) self.res_assert_for_prod(resources)
self.output_assert_for_prod(stack_identifier)
def test_stack_create_update_hot_template_prod_to_test(self): def test_stack_create_update_hot_template_prod_to_test(self):
parms = {'env_type': 'prod'} parms = {'env_type': 'prod'}
@ -121,6 +145,7 @@ class CreateUpdateResConditionTest(functional_base.FunctionalTestsBase):
parameters=parms) parameters=parms)
resources = self.client.resources.list(stack_identifier) resources = self.client.resources.list(stack_identifier)
self.res_assert_for_prod(resources) self.res_assert_for_prod(resources)
self.output_assert_for_prod(stack_identifier)
parms = {'env_type': 'test'} parms = {'env_type': 'test'}
self.update_stack(stack_identifier, self.update_stack(stack_identifier,
@ -129,3 +154,4 @@ class CreateUpdateResConditionTest(functional_base.FunctionalTestsBase):
resources = self.client.resources.list(stack_identifier) resources = self.client.resources.list(stack_identifier)
self.res_assert_for_test(resources) self.res_assert_for_test(resources)
self.output_assert_for_test(stack_identifier)