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.