Skip to content

Commit

Permalink
Refactoring delegate_to code
Browse files Browse the repository at this point in the history
Now, instead of relying on hostvars on the executor side, we compile
the vars for the delegated to host in a special internal variable and
have the PlayContext object look for things there when applying task/
var overrides, which is much cleaner and takes advantage of the code
already dealing with all of the magic variable variations.

Fixes #12127
Fixes #12079
  • Loading branch information
jimi-c committed Sep 18, 2015
1 parent fa69e8e commit 1f5584a
Show file tree
Hide file tree
Showing 3 changed files with 55 additions and 77 deletions.
84 changes: 12 additions & 72 deletions lib/ansible/executor/task_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -474,7 +474,18 @@ def _get_connection(self, variables):
self._play_context.remote_addr = self._host.address

if self._task.delegate_to is not None:
self._compute_delegate(variables)
# since we're delegating, we don't want to use interpreter values
# which would have been set for the original target host
for i in variables.keys():
if i.startswith('ansible_') and i.endswith('_interpreter'):
del variables[i]
# now replace the interpreter values with those that may have come
# from the delegated-to host
delegated_vars = variables.get('ansible_delegated_vars', dict())
if isinstance(delegated_vars, dict):
for i in delegated_vars:
if i.startswith("ansible_") and i.endswith("_interpreter"):
variables[i] = delegated_vars[i]

conn_type = self._play_context.connection
if conn_type == 'smart':
Expand Down Expand Up @@ -529,74 +540,3 @@ def _get_action_handler(self, connection, templar):

return handler

def _compute_delegate(self, variables):

# get the vars for the delegate by its name
try:
self._display.debug("Delegating to %s" % self._task.delegate_to)
if self._task.delegate_to in C.LOCALHOST and self._task.delegate_to not in variables['hostvars']:
this_info = dict(ansible_connection="local")
for alt_local in C.LOCALHOST:
if alt_local in variables['hostvars']:
this_info = variables['hostvars'][self._task.delegate_to]
if this_info == Undefined:
this_info = dict(ansible_connection="local")
break
else:
this_info = variables['hostvars'][self._task.delegate_to]

except Exception as e:
# make sure the inject is empty for non-inventory hosts
this_info = {}
self._display.debug("Delegate to lookup failed due to: %s" % str(e))

conn = this_info.get('ansible_connection')
if conn:
self._play_context.connection = conn
if conn in ('smart', 'paramiko'):
# smart and paramiko connections will be using some kind of ssh,
# so use 'ssh' for the string to check connection variables
conn_test = 'ssh'
else:
conn_test = conn
else:
# default to ssh for the connection variable test, as
# that's the historical default
conn_test = 'ssh'

# get the real ssh_address for the delegate and allow ansible_ssh_host to be templated
self._play_context.remote_addr = this_info.get('ansible_%s_host' % conn_test, this_info.get('ansible_host', self._task.delegate_to))
self._play_context.remote_user = this_info.get('ansible_%s_user' % conn_test, this_info.get('ansible_user', self._play_context.remote_user))
self._play_context.port = this_info.get('ansible_%s_port' % conn_test, this_info.get('ansible_port', self._play_context.port))
self._play_context.password = this_info.get('ansible_%s_pass' % conn_test, this_info.get('ansible_pass', self._play_context.password))
self._play_context.private_key_file = this_info.get('ansible_%s_private_key_file' % conn_test, self._play_context.private_key_file)

# because of the switch from su/sudo -> become, the become pass for the
# delegated-to host may be in one of several fields, so try each until
# (maybe) one is found.
for become_pass in ('ansible_become_password', 'ansible_become_pass', 'ansible_sudo_password', 'ansible_sudo_pass'):
if become_pass in this_info:
self._play_context.become_pass = this_info[become_pass]
break

# Last chance to get private_key_file from global variables.
# this is useful if delegated host is not defined in the inventory
if self._play_context.private_key_file is None:
self._play_context.private_key_file = this_info.get('ansible_ssh_private_key_file', None)

if self._play_context.private_key_file is None:
key = this_info.get('private_key_file', None)
if key:
self._play_context.private_key_file = os.path.expanduser(key)

# since we're delegating, we don't want to use interpreter values
# which would have been set for the original target host
for i in variables.keys():
if i.startswith('ansible_') and i.endswith('_interpreter'):
del variables[i]
# now replace the interpreter values with those that may have come
# from the delegated-to host
for i in this_info:
if i.startswith("ansible_") and i.endswith("_interpreter"):
variables[i] = this_info[i]

11 changes: 9 additions & 2 deletions lib/ansible/playbook/play_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,10 +279,16 @@ def set_task_and_variable_override(self, task, variables):
setattr(new_info, attr, attr_val)

# next, use the MAGIC_VARIABLE_MAPPING dictionary to update this
# connection info object with 'magic' variables from the variable list
# connection info object with 'magic' variables from the variable list.
# 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
# at the variables in general
delegated_vars = variables.get('ansible_delegated_vars', dict())
for (attr, variable_names) in iteritems(MAGIC_VARIABLE_MAPPING):
for variable_name in variable_names:
if variable_name in variables:
if isinstance(delegated_vars, dict) and variable_name in delegated_vars:
setattr(new_info, attr, delegated_vars[variable_name])
elif variable_name in variables:
setattr(new_info, attr, variables[variable_name])

# make sure we get port defaults if needed
Expand All @@ -296,6 +302,7 @@ def set_task_and_variable_override(self, task, variables):
elif new_info.become_method == 'su' and new_info.su_pass:
setattr(new_info, 'become_pass', new_info.su_pass)


# finally, in the special instance that the task was specified
# as a local action, override the connection in case it was changed
# during some other step in the process
Expand Down
37 changes: 34 additions & 3 deletions lib/ansible/vars/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
from ansible import constants as C
from ansible.cli import CLI
from ansible.errors import AnsibleError
from ansible.inventory.host import Host
from ansible.parsing import DataLoader
from ansible.plugins.cache import FactCache
from ansible.template import Templar
Expand Down Expand Up @@ -127,7 +128,7 @@ def _preprocess_vars(self, a):
return data


def get_vars(self, loader, play=None, host=None, task=None, include_hostvars=True, use_cache=True):
def get_vars(self, loader, play=None, host=None, task=None, include_hostvars=True, include_delegate_to=True, use_cache=True):
'''
Returns the variables, with optional "context" given via the parameters
for the play, host, and task (which could possibly result in different
Expand Down Expand Up @@ -276,6 +277,38 @@ def get_vars(self, loader, play=None, host=None, task=None, include_hostvars=Tru
if task._role:
all_vars['role_path'] = task._role._role_path

# if we have a task and we're delegating to another host, figure out the
# variables for that host now so we don't have to rely on hostvars later
if task.delegate_to is not None and include_delegate_to:
# we unfortunately need to template the delegate_to field here,
# as we're fetching vars before post_validate has been called on
# the task that has been passed in
templar = Templar(loader=loader, variables=all_vars)
delegated_host_name = templar.template(task.delegate_to)

# 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)
else:
delegated_host = Host(name=delegated_host_name)

# 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
all_vars['ansible_delegated_vars'] = self.get_vars(loader=loader, play=play, host=delegated_host, task=task, include_delegate_to=False, include_hostvars=False)

if self._inventory is not None:
all_vars['inventory_dir'] = self._inventory.basedir()
if play:
Expand All @@ -287,10 +320,8 @@ def get_vars(self, loader, play=None, host=None, task=None, include_hostvars=Tru
all_vars['play_hosts'] = host_list
all_vars['ansible_play_hosts'] = host_list


# the 'omit' value alows params to be left out if the variable they are based on is undefined
all_vars['omit'] = self._omit_token

all_vars['ansible_version'] = CLI.version_info(gitinfo=False)

if 'hostvars' in all_vars and host:
Expand Down

0 comments on commit 1f5584a

Please sign in to comment.