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