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 # License for the specific language governing permissions and limitations
# under the License. # under the License.
import collections
from heat.common import exception from heat.common import exception
from heat.engine import function
from heat.engine import template from heat.engine import template
from heat.engine import parameters from heat.engine import parameters
from heat.engine import constraints as constr from heat.engine import constraints as constr
@ -154,202 +151,8 @@ class HOTemplate(template.Template):
validate_value=validate_value, context=context) validate_value=validate_value, context=context)
def functions(self): def functions(self):
return { from heat.engine.hot import functions
'Fn::FindInMap': template.FindInMap, return functions.function_mapping(*self.version())
'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
class HOTParamSchema(parameters.Schema): 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 collections
import functools import functools
import json
from heat.api.aws import utils as aws_utils
from heat.db import api as db_api from heat.db import api as db_api
from heat.common import exception
from heat.engine import parameters from heat.engine import parameters
from heat.engine import function from heat.engine.cfn import functions
class Template(collections.Mapping): class Template(collections.Mapping):
@ -118,32 +115,7 @@ class Template(collections.Mapping):
context=context) context=context)
def functions(self): def functions(self):
version_key, version = self.version() return functions.function_mapping(*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,
}
def parse(self, stack, snippet): def parse(self, stack, snippet):
parse = functools.partial(self.parse, stack) parse = functools.partial(self.parse, stack)
@ -160,516 +132,3 @@ class Template(collections.Mapping):
return [parse(v) for v in snippet] return [parse(v) for v in snippet]
else: else:
return snippet 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 scheduler
from heat.engine import template from heat.engine import template
import heat.engine.cfn.functions
from heat.tests.fakes import FakeKeystoneClient from heat.tests.fakes import FakeKeystoneClient
from heat.tests.common import HeatTestCase from heat.tests.common import HeatTestCase
from heat.tests import utils from heat.tests import utils
@ -260,7 +262,7 @@ Mappings:
p_snippet = {"Ref": "baz"} p_snippet = {"Ref": "baz"}
parsed = tmpl.parse(stack, p_snippet) 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): def test_select_from_list(self):
tmpl = parser.Template(empty_template) tmpl = parser.Template(empty_template)