Use the task loop to calculate multiple delegated hosts

Due to the way we're now calculating delegate_to, if that value is based
on a loop variable ('item') we need to calculate all of the possible
delegated_to variables for that loop.

Fixes #12499
pull/12521/head
James Cammarata 9 years ago
parent a1428d6bed
commit 31d5f88a1d

@ -112,14 +112,9 @@ class WorkerProcess(multiprocessing.Process):
# the task handles updating parent/child objects as needed. # the task handles updating parent/child objects as needed.
task.set_loader(self._loader) task.set_loader(self._loader)
# apply the given task's information to the connection info,
# which may override some fields already set by the play or
# the options specified on the command line
new_play_context = play_context.set_task_and_variable_override(task=task, variables=job_vars)
# execute the task and build a TaskResult from the result # execute the task and build a TaskResult from the result
debug("running TaskExecutor() for %s/%s" % (host, task)) debug("running TaskExecutor() for %s/%s" % (host, task))
executor_result = TaskExecutor(host, task, job_vars, new_play_context, self._new_stdin, self._loader, shared_loader_obj).run() executor_result = TaskExecutor(host, task, job_vars, play_context, self._new_stdin, self._loader, shared_loader_obj).run()
debug("done running TaskExecutor() for %s/%s" % (host, task)) debug("done running TaskExecutor() for %s/%s" % (host, task))
task_result = TaskResult(host, task, executor_result) task_result = TaskResult(host, task, executor_result)

@ -261,6 +261,11 @@ class TaskExecutor:
templar = Templar(loader=self._loader, shared_loader_obj=self._shared_loader_obj, variables=variables) templar = Templar(loader=self._loader, shared_loader_obj=self._shared_loader_obj, variables=variables)
# apply the given task's information to the connection info,
# which may override some fields already set by the play or
# the options specified on the command line
self._play_context = self._play_context.set_task_and_variable_override(task=self._task, variables=variables, templar=templar)
# fields set from the play/task may be based on variables, so we have to # fields set from the play/task may be based on variables, so we have to
# do the same kind of post validation step on it here before we use it. # do the same kind of post validation step on it here before we use it.
# We also add "magic" variables back into the variables dict to make sure # We also add "magic" variables back into the variables dict to make sure

@ -267,7 +267,7 @@ class PlayContext(Base):
elif isinstance(options.skip_tags, string_types): elif isinstance(options.skip_tags, string_types):
self.skip_tags.update(options.skip_tags.split(',')) self.skip_tags.update(options.skip_tags.split(','))
def set_task_and_variable_override(self, task, variables): def set_task_and_variable_override(self, task, variables, templar):
''' '''
Sets attributes from the task if they are set, which will override Sets attributes from the task if they are set, which will override
those from the play. those from the play.
@ -288,7 +288,15 @@ class PlayContext(Base):
# If the value 'ansible_delegated_vars' is in the variables, it means # If the value 'ansible_delegated_vars' is in the variables, it means
# we have a delegated-to host, so we check there first before looking # we have a delegated-to host, so we check there first before looking
# at the variables in general # at the variables in general
delegated_vars = variables.get('ansible_delegated_vars', dict()) if task.delegate_to is not None:
# In the case of a loop, the delegated_to host may have been
# templated based on the loop variable, so we try and locate
# the host name in the delegated variable dictionary here
delegated_host_name = templar.template(task.delegate_to)
delegated_vars = variables.get('ansible_delegated_vars', dict()).get(delegated_host_name, dict())
else:
delegated_vars = dict()
for (attr, variable_names) in iteritems(MAGIC_VARIABLE_MAPPING): for (attr, variable_names) in iteritems(MAGIC_VARIABLE_MAPPING):
for variable_name in variable_names: for variable_name in variable_names:
if isinstance(delegated_vars, dict) and variable_name in delegated_vars: if isinstance(delegated_vars, dict) and variable_name in delegated_vars:

@ -36,9 +36,11 @@ from ansible.cli import CLI
from ansible.errors import AnsibleError, AnsibleParserError, AnsibleUndefinedVariable, AnsibleFileNotFound from ansible.errors import AnsibleError, AnsibleParserError, AnsibleUndefinedVariable, AnsibleFileNotFound
from ansible.inventory.host import Host from ansible.inventory.host import Host
from ansible.parsing import DataLoader from ansible.parsing import DataLoader
from ansible.plugins import lookup_loader
from ansible.plugins.cache import FactCache from ansible.plugins.cache import FactCache
from ansible.template import Templar from ansible.template import Templar
from ansible.utils.debug import debug from ansible.utils.debug import debug
from ansible.utils.listify import listify_lookup_plugin_terms
from ansible.utils.vars import combine_vars from ansible.utils.vars import combine_vars
from ansible.vars.hostvars import HostVars from ansible.vars.hostvars import HostVars
from ansible.vars.unsafe_proxy import UnsafeProxy from ansible.vars.unsafe_proxy import UnsafeProxy
@ -333,39 +335,78 @@ class VariableManager:
# as we're fetching vars before post_validate has been called on # as we're fetching vars before post_validate has been called on
# the task that has been passed in # the task that has been passed in
templar = Templar(loader=loader, variables=all_vars) templar = Templar(loader=loader, variables=all_vars)
delegated_host_name = templar.template(task.delegate_to)
items = []
# a dictionary of variables to use if we have to create a new host below if task.loop is not None:
new_delegated_host_vars = dict( if task.loop in lookup_loader:
ansible_host=delegated_host_name, #TODO: remove convert_bare true and deprecate this in with_
ansible_user=C.DEFAULT_REMOTE_USER, try:
ansible_connection=C.DEFAULT_TRANSPORT, loop_terms = listify_lookup_plugin_terms(terms=task.loop_args, templar=templar, loader=loader, fail_on_undefined=True, convert_bare=True)
) except AnsibleUndefinedVariable as e:
if 'has no attribute' in str(e):
# now try to find the delegated-to host in inventory, or failing that, loop_terms = []
# create a new host on the fly so we can fetch variables for it self._display.deprecated("Skipping task due to undefined attribute, in the future this will be a fatal error.")
delegated_host = None else:
if self._inventory is not None: raise
delegated_host = self._inventory.get_host(delegated_host_name) items = lookup_loader.get(task.loop, loader=loader, templar=templar).run(terms=loop_terms, variables=all_vars)
# try looking it up based on the address field, and finally else:
# fall back to creating a host on the fly to use for the var lookup raise AnsibleError("Unexpected failure in finding the lookup named '%s' in the available lookup plugins" % task.loop)
if delegated_host is None:
for h in self._inventory.get_hosts(ignore_limits_and_restrictions=True):
# check if the address matches, or if both the delegated_to host
# and the current host are in the list of localhost aliases
if h.address == delegated_host_name or h.name in C.LOCALHOST and delegated_host_name in C.LOCALHOST:
delegated_host = h
break
else:
delegated_host = Host(name=delegated_host_name)
delegated_host.vars.update(new_delegated_host_vars)
else: else:
delegated_host = Host(name=delegated_host_name) items = [None]
delegated_host.vars.update(new_delegated_host_vars)
vars_copy = all_vars.copy()
delegated_host_vars = dict()
for item in items:
# update the variables with the item value for templating, in case we need it
if item is not None:
vars_copy['item'] = item
templar.set_available_variables(vars_copy)
delegated_host_name = templar.template(task.delegate_to, fail_on_undefined=False)
if delegated_host_name in delegated_host_vars:
# no need to repeat ourselves, as the delegate_to value
# does not appear to be tied to the loop item variable
continue
# now we go fetch the vars for the delegated-to host and save them in our # a dictionary of variables to use if we have to create a new host below
# master dictionary of variables to be used later in the TaskExecutor/PlayContext new_delegated_host_vars = dict(
all_vars['ansible_delegated_vars'] = self.get_vars(loader=loader, play=play, host=delegated_host, task=task, include_delegate_to=False, include_hostvars=False) ansible_host=delegated_host_name,
ansible_user=C.DEFAULT_REMOTE_USER,
ansible_connection=C.DEFAULT_TRANSPORT,
)
# now try to find the delegated-to host in inventory, or failing that,
# create a new host on the fly so we can fetch variables for it
delegated_host = None
if self._inventory is not None:
delegated_host = self._inventory.get_host(delegated_host_name)
# try looking it up based on the address field, and finally
# fall back to creating a host on the fly to use for the var lookup
if delegated_host is None:
for h in self._inventory.get_hosts(ignore_limits_and_restrictions=True):
# check if the address matches, or if both the delegated_to host
# and the current host are in the list of localhost aliases
if h.address == delegated_host_name or h.name in C.LOCALHOST and delegated_host_name in C.LOCALHOST:
delegated_host = h
break
else:
delegated_host = Host(name=delegated_host_name)
delegated_host.vars.update(new_delegated_host_vars)
else:
delegated_host = Host(name=delegated_host_name)
delegated_host.vars.update(new_delegated_host_vars)
# now we go fetch the vars for the delegated-to host and save them in our
# master dictionary of variables to be used later in the TaskExecutor/PlayContext
delegated_host_vars[delegated_host_name] = self.get_vars(
loader=loader,
play=play,
host=delegated_host,
task=task,
include_delegate_to=False,
include_hostvars=False,
)
all_vars['ansible_delegated_vars'] = delegated_host_vars
if self._inventory is not None: if self._inventory is not None:
all_vars['inventory_dir'] = self._inventory.basedir() all_vars['inventory_dir'] = self._inventory.basedir()

@ -99,8 +99,10 @@ class TestPlayContext(unittest.TestCase):
ansible_ssh_port = 4321, ansible_ssh_port = 4321,
) )
mock_templar = MagicMock()
play_context = PlayContext(play=mock_play, options=options) play_context = PlayContext(play=mock_play, options=options)
play_context = play_context.set_task_and_variable_override(task=mock_task, variables=all_vars) play_context = play_context.set_task_and_variable_override(task=mock_task, variables=all_vars, templar=mock_templar)
self.assertEqual(play_context.connection, 'mock_inventory') self.assertEqual(play_context.connection, 'mock_inventory')
self.assertEqual(play_context.remote_user, 'mocktask') self.assertEqual(play_context.remote_user, 'mocktask')
self.assertEqual(play_context.port, 4321) self.assertEqual(play_context.port, 4321)

@ -171,6 +171,7 @@ class TestVariableManager(unittest.TestCase):
mock_task = MagicMock() mock_task = MagicMock()
mock_task._role = None mock_task._role = None
mock_task.loop = None
mock_task.get_vars.return_value = dict(foo="bar") mock_task.get_vars.return_value = dict(foo="bar")
v = VariableManager() v = VariableManager()

Loading…
Cancel
Save