Merge "Support to generate hot template based on resource type"
This commit is contained in:
commit
57bbbfe346
|
@ -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