Decouple hot and cfn for outputs

The changes including:
1. Avoid hard code of resource and output keys
2. Decouple hot and cfn for outputs

Change-Id: I1fd7e08ff5c699ddfcf98c81aed5f0d91c4248b3
This commit is contained in:
huangtianhua 2016-06-13 11:04:46 +08:00
parent 7ea3e68eb9
commit faec3a0962
12 changed files with 169 additions and 74 deletions

View File

@ -123,5 +123,7 @@ Resources:
Timeout: "600" Timeout: "600"
Outputs: Outputs:
Endpoint.Address: {'Fn::GetAtt': [DatabaseInstance, PublicIp]} Endpoint.Address:
Endpoint.Port: {Ref: Port} Value: {'Fn::GetAtt': [DatabaseInstance, PublicIp]}
Endpoint.Port:
Value: {Ref: Port}

View File

@ -184,7 +184,7 @@ def format_stack_outputs(stack, outputs, resolve_value=False):
def format_stack_output(stack, outputs, k, resolve_value=True): def format_stack_output(stack, outputs, k, resolve_value=True):
result = { result = {
rpc_api.OUTPUT_KEY: k, rpc_api.OUTPUT_KEY: k,
rpc_api.OUTPUT_DESCRIPTION: outputs[k].get('Description', rpc_api.OUTPUT_DESCRIPTION: outputs[k].get(stack.t.OUTPUT_DESCRIPTION,
'No description given'), 'No description given'),
} }

View File

@ -44,6 +44,12 @@ class CfnTemplate(template.Template):
'Description', 'Mappings', 'Parameters', 'Resources', 'Outputs' 'Description', 'Mappings', 'Parameters', 'Resources', 'Outputs'
) )
OUTPUT_KEYS = (
OUTPUT_DESCRIPTION, OUTPUT_VALUE,
) = (
'Description', 'Value',
)
SECTIONS_NO_DIRECT_ACCESS = set([PARAMETERS, VERSION, ALTERNATE_VERSION]) SECTIONS_NO_DIRECT_ACCESS = set([PARAMETERS, VERSION, ALTERNATE_VERSION])
functions = { functions = {

View File

@ -282,7 +282,8 @@ def construct_input_data(rsrc, curr_stack):
dep_attrs = curr_stack.get_dep_attrs( dep_attrs = curr_stack.get_dep_attrs(
six.itervalues(curr_stack.resources), six.itervalues(curr_stack.resources),
curr_stack.outputs, curr_stack.outputs,
rsrc.name) rsrc.name,
curr_stack.t.OUTPUT_VALUE)
input_data = {'id': rsrc.id, input_data = {'id': rsrc.id,
'name': rsrc.name, 'name': rsrc.name,
'reference_id': rsrc.get_reference_id(), 'reference_id': rsrc.get_reference_id(),

View File

@ -27,10 +27,10 @@ from heat.engine import template
_RESOURCE_KEYS = ( _RESOURCE_KEYS = (
RES_TYPE, RES_PROPERTIES, RES_METADATA, RES_DEPENDS_ON, RES_TYPE, RES_PROPERTIES, RES_METADATA, RES_DEPENDS_ON,
RES_DELETION_POLICY, RES_UPDATE_POLICY, RES_DELETION_POLICY, RES_UPDATE_POLICY, RES_DESCRIPTION,
) = ( ) = (
'type', 'properties', 'metadata', 'depends_on', 'type', 'properties', 'metadata', 'depends_on',
'deletion_policy', 'update_policy', 'deletion_policy', 'update_policy', 'description',
) )
@ -45,6 +45,12 @@ class HOTemplate20130523(template.Template):
'parameters', 'resources', 'outputs', '__undefined__' 'parameters', 'resources', 'outputs', '__undefined__'
) )
OUTPUT_KEYS = (
OUTPUT_DESCRIPTION, OUTPUT_VALUE,
) = (
'description', 'value',
)
SECTIONS_NO_DIRECT_ACCESS = set([PARAMETERS, VERSION]) SECTIONS_NO_DIRECT_ACCESS = set([PARAMETERS, VERSION])
_CFN_TO_HOT_SECTIONS = {cfn_template.CfnTemplate.VERSION: VERSION, _CFN_TO_HOT_SECTIONS = {cfn_template.CfnTemplate.VERSION: VERSION,
@ -54,14 +60,19 @@ class HOTemplate20130523(template.Template):
cfn_template.CfnTemplate.RESOURCES: RESOURCES, cfn_template.CfnTemplate.RESOURCES: RESOURCES,
cfn_template.CfnTemplate.OUTPUTS: OUTPUTS} cfn_template.CfnTemplate.OUTPUTS: OUTPUTS}
_RESOURCE_HOT_TO_CFN_ATTRS = {'type': 'Type', _RESOURCE_HOT_TO_CFN_ATTRS = {
'properties': 'Properties', RES_TYPE: cfn_template.RES_TYPE,
'metadata': 'Metadata', RES_PROPERTIES: cfn_template.RES_PROPERTIES,
'depends_on': 'DependsOn', RES_METADATA: cfn_template.RES_METADATA,
'deletion_policy': 'DeletionPolicy', RES_DEPENDS_ON: cfn_template.RES_DEPENDS_ON,
'update_policy': 'UpdatePolicy', RES_DELETION_POLICY: cfn_template.RES_DELETION_POLICY,
'description': 'Description', RES_UPDATE_POLICY: cfn_template.RES_UPDATE_POLICY,
'value': 'Value'} RES_DESCRIPTION: cfn_template.RES_DESCRIPTION}
_HOT_TO_CFN_ATTRS = _RESOURCE_HOT_TO_CFN_ATTRS
_HOT_TO_CFN_ATTRS.update(
{OUTPUT_VALUE: cfn_template.CfnTemplate.OUTPUT_VALUE})
functions = { functions = {
'Fn::GetAZs': cfn_funcs.GetAZs, 'Fn::GetAZs': cfn_funcs.GetAZs,
'get_param': hot_funcs.GetParam, 'get_param': hot_funcs.GetParam,
@ -121,7 +132,8 @@ class HOTemplate20130523(template.Template):
return self._translate_resources(the_section) return self._translate_resources(the_section)
if section == self.OUTPUTS: if section == self.OUTPUTS:
return self._translate_outputs(the_section) self.validate_section(self.OUTPUTS, self.OUTPUT_VALUE,
the_section, self.OUTPUT_KEYS)
return the_section return the_section
@ -136,57 +148,33 @@ class HOTemplate20130523(template.Template):
raise ke raise ke
def _translate_section(self, section, sub_section, data, mapping): def _translate_section(self, section, sub_section, data, mapping):
self.validate_section(section, sub_section, data, mapping)
cfn_objects = {} cfn_objects = {}
obj_name = section[:-1]
err_msg = _('"%%s" is not a valid keyword inside a %s '
'definition') % obj_name
for name, attrs in sorted(data.items()): for name, attrs in sorted(data.items()):
cfn_object = {} cfn_object = {}
if not attrs:
args = {'object_name': obj_name, 'sub_section': sub_section}
message = _('Each %(object_name)s must contain a '
'%(sub_section)s key.') % args
raise exception.StackValidationFailed(message=message)
try:
for attr, attr_value in six.iteritems(attrs): for attr, attr_value in six.iteritems(attrs):
cfn_attr = self._translate(attr, mapping, err_msg) cfn_attr = mapping[attr]
cfn_object[cfn_attr] = attr_value cfn_object[cfn_attr] = attr_value
cfn_objects[name] = cfn_object cfn_objects[name] = cfn_object
except AttributeError:
message = _('"%(section)s" must contain a map of '
'%(obj_name)s maps. Found a [%(_type)s] '
'instead') % {'section': section,
'_type': type(attrs),
'obj_name': obj_name}
raise exception.StackValidationFailed(message=message)
except KeyError as e:
# an invalid keyword was found
raise exception.StackValidationFailed(message=e.args[0])
return cfn_objects return cfn_objects
def _translate_resources(self, resources): def _translate_resources(self, resources):
"""Get the resources of the template translated into CFN format.""" """Get the resources of the template translated into CFN format."""
return self._translate_section('resources', 'type', resources, return self._translate_section(self.RESOURCES, RES_TYPE, resources,
self._RESOURCE_HOT_TO_CFN_ATTRS) self._RESOURCE_HOT_TO_CFN_ATTRS)
def get_section_name(self, section): def get_section_name(self, section):
cfn_to_hot_attrs = dict( cfn_to_hot_attrs = dict(
zip(six.itervalues(self._RESOURCE_HOT_TO_CFN_ATTRS), zip(six.itervalues(self._HOT_TO_CFN_ATTRS),
six.iterkeys(self._RESOURCE_HOT_TO_CFN_ATTRS))) six.iterkeys(self._HOT_TO_CFN_ATTRS)))
return cfn_to_hot_attrs.get(section, section) return cfn_to_hot_attrs.get(section, section)
def _translate_outputs(self, outputs):
"""Get the outputs of the template translated into CFN format."""
HOT_TO_CFN_ATTRS = {'description': 'Description',
'value': 'Value'}
return self._translate_section('outputs', 'value', outputs,
HOT_TO_CFN_ATTRS)
def param_schemata(self, param_defaults=None): def param_schemata(self, param_defaults=None):
parameter_section = self.t.get(self.PARAMETERS) or {} parameter_section = self.t.get(self.PARAMETERS) or {}
pdefaults = param_defaults or {} pdefaults = param_defaults or {}

View File

@ -37,7 +37,7 @@ def generate_class_from_template(name, data, param_defaults):
cls = type(name, (TemplateResource,), cls = type(name, (TemplateResource,),
{'properties_schema': props, {'properties_schema': props,
'attributes_schema': attrs, 'attributes_schema': attrs,
'__doc__': tmpl.t.get(tmpl.get_section_name('Description'))}) '__doc__': tmpl.t.get(tmpl.DESCRIPTION)})
return cls return cls

View File

@ -424,7 +424,7 @@ class Stack(collections.Mapping):
LOG.warning(_LW("Unable to set parameters StackId identifier")) LOG.warning(_LW("Unable to set parameters StackId identifier"))
@staticmethod @staticmethod
def get_dep_attrs(resources, outputs, resource_name): def get_dep_attrs(resources, outputs, resource_name, value_sec):
"""Return the attributes of the specified resource that are referenced. """Return the attributes of the specified resource that are referenced.
Return an iterator over any attributes of the specified resource that Return an iterator over any attributes of the specified resource that
@ -432,8 +432,8 @@ class Stack(collections.Mapping):
""" """
attr_lists = itertools.chain((res.dep_attrs(resource_name) attr_lists = itertools.chain((res.dep_attrs(resource_name)
for res in resources), for res in resources),
(function.dep_attrs(out.get('Value', ''), (function.dep_attrs(
resource_name) out.get(value_sec, ''), resource_name)
for out in six.itervalues(outputs))) for out in six.itervalues(outputs)))
return set(itertools.chain.from_iterable(attr_lists)) return set(itertools.chain.from_iterable(attr_lists))
@ -796,14 +796,14 @@ class Stack(collections.Mapping):
path=[self.t.OUTPUTS], path=[self.t.OUTPUTS],
message=message) message=message)
try: try:
if not val or 'Value' not in val: if not val or self.t.OUTPUT_VALUE not in val:
message = _('Each Output must contain ' message = _('Each Output must contain '
'a Value key.') 'a Value key.')
raise exception.StackValidationFailed( raise exception.StackValidationFailed(
error='Output validation error', error='Output validation error',
path=[self.t.OUTPUTS, key], path=[self.t.OUTPUTS, key],
message=message) message=message)
function.validate(val.get('Value')) function.validate(val.get(self.t.OUTPUT_VALUE))
except exception.StackValidationFailed as ex: except exception.StackValidationFailed as ex:
raise raise
except AssertionError: except AssertionError:
@ -812,7 +812,7 @@ class Stack(collections.Mapping):
raise exception.StackValidationFailed( raise exception.StackValidationFailed(
error='Output validation error', error='Output validation error',
path=[self.t.OUTPUTS, key, path=[self.t.OUTPUTS, key,
self.t.get_section_name('Value')], self.t.OUTPUT_VALUE],
message=six.text_type(ex)) message=six.text_type(ex))
def requires_deferred_auth(self): def requires_deferred_auth(self):
@ -1832,7 +1832,7 @@ class Stack(collections.Mapping):
@profiler.trace('Stack.output', hide_args=False) @profiler.trace('Stack.output', hide_args=False)
def output(self, key): def output(self, key):
"""Get the value of the specified stack output.""" """Get the value of the specified stack output."""
value = self.outputs[key].get('Value', '') value = self.outputs[key].get(self.t.OUTPUT_VALUE, '')
try: try:
return function.resolve(value) return function.resolve(value)
except Exception as ex: except Exception as ex:

View File

@ -221,6 +221,33 @@ class Template(collections.Mapping):
""" """
pass pass
def validate_section(self, section, sub_section, data, allowed_keys):
obj_name = section[:-1]
err_msg = _('"%%s" is not a valid keyword inside a %s '
'definition') % obj_name
args = {'object_name': obj_name, 'sub_section': sub_section}
message = _('Each %(object_name)s must contain a '
'%(sub_section)s key.') % args
for name, attrs in sorted(data.items()):
if not attrs:
raise exception.StackValidationFailed(message=message)
try:
for attr, attr_value in six.iteritems(attrs):
if attr not in allowed_keys:
raise KeyError(err_msg % attr)
if sub_section not in attrs:
raise exception.StackValidationFailed(message=message)
except AttributeError:
message = _('"%(section)s" must contain a map of '
'%(obj_name)s maps. Found a [%(_type)s] '
'instead') % {'section': section,
'_type': type(attrs),
'obj_name': obj_name}
raise exception.StackValidationFailed(message=message)
except KeyError as e:
# an invalid keyword was found
raise exception.StackValidationFailed(message=e.args[0])
def remove_resource(self, name): def remove_resource(self, name):
"""Remove a resource from the template.""" """Remove a resource from the template."""
self.t.get(self.RESOURCES, {}).pop(name) self.t.get(self.RESOURCES, {}).pop(name)

View File

@ -402,8 +402,8 @@ class HOTemplateTest(common.HeatTestCase):
'inside a resource definition', 'inside a resource definition',
six.text_type(err)) six.text_type(err))
def test_translate_outputs_good(self): def test_get_outputs_good(self):
"""Test translation of outputs into internal engine format.""" """Test get outputs."""
hot_tpl = template_format.parse(''' hot_tpl = template_format.parse('''
heat_template_version: 2013-05-23 heat_template_version: 2013-05-23
@ -413,13 +413,13 @@ class HOTemplateTest(common.HeatTestCase):
value: value1 value: value1
''') ''')
expected = {'output1': {'Description': 'output1', 'Value': 'value1'}} expected = {'output1': {'description': 'output1', 'value': 'value1'}}
tmpl = template.Template(hot_tpl) tmpl = template.Template(hot_tpl)
self.assertEqual(expected, tmpl[tmpl.OUTPUTS]) self.assertEqual(expected, tmpl[tmpl.OUTPUTS])
def test_translate_outputs_bad_no_data(self): def test_get_outputs_bad_no_data(self):
"""Test translation of outputs without any mapping.""" """Test get outputs without any mapping."""
hot_tpl = template_format.parse(""" hot_tpl = template_format.parse("""
heat_template_version: 2013-05-23 heat_template_version: 2013-05-23
@ -433,8 +433,8 @@ class HOTemplateTest(common.HeatTestCase):
self.assertEqual('Each output must contain a value key.', self.assertEqual('Each output must contain a value key.',
six.text_type(error)) six.text_type(error))
def test_translate_outputs_bad_without_name(self): def test_get_outputs_bad_without_name(self):
"""Test translation of outputs without name.""" """Test get outputs without name."""
hot_tpl = template_format.parse(""" hot_tpl = template_format.parse("""
heat_template_version: 2013-05-23 heat_template_version: 2013-05-23
@ -450,8 +450,8 @@ class HOTemplateTest(common.HeatTestCase):
'Found a [%s] instead' % six.text_type, 'Found a [%s] instead' % six.text_type,
six.text_type(error)) six.text_type(error))
def test_translate_outputs_bad_description(self): def test_get_outputs_bad_description(self):
"""Test translation of outputs into internal engine format.""" """Test get outputs with bad description name."""
hot_tpl = template_format.parse(''' hot_tpl = template_format.parse('''
heat_template_version: 2013-05-23 heat_template_version: 2013-05-23
@ -466,8 +466,8 @@ class HOTemplateTest(common.HeatTestCase):
tmpl.__getitem__, tmpl.OUTPUTS) tmpl.__getitem__, tmpl.OUTPUTS)
self.assertIn('Description', six.text_type(err)) self.assertIn('Description', six.text_type(err))
def test_translate_outputs_bad_value(self): def test_get_outputs_bad_value(self):
"""Test translation of outputs into internal engine format.""" """Test get outputs with bad value name."""
hot_tpl = template_format.parse(''' hot_tpl = template_format.parse('''
heat_template_version: 2013-05-23 heat_template_version: 2013-05-23

View File

@ -237,5 +237,8 @@ class DepAttrsTest(common.HeatTestCase):
outputs = self.stack.outputs outputs = self.stack.outputs
resources = six.itervalues(self.stack.resources) resources = six.itervalues(self.stack.resources)
self.assertEqual(self.expected[res.name], self.assertEqual(self.expected[res.name],
self.stack.get_dep_attrs(resources, outputs, self.stack.get_dep_attrs(
res.name)) resources,
outputs,
res.name,
self.stack.t.OUTPUT_VALUE))

View File

@ -33,9 +33,8 @@ from heat.tests import generic_resource as generic_rsrc
from heat.tests import utils from heat.tests import utils
ws_res_snippet = {"HeatTemplateFormatVersion": "2012-12-12", ws_res_snippet = {"Type": "StackResourceType",
"Type": "StackResourceType", "Metadata": {
"metadata": {
"key": "value", "key": "value",
"some": "more stuff"}} "some": "more stuff"}}
@ -463,7 +462,10 @@ class StackResourceTest(StackResourceBaseTest):
stack_resource.cfg.CONF.set_override('max_resources_per_stack', 2, stack_resource.cfg.CONF.set_override('max_resources_per_stack', 2,
enforce_type=True) enforce_type=True)
tmpl = {'HeatTemplateFormatVersion': '2012-12-12', tmpl = {'HeatTemplateFormatVersion': '2012-12-12',
'Resources': [1]} 'Resources': {
'r': {
'Type': 'OS::Heat::None'
}}}
template = stack_resource.template.Template(tmpl) template = stack_resource.template.Template(tmpl)
root_resources = mock.Mock(return_value=2) root_resources = mock.Mock(return_value=2)
self.parent_resource.stack.total_resources = root_resources self.parent_resource.stack.total_resources = root_resources

View File

@ -363,6 +363,72 @@ class TestTemplateValidate(common.HeatTestCase):
err = tmpl.validate() err = tmpl.validate()
self.assertIsNone(err) self.assertIsNone(err)
def test_get_resources_good(self):
"""Test get resources successful."""
t = template_format.parse('''
AWSTemplateFormatVersion: 2010-09-09
Resources:
resource1:
Type: AWS::EC2::Instance
Properties:
property1: value1
Metadata:
foo: bar
DependsOn: dummy
DeletionPolicy: dummy
UpdatePolicy:
foo: bar
''')
expected = {'resource1': {'Type': 'AWS::EC2::Instance',
'Properties': {'property1': 'value1'},
'Metadata': {'foo': 'bar'},
'DependsOn': 'dummy',
'DeletionPolicy': 'dummy',
'UpdatePolicy': {'foo': 'bar'}}}
tmpl = template.Template(t)
self.assertEqual(expected, tmpl[tmpl.RESOURCES])
def test_get_resources_bad_no_data(self):
"""Test get resources without any mapping."""
t = template_format.parse('''
AWSTemplateFormatVersion: 2010-09-09
Resources:
resource1:
''')
tmpl = template.Template(t)
error = self.assertRaises(exception.StackValidationFailed,
tmpl.validate)
self.assertEqual('Each Resource must contain a Type key.',
six.text_type(error))
def test_get_resources_no_type(self):
"""Test get resources with invalid key."""
t = template_format.parse('''
AWSTemplateFormatVersion: 2010-09-09
Resources:
resource1:
Properties:
property1: value1
Metadata:
foo: bar
DependsOn: dummy
DeletionPolicy: dummy
UpdatePolicy:
foo: bar
''')
tmpl = template.Template(t)
error = self.assertRaises(exception.StackValidationFailed,
tmpl.validate)
self.assertEqual('Each Resource must contain a Type key.',
six.text_type(error))
def test_template_validate_hot_check_t_digest(self): def test_template_validate_hot_check_t_digest(self):
t = { t = {
'heat_template_version': '2015-04-30', 'heat_template_version': '2015-04-30',