From a23fe66753fd657be00bedced8e906e1d3749c38 Mon Sep 17 00:00:00 2001 From: Zane Bitter Date: Wed, 19 Feb 2014 00:01:55 -0500 Subject: [PATCH] Move built-in functions to separate modules partial-blueprint function-plugins Change-Id: I6ee08f962935c07d292ab4f3f298fbb5e5176e8f --- heat/engine/cfn/__init__.py | 0 heat/engine/cfn/functions.py | 567 +++++++++++++++++++++++++++++++++++ heat/engine/hot/__init__.py | 201 +------------ heat/engine/hot/functions.py | 225 ++++++++++++++ heat/engine/template.py | 545 +-------------------------------- heat/tests/test_parser.py | 4 +- 6 files changed, 799 insertions(+), 743 deletions(-) create mode 100644 heat/engine/cfn/__init__.py create mode 100644 heat/engine/cfn/functions.py create mode 100644 heat/engine/hot/functions.py diff --git a/heat/engine/cfn/__init__.py b/heat/engine/cfn/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/heat/engine/cfn/functions.py b/heat/engine/cfn/functions.py new file mode 100644 index 000000000..871e26a09 --- /dev/null +++ b/heat/engine/cfn/functions.py @@ -0,0 +1,567 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import collections +import json + +from heat.api.aws import utils as aws_utils +from heat.common import exception + +from heat.engine import function + + +class FindInMap(function.Function): + ''' + A function for resolving keys in the template mappings. + + Takes the form:: + + { "Fn::FindInMap" : [ "mapping", + "key", + "value" ] } + ''' + + def __init__(self, stack, fn_name, args): + super(FindInMap, self).__init__(stack, fn_name, args) + + try: + self._mapname, self._mapkey, self._mapvalue = self.args + except ValueError as ex: + raise KeyError(str(ex)) + + def result(self): + mapping = self.stack.t.maps[function.resolve(self._mapname)] + key = function.resolve(self._mapkey) + value = function.resolve(self._mapvalue) + return mapping[key][value] + + +class GetAZs(function.Function): + ''' + A function for retrieving the availability zones. + + Takes the form:: + + { "Fn::GetAZs" : "" } + ''' + + def result(self): + # TODO(therve): Implement region scoping + #region = function.resolve(self.args) + + if self.stack is None: + return ['nova'] + else: + return self.stack.get_availability_zones() + + +class ParamRef(function.Function): + ''' + A function for resolving parameter references. + + Takes the form:: + + { "Ref" : "" } + ''' + + def result(self): + param_name = function.resolve(self.args) + + try: + return self.stack.parameters[param_name] + except (KeyError, ValueError): + raise exception.UserParameterMissing(key=param_name) + + +class ResourceRef(function.Function): + ''' + A function for resolving resource references. + + Takes the form:: + + { "Ref" : "" } + ''' + + def _resource(self): + resource_name = function.resolve(self.args) + + return self.stack[resource_name] + + def result(self): + return self._resource().FnGetRefId() + + +def Ref(stack, fn_name, args): + ''' + A function for resolving parameters or resource references. + + Takes the form:: + + { "Ref" : "" } + + or:: + + { "Ref" : "" } + ''' + if args in stack.t[stack.t.RESOURCES]: + RefClass = ResourceRef + else: + RefClass = ParamRef + return RefClass(stack, fn_name, args) + + +class GetAtt(function.Function): + ''' + A function for resolving resource attributes. + + Takes the form:: + + { "Fn::GetAtt" : [ "", + "", [ "", "", ... ] ] } + + Takes the form (for a map lookup):: + + { "Fn::Select" : [ "", { "": "", ... } ] } + + If the selected index is not found, this function resolves to an empty + string. + ''' + + def __init__(self, stack, fn_name, args): + super(Select, self).__init__(stack, fn_name, args) + + try: + self._lookup, self._strings = self.args + except ValueError: + raise ValueError(_('Arguments to "%s" must be of the form ' + '[index, collection]') % self.fn_name) + + def result(self): + index = function.resolve(self._lookup) + + try: + index = int(index) + except (ValueError, TypeError): + pass + + strings = function.resolve(self._strings) + + if strings == '': + # an empty string is a common response from other + # functions when result is not currently available. + # Handle by returning an empty string + return '' + + if isinstance(strings, basestring): + # might be serialized json. + try: + strings = json.loads(strings) + except ValueError as json_ex: + fmt_data = {'fn_name': self.fn_name, + 'err': json_ex} + raise ValueError(_('"%(fn_name)s": %(err)s') % fmt_data) + + if isinstance(strings, collections.Mapping): + if not isinstance(index, basestring): + raise TypeError(_('Index to "%s" must be a string') % + self.fn_name) + return strings.get(index, '') + + if (isinstance(strings, collections.Sequence) and + not isinstance(strings, basestring)): + if not isinstance(index, (int, long)): + raise TypeError(_('Index to "%s" must be an integer') % + self.fn_name) + + try: + return strings[index] + except IndexError: + return '' + + if strings is None: + return '' + + raise TypeError(_('Arguments to %s not fully resolved') % + self.fn_name) + + +class Join(function.Function): + ''' + A function for joining strings. + + Takes the form:: + + { "Fn::Join" : [ "", [ "", "", ... ] } + + And resolves to:: + + "..." + ''' + + def __init__(self, stack, fn_name, args): + super(Join, self).__init__(stack, fn_name, args) + + example = '"%s" : [ " ", [ "str1", "str2"]]' % self.fn_name + fmt_data = {'fn_name': self.fn_name, + 'example': example} + + if isinstance(self.args, (basestring, collections.Mapping)): + raise TypeError(_('Incorrect arguments to "%(fn_name)s" ' + 'should be: %(example)s') % fmt_data) + + try: + self._delim, self._strings = self.args + except ValueError: + raise ValueError(_('Incorrect arguments to "%(fn_name)s" ' + 'should be: %(example)s') % fmt_data) + + def result(self): + strings = function.resolve(self._strings) + if (isinstance(strings, basestring) or + not isinstance(strings, collections.Sequence)): + raise TypeError(_('"%s" must operate on a list') % self.fn_name) + + delim = function.resolve(self._delim) + if not isinstance(delim, basestring): + raise TypeError(_('"%s" delimiter must be a string') % + self.fn_name) + + def ensure_string(s): + if s is None: + return '' + if not isinstance(s, basestring): + raise TypeError(_('Items to join must be strings')) + return s + + return delim.join(ensure_string(s) for s in strings) + + +class Split(function.Function): + ''' + A function for splitting strings. + + Takes the form:: + + { "Fn::Split" : [ "", "..." ] } + + And resolves to:: + + [ "", "", ... ] + ''' + + def __init__(self, stack, fn_name, args): + super(Split, self).__init__(stack, fn_name, args) + + example = '"%s" : [ ",", "str1,str2"]]' % self.fn_name + fmt_data = {'fn_name': self.fn_name, + 'example': example} + + if isinstance(self.args, (basestring, collections.Mapping)): + raise TypeError(_('Incorrect arguments to "%(fn_name)s" ' + 'should be: %(example)s') % fmt_data) + + try: + self._delim, self._strings = self.args + except ValueError: + raise ValueError(_('Incorrect arguments to "%(fn_name)s" ' + 'should be: %(example)s') % fmt_data) + + def result(self): + strings = function.resolve(self._strings) + + if not isinstance(self._delim, basestring): + raise TypeError(_("Delimiter for %s must be string") % + self.fn_name) + if not isinstance(strings, basestring): + raise TypeError(_("String to split must be string; got %s") % + type(strings)) + + return strings.split(self._delim) + + +class Replace(function.Function): + ''' + A function for performing string subsitutions. + + Takes the form:: + + { "Fn::Replace" : [ + { "": "", "": "", ... }, + " " + ] } + + And resolves to:: + + " " + + This is implemented using python str.replace on each key. The order in + which replacements are performed is undefined. + ''' + + def __init__(self, stack, fn_name, args): + super(Replace, self).__init__(stack, fn_name, args) + + self._mapping, self._string = self._parse_args() + + if not isinstance(self._mapping, collections.Mapping): + raise TypeError(_('"%s" parameters must be a mapping') % + self.fn_name) + + def _parse_args(self): + + example = ('{"%s": ' + '[ {"$var1": "foo", "%%var2%%": "bar"}, ' + '"$var1 is %%var2%%"]}' % self.fn_name) + fmt_data = {'fn_name': self.fn_name, + 'example': example} + + if isinstance(self.args, (basestring, collections.Mapping)): + raise TypeError(_('Incorrect arguments to "%(fn_name)s" ' + 'should be: %(example)s') % fmt_data) + + try: + mapping, string = self.args + except ValueError: + raise ValueError(_('Incorrect arguments to "%(fn_name)s" ' + 'should be: %(example)s') % fmt_data) + else: + return mapping, string + + def result(self): + template = function.resolve(self._string) + mapping = function.resolve(self._mapping) + + if not isinstance(template, basestring): + raise TypeError(_('"%s" template must be a string') % self.fn_name) + + if not isinstance(mapping, collections.Mapping): + raise TypeError(_('"%s" params must be a map') % self.fn_name) + + def replace(string, change): + placeholder, value = change + + if not isinstance(placeholder, basestring): + raise TypeError(_('"%s" param placeholders must be strings') % + self.fn_name) + + if value is None: + value = '' + + if not isinstance(value, (basestring, int, long, float, bool)): + raise TypeError(_('"%s" params must be strings or numbers') % + self.fn_name) + + return string.replace(placeholder, unicode(value)) + + return reduce(replace, mapping.iteritems(), template) + + +class Base64(function.Function): + ''' + A placeholder function for converting to base64. + + Takes the form:: + + { "Fn::Base64" : "" } + + This function actually performs no conversion. It is included for the + benefit of templates that convert UserData to Base64. Heat accepts UserData + in plain text. + ''' + + def result(self): + resolved = function.resolve(self.args) + if not isinstance(resolved, basestring): + raise TypeError(_('"%s" argument must be a string') % self.fn_name) + return resolved + + +class MemberListToMap(function.Function): + ''' + A function for converting lists containing enumerated keys and values to + a mapping. + + Takes the form:: + + { 'Fn::MemberListToMap' : [ 'Name', + 'Value', + [ '.member.0.Name=', + '.member.0.Value=', + ... ] ] } + + And resolves to:: + + { "" : "", ... } + + The first two arguments are the names of the key and value. + ''' + + def __init__(self, stack, fn_name, args): + super(MemberListToMap, self).__init__(stack, fn_name, args) + + try: + self._keyname, self._valuename, self._list = self.args + except ValueError: + correct = ''' + {'Fn::MemberListToMap': ['Name', 'Value', + ['.member.0.Name=key', + '.member.0.Value=door']]} + ''' + raise TypeError(_('Wrong Arguments try: "%s"') % correct) + + if not isinstance(self._keyname, basestring): + raise TypeError(_('%s Key Name must be a string') % self.fn_name) + + if not isinstance(self._valuename, basestring): + raise TypeError(_('%s Value Name must be a string') % self.fn_name) + + def result(self): + member_list = function.resolve(self._list) + + if not isinstance(member_list, collections.Iterable): + raise TypeError(_('Member list must be a list')) + + def item(s): + if not isinstance(s, basestring): + raise TypeError(_("Member list items must be strings")) + return s.split('=', 1) + + partials = dict(item(s) for s in member_list) + return aws_utils.extract_param_pairs(partials, + prefix='', + keyname=self._keyname, + valuename=self._valuename) + + +class ResourceFacade(function.Function): + ''' + A function for obtaining data from the facade resource from within the + corresponding provider template. + + Takes the form:: + + { "Fn::ResourceFacade": "" } + + where the valid attribute types are "Metadata", "DeletionPolicy" and + "UpdatePolicy". + ''' + + _RESOURCE_ATTRIBUTES = ( + METADATA, DELETION_POLICY, UPDATE_POLICY, + ) = ( + 'Metadata', 'DeletionPolicy', 'UpdatePolicy' + ) + + def __init__(self, stack, fn_name, args): + super(ResourceFacade, self).__init__(stack, fn_name, args) + + if self.args not in self._RESOURCE_ATTRIBUTES: + fmt_data = {'fn_name': self.fn_name, + 'allowed': ', '.join(self._RESOURCE_ATTRIBUTES)} + raise ValueError(_('Incorrect arguments to "%(fn_name)s" ' + 'should be one of: %(allowed)s') % fmt_data) + + def result(self): + attr = function.resolve(self.args) + + if attr == self.METADATA: + return self.stack.parent_resource.metadata + elif attr == self.UPDATE_POLICY: + return self.stack.parent_resource.t.get(attr, {}) + elif attr == self.DELETION_POLICY: + try: + return self.stack.parent_resource.t[attr] + except KeyError: + # TODO(zaneb): This should have a default! + fmt_data = {'fn_name': self.fn_name, + 'key': attr} + raise KeyError(_('"%(fn_name)s" ' + 'key "%(key)s" not found') % fmt_data) + + +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 {} diff --git a/heat/engine/hot/__init__.py b/heat/engine/hot/__init__.py index 3ea1dcf47..33e1b9d17 100644 --- a/heat/engine/hot/__init__.py +++ b/heat/engine/hot/__init__.py @@ -12,10 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. -import collections - from heat.common import exception -from heat.engine import function from heat.engine import template from heat.engine import parameters from heat.engine import constraints as constr @@ -154,202 +151,8 @@ class HOTemplate(template.Template): validate_value=validate_value, context=context) def functions(self): - return { - 'Fn::FindInMap': template.FindInMap, - 'Fn::GetAZs': template.GetAZs, - 'get_param': GetParam, - 'get_resource': template.ResourceRef, - 'Ref': template.Ref, - 'get_attr': GetAtt, - 'Fn::Select': template.Select, - 'Fn::Join': template.Join, - 'Fn::Split': template.Split, - 'str_replace': Replace, - 'Fn::Replace': template.Replace, - 'Fn::Base64': template.Base64, - 'Fn::MemberListToMap': template.MemberListToMap, - 'Fn::ResourceFacade': template.ResourceFacade, - 'get_file': GetFile, - } - - -class GetParam(function.Function): - ''' - A function for resolving parameter references. - - Takes the form:: - - get_param: - - or:: - - get_param: - - - - - - ... - ''' - - def result(self): - args = function.resolve(self.args) - - if not args: - raise ValueError(_('Function "%s" must have arguments') % - self.fn_name) - - if isinstance(args, basestring): - param_name = args - path_components = [] - elif isinstance(args, collections.Sequence): - param_name = args[0] - path_components = args[1:] - else: - raise TypeError(_('Argument to "%s" must be string or list') % - self.fn_name) - - if not isinstance(param_name, basestring): - raise TypeError(_('Parameter name in "%s" must be string') % - self.fn_name) - - try: - parameter = self.stack.parameters[param_name] - except KeyError: - raise exception.UserParameterMissing(key=param_name) - - def get_path_component(collection, key): - if not isinstance(collection, (collections.Mapping, - collections.Sequence)): - raise TypeError(_('"%s" can\'t traverse path') % self.fn_name) - - if not isinstance(key, (basestring, int)): - raise TypeError(_('Path components in "%s" ' - 'must be strings') % self.fn_name) - - return collection[key] - - try: - return reduce(get_path_component, path_components, parameter) - except (KeyError, IndexError, TypeError): - return '' - - -class GetAtt(template.GetAtt): - ''' - A function for resolving resource attributes. - - Takes the form:: - - get_attr: - - - - - - - - ... - ''' - - def _parse_args(self): - if (not isinstance(self.args, collections.Sequence) or - isinstance(self.args, basestring)): - raise TypeError(_('Argument to "%s" must be a list') % - self.fn_name) - - if len(self.args) < 2: - raise ValueError(_('Arguments to "%s" must be of the form ' - '[resource_name, attribute, (path), ...]') % - self.fn_name) - - self._path_components = self.args[2:] - - return tuple(self.args[:2]) - - def result(self): - attribute = super(GetAtt, self).result() - if attribute is None: - return '' - - path_components = function.resolve(self._path_components) - - def get_path_component(collection, key): - if not isinstance(collection, (collections.Mapping, - collections.Sequence)): - raise TypeError(_('"%s" can\'t traverse path') % self.fn_name) - - if not isinstance(key, (basestring, int)): - raise TypeError(_('Path components in "%s" ' - 'must be strings') % self.fn_name) - - return collection[key] - - try: - return reduce(get_path_component, path_components, attribute) - except (KeyError, IndexError, TypeError): - return '' - - -class Replace(template.Replace): - ''' - A function for performing string substitutions. - - Takes the form:: - - str_replace: - template: - params: - : - : - ... - - And resolves to:: - - " " - - This is implemented using Python's str.replace on each key. The order in - which replacements are performed is undefined. - ''' - - def _parse_args(self): - if not isinstance(self.args, collections.Mapping): - raise TypeError(_('Arguments to "%s" must be a map') % - self.fn_name) - - try: - mapping = self.args['params'] - string = self.args['template'] - except (KeyError, TypeError): - example = ('''str_replace: - template: This is var1 template var2 - params: - var1: a - var2: string''') - raise KeyError(_('"str_replace" syntax should be %s') % - example) - else: - return mapping, string - - -class GetFile(function.Function): - """ - A function for including a file inline. - - Takes the form:: - - get_file: - - And resolves to the content stored in the files dictionary under the given - key. - """ - - def result(self): - args = function.resolve(self.args) - if not (isinstance(args, basestring)): - raise TypeError(_('Argument to "%s" must be a string') % - self.fn_name) - - f = self.stack.t.files.get(args) - if f is None: - fmt_data = {'fn_name': self.fn_name, - 'file_key': args} - raise ValueError(_('No content found in the "files" section for ' - '%(fn_name)s path: %(file_key)s') % fmt_data) - return f + from heat.engine.hot import functions + return functions.function_mapping(*self.version()) class HOTParamSchema(parameters.Schema): diff --git a/heat/engine/hot/functions.py b/heat/engine/hot/functions.py new file mode 100644 index 000000000..731a796e1 --- /dev/null +++ b/heat/engine/hot/functions.py @@ -0,0 +1,225 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import collections + +from heat.common import exception + +from heat.engine.cfn import functions as cfn_funcs +from heat.engine import function + + +class GetParam(function.Function): + ''' + A function for resolving parameter references. + + Takes the form:: + + get_param: + + or:: + + get_param: + - + - + - ... + ''' + + def result(self): + args = function.resolve(self.args) + + if not args: + raise ValueError(_('Function "%s" must have arguments') % + self.fn_name) + + if isinstance(args, basestring): + param_name = args + path_components = [] + elif isinstance(args, collections.Sequence): + param_name = args[0] + path_components = args[1:] + else: + raise TypeError(_('Argument to "%s" must be string or list') % + self.fn_name) + + if not isinstance(param_name, basestring): + raise TypeError(_('Parameter name in "%s" must be string') % + self.fn_name) + + try: + parameter = self.stack.parameters[param_name] + except KeyError: + raise exception.UserParameterMissing(key=param_name) + + def get_path_component(collection, key): + if not isinstance(collection, (collections.Mapping, + collections.Sequence)): + raise TypeError(_('"%s" can\'t traverse path') % self.fn_name) + + if not isinstance(key, (basestring, int)): + raise TypeError(_('Path components in "%s" ' + 'must be strings') % self.fn_name) + + return collection[key] + + try: + return reduce(get_path_component, path_components, parameter) + except (KeyError, IndexError, TypeError): + return '' + + +class GetAtt(cfn_funcs.GetAtt): + ''' + A function for resolving resource attributes. + + Takes the form:: + + get_attr: + - + - + - + - ... + ''' + + def _parse_args(self): + if (not isinstance(self.args, collections.Sequence) or + isinstance(self.args, basestring)): + raise TypeError(_('Argument to "%s" must be a list') % + self.fn_name) + + if len(self.args) < 2: + raise ValueError(_('Arguments to "%s" must be of the form ' + '[resource_name, attribute, (path), ...]') % + self.fn_name) + + self._path_components = self.args[2:] + + return tuple(self.args[:2]) + + def result(self): + attribute = super(GetAtt, self).result() + if attribute is None: + return '' + + path_components = function.resolve(self._path_components) + + def get_path_component(collection, key): + if not isinstance(collection, (collections.Mapping, + collections.Sequence)): + raise TypeError(_('"%s" can\'t traverse path') % self.fn_name) + + if not isinstance(key, (basestring, int)): + raise TypeError(_('Path components in "%s" ' + 'must be strings') % self.fn_name) + + return collection[key] + + try: + return reduce(get_path_component, path_components, attribute) + except (KeyError, IndexError, TypeError): + return '' + + +class Replace(cfn_funcs.Replace): + ''' + A function for performing string substitutions. + + Takes the form:: + + str_replace: + template: + params: + : + : + ... + + And resolves to:: + + " " + + This is implemented using Python's str.replace on each key. The order in + which replacements are performed is undefined. + ''' + + def _parse_args(self): + if not isinstance(self.args, collections.Mapping): + raise TypeError(_('Arguments to "%s" must be a map') % + self.fn_name) + + try: + mapping = self.args['params'] + string = self.args['template'] + except (KeyError, TypeError): + example = ('''str_replace: + template: This is var1 template var2 + params: + var1: a + var2: string''') + raise KeyError(_('"str_replace" syntax should be %s') % + example) + else: + return mapping, string + + +class GetFile(function.Function): + """ + A function for including a file inline. + + Takes the form:: + + get_file: + + And resolves to the content stored in the files dictionary under the given + key. + """ + + def result(self): + args = function.resolve(self.args) + if not (isinstance(args, basestring)): + raise TypeError(_('Argument to "%s" must be a string') % + self.fn_name) + + f = self.stack.t.files.get(args) + if f is None: + fmt_data = {'fn_name': self.fn_name, + 'file_key': args} + raise ValueError(_('No content found in the "files" section for ' + '%(fn_name)s path: %(file_key)s') % fmt_data) + return f + + +def function_mapping(version_key, version): + if version_key != 'heat_template_version': + return {} + + if version == '2013-05-23': + return { + 'Fn::FindInMap': cfn_funcs.FindInMap, + 'Fn::GetAZs': cfn_funcs.GetAZs, + 'get_param': GetParam, + 'get_resource': cfn_funcs.ResourceRef, + 'Ref': cfn_funcs.Ref, + 'get_attr': GetAtt, + '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, + 'Fn::ResourceFacade': cfn_funcs.ResourceFacade, + 'get_file': GetFile, + } + + return {} diff --git a/heat/engine/template.py b/heat/engine/template.py index d8e91e81d..a6bb2409d 100644 --- a/heat/engine/template.py +++ b/heat/engine/template.py @@ -15,13 +15,10 @@ import collections import functools -import json -from heat.api.aws import utils as aws_utils from heat.db import api as db_api -from heat.common import exception from heat.engine import parameters -from heat.engine import function +from heat.engine.cfn import functions class Template(collections.Mapping): @@ -118,32 +115,7 @@ class Template(collections.Mapping): context=context) def functions(self): - version_key, version = self.version() - - if version_key == self.VERSION: - return { - 'Fn::FindInMap': FindInMap, - 'Fn::GetAZs': GetAZs, - 'Ref': Ref, - 'Fn::GetAtt': GetAtt, - 'Fn::Select': Select, - 'Fn::Join': Join, - 'Fn::Base64': Base64, - } - - 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 functions.function_mapping(*self.version()) def parse(self, stack, snippet): parse = functools.partial(self.parse, stack) @@ -160,516 +132,3 @@ class Template(collections.Mapping): return [parse(v) for v in snippet] else: return snippet - - -class FindInMap(function.Function): - ''' - A function for resolving keys in the template mappings. - - Takes the form:: - - { "Fn::FindInMap" : [ "mapping", - "key", - "value" ] } - ''' - - def __init__(self, stack, fn_name, args): - super(FindInMap, self).__init__(stack, fn_name, args) - - try: - self._mapname, self._mapkey, self._mapvalue = self.args - except ValueError as ex: - raise KeyError(str(ex)) - - def result(self): - mapping = self.stack.t.maps[function.resolve(self._mapname)] - key = function.resolve(self._mapkey) - value = function.resolve(self._mapvalue) - return mapping[key][value] - - -class GetAZs(function.Function): - ''' - A function for retrieving the availability zones. - - Takes the form:: - - { "Fn::GetAZs" : "" } - ''' - - def result(self): - # TODO(therve): Implement region scoping - #region = function.resolve(self.args) - - if self.stack is None: - return ['nova'] - else: - return self.stack.get_availability_zones() - - -class ParamRef(function.Function): - ''' - A function for resolving parameter references. - - Takes the form:: - - { "Ref" : "" } - ''' - - def result(self): - param_name = function.resolve(self.args) - - try: - return self.stack.parameters[param_name] - except (KeyError, ValueError): - raise exception.UserParameterMissing(key=param_name) - - -class ResourceRef(function.Function): - ''' - A function for resolving resource references. - - Takes the form:: - - { "Ref" : "" } - ''' - - def _resource(self): - resource_name = function.resolve(self.args) - - return self.stack[resource_name] - - def result(self): - return self._resource().FnGetRefId() - - -def Ref(stack, fn_name, args): - ''' - A function for resolving parameters or resource references. - - Takes the form:: - - { "Ref" : "" } - - or:: - - { "Ref" : "" } - ''' - if args in stack.t[stack.t.RESOURCES]: - RefClass = ResourceRef - else: - RefClass = ParamRef - return RefClass(stack, fn_name, args) - - -class GetAtt(function.Function): - ''' - A function for resolving resource attributes. - - Takes the form:: - - { "Fn::GetAtt" : [ "", - "", [ "", "", ... ] ] } - - Takes the form (for a map lookup):: - - { "Fn::Select" : [ "", { "": "", ... } ] } - - If the selected index is not found, this function resolves to an empty - string. - ''' - - def __init__(self, stack, fn_name, args): - super(Select, self).__init__(stack, fn_name, args) - - try: - self._lookup, self._strings = self.args - except ValueError: - raise ValueError(_('Arguments to "%s" must be of the form ' - '[index, collection]') % self.fn_name) - - def result(self): - index = function.resolve(self._lookup) - - try: - index = int(index) - except (ValueError, TypeError): - pass - - strings = function.resolve(self._strings) - - if strings == '': - # an empty string is a common response from other - # functions when result is not currently available. - # Handle by returning an empty string - return '' - - if isinstance(strings, basestring): - # might be serialized json. - try: - strings = json.loads(strings) - except ValueError as json_ex: - fmt_data = {'fn_name': self.fn_name, - 'err': json_ex} - raise ValueError(_('"%(fn_name)s": %(err)s') % fmt_data) - - if isinstance(strings, collections.Mapping): - if not isinstance(index, basestring): - raise TypeError(_('Index to "%s" must be a string') % - self.fn_name) - return strings.get(index, '') - - if (isinstance(strings, collections.Sequence) and - not isinstance(strings, basestring)): - if not isinstance(index, (int, long)): - raise TypeError(_('Index to "%s" must be an integer') % - self.fn_name) - - try: - return strings[index] - except IndexError: - return '' - - if strings is None: - return '' - - raise TypeError(_('Arguments to %s not fully resolved') % - self.fn_name) - - -class Join(function.Function): - ''' - A function for joining strings. - - Takes the form:: - - { "Fn::Join" : [ "", [ "", "", ... ] } - - And resolves to:: - - "..." - ''' - - def __init__(self, stack, fn_name, args): - super(Join, self).__init__(stack, fn_name, args) - - example = '"%s" : [ " ", [ "str1", "str2"]]' % self.fn_name - fmt_data = {'fn_name': self.fn_name, - 'example': example} - - if isinstance(self.args, (basestring, collections.Mapping)): - raise TypeError(_('Incorrect arguments to "%(fn_name)s" ' - 'should be: %(example)s') % fmt_data) - - try: - self._delim, self._strings = self.args - except ValueError: - raise ValueError(_('Incorrect arguments to "%(fn_name)s" ' - 'should be: %(example)s') % fmt_data) - - def result(self): - strings = function.resolve(self._strings) - if (isinstance(strings, basestring) or - not isinstance(strings, collections.Sequence)): - raise TypeError(_('"%s" must operate on a list') % self.fn_name) - - delim = function.resolve(self._delim) - if not isinstance(delim, basestring): - raise TypeError(_('"%s" delimiter must be a string') % - self.fn_name) - - def ensure_string(s): - if s is None: - return '' - if not isinstance(s, basestring): - raise TypeError(_('Items to join must be strings')) - return s - - return delim.join(ensure_string(s) for s in strings) - - -class Split(function.Function): - ''' - A function for splitting strings. - - Takes the form:: - - { "Fn::Split" : [ "", "..." ] } - - And resolves to:: - - [ "", "", ... ] - ''' - - def __init__(self, stack, fn_name, args): - super(Split, self).__init__(stack, fn_name, args) - - example = '"%s" : [ ",", "str1,str2"]]' % self.fn_name - fmt_data = {'fn_name': self.fn_name, - 'example': example} - - if isinstance(self.args, (basestring, collections.Mapping)): - raise TypeError(_('Incorrect arguments to "%(fn_name)s" ' - 'should be: %(example)s') % fmt_data) - - try: - self._delim, self._strings = self.args - except ValueError: - raise ValueError(_('Incorrect arguments to "%(fn_name)s" ' - 'should be: %(example)s') % fmt_data) - - def result(self): - strings = function.resolve(self._strings) - - if not isinstance(self._delim, basestring): - raise TypeError(_("Delimiter for %s must be string") % - self.fn_name) - if not isinstance(strings, basestring): - raise TypeError(_("String to split must be string; got %s") % - type(strings)) - - return strings.split(self._delim) - - -class Replace(function.Function): - ''' - A function for performing string subsitutions. - - Takes the form:: - - { "Fn::Replace" : [ - { "": "", "": "", ... }, - " " - ] } - - And resolves to:: - - " " - - This is implemented using python str.replace on each key. The order in - which replacements are performed is undefined. - ''' - - def __init__(self, stack, fn_name, args): - super(Replace, self).__init__(stack, fn_name, args) - - self._mapping, self._string = self._parse_args() - - if not isinstance(self._mapping, collections.Mapping): - raise TypeError(_('"%s" parameters must be a mapping') % - self.fn_name) - - def _parse_args(self): - - example = ('{"%s": ' - '[ {"$var1": "foo", "%%var2%%": "bar"}, ' - '"$var1 is %%var2%%"]}' % self.fn_name) - fmt_data = {'fn_name': self.fn_name, - 'example': example} - - if isinstance(self.args, (basestring, collections.Mapping)): - raise TypeError(_('Incorrect arguments to "%(fn_name)s" ' - 'should be: %(example)s') % fmt_data) - - try: - mapping, string = self.args - except ValueError: - raise ValueError(_('Incorrect arguments to "%(fn_name)s" ' - 'should be: %(example)s') % fmt_data) - else: - return mapping, string - - def result(self): - template = function.resolve(self._string) - mapping = function.resolve(self._mapping) - - if not isinstance(template, basestring): - raise TypeError(_('"%s" template must be a string') % self.fn_name) - - if not isinstance(mapping, collections.Mapping): - raise TypeError(_('"%s" params must be a map') % self.fn_name) - - def replace(string, change): - placeholder, value = change - - if not isinstance(placeholder, basestring): - raise TypeError(_('"%s" param placeholders must be strings') % - self.fn_name) - - if value is None: - value = '' - - if not isinstance(value, (basestring, int, long, float, bool)): - raise TypeError(_('"%s" params must be strings or numbers') % - self.fn_name) - - return string.replace(placeholder, unicode(value)) - - return reduce(replace, mapping.iteritems(), template) - - -class Base64(function.Function): - ''' - A placeholder function for converting to base64. - - Takes the form:: - - { "Fn::Base64" : "" } - - This function actually performs no conversion. It is included for the - benefit of templates that convert UserData to Base64. Heat accepts UserData - in plain text. - ''' - - def result(self): - resolved = function.resolve(self.args) - if not isinstance(resolved, basestring): - raise TypeError(_('"%s" argument must be a string') % self.fn_name) - return resolved - - -class MemberListToMap(function.Function): - ''' - A function for converting lists containing enumerated keys and values to - a mapping. - - Takes the form:: - - { 'Fn::MemberListToMap' : [ 'Name', - 'Value', - [ '.member.0.Name=', - '.member.0.Value=', - ... ] ] } - - And resolves to:: - - { "" : "", ... } - - The first two arguments are the names of the key and value. - ''' - - def __init__(self, stack, fn_name, args): - super(MemberListToMap, self).__init__(stack, fn_name, args) - - try: - self._keyname, self._valuename, self._list = self.args - except ValueError: - correct = ''' - {'Fn::MemberListToMap': ['Name', 'Value', - ['.member.0.Name=key', - '.member.0.Value=door']]} - ''' - raise TypeError(_('Wrong Arguments try: "%s"') % correct) - - if not isinstance(self._keyname, basestring): - raise TypeError(_('%s Key Name must be a string') % self.fn_name) - - if not isinstance(self._valuename, basestring): - raise TypeError(_('%s Value Name must be a string') % self.fn_name) - - def result(self): - member_list = function.resolve(self._list) - - if not isinstance(member_list, collections.Iterable): - raise TypeError(_('Member list must be a list')) - - def item(s): - if not isinstance(s, basestring): - raise TypeError(_("Member list items must be strings")) - return s.split('=', 1) - - partials = dict(item(s) for s in member_list) - return aws_utils.extract_param_pairs(partials, - prefix='', - keyname=self._keyname, - valuename=self._valuename) - - -class ResourceFacade(function.Function): - ''' - A function for obtaining data from the facade resource from within the - corresponding provider template. - - Takes the form:: - - { "Fn::ResourceFacade": "" } - - where the valid attribute types are "Metadata", "DeletionPolicy" and - "UpdatePolicy". - ''' - - _RESOURCE_ATTRIBUTES = ( - METADATA, DELETION_POLICY, UPDATE_POLICY, - ) = ( - 'Metadata', 'DeletionPolicy', 'UpdatePolicy' - ) - - def __init__(self, stack, fn_name, args): - super(ResourceFacade, self).__init__(stack, fn_name, args) - - if self.args not in self._RESOURCE_ATTRIBUTES: - fmt_data = {'fn_name': self.fn_name, - 'allowed': ', '.join(self._RESOURCE_ATTRIBUTES)} - raise ValueError(_('Incorrect arguments to "%(fn_name)s" ' - 'should be one of: %(allowed)s') % fmt_data) - - def result(self): - attr = function.resolve(self.args) - - if attr == self.METADATA: - return self.stack.parent_resource.metadata - elif attr == self.UPDATE_POLICY: - return self.stack.parent_resource.t.get(attr, {}) - elif attr == self.DELETION_POLICY: - try: - return self.stack.parent_resource.t[attr] - except KeyError: - # TODO(zaneb): This should have a default! - fmt_data = {'fn_name': self.fn_name, - 'key': attr} - raise KeyError(_('"%(fn_name)s" ' - 'key "%(key)s" not found') % fmt_data) diff --git a/heat/tests/test_parser.py b/heat/tests/test_parser.py index 39fe1fe69..9325e16a2 100644 --- a/heat/tests/test_parser.py +++ b/heat/tests/test_parser.py @@ -32,6 +32,8 @@ from heat.engine import parser from heat.engine import scheduler from heat.engine import template +import heat.engine.cfn.functions + from heat.tests.fakes import FakeKeystoneClient from heat.tests.common import HeatTestCase from heat.tests import utils @@ -260,7 +262,7 @@ Mappings: p_snippet = {"Ref": "baz"} parsed = tmpl.parse(stack, p_snippet) - self.assertTrue(isinstance(parsed, template.ParamRef)) + self.assertTrue(isinstance(parsed, heat.engine.cfn.functions.ParamRef)) def test_select_from_list(self): tmpl = parser.Template(empty_template)