Move built-in functions to separate modules

partial-blueprint function-plugins

Change-Id: I6ee08f962935c07d292ab4f3f298fbb5e5176e8f
This commit is contained in:
Zane Bitter 2014-02-19 00:01:55 -05:00
parent 7ab425ca79
commit a23fe66753
6 changed files with 799 additions and 743 deletions

View File

View File

@ -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" : "<region>" }
'''
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" : "<param_name>" }
'''
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" : "<resource_name>" }
'''
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" : "<param_name>" }
or::
{ "Ref" : "<resource_name>" }
'''
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" : [ "<resource_name>",
"<attribute_name" ] }
'''
def __init__(self, stack, fn_name, args):
super(GetAtt, self).__init__(stack, fn_name, args)
self._resource_name, self._attribute = self._parse_args()
def _parse_args(self):
try:
resource_name, attribute = self.args
except ValueError:
raise ValueError(_('Arguments to "%s" must be of the form '
'[resource_name, attribute]') % self.fn_name)
return resource_name, attribute
def _resource(self):
resource_name = function.resolve(self._resource_name)
try:
return self.stack[resource_name]
except KeyError:
raise exception.InvalidTemplateAttribute(
resource=resource_name,
key=function.resolve(self._attribute))
def result(self):
attribute = function.resolve(self._attribute)
r = self._resource()
if (r.status in (r.IN_PROGRESS, r.COMPLETE) and
r.action in (r.CREATE, r.RESUME, r.UPDATE)):
return r.FnGetAtt(attribute)
else:
return None
class Select(function.Function):
'''
A function for selecting an item from a list or map.
Takes the form (for a list lookup)::
{ "Fn::Select" : [ "<index>", [ "<value_1>", "<value_2>", ... ] ] }
Takes the form (for a map lookup)::
{ "Fn::Select" : [ "<index>", { "<key_1>": "<value_1>", ... } ] }
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" : [ "<delim>", [ "<string_1>", "<string_2>", ... ] }
And resolves to::
"<string_1><delim><string_2><delim>..."
'''
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" : [ "<delim>", "<string_1><delim><string_2>..." ] }
And resolves to::
[ "<string_1>", "<string_2>", ... ]
'''
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" : [
{ "<key_1>": "<value_1>", "<key_2>": "<value_2>", ... },
"<key_1> <key_2>"
] }
And resolves to::
"<value_1> <value_2>"
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" : "<string>" }
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=<key_0>',
'.member.0.Value=<value_0>',
... ] ] }
And resolves to::
{ "<key_0>" : "<value_0>", ... }
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": "<attribute_type>" }
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 {}

View File

@ -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: <param_name>
or::
get_param:
- <param_name>
- <path1>
- ...
'''
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:
- <resource_name>
- <attribute_name>
- <path1>
- ...
'''
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: <key_1> <key_2>
params:
<key_1>: <value_1>
<key_2>: <value_2>
...
And resolves to::
"<value_1> <value_2>"
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: <file_key>
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):

View File

@ -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: <param_name>
or::
get_param:
- <param_name>
- <path1>
- ...
'''
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:
- <resource_name>
- <attribute_name>
- <path1>
- ...
'''
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: <key_1> <key_2>
params:
<key_1>: <value_1>
<key_2>: <value_2>
...
And resolves to::
"<value_1> <value_2>"
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: <file_key>
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 {}

View File

@ -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" : "<region>" }
'''
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" : "<param_name>" }
'''
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" : "<resource_name>" }
'''
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" : "<param_name>" }
or::
{ "Ref" : "<resource_name>" }
'''
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" : [ "<resource_name>",
"<attribute_name" ] }
'''
def __init__(self, stack, fn_name, args):
super(GetAtt, self).__init__(stack, fn_name, args)
self._resource_name, self._attribute = self._parse_args()
def _parse_args(self):
try:
resource_name, attribute = self.args
except ValueError:
raise ValueError(_('Arguments to "%s" must be of the form '
'[resource_name, attribute]') % self.fn_name)
return resource_name, attribute
def _resource(self):
resource_name = function.resolve(self._resource_name)
try:
return self.stack[resource_name]
except KeyError:
raise exception.InvalidTemplateAttribute(
resource=resource_name,
key=function.resolve(self._attribute))
def result(self):
attribute = function.resolve(self._attribute)
r = self._resource()
if (r.status in (r.IN_PROGRESS, r.COMPLETE) and
r.action in (r.CREATE, r.RESUME, r.UPDATE)):
return r.FnGetAtt(attribute)
else:
return None
class Select(function.Function):
'''
A function for selecting an item from a list or map.
Takes the form (for a list lookup)::
{ "Fn::Select" : [ "<index>", [ "<value_1>", "<value_2>", ... ] ] }
Takes the form (for a map lookup)::
{ "Fn::Select" : [ "<index>", { "<key_1>": "<value_1>", ... } ] }
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" : [ "<delim>", [ "<string_1>", "<string_2>", ... ] }
And resolves to::
"<string_1><delim><string_2><delim>..."
'''
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" : [ "<delim>", "<string_1><delim><string_2>..." ] }
And resolves to::
[ "<string_1>", "<string_2>", ... ]
'''
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" : [
{ "<key_1>": "<value_1>", "<key_2>": "<value_2>", ... },
"<key_1> <key_2>"
] }
And resolves to::
"<value_1> <value_2>"
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" : "<string>" }
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=<key_0>',
'.member.0.Value=<value_0>',
... ] ] }
And resolves to::
{ "<key_0>" : "<value_0>", ... }
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": "<attribute_type>" }
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)

View File

@ -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)