Convert functions into a fixed part of the template

http://www.mail-archive.com/openstack-dev@lists.openstack.org/msg28987.html
part of blueprint stevedore-plugins
Change-Id: Iabfa077077fa2170f5da8e7752e05e00db91a692
This commit is contained in:
Angus Salkeld 2014-09-09 11:13:43 +10:00
parent b3cb23e5b3
commit b49f283a4d
10 changed files with 160 additions and 159 deletions

View File

@ -557,35 +557,3 @@ class ResourceFacade(function.Function):
elif attr == self.DELETION_POLICY: elif attr == self.DELETION_POLICY:
dp = self.stack.parent_resource.t.deletion_policy() dp = self.stack.parent_resource.t.deletion_policy()
return function.resolve(dp) return function.resolve(dp)
def function_mapping(version_key, version):
if version_key == 'AWSTemplateFormatVersion':
return {
'Fn::FindInMap': FindInMap,
'Fn::GetAZs': GetAZs,
'Ref': Ref,
'Fn::GetAtt': GetAtt,
'Fn::Select': Select,
'Fn::Join': Join,
'Fn::Base64': Base64,
}
elif version_key != 'HeatTemplateFormatVersion':
return {}
if version == '2012-12-12':
return {
'Fn::FindInMap': FindInMap,
'Fn::GetAZs': GetAZs,
'Ref': Ref,
'Fn::GetAtt': GetAtt,
'Fn::Select': Select,
'Fn::Join': Join,
'Fn::Split': Split,
'Fn::Replace': Replace,
'Fn::Base64': Base64,
'Fn::MemberListToMap': MemberListToMap,
'Fn::ResourceFacade': ResourceFacade,
}
return {}

View File

@ -15,6 +15,7 @@
import collections import collections
import six import six
from heat.engine.cfn import functions as cfn_funcs
from heat.engine import function from heat.engine import function
from heat.engine import parameters from heat.engine import parameters
from heat.engine import rsrc_defn from heat.engine import rsrc_defn
@ -41,6 +42,16 @@ class CfnTemplate(template.Template):
SECTIONS_NO_DIRECT_ACCESS = set([PARAMETERS, VERSION, ALTERNATE_VERSION]) SECTIONS_NO_DIRECT_ACCESS = set([PARAMETERS, VERSION, ALTERNATE_VERSION])
functions = {
'Fn::FindInMap': cfn_funcs.FindInMap,
'Fn::GetAZs': cfn_funcs.GetAZs,
'Ref': cfn_funcs.Ref,
'Fn::GetAtt': cfn_funcs.GetAtt,
'Fn::Select': cfn_funcs.Select,
'Fn::Join': cfn_funcs.Join,
'Fn::Base64': cfn_funcs.Base64,
}
def __getitem__(self, section): def __getitem__(self, section):
'''Get the relevant section in the template.''' '''Get the relevant section in the template.'''
if section not in self.SECTIONS: if section not in self.SECTIONS:
@ -152,3 +163,19 @@ class CfnTemplate(template.Template):
if self.t.get(self.RESOURCES) is None: if self.t.get(self.RESOURCES) is None:
self.t[self.RESOURCES] = {} self.t[self.RESOURCES] = {}
self.t[self.RESOURCES][name] = cfn_tmpl self.t[self.RESOURCES][name] = cfn_tmpl
class HeatTemplate(CfnTemplate):
functions = {
'Fn::FindInMap': cfn_funcs.FindInMap,
'Fn::GetAZs': cfn_funcs.GetAZs,
'Ref': cfn_funcs.Ref,
'Fn::GetAtt': cfn_funcs.GetAtt,
'Fn::Select': cfn_funcs.Select,
'Fn::Join': cfn_funcs.Join,
'Fn::Split': cfn_funcs.Split,
'Fn::Replace': cfn_funcs.Replace,
'Fn::Base64': cfn_funcs.Base64,
'Fn::MemberListToMap': cfn_funcs.MemberListToMap,
'Fn::ResourceFacade': cfn_funcs.ResourceFacade,
}

View File

@ -261,50 +261,3 @@ class Removed(function.Function):
def result(self): def result(self):
return super(Removed, self).result() return super(Removed, self).result()
def function_mapping(version_key, version):
if version_key != 'heat_template_version':
return {}
if version == '2013-05-23':
return {
'Fn::GetAZs': cfn_funcs.GetAZs,
'get_param': GetParam,
'get_resource': cfn_funcs.ResourceRef,
'Ref': cfn_funcs.Ref,
'get_attr': GetAttThenSelect,
'Fn::Select': cfn_funcs.Select,
'Fn::Join': cfn_funcs.Join,
'Fn::Split': cfn_funcs.Split,
'str_replace': Replace,
'Fn::Replace': cfn_funcs.Replace,
'Fn::Base64': cfn_funcs.Base64,
'Fn::MemberListToMap': cfn_funcs.MemberListToMap,
'resource_facade': ResourceFacade,
'Fn::ResourceFacade': cfn_funcs.ResourceFacade,
'get_file': GetFile,
}
if version == '2014-10-16':
return {
'get_param': GetParam,
'get_resource': cfn_funcs.ResourceRef,
'get_attr': GetAtt,
'list_join': Join,
'str_replace': Replace,
'resource_facade': ResourceFacade,
'get_file': GetFile,
'Fn::Select': cfn_funcs.Select,
'Fn::GetAZs': Removed,
'Ref': Removed,
'Fn::Join': Removed,
'Fn::Split': Removed,
'Fn::Replace': Removed,
'Fn::Base64': Removed,
'Fn::MemberListToMap': Removed,
'Fn::ResourceFacade': Removed,
}
return {}

View File

@ -13,8 +13,10 @@
import collections import collections
import six import six
from heat.engine.cfn import functions as cfn_funcs
from heat.engine.cfn import template as cfn_template from heat.engine.cfn import template as cfn_template
from heat.engine import function from heat.engine import function
from heat.engine.hot import functions as hot_funcs
from heat.engine.hot import parameters from heat.engine.hot import parameters
from heat.engine import rsrc_defn from heat.engine import rsrc_defn
from heat.engine import template from heat.engine import template
@ -30,7 +32,7 @@ _RESOURCE_KEYS = (
) )
class HOTemplate(template.Template): class HOTemplate20130523(template.Template):
""" """
A Heat Orchestration Template format stack template. A Heat Orchestration Template format stack template.
""" """
@ -49,13 +51,32 @@ class HOTemplate(template.Template):
cfn_template.CfnTemplate.RESOURCES: RESOURCES, cfn_template.CfnTemplate.RESOURCES: RESOURCES,
cfn_template.CfnTemplate.OUTPUTS: OUTPUTS} cfn_template.CfnTemplate.OUTPUTS: OUTPUTS}
functions = {
'Fn::GetAZs': cfn_funcs.GetAZs,
'get_param': hot_funcs.GetParam,
'get_resource': cfn_funcs.ResourceRef,
'Ref': cfn_funcs.Ref,
'get_attr': hot_funcs.GetAttThenSelect,
'Fn::Select': cfn_funcs.Select,
'Fn::Join': cfn_funcs.Join,
'list_join': hot_funcs.Join,
'Fn::Split': cfn_funcs.Split,
'str_replace': hot_funcs.Replace,
'Fn::Replace': cfn_funcs.Replace,
'Fn::Base64': cfn_funcs.Base64,
'Fn::MemberListToMap': cfn_funcs.MemberListToMap,
'resource_facade': hot_funcs.ResourceFacade,
'Fn::ResourceFacade': cfn_funcs.ResourceFacade,
'get_file': hot_funcs.GetFile,
}
def __getitem__(self, section): def __getitem__(self, section):
""""Get the relevant section in the template.""" """"Get the relevant section in the template."""
#first translate from CFN into HOT terminology if necessary #first translate from CFN into HOT terminology if necessary
if section not in self.SECTIONS: if section not in self.SECTIONS:
section = HOTemplate._translate(section, self._CFN_TO_HOT_SECTIONS, section = HOTemplate20130523._translate(
_('"%s" is not a valid template ' section, self._CFN_TO_HOT_SECTIONS,
'section')) _('"%s" is not a valid template section'))
if section not in self.SECTIONS: if section not in self.SECTIONS:
raise KeyError(_('"%s" is not a valid template section') % section) raise KeyError(_('"%s" is not a valid template section') % section)
@ -226,3 +247,27 @@ class HOTemplate(template.Template):
if self.t.get(self.RESOURCES) is None: if self.t.get(self.RESOURCES) is None:
self.t[self.RESOURCES] = {} self.t[self.RESOURCES] = {}
self.t[self.RESOURCES][name] = definition.render_hot() self.t[self.RESOURCES][name] = definition.render_hot()
class HOTemplate20141016(HOTemplate20130523):
functions = {
'get_attr': hot_funcs.GetAtt,
'get_file': hot_funcs.GetFile,
'get_param': hot_funcs.GetParam,
'get_resource': cfn_funcs.ResourceRef,
'list_join': hot_funcs.Join,
'resource_facade': hot_funcs.ResourceFacade,
'str_replace': hot_funcs.Replace,
'Fn::Select': cfn_funcs.Select,
# functions removed from 20130523
'Fn::GetAZs': hot_funcs.Removed,
'Fn::Join': hot_funcs.Removed,
'Fn::Split': hot_funcs.Removed,
'Fn::Replace': hot_funcs.Removed,
'Fn::Base64': hot_funcs.Removed,
'Fn::MemberListToMap': hot_funcs.Removed,
'Fn::ResourceFacade': hot_funcs.Removed,
'Ref': hot_funcs.Removed,
}

View File

@ -20,7 +20,6 @@ from stevedore import extension
from heat.common import exception from heat.common import exception
from heat.db import api as db_api from heat.db import api as db_api
from heat.engine import plugin_manager
from heat.openstack.common import log as logging from heat.openstack.common import log as logging
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@ -31,31 +30,6 @@ __all__ = ['Template']
_template_classes = None _template_classes = None
class TemplatePluginManager(object):
'''A Descriptor class for caching PluginManagers.
Keeps a cache of PluginManagers with the search directories corresponding
to the package containing the owner class.
'''
def __init__(self):
self.plugin_managers = {}
@staticmethod
def package_name(obj_class):
'''Return the package containing the given class.'''
module_name = obj_class.__module__
return module_name.rsplit('.', 1)[0]
def __get__(self, obj, obj_class):
'''Get a PluginManager for a class.'''
pkg = self.package_name(obj_class)
if pkg not in self.plugin_managers:
self.plugin_managers[pkg] = plugin_manager.PluginManager(pkg)
return self.plugin_managers[pkg]
def get_version(template_data, available_versions): def get_version(template_data, available_versions):
version_keys = set(key for key, version in available_versions) version_keys = set(key for key, version in available_versions)
candidate_keys = set(k for k, v in six.iteritems(template_data) if candidate_keys = set(k for k, v in six.iteritems(template_data) if
@ -74,14 +48,18 @@ def get_version(template_data, available_versions):
return version_key, template_data[version_key] return version_key, template_data[version_key]
def get_template_class(plugin_mgr, template_data): def _get_template_extension_manager():
return extension.ExtensionManager(
namespace='heat.templates',
invoke_on_load=False,
verify_requirements=True)
def get_template_class(template_data):
global _template_classes global _template_classes
if _template_classes is None: if _template_classes is None:
mgr = extension.ExtensionManager( mgr = _get_template_extension_manager()
namespace='heat.templates',
invoke_on_load=False,
verify_requirements=True)
_template_classes = dict((tuple(name.split('.')), mgr[name].plugin) _template_classes = dict((tuple(name.split('.')), mgr[name].plugin)
for name in mgr.names()) for name in mgr.names())
@ -108,16 +86,13 @@ def get_template_class(plugin_mgr, template_data):
class Template(collections.Mapping): class Template(collections.Mapping):
'''A stack template.''' '''A stack template.'''
_plugins = TemplatePluginManager()
_functionmaps = {}
def __new__(cls, template, *args, **kwargs): def __new__(cls, template, *args, **kwargs):
'''Create a new Template of the appropriate class.''' '''Create a new Template of the appropriate class.'''
if cls != Template: if cls != Template:
TemplateClass = cls TemplateClass = cls
else: else:
TemplateClass = get_template_class(cls._plugins, template) TemplateClass = get_template_class(template)
return super(Template, cls).__new__(TemplateClass) return super(Template, cls).__new__(TemplateClass)
@ -191,17 +166,8 @@ class Template(collections.Mapping):
'''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)
def functions(self):
'''Return a dict of template functions keyed by name.'''
if self.version not in self._functionmaps:
mappings = plugin_manager.PluginMapping('function', *self.version)
funcs = dict(mappings.load_all(self._plugins))
self._functionmaps[self.version] = funcs
return self._functionmaps[self.version]
def parse(self, stack, snippet): def parse(self, stack, snippet):
return parse(self.functions(), stack, snippet) return parse(self.functions, stack, snippet)
def validate(self): def validate(self):
'''Validate the template. '''Validate the template.

View File

@ -99,7 +99,7 @@ class HOTemplateTest(HeatTestCase):
tmpl = parser.Template(hot_tpl_empty) tmpl = parser.Template(hot_tpl_empty)
# check if we get the right class # check if we get the right class
self.assertIsInstance(tmpl, hot_template.HOTemplate) self.assertIsInstance(tmpl, hot_template.HOTemplate20130523)
# test getting an invalid section # test getting an invalid section
self.assertNotIn('foobar', tmpl) self.assertNotIn('foobar', tmpl)
@ -113,7 +113,7 @@ class HOTemplateTest(HeatTestCase):
tmpl = parser.Template(hot_tpl_empty_sections) tmpl = parser.Template(hot_tpl_empty_sections)
# check if we get the right class # check if we get the right class
self.assertIsInstance(tmpl, hot_template.HOTemplate) self.assertIsInstance(tmpl, hot_template.HOTemplate20130523)
# test getting an invalid section # test getting an invalid section
self.assertNotIn('foobar', tmpl) self.assertNotIn('foobar', tmpl)

View File

@ -212,8 +212,8 @@ class TemplateTest(HeatTestCase):
"heat_template_version" : "2012-12-12", "heat_template_version" : "2012-12-12",
}''') }''')
versions = { versions = {
('heat_template_version', '2013-05-23'): hot_t.HOTemplate, ('heat_template_version', '2013-05-23'): hot_t.HOTemplate20130523,
('heat_template_version', '2013-06-23'): hot_t.HOTemplate ('heat_template_version', '2013-06-23'): hot_t.HOTemplate20130523
} }
temp_copy = copy.deepcopy(template._template_classes) temp_copy = copy.deepcopy(template._template_classes)

View File

@ -12,38 +12,80 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import fixtures
from oslotest import mockpatch
import six import six
from stevedore import extension
from heat.common import exception from heat.common import exception
from heat.common import template_format from heat.common import template_format
from heat.engine.cfn.template import CfnTemplate from heat.engine import function
from heat.engine import plugin_manager
from heat.engine import template from heat.engine import template
from heat.tests.common import HeatTestCase from heat.tests import common
class TestTemplatePluginManager(HeatTestCase): class TemplatePluginFixture(fixtures.Fixture):
def __init__(self, templates={}):
super(TemplatePluginFixture, self).__init__()
self.templates = [extension.Extension(k, None, v, None)
for (k, v) in templates.items()]
def test_pkg_name(self): def _get_template_extension_manager(self):
cfn_tmpl_pkg = template.TemplatePluginManager.package_name(CfnTemplate) return extension.ExtensionManager.make_test_instance(self.templates)
self.assertEqual('heat.engine.cfn', cfn_tmpl_pkg)
def test_get(self): def setUp(self):
super(TemplatePluginFixture, self).setUp()
tpm = template.TemplatePluginManager() def clear_template_classes():
template._template_classes = None
self.assertFalse(tpm.plugin_managers) clear_template_classes()
self.useFixture(mockpatch.PatchObject(
class Test(object): template,
plugins = tpm '_get_template_extension_manager',
new=self._get_template_extension_manager))
test_pm = Test().plugins self.addCleanup(clear_template_classes)
self.assertTrue(isinstance(test_pm, plugin_manager.PluginManager))
self.assertEqual(tpm.plugin_managers['heat.tests'], test_pm)
class TestTemplateVersion(HeatTestCase): class TestTemplatePluginManager(common.HeatTestCase):
def test_template_NEW_good(self):
class NewTemplate(template.Template):
SECTIONS = (VERSION, MAPPINGS) = ('NEWTemplateFormatVersion',
'__undefined__')
RESOURCES = 'thingies'
def param_schemata(self):
pass
def parameters(self, stack_identifier, user_params):
pass
def resource_definitions(self, stack):
pass
def add_resource(self, definition, name=None):
pass
def __getitem__(self, section):
return {}
def functions(self):
return {}
class NewTemplatePrint(function.Function):
def result(self):
return 'always this'
self.useFixture(TemplatePluginFixture(
{'NEWTemplateFormatVersion.2345-01-01': NewTemplate}))
t = {'NEWTemplateFormatVersion': '2345-01-01'}
tmpl = template.Template(t)
err = tmpl.validate()
self.assertIsNone(err)
class TestTemplateVersion(common.HeatTestCase):
versions = (('heat_template_version', '2013-05-23'), versions = (('heat_template_version', '2013-05-23'),
('HeatTemplateFormatVersion', '2012-12-12'), ('HeatTemplateFormatVersion', '2012-12-12'),
@ -97,7 +139,7 @@ class TestTemplateVersion(HeatTestCase):
template.get_version, tmpl, self.versions) template.get_version, tmpl, self.versions)
class TestTemplateValidate(HeatTestCase): class TestTemplateValidate(common.HeatTestCase):
def test_template_validate_cfn_good(self): def test_template_validate_cfn_good(self):
t = { t = {

View File

@ -19,7 +19,7 @@ from heat.common import template_format
from heat.engine.clients.os import glance from heat.engine.clients.os import glance
from heat.engine.clients.os import nova from heat.engine.clients.os import nova
from heat.engine import environment from heat.engine import environment
from heat.engine.hot.template import HOTemplate from heat.engine.hot.template import HOTemplate20130523
from heat.engine import parser from heat.engine import parser
from heat.engine import resources from heat.engine import resources
from heat.engine import service from heat.engine import service
@ -1331,7 +1331,7 @@ class validateTest(HeatTestCase):
def test_validate_duplicate_parameters_in_group(self): def test_validate_duplicate_parameters_in_group(self):
t = template_format.parse(test_template_duplicate_parameters) t = template_format.parse(test_template_duplicate_parameters)
template = HOTemplate(t) template = HOTemplate20130523(t)
stack = parser.Stack(self.ctx, 'test_stack', template, stack = parser.Stack(self.ctx, 'test_stack', template,
environment.Environment({ environment.Environment({
'KeyName': 'test', 'KeyName': 'test',
@ -1346,7 +1346,7 @@ class validateTest(HeatTestCase):
def test_validate_invalid_parameter_in_group(self): def test_validate_invalid_parameter_in_group(self):
t = template_format.parse(test_template_invalid_parameter_name) t = template_format.parse(test_template_invalid_parameter_name)
template = HOTemplate(t) template = HOTemplate20130523(t)
stack = parser.Stack(self.ctx, 'test_stack', template, stack = parser.Stack(self.ctx, 'test_stack', template,
environment.Environment({ environment.Environment({
'KeyName': 'test', 'KeyName': 'test',
@ -1362,7 +1362,7 @@ class validateTest(HeatTestCase):
def test_validate_no_parameters_in_group(self): def test_validate_no_parameters_in_group(self):
t = template_format.parse(test_template_no_parameters) t = template_format.parse(test_template_no_parameters)
template = HOTemplate(t) template = HOTemplate20130523(t)
stack = parser.Stack(self.ctx, 'test_stack', template) stack = parser.Stack(self.ctx, 'test_stack', template)
exc = self.assertRaises(exception.StackValidationFailed, exc = self.assertRaises(exception.StackValidationFailed,
stack.validate) stack.validate)

View File

@ -62,9 +62,9 @@ heat.constraints =
heat.stack_lifecycle_plugins = heat.stack_lifecycle_plugins =
heat.templates = heat.templates =
heat_template_version.2013-05-23 = heat.engine.hot.template:HOTemplate heat_template_version.2013-05-23 = heat.engine.hot.template:HOTemplate20130523
heat_template_version.2014-10-16 = heat.engine.hot.template:HOTemplate heat_template_version.2014-10-16 = heat.engine.hot.template:HOTemplate20141016
HeatTemplateFormatVersion.2012-12-12 = heat.engine.cfn.template:CfnTemplate HeatTemplateFormatVersion.2012-12-12 = heat.engine.cfn.template:HeatTemplate
AWSTemplateFormatVersion.2010-09-09 = heat.engine.cfn.template:CfnTemplate AWSTemplateFormatVersion.2010-09-09 = heat.engine.cfn.template:CfnTemplate
# These are for backwards compat with Icehouse notification_driver configuration values # These are for backwards compat with Icehouse notification_driver configuration values