diff --git a/doc/source/template_guide/hot_spec.rst b/doc/source/template_guide/hot_spec.rst
index 7c093ed9d5..4d9f6bcb2e 100644
--- a/doc/source/template_guide/hot_spec.rst
+++ b/doc/source/template_guide/hot_spec.rst
@@ -1410,6 +1410,44 @@ nested for-loops in most programming languages.
From HOT version ``2016-10-14`` you may also pass a map as value for the
``for_each`` key, in which case the list of map keys will be used as value.
+From HOT version ``2017-09-01`` (or pike) you may specify a argument
+``permutations`` to decide whether to iterate nested the over all the
+permutations of the elements in the given lists. If 'permutations' is not
+specified, we set the default value to true to compatible with before behavior.
+The args have to be lists instead of dicts if 'permutations' is False because
+keys in a dict are unordered, and the list args all have to be of the
+same length.
+
+.. code-block:: yaml
+
+ parameters:
+ subnets:
+ type: comma_delimited_list
+ label: subnets
+ default: "sub1, sub2"
+ networks:
+ type: comma_delimited_list
+ label: networks
+ default: "net1, net2"
+
+ resources:
+ my_server:
+ type: OS::Nova:Server
+ properties:
+ networks:
+ repeat:
+ for_each:
+ <%sub%>: { get_param: subnets }
+ <%net%>: { get_param: networks }
+ template:
+ subnet: <%sub%>
+ network: <%net%>
+ permutations: false
+
+After resolved, we will get the networks of server like:
+[{subnet: sub1, network: net1}, {subnet: sub2, network: net2}]
+
+
resource_facade
---------------
The ``resource_facade`` function retrieves data in a parent
diff --git a/heat/engine/hot/functions.py b/heat/engine/hot/functions.py
index eef946f160..c461b4dce3 100644
--- a/heat/engine/hot/functions.py
+++ b/heat/engine/hot/functions.py
@@ -888,9 +888,12 @@ class Repeat(function.Function):
is a copy of
with any occurrences of replaced with the
corresponding item of .
"""
+
def __init__(self, stack, fn_name, args):
super(Repeat, self).__init__(stack, fn_name, args)
+ 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)
@@ -907,6 +910,8 @@ class Repeat(function.Function):
%var%: ['a', 'b', 'c']''')
raise KeyError(_('"repeat" syntax should be %s') % example)
+ self._nested_loop = True
+
def validate(self):
super(Repeat, self).validate()
@@ -943,20 +948,37 @@ class Repeat(function.Function):
# use empty list for references(None) else validation will fail
values = [[] if value is None else value for value in lists]
+ value_lens = []
for arg in values:
self._valid_arg(arg)
+ value_lens.append(len(arg))
+ if not self._nested_loop:
+ if len(set(value_lens)) != 1:
+ raise ValueError(_('For %s, the length of for_each values '
+ 'should be equal if no nested '
+ 'loop.') % self.fn_name)
template = function.resolve(self._template)
+ iter_func = itertools.product if self._nested_loop else six.moves.zip
return [self._do_replacement(keys, replacements, template)
- for replacements in itertools.product(*values)]
+ for replacements in iter_func(*values)]
class RepeatWithMap(Repeat):
- """A function for iterating over a list or map of items.
+ """A function for iterating over a list of items or a dict of keys.
- Behaves the same as Repeat, but if tolerates a map as
- values to be repeated, in which case it iterates the map keys.
+ Takes the form::
+
+ repeat:
+ template:
+
+ for_each:
+ : or
+
+ The result is a new list of the same size as or , where each
+ element is a copy of with any occurrences of replaced with the
+ corresponding item of or key of .
"""
def _valid_arg(self, arg):
@@ -968,6 +990,57 @@ class RepeatWithMap(Repeat):
'"%s" must be lists or maps') % self.fn_name)
+class RepeatWithNestedLoop(RepeatWithMap):
+ """A function for iterating over a list of items or a dict of keys.
+
+ Takes the form::
+
+ repeat:
+ template:
+
+ for_each:
+ : or
+
+ The result is a new list of the same size as or , where each
+ element is a copy of with any occurrences of replaced with the
+ corresponding item of or key of .
+
+ This function also allows to specify 'permutations' to decide
+ whether to iterate nested the over all the permutations of the
+ elements in the given lists.
+
+ Takes the form::
+
+ repeat:
+ template:
+ var: %var%
+ bar: %bar%
+ for_each:
+ %var%:
+ %bar%:
+ permutations: false
+
+ If 'permutations' is not specified, we set the default value to true to
+ compatible with before behavior. The args have to be lists instead of
+ dicts if 'permutations' is False because keys in a dict are unordered,
+ and the list args all have to be of the same length.
+ """
+
+ def _parse_args(self):
+ super(RepeatWithNestedLoop, self)._parse_args()
+ self._nested_loop = self.args.get('permutations', True)
+
+ if not isinstance(self._nested_loop, bool):
+ raise TypeError(_('"permutations" should be boolean type '
+ 'for %s function.') % self.fn_name)
+
+ def _valid_arg(self, arg):
+ if self._nested_loop:
+ super(RepeatWithNestedLoop, self)._valid_arg(arg)
+ else:
+ Repeat._valid_arg(self, arg)
+
+
class Digest(function.Function):
"""A function for performing digest operations.
diff --git a/heat/engine/hot/template.py b/heat/engine/hot/template.py
index 0a93bbc763..778596a041 100644
--- a/heat/engine/hot/template.py
+++ b/heat/engine/hot/template.py
@@ -571,7 +571,7 @@ class HOTemplate20170901(HOTemplate20170224):
'get_param': hot_funcs.GetParam,
'get_resource': hot_funcs.GetResource,
'list_join': hot_funcs.JoinMultiple,
- 'repeat': hot_funcs.RepeatWithMap,
+ 'repeat': hot_funcs.RepeatWithNestedLoop,
'resource_facade': hot_funcs.ResourceFacade,
'str_replace': hot_funcs.ReplaceJson,
diff --git a/heat/tests/test_hot.py b/heat/tests/test_hot.py
index e238caab3b..cde6b44c80 100644
--- a/heat/tests/test_hot.py
+++ b/heat/tests/test_hot.py
@@ -1448,16 +1448,22 @@ conditions:
six.text_type(exc))
self.assertIn('if:', six.text_type(exc))
- def test_repeat(self):
+ def _test_repeat(self, templ=hot_kilo_tpl_empty):
"""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 = template.Template(hot_kilo_tpl_empty)
+ tmpl = template.Template(templ)
self.assertEqual(snippet_resolved, self.resolve(snippet, tmpl))
+ def test_repeat(self):
+ self._test_repeat()
+
+ def test_repeat_with_pike_version(self):
+ self._test_repeat(templ=hot_pike_tpl_empty)
+
def test_repeat_get_param(self):
"""Test repeat function with get_param function as an argument."""
hot_tpl = template_format.parse('''
@@ -1476,16 +1482,23 @@ conditions:
self.assertEqual(snippet_resolved, self.resolve(snippet, tmpl, stack))
- def test_repeat_dict_with_no_replacement(self):
+ def _test_repeat_dict_with_no_replacement(self,
+ templ=hot_newton_tpl_empty):
snippet = {'repeat': {'template': {'SERVICE_enabled': True},
'for_each': {'SERVICE': ['x', 'y', 'z']}}}
snippet_resolved = [{'x_enabled': True},
{'y_enabled': True},
{'z_enabled': True}]
- tmpl = template.Template(hot_newton_tpl_empty)
+ tmpl = template.Template(templ)
self.assertEqual(snippet_resolved, self.resolve(snippet, tmpl))
- def test_repeat_dict_template(self):
+ def test_repeat_dict_with_no_replacement(self):
+ self._test_repeat_dict_with_no_replacement()
+
+ def test_repeat_dict_with_no_replacement_pike_version(self):
+ self._test_repeat_dict_with_no_replacement(templ=hot_pike_tpl_empty)
+
+ def _test_repeat_dict_template(self, templ=hot_kilo_tpl_empty):
"""Test repeat function with a dictionary as a template."""
snippet = {'repeat': {'template': {'key-%var%': 'this is %var%'},
'for_each': {'%var%': ['a', 'b', 'c']}}}
@@ -1493,11 +1506,17 @@ conditions:
{'key-b': 'this is b'},
{'key-c': 'this is c'}]
- tmpl = template.Template(hot_kilo_tpl_empty)
+ tmpl = template.Template(templ)
self.assertEqual(snippet_resolved, self.resolve(snippet, tmpl))
- def test_repeat_list_template(self):
+ def test_repeat_dict_template(self):
+ self._test_repeat_dict_template()
+
+ def test_repeat_dict_template_pike_version(self):
+ self._test_repeat_dict_template(templ=hot_pike_tpl_empty)
+
+ def _test_repeat_list_template(self, templ=hot_kilo_tpl_empty):
"""Test repeat function with a list as a template."""
snippet = {'repeat': {'template': ['this is %var%', 'static'],
'for_each': {'%var%': ['a', 'b', 'c']}}}
@@ -1505,11 +1524,17 @@ conditions:
['this is b', 'static'],
['this is c', 'static']]
- tmpl = template.Template(hot_kilo_tpl_empty)
+ tmpl = template.Template(templ)
self.assertEqual(snippet_resolved, self.resolve(snippet, tmpl))
- def test_repeat_multi_list(self):
+ def test_repeat_list_template(self):
+ self._test_repeat_list_template()
+
+ def test_repeat_list_template_pike_version(self):
+ self._test_repeat_list_template(templ=hot_pike_tpl_empty)
+
+ def _test_repeat_multi_list(self, templ=hot_kilo_tpl_empty):
"""Test repeat function with multiple input lists."""
snippet = {'repeat': {'template': 'this is %var1%-%var2%',
'for_each': {'%var1%': ['a', 'b', 'c'],
@@ -1517,13 +1542,19 @@ conditions:
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 = template.Template(hot_kilo_tpl_empty)
+ tmpl = template.Template(templ)
result = self.resolve(snippet, tmpl)
self.assertEqual(len(result), len(snippet_resolved))
for item in result:
self.assertIn(item, snippet_resolved)
+ def test_repeat_multi_list(self):
+ self._test_repeat_multi_list()
+
+ def test_repeat_multi_list_pike_version(self):
+ self._test_repeat_multi_list(templ=hot_pike_tpl_empty)
+
def test_repeat_list_and_map(self):
"""Test repeat function with a list and a map."""
snippet = {'repeat': {'template': 'this is %var1%-%var2%',
@@ -1539,6 +1570,59 @@ conditions:
for item in result:
self.assertIn(item, snippet_resolved)
+ def test_repeat_with_no_nested_loop(self):
+ snippet = {'repeat': {'template': {'network': '%net%',
+ 'port': '%port%',
+ 'subnet': '%sub%'},
+ 'for_each': {'%net%': ['n1', 'n2', 'n3', 'n4'],
+ '%port%': ['p1', 'p2', 'p3', 'p4'],
+ '%sub%': ['s1', 's2', 's3', 's4']},
+ 'permutations': False}}
+ tmpl = template.Template(hot_pike_tpl_empty)
+ snippet_resolved = [{'network': 'n1', 'port': 'p1', 'subnet': 's1'},
+ {'network': 'n2', 'port': 'p2', 'subnet': 's2'},
+ {'network': 'n3', 'port': 'p3', 'subnet': 's3'},
+ {'network': 'n4', 'port': 'p4', 'subnet': 's4'}]
+
+ result = self.resolve(snippet, tmpl)
+ self.assertEqual(snippet_resolved, result)
+
+ def test_repeat_no_nested_loop_different_len(self):
+ snippet = {'repeat': {'template': {'network': '%net%',
+ 'port': '%port%',
+ 'subnet': '%sub%'},
+ 'for_each': {'%net%': ['n1', 'n2', 'n3'],
+ '%port%': ['p1', 'p2'],
+ '%sub%': ['s1', 's2']},
+ 'permutations': False}}
+ tmpl = template.Template(hot_pike_tpl_empty)
+ self.assertRaises(ValueError, self.resolve, snippet, tmpl)
+
+ def test_repeat_no_nested_loop_with_dict_type(self):
+ snippet = {'repeat': {'template': {'network': '%net%',
+ 'port': '%port%',
+ 'subnet': '%sub%'},
+ 'for_each': {'%net%': ['n1', 'n2'],
+ '%port%': {'p1': 'pp', 'p2': 'qq'},
+ '%sub%': ['s1', 's2']},
+ 'permutations': False}}
+ tmpl = template.Template(hot_pike_tpl_empty)
+ self.assertRaises(TypeError, self.resolve, snippet, tmpl)
+
+ def test_repeat_permutations_non_bool(self):
+ snippet = {'repeat': {'template': {'network': '%net%',
+ 'port': '%port%',
+ 'subnet': '%sub%'},
+ 'for_each': {'%net%': ['n1', 'n2'],
+ '%port%': ['p1', 'p2'],
+ '%sub%': ['s1', 's2']},
+ 'permutations': 'non bool'}}
+ tmpl = template.Template(hot_pike_tpl_empty)
+ exc = self.assertRaises(exception.StackValidationFailed,
+ self.resolve, snippet, tmpl)
+ self.assertIn('"permutations" should be boolean type '
+ 'for repeat function', six.text_type(exc))
+
def test_repeat_bad_args(self):
"""Tests reporting error by repeat function.