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>:
description: <description>
value: <parameter value>
condition: <condition name>
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:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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