Support to generate hot template based on resource type

Currently heat supports to create CFN template based on
the given resource type. And this patch adds an option
to allow user to specify template type to generate(support
HOT template at the same time).

blueprint support-to-generate-hot-templates

Change-Id: I55cfb9b0f87e638350f2f6367fb399d772fff7e1
This commit is contained in:
huangtianhua 2015-04-14 11:52:48 +08:00
parent d7cadf02c9
commit b44df7a1db
13 changed files with 259 additions and 43 deletions

View File

@ -506,7 +506,18 @@ class StackController(object):
"""
Generates a template based on the specified type.
"""
return self.rpc_client.generate_template(req.context, type_name)
template_type = 'cfn'
if rpc_api.TEMPLATE_TYPE in req.params:
try:
template_type = param_utils.extract_template_type(
req.params.get(rpc_api.TEMPLATE_TYPE))
except ValueError as ex:
msg = _("Template type is not supported: %s") % ex
raise exc.HTTPBadRequest(six.text_type(msg))
return self.rpc_client.generate_template(req.context,
type_name,
template_type)
@util.identified_stack
def snapshot(self, req, identity, body):

View File

@ -62,3 +62,12 @@ def extract_tags(subject):
raise ValueError(_('Invalid tag, "%s" is longer than 80 '
'characters') % tag)
return tags
def extract_template_type(subject):
template_type = subject.lower()
if template_type not in ('cfn', 'hot'):
raise ValueError(_('Invalid template type "%(value)s", valid '
'types are: cfn, hot.') %
{'value': subject})
return template_type

View File

@ -95,19 +95,28 @@ class Attribute(object):
def support_status(self):
return self.schema.support_status
def as_output(self, resource_name):
def as_output(self, resource_name, template_type='cfn'):
"""
Return an Output schema entry for a provider template with the given
resource name.
:param resource_name: the logical name of the provider resource
:returns: This attribute as a template 'Output' entry
:param template_type: the template type to generate
:returns: This attribute as a template 'Output' entry for
cfn template and 'output' entry for hot template
"""
return {
"Value": '{"Fn::GetAtt": ["%s", "%s"]}' % (resource_name,
self.name),
"Description": self.schema.description
}
if template_type == 'hot':
return {
"value": '{"get_attr": ["%s", "%s"]}' % (resource_name,
self.name),
"description": self.schema.description
}
else:
return {
"Value": '{"Fn::GetAtt": ["%s", "%s"]}' % (resource_name,
self.name),
"Description": self.schema.description
}
class Attributes(collections.Mapping):
@ -127,7 +136,7 @@ class Attributes(collections.Mapping):
return dict((n, Attribute(n, d)) for n, d in schema.items())
@staticmethod
def as_outputs(resource_name, resource_class):
def as_outputs(resource_name, resource_class, template_type='cfn'):
"""
:param resource_name: logical name of the resource
:param resource_class: resource implementation class
@ -137,7 +146,8 @@ class Attributes(collections.Mapping):
schema = resource_class.attributes_schema
attribs = Attributes._make_attributes(schema).items()
return dict((n, att.as_output(resource_name)) for n, att in attribs)
return dict((n, att.as_output(resource_name,
template_type)) for n, att in attribs)
@staticmethod
def schema_from_outputs(json_snippet):

View File

@ -21,7 +21,6 @@ from heat.api.aws import utils as aws_utils
from heat.common import exception
from heat.common.i18n import _
from heat.engine import function
from heat.engine import resource
class FindInMap(function.Function):
@ -187,6 +186,7 @@ class GetAtt(function.Function):
super(GetAtt, self).validate()
res = self._resource()
attr = function.resolve(self._attribute)
from heat.engine import resource
if (type(res).FnGetAtt == resource.Resource.FnGetAtt and
attr not in six.iterkeys(res.attributes_schema)):
raise exception.InvalidTemplateAttribute(

View File

@ -20,6 +20,7 @@ from heat.common import exception
from heat.common.i18n import _
from heat.engine import constraints as constr
from heat.engine import function
from heat.engine.hot import parameters as hot_param
from heat.engine import parameters
from heat.engine import support
@ -516,8 +517,57 @@ class Properties(collections.Mapping):
else:
return {'Ref': name}
@staticmethod
def _hot_param_def_from_prop(schema):
"""
Return parameter definition corresponding to a property for
hot template.
"""
param_type_map = {
schema.INTEGER: hot_param.HOTParamSchema.NUMBER,
schema.STRING: hot_param.HOTParamSchema.STRING,
schema.NUMBER: hot_param.HOTParamSchema.NUMBER,
schema.BOOLEAN: hot_param.HOTParamSchema.BOOLEAN,
schema.MAP: hot_param.HOTParamSchema.MAP,
schema.LIST: hot_param.HOTParamSchema.LIST,
}
def param_items():
yield hot_param.HOTParamSchema.TYPE, param_type_map[schema.type]
if schema.description is not None:
yield hot_param.HOTParamSchema.DESCRIPTION, schema.description
if schema.default is not None:
yield hot_param.HOTParamSchema.DEFAULT, schema.default
for constraint in schema.constraints:
if (isinstance(constraint, constr.Length) or
isinstance(constraint, constr.Range)):
if constraint.min is not None:
yield hot_param.MIN, constraint.min
if constraint.max is not None:
yield hot_param.MAX, constraint.max
elif isinstance(constraint, constr.AllowedValues):
yield hot_param.ALLOWED_VALUES, list(constraint.allowed)
elif isinstance(constraint, constr.AllowedPattern):
yield hot_param.ALLOWED_PATTERN, constraint.pattern
if schema.type == schema.BOOLEAN:
yield hot_param.ALLOWED_VALUES, ['True', 'true',
'False', 'false']
return dict(param_items())
@staticmethod
def _hot_prop_def_from_prop(name, schema):
"""
Return a provider template property definition for a property.
"""
return {'get_param': name}
@classmethod
def schema_to_parameters_and_properties(cls, schema):
def schema_to_parameters_and_properties(cls, schema, template_type='cfn'):
"""Generates properties with params resolved for a resource's
properties_schema.
@ -532,16 +582,19 @@ class Properties(collections.Mapping):
output: {'foo': {'Type': 'String'}, 'bar': {'Type': 'Json'}},
{'foo': {'Ref': 'foo'}, 'bar': {'Ref': 'bar'}}
"""
def param_prop_def_items(name, schema):
param_def = cls._param_def_from_prop(schema)
prop_def = cls._prop_def_from_prop(name, schema)
def param_prop_def_items(name, schema, template_type):
if template_type == 'hot':
param_def = cls._hot_param_def_from_prop(schema)
prop_def = cls._hot_prop_def_from_prop(name, schema)
else:
param_def = cls._param_def_from_prop(schema)
prop_def = cls._prop_def_from_prop(name, schema)
return (name, param_def), (name, prop_def)
if not schema:
return {}, {}
param_prop_defs = [param_prop_def_items(n, s)
param_prop_defs = [param_prop_def_items(n, s, template_type)
for n, s in six.iteritems(schemata(schema))
if s.implemented]
param_items, prop_items = zip(*param_prop_defs)

View File

@ -31,9 +31,11 @@ from heat.common import identifier
from heat.common import short_id
from heat.common import timeutils
from heat.engine import attributes
from heat.engine.cfn import template as cfn_tmpl
from heat.engine import environment
from heat.engine import event
from heat.engine import function
from heat.engine.hot import template as hot_tmpl
from heat.engine import properties
from heat.engine import resources
from heat.engine import rsrc_defn
@ -1183,29 +1185,52 @@ class Resource(object):
self.name)
@classmethod
def resource_to_template(cls, resource_type):
def resource_to_template(cls, resource_type, template_type='cfn'):
'''
:param resource_type: The resource type to be displayed in the template
:param template_type: the template type to generate, cfn or hot.
:returns: A template where the resource's properties_schema is mapped
as parameters, and the resource's attributes_schema is mapped as
outputs
'''
schema = cls.properties_schema
params, props = (properties.Properties.
schema_to_parameters_and_properties(schema))
schema_to_parameters_and_properties(schema,
template_type))
resource_name = cls.__name__
return {
'HeatTemplateFormatVersion': '2012-12-12',
'Parameters': params,
'Resources': {
resource_name: {
'Type': resource_type,
'Properties': props
}
},
'Outputs': attributes.Attributes.as_outputs(resource_name, cls)
}
outputs = attributes.Attributes.as_outputs(resource_name, cls,
template_type)
description = 'Initial template of %s' % resource_name
return cls.build_template_dict(resource_name, resource_type,
template_type, params, props,
outputs, description)
@staticmethod
def build_template_dict(res_name, res_type, tmpl_type,
params, props, outputs, description):
if tmpl_type == 'hot':
tmpl_dict = {
hot_tmpl.HOTemplate20150430.VERSION: '2015-04-30',
hot_tmpl.HOTemplate20150430.DESCRIPTION: description,
hot_tmpl.HOTemplate20150430.PARAMETERS: params,
hot_tmpl.HOTemplate20150430.OUTPUTS: outputs,
hot_tmpl.HOTemplate20150430.RESOURCES: {
res_name: {
hot_tmpl.RES_TYPE: res_type,
hot_tmpl.RES_PROPERTIES: props}}}
else:
tmpl_dict = {
cfn_tmpl.CfnTemplate.ALTERNATE_VERSION: '2012-12-12',
cfn_tmpl.CfnTemplate.DESCRIPTION: description,
cfn_tmpl.CfnTemplate.PARAMETERS: params,
cfn_tmpl.CfnTemplate.RESOURCES: {
res_name: {
cfn_tmpl.RES_TYPE: res_type,
cfn_tmpl.RES_PROPERTIES: props}
},
cfn_tmpl.CfnTemplate.OUTPUTS: outputs}
return tmpl_dict
def data(self):
'''

View File

@ -266,7 +266,7 @@ class EngineService(service.Service):
by the RPC caller.
"""
RPC_API_VERSION = '1.8'
RPC_API_VERSION = '1.9'
def __init__(self, host, topic, manager=None):
super(EngineService, self).__init__()
@ -1028,16 +1028,17 @@ class EngineService(service.Service):
rpc_api.RES_SCHEMA_ATTRIBUTES: dict(attributes_schema()),
}
def generate_template(self, cnxt, type_name):
def generate_template(self, cnxt, type_name, template_type='cfn'):
"""
Generate a template based on the specified type.
:param cnxt: RPC context.
:param type_name: Name of the resource type to generate a template for.
:param template_type: the template type to generate, cfn or hot.
"""
try:
return resources.global_env().get_class(
type_name).resource_to_template(type_name)
type_name).resource_to_template(type_name, template_type)
except (exception.InvalidResourceType,
exception.ResourceTypeNotFound,
exception.TemplateNotFound) as ex:

View File

@ -19,13 +19,13 @@ PARAM_KEYS = (
PARAM_SHOW_DELETED, PARAM_SHOW_NESTED, PARAM_EXISTING,
PARAM_CLEAR_PARAMETERS, PARAM_GLOBAL_TENANT, PARAM_LIMIT,
PARAM_NESTED_DEPTH, PARAM_TAGS, PARAM_SHOW_HIDDEN, PARAM_TAGS_ANY,
PARAM_NOT_TAGS, PARAM_NOT_TAGS_ANY
PARAM_NOT_TAGS, PARAM_NOT_TAGS_ANY, TEMPLATE_TYPE,
) = (
'timeout_mins', 'disable_rollback', 'adopt_stack_data',
'show_deleted', 'show_nested', 'existing',
'clear_parameters', 'global_tenant', 'limit',
'nested_depth', 'tags', 'show_hidden', 'tags_any',
'not_tags', 'not_tags_any'
'not_tags', 'not_tags_any', 'template_type',
)
STACK_KEYS = (

View File

@ -29,6 +29,7 @@ class EngineClient(object):
1.0 - Initial version.
1.1 - Add support_status argument to list_resource_types()
1.4 - Add support for service list
1.9 - Add template_type option to generate_template()
'''
BASE_RPC_API_VERSION = '1.0'
@ -334,15 +335,18 @@ class EngineClient(object):
return self.call(ctxt, self.make_msg('resource_schema',
type_name=type_name))
def generate_template(self, ctxt, type_name):
def generate_template(self, ctxt, type_name, template_type='cfn'):
"""
Generate a template based on the specified type.
:param ctxt: RPC context.
:param type_name: The resource type name to generate a template for.
:param template_type: the template type to generate, cfn or hot.
"""
return self.call(ctxt, self.make_msg('generate_template',
type_name=type_name))
type_name=type_name,
template_type=template_type),
version='1.9')
def list_events(self, ctxt, stack_identity, filters=None, limit=None,
marker=None, sort_keys=None, sort_dir=None,):

View File

@ -2071,13 +2071,32 @@ class StackControllerTest(ControllerTest, common.HeatTestCase):
self.m.StubOutWithMock(rpc_client.EngineClient, 'call')
rpc_client.EngineClient.call(
req.context,
('generate_template', {'type_name': 'TEST_TYPE'})
('generate_template', {'type_name': 'TEST_TYPE',
'template_type': 'cfn'}),
version='1.9'
).AndReturn(engine_response)
self.m.ReplayAll()
self.controller.generate_template(req, tenant_id=self.tenant,
type_name='TEST_TYPE')
self.m.VerifyAll()
def test_generate_template_invalid_template_type(self, mock_enforce):
self._mock_enforce_setup(mock_enforce, 'generate_template', True)
params = {'template_type': 'invalid'}
mock_call = self.patchobject(rpc_client.EngineClient, 'call')
req = self._get('/resource_types/TEST_TYPE/template',
params=params)
ex = self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.generate_template,
req, tenant_id=self.tenant,
type_name='TEST_TYPE')
self.assertIn('Template type is not supported: Invalid template '
'type "invalid", valid types are: cfn, hot.',
six.text_type(ex))
self.assertFalse(mock_call.called)
def test_generate_template_not_found(self, mock_enforce):
self._mock_enforce_setup(mock_enforce, 'generate_template', True)
req = self._get('/resource_types/NOT_FOUND/template')
@ -2086,7 +2105,9 @@ class StackControllerTest(ControllerTest, common.HeatTestCase):
self.m.StubOutWithMock(rpc_client.EngineClient, 'call')
rpc_client.EngineClient.call(
req.context,
('generate_template', {'type_name': 'NOT_FOUND'})
('generate_template', {'type_name': 'NOT_FOUND',
'template_type': 'cfn'}),
version='1.9'
).AndRaise(to_remote_error(error))
self.m.ReplayAll()
resp = request_with_middleware(fault.FaultWrapper,

View File

@ -1402,7 +1402,7 @@ class StackServiceTest(common.HeatTestCase):
def test_make_sure_rpc_version(self):
self.assertEqual(
'1.8',
'1.9',
service.EngineService.RPC_API_VERSION,
('RPC version is changed, please update this test to new version '
'and make sure additional test cases are added for RPC APIs '

View File

@ -1011,7 +1011,7 @@ class ResourceTest(common.HeatTestCase):
self.assertRaises(exception.ResourceFailure, resume)
self.assertEqual((res.RESUME, res.FAILED), res.state)
def test_resource_class_to_template(self):
def test_resource_class_to_cfn_template(self):
class TestResource(resource.Resource):
list_schema = {'wont_show_up': {'Type': 'Number'}}
@ -1044,6 +1044,7 @@ class ResourceTest(common.HeatTestCase):
expected_template = {
'HeatTemplateFormatVersion': '2012-12-12',
'Description': 'Initial template of TestResource',
'Parameters': {
'name': {'Type': 'String'},
'bool': {'Type': 'Boolean',
@ -1089,6 +1090,86 @@ class ResourceTest(common.HeatTestCase):
TestResource.resource_to_template(
'Test::Resource::resource'))
def test_resource_class_to_hot_template(self):
class TestResource(resource.Resource):
list_schema = {'wont_show_up': {'Type': 'Number'}}
map_schema = {'will_show_up': {'Type': 'Integer'}}
properties_schema = {
'name': {'Type': 'String'},
'bool': {'Type': 'Boolean'},
'implemented': {'Type': 'String',
'Implemented': True,
'AllowedPattern': '.*',
'MaxLength': 7,
'MinLength': 2,
'Required': True},
'not_implemented': {'Type': 'String',
'Implemented': False},
'number': {'Type': 'Number',
'MaxValue': 77,
'MinValue': 41,
'Default': 42},
'list': {'Type': 'List', 'Schema': {'Type': 'Map',
'Schema': list_schema}},
'map': {'Type': 'Map', 'Schema': map_schema},
}
attributes_schema = {
'output1': attributes.Schema('output1_desc'),
'output2': attributes.Schema('output2_desc')
}
expected_template = {
'heat_template_version': '2015-04-30',
'description': 'Initial template of TestResource',
'parameters': {
'name': {'type': 'string'},
'bool': {'type': 'boolean',
'allowed_values': ['True', 'true', 'False', 'false']},
'implemented': {
'type': 'string',
'allowed_pattern': '.*',
'max': 7,
'min': 2
},
'number': {'type': 'number',
'max': 77,
'min': 41,
'default': 42},
'list': {'type': 'comma_delimited_list'},
'map': {'type': 'json'}
},
'resources': {
'TestResource': {
'type': 'Test::Resource::resource',
'properties': {
'name': {'get_param': 'name'},
'bool': {'get_param': 'bool'},
'implemented': {'get_param': 'implemented'},
'number': {'get_param': 'number'},
'list': {'get_param': 'list'},
'map': {'get_param': 'map'}
}
}
},
'outputs': {
'output1': {
'description': 'output1_desc',
'value': '{"get_attr": ["TestResource", "output1"]}'
},
'output2': {
'description': 'output2_desc',
'value': '{"get_attr": ["TestResource", "output2"]}'
}
}
}
self.assertEqual(expected_template,
TestResource.resource_to_template(
'Test::Resource::resource',
template_type='hot'))
def test_is_using_neutron(self):
snippet = rsrc_defn.ResourceDefinition('aresource',
'GenericResourceType')

View File

@ -200,7 +200,8 @@ class EngineRpcAPITestCase(common.HeatTestCase):
self._test_engine_api('resource_schema', 'call', type_name="TYPE")
def test_generate_template(self):
self._test_engine_api('generate_template', 'call', type_name="TYPE")
self._test_engine_api('generate_template', 'call',
type_name="TYPE", template_type='cfn')
def test_list_events(self):
kwargs = {'stack_identity': self.identity,