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:
parent
7ea3e68eb9
commit
faec3a0962
@ -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}
|
||||||
|
@ -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'),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 = {
|
||||||
|
@ -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(),
|
||||||
|
@ -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 {}
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
@ -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:
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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))
|
||||||
|
@ -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
|
||||||
|
@ -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',
|
||||||
|
Loading…
Reference in New Issue
Block a user