Merge "Support 'permutations' for 'repeat' function"
This commit is contained in:
commit
798f64ee91
|
@ -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
|
||||
|
|
|
@ -888,9 +888,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)
|
||||
|
@ -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:
|
||||
<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):
|
||||
|
@ -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:
|
||||
<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…
Reference in New Issue