Support 'permutations' for 'repeat' function
Adds a new section 'permutations' for 'repeat' function to decide whether to do nested loops to iterate over all the permutations of the elements in the given lists, the default value is True if no specify and keep the same behavior as before. For example: repeat: template: network: %net% port: %port% ip: %ip% for_each: %net%: [n1, n2] %port%: [p1, p2] %ip%: [ip1, ip2] permutations: False Will be resolve to: [{network: n1, port: p1, ip: ip1}, {network: n2, port: p2, ip: ip2}] Change-Id: I2a008400fb71453f6a78656f2e041ae2efa098a2 Blueprint: improve-repeat-function
This commit is contained in:
parent
3ae539bbf9
commit
ba7f7888f6
@ -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
|
||||
|
@ -887,9 +887,12 @@ class Repeat(function.Function):
|
||||
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._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)
|
||||
@ -906,6 +909,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()
|
||||
|
||||
@ -942,20 +947,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:
|
||||
<body>
|
||||
for_each:
|
||||
<var>: <list> or <dict>
|
||||
|
||||
The result is a new list of the same size as <list> or <dict>, where each
|
||||
element is a copy of <body> with any occurrences of <var> replaced with the
|
||||
corresponding item of <list> or key of <dict>.
|
||||
"""
|
||||
|
||||
def _valid_arg(self, arg):
|
||||
@ -967,6 +989,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:
|
||||
<body>
|
||||
for_each:
|
||||
<var>: <list> or <dict>
|
||||
|
||||
The result is a new list of the same size as <list> or <dict>, where each
|
||||
element is a copy of <body> with any occurrences of <var> replaced with the
|
||||
corresponding item of <list> or key of <dict>.
|
||||
|
||||
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%: <list1>
|
||||
%bar%: <list2>
|
||||
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.
|
||||
|
||||
|
@ -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,
|
||||
|
||||
|
@ -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.
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user