If we can't squash for any reason, then simply do not optimize the items loop.

Also add more squashing testcases

Fixes #15649
stable-2.0
Toshio Kuratomi 9 years ago
parent c4388285fa
commit de3f013152

@ -256,59 +256,67 @@ class TaskExecutor:
Squash items down to a comma-separated list for certain modules which support it Squash items down to a comma-separated list for certain modules which support it
(typically package management modules). (typically package management modules).
''' '''
# _task.action could contain templatable strings (via action: and try:
# local_action:) Template it before comparing. If we don't end up # Hack just for the backport to 2.0
# optimizing it here, the templatable string might use template vars loop_var = 'item'
# that aren't available until later (it could even use vars from the
# with_items loop) so don't make the templated string permanent yet. # _task.action could contain templatable strings (via action: and
templar = Templar(loader=self._loader, shared_loader_obj=self._shared_loader_obj, variables=variables) # local_action:) Template it before comparing. If we don't end up
task_action = self._task.action # optimizing it here, the templatable string might use template vars
if templar._contains_vars(task_action): # that aren't available until later (it could even use vars from the
task_action = templar.template(task_action, fail_on_undefined=False) # with_items loop) so don't make the templated string permanent yet.
templar = Templar(loader=self._loader, shared_loader_obj=self._shared_loader_obj, variables=variables)
if len(items) > 0 and task_action in self.SQUASH_ACTIONS: task_action = self._task.action
if all(isinstance(o, string_types) for o in items): if templar._contains_vars(task_action):
final_items = [] task_action = templar.template(task_action, fail_on_undefined=False)
name = None if len(items) > 0 and task_action in self.SQUASH_ACTIONS:
for allowed in ['name', 'pkg', 'package']: if all(isinstance(o, string_types) for o in items):
name = self._task.args.pop(allowed, None) final_items = []
if name is not None:
break name = None
for allowed in ['name', 'pkg', 'package']:
# This gets the information to check whether the name field name = self._task.args.pop(allowed, None)
# contains a template that we can squash for if name is not None:
template_no_item = template_with_item = None break
if name:
if templar._contains_vars(name): # This gets the information to check whether the name field
variables['item'] = '\0$' # contains a template that we can squash for
template_no_item = templar.template(name, variables, cache=False) template_no_item = template_with_item = None
variables['item'] = '\0@' if name:
template_with_item = templar.template(name, variables, cache=False) if templar._contains_vars(name):
del variables['item'] variables[loop_var] = '\0$'
template_no_item = templar.template(name, variables, cache=False)
# Check if the user is doing some operation that doesn't take variables[loop_var] = '\0@'
# name/pkg or the name/pkg field doesn't have any variables template_with_item = templar.template(name, variables, cache=False)
# and thus the items can't be squashed del variables[loop_var]
if template_no_item != template_with_item:
for item in items: # Check if the user is doing some operation that doesn't take
variables['item'] = item # name/pkg or the name/pkg field doesn't have any variables
if self._task.evaluate_conditional(templar, variables): # and thus the items can't be squashed
new_item = templar.template(name, cache=False) if template_no_item != template_with_item:
final_items.append(new_item) for item in items:
self._task.args['name'] = final_items variables[loop_var] = item
# Wrap this in a list so that the calling function loop if self._task.evaluate_conditional(templar, variables):
# executes exactly once new_item = templar.template(name, cache=False)
return [final_items] final_items.append(new_item)
else: self._task.args['name'] = final_items
# Restore the name parameter # Wrap this in a list so that the calling function loop
self._task.args['name'] = name # executes exactly once
#elif: return [final_items]
# Right now we only optimize single entries. In the future we else:
# could optimize more types: # Restore the name parameter
# * lists can be squashed together self._task.args['name'] = name
# * dicts could squash entries that match in all cases except the #elif:
# name or pkg field. # Right now we only optimize single entries. In the future we
# could optimize more types:
# * lists can be squashed together
# * dicts could squash entries that match in all cases except the
# name or pkg field.
except:
# Squashing is an optimization. If it fails for any reason,
# simply use the unoptimized list of items.
pass
return items return items
def _execute(self, variables=None): def _execute(self, variables=None):

@ -230,9 +230,31 @@ class TestTaskExecutor(unittest.TestCase):
new_items = te._squash_items(items=items, variables=job_vars) new_items = te._squash_items(items=items, variables=job_vars)
self.assertEqual(new_items, ['a', 'b', 'c']) self.assertEqual(new_items, ['a', 'b', 'c'])
mock_task.action = '{{unknown}}'
mock_task.args={'name': '{{item}}'}
new_items = te._squash_items(items=items, variables=job_vars)
self.assertEqual(new_items, ['a', 'b', 'c'])
# Maybe should raise an error in this case. The user would have to specify:
# - yum: name="{{ packages[item] }}"
# with_items:
# - ['a', 'b']
# - ['foo', 'bar']
# you can't use a list as a dict key so that would probably throw
# an error later. If so, we can throw it now instead.
# Squashing in this case would not be intuitive as the user is being
# explicit in using each list entry as a key.
job_vars = dict(pkg_mgr='yum', packages={ "a": "foo", "b": "bar", "foo": "baz", "bar": "quux" })
items = [['a', 'b'], ['foo', 'bar']]
mock_task.action = 'yum'
mock_task.args = {'name': '{{ packages[item] }}'}
new_items = te._squash_items(items=items, variables=job_vars)
self.assertEqual(new_items, items)
# #
# Replaces # Replaces
# #
items = ['a', 'b', 'c']
mock_task.action = 'yum' mock_task.action = 'yum'
mock_task.args={'name': '{{item}}'} mock_task.args={'name': '{{item}}'}
new_items = te._squash_items(items=items, variables=job_vars) new_items = te._squash_items(items=items, variables=job_vars)
@ -243,22 +265,75 @@ class TestTaskExecutor(unittest.TestCase):
new_items = te._squash_items(items=items, variables=job_vars) new_items = te._squash_items(items=items, variables=job_vars)
self.assertEqual(new_items, [['a', 'c']]) self.assertEqual(new_items, [['a', 'c']])
# Loop var isn't present until 2.1.x
# New loop_var
#mock_task.action = 'yum'
#mock_task.args = {'name': '{{a_loop_var_item}}'}
#mock_task.loop_control = {'loop_var': 'a_loop_var_item'}
#loop_var = 'a_loop_var_item'
#new_items = te._squash_items(items=items, loop_var='a_loop_var_item', variables=job_vars)
#self.assertEqual(new_items, [['a', 'c']])
#loop_var = 'item'
# #
# Smoketests -- these won't optimize but make sure that they don't # These are presently not optimized but could be in the future.
# traceback either # Expected output if they were optimized is given as a comment
# Please move these to a different section if they are optimized
# #
mock_task.action = '{{unknown}}'
mock_task.args={'name': '{{item}}'} # Squashing lists
job_vars = dict(pkg_mgr='yum')
items = [['a', 'b'], ['foo', 'bar']]
mock_task.action = 'yum'
mock_task.args = {'name': '{{ item }}'}
new_items = te._squash_items(items=items, variables=job_vars) new_items = te._squash_items(items=items, variables=job_vars)
self.assertEqual(new_items, ['a', 'b', 'c']) #self.assertEqual(new_items, [['a', 'b', 'foo', 'bar']])
self.assertEqual(new_items, items)
# Retrieving from a dict
items = ['a', 'b', 'foo']
mock_task.action = 'yum'
mock_task.args = {'name': '{{ packages[item] }}'}
new_items = te._squash_items(items=items, variables=job_vars)
#self.assertEqual(new_items, [['foo', 'baz']])
self.assertEqual(new_items, items)
# Another way to retrieve from a dict
job_vars = dict(pkg_mgr='yum')
items = [{'package': 'foo'}, {'package': 'bar'}]
mock_task.action = 'yum'
mock_task.args = {'name': '{{ item["package"] }}'}
new_items = te._squash_items(items=items, variables=job_vars)
#self.assertEqual(new_items, [['foo', 'bar']])
self.assertEqual(new_items, items)
items = [dict(name='a', state='present'), items = [dict(name='a', state='present'),
dict(name='b', state='present'), dict(name='b', state='present'),
dict(name='c', state='present')] dict(name='c', state='present')]
mock_task.action = 'yum' mock_task.action = 'yum'
mock_task.args={'name': '{{item}}'} mock_task.args={'name': '{{item.name}}', 'state': '{{item.state}}'}
new_items = te._squash_items(items=items, variables=job_vars) new_items = te._squash_items(items=items, variables=job_vars)
self.assertEqual(new_items, items) self.assertEqual(new_items, items)
#self.assertEqual(new_items, [dict(name=['a', 'b', 'c'], state='present')])
items = [dict(name='a', state='present'),
dict(name='b', state='present'),
dict(name='c', state='absent')]
mock_task.action = 'yum'
mock_task.args={'name': '{{item.name}}', 'state': '{{item.state}}'}
new_items = te._squash_items(items=items, variables=job_vars)
self.assertEqual(new_items, items)
#self.assertEqual(new_items, [dict(name=['a', 'b'], state='present'),
# dict(name='c', state='absent')])
# Could do something like this to recover from bad deps in a package
job_vars = dict(pkg_mgr='yum', packages=['a', 'b'])
items = [ 'absent', 'latest' ]
mock_task.action = 'yum'
mock_task.args = {'name': '{{ packages }}', 'state': '{{ item }}'}
new_items = te._squash_items(items=items, variables=job_vars)
self.assertEqual(new_items, items)
def test_task_executor_execute(self): def test_task_executor_execute(self):
fake_loader = DictDataLoader({}) fake_loader = DictDataLoader({})

Loading…
Cancel
Save