Merge ""repeat" function for HOT templates"

This commit is contained in:
Jenkins 2015-03-01 21:22:29 +00:00 committed by Gerrit Code Review
commit f1fd6e9f06
6 changed files with 289 additions and 1 deletions

View File

@ -140,6 +140,23 @@ For example, Heat currently supports the following values for the
str_replace
Fn::Select
2015-04-30
The key with value *2015-04-30* indicates that the YAML document is a HOT
template and it may contain features added and/or removed up until the Kilo
release. This version adds the *repeat* function. So the complete list of
supported functions is:
::
get_attr
get_file
get_param
get_resource
list_join
repeat
resource_facade
str_replace
Fn::Select
.. _hot_spec_parameter_groups:
@ -801,6 +818,97 @@ A sample use of this function with a simple list is shown below.
This would resolve to "one, two, and three".
repeat
------
The *repeat* function allows for dynamically transforming lists by iterating
over the contents of one or more source lists and replacing the list elements
into a template. The result of this function is a new list, where the elements
are set to the template, rendered for each list item.
The syntax of the repeat function is as follows:
::
repeat:
template:
<template>
for_each:
<var>: <list>
template
The *template* argument defines the content generated for each iteration,
with placeholders for the elements that need to be replaced at runtime.
This argument can be of any supported type.
for_each
The *for_each* argument is a dictionary that defines how to generate the
repetitions of the template and perform substitutions. In this dictionary
the keys are the placeholder names that will be replaced in the template,
and the values are the lists to iterate on. On each iteration, the function
will render the template by performing substitution with elements of the
given lists. If a single key/value pair is given in this argument, the
template will be rendered once for each element in the list. When more
than one key/value pairs are given, the iterations will be performed on all
the permutations of values between the given lists. The values in this
dictionary can be given as functions such as get_attr or get_param.
The example below shows how a security group resource can be defined to
include a list of ports given as a parameter.
::
parameters:
ports:
type: comma_delimited_list
label: ports
default: "80,443,8080"
resources:
security_group:
type: OS::Neutron::SecurityGroup
properties:
name: web_server_security_group
rules:
repeat:
for_each:
%port%: { get_param: ports }
template:
protocol: tcp
port_range_min: %port%
port_range_max: %port%
The next example demonstrates how the use of multiple lists enables the
security group to also include parameterized protocols.
::
parameters:
ports:
type: comma_delimited_list
label: ports
default: "80,443,8080"
protocols:
type: comma_delimited_list
label: protocols
default: "tcp,udp"
resources:
security_group:
type: OS::Neutron::SecurityGroup
properties:
name: web_server_security_group
rules:
repeat:
for_each:
%port%: { get_param: ports }
%protocol%: { get_param: protocols }
template:
protocol: %protocol%
port_range_min: %port%
Note how multiple entries in the ``for_each`` argument are equivalent to
nested for-loops in most programming languages.
resource_facade
---------------
The *resource_facade* function allows a provider template to retrieve data

View File

@ -12,6 +12,7 @@
# under the License.
import collections
import itertools
import six
@ -264,3 +265,72 @@ class Removed(function.Function):
def result(self):
return super(Removed, self).result()
class Repeat(function.Function):
'''
A function for iterating over a list of items.
Takes the form::
repeat:
template:
<body>
for_each:
<var>: <list>
The result is a new list of the same size as <list>, where each element
is a copy of <body> with any occurrences of <var> replaced with the
corresponding item of <list>.
'''
def __init__(self, stack, fn_name, args):
super(Repeat, self).__init__(stack, fn_name, args)
self._for_each, self._template = self._parse_args()
def _parse_args(self):
if not isinstance(self.args, collections.Mapping):
raise TypeError(_('Arguments to "%s" must be a map') %
self.fn_name)
try:
for_each = self.args['for_each']
template = self.args['template']
except (KeyError, TypeError):
example = ('''repeat:
template: This is %var%
for_each:
%var%: ['a', 'b', 'c']''')
raise KeyError(_('"repeat" syntax should be %s') %
example)
if not isinstance(for_each, collections.Mapping):
raise TypeError(_('The "for_each" argument to "%s" must contain '
'a map') % self.fn_name)
for v in six.itervalues(for_each):
if not isinstance(v, list):
raise TypeError(_('The values of the "for_each" argument to '
'"%s" must be lists') % self.fn_name)
return for_each, template
def _do_replacement(self, keys, values, template):
if isinstance(template, six.string_types):
for (key, value) in zip(keys, values):
template = template.replace(key, value)
return template
elif isinstance(template, collections.Sequence):
return [self._do_replacement(keys, values, elem)
for elem in template]
elif isinstance(template, collections.Mapping):
return dict((self._do_replacement(keys, values, k),
self._do_replacement(keys, values, v))
for (k, v) in template.items())
def result(self):
for_each = function.resolve(self._for_each)
keys = for_each.keys()
lists = [for_each[key] for key in keys]
template = function.resolve(self._template)
return [self._do_replacement(keys, items, template)
for items in itertools.product(*lists)]

View File

@ -293,3 +293,28 @@ class HOTemplate20141016(HOTemplate20130523):
'Fn::ResourceFacade': hot_funcs.Removed,
'Ref': hot_funcs.Removed,
}
class HOTemplate20150430(HOTemplate20141016):
functions = {
'get_attr': hot_funcs.GetAtt,
'get_file': hot_funcs.GetFile,
'get_param': hot_funcs.GetParam,
'get_resource': cfn_funcs.ResourceRef,
'list_join': hot_funcs.Join,
'repeat': hot_funcs.Repeat,
'resource_facade': hot_funcs.ResourceFacade,
'str_replace': hot_funcs.Replace,
'Fn::Select': cfn_funcs.Select,
# functions removed from 20130523
'Fn::GetAZs': hot_funcs.Removed,
'Fn::Join': hot_funcs.Removed,
'Fn::Split': hot_funcs.Removed,
'Fn::Replace': hot_funcs.Removed,
'Fn::Base64': hot_funcs.Removed,
'Fn::MemberListToMap': hot_funcs.Removed,
'Fn::ResourceFacade': hot_funcs.Removed,
'Ref': hot_funcs.Removed,
}

View File

@ -43,6 +43,10 @@ hot_juno_tpl_empty = template_format.parse('''
heat_template_version: 2014-10-16
''')
hot_kilo_tpl_empty = template_format.parse('''
heat_template_version: 2015-04-30
''')
hot_tpl_empty_sections = template_format.parse('''
heat_template_version: 2013-05-23
parameters:
@ -585,6 +589,86 @@ class HOTemplateTest(common.HeatTestCase):
self.assertEqual(snippet_resolved, self.resolve(snippet, tmpl, stack))
def test_repeat(self):
"""Test repeat function."""
snippet = {'repeat': {'template': 'this is %var%',
'for_each': {'%var%': ['a', 'b', 'c']}}}
snippet_resolved = ['this is a', 'this is b', 'this is c']
tmpl = parser.Template(hot_kilo_tpl_empty)
self.assertEqual(snippet_resolved, self.resolve(snippet, tmpl))
def test_repeat_dict_template(self):
"""Test repeat function with a dictionary as a template."""
snippet = {'repeat': {'template': {'key-%var%': 'this is %var%'},
'for_each': {'%var%': ['a', 'b', 'c']}}}
snippet_resolved = [{'key-a': 'this is a'},
{'key-b': 'this is b'},
{'key-c': 'this is c'}]
tmpl = parser.Template(hot_kilo_tpl_empty)
self.assertEqual(snippet_resolved, self.resolve(snippet, tmpl))
def test_repeat_list_template(self):
"""Test repeat function with a list as a template."""
snippet = {'repeat': {'template': ['this is %var%', 'static'],
'for_each': {'%var%': ['a', 'b', 'c']}}}
snippet_resolved = [['this is a', 'static'],
['this is b', 'static'],
['this is c', 'static']]
tmpl = parser.Template(hot_kilo_tpl_empty)
self.assertEqual(snippet_resolved, self.resolve(snippet, tmpl))
def test_repeat_multi_list(self):
"""Test repeat function with multiple input lists."""
snippet = {'repeat': {'template': 'this is %var1%-%var2%',
'for_each': {'%var1%': ['a', 'b', 'c'],
'%var2%': ['1', '2']}}}
snippet_resolved = ['this is a-1', 'this is b-1', 'this is c-1',
'this is a-2', 'this is b-2', 'this is c-2']
tmpl = parser.Template(hot_kilo_tpl_empty)
result = self.resolve(snippet, tmpl)
self.assertEqual(len(result), len(snippet_resolved))
for item in result:
self.assertIn(item, snippet_resolved)
def test_repeat_bad_args(self):
"""
Test that the repeat function reports a proper error when missing
or invalid arguments.
"""
tmpl = parser.Template(hot_kilo_tpl_empty)
# missing for_each
snippet = {'repeat': {'template': 'this is %var%'}}
self.assertRaises(KeyError, self.resolve, snippet, tmpl)
# mispelled for_each
snippet = {'repeat': {'template': 'this is %var%',
'foreach': {'%var%': ['a', 'b', 'c']}}}
self.assertRaises(KeyError, self.resolve, snippet, tmpl)
# for_each is not a map
snippet = {'repeat': {'template': 'this is %var%',
'for_each': '%var%'}}
self.assertRaises(TypeError, self.resolve, snippet, tmpl)
# value given to for_each entry is not a list
snippet = {'repeat': {'template': 'this is %var%',
'for_each': {'%var%': 'a'}}}
self.assertRaises(TypeError, self.resolve, snippet, tmpl)
# mispelled template
snippet = {'repeat': {'templte': 'this is %var%',
'for_each': {'%var%': ['a', 'b', 'c']}}}
self.assertRaises(KeyError, self.resolve, snippet, tmpl)
def test_prevent_parameters_access(self):
"""
Test that the parameters section can't be accessed using the template

View File

@ -198,7 +198,7 @@ class TemplateTest(common.HeatTestCase):
}''')
init_ex = self.assertRaises(exception.InvalidTemplateVersion,
parser.Template, invalid_hot_version_tmp)
valid_versions = ['2014-10-16', '2013-05-23']
valid_versions = ['2014-10-16', '2013-05-23', '2015-04-30']
ex_error_msg = ('The template version is invalid: '
'"heat_template_version: 2012-12-12". '
'"heat_template_version" should be one of: %s'

View File

@ -77,6 +77,7 @@ heat.stack_lifecycle_plugins =
heat.templates =
heat_template_version.2013-05-23 = heat.engine.hot.template:HOTemplate20130523
heat_template_version.2014-10-16 = heat.engine.hot.template:HOTemplate20141016
heat_template_version.2015-04-30 = heat.engine.hot.template:HOTemplate20150430
HeatTemplateFormatVersion.2012-12-12 = heat.engine.cfn.template:HeatTemplate
AWSTemplateFormatVersion.2010-09-09 = heat.engine.cfn.template:CfnTemplate