Merge "Support to generate hot template based on resource type"

This commit is contained in:
Jenkins 2015-04-30 02:19:17 +00:00 committed by Gerrit Code Review
commit 57bbbfe346
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,