diff --git a/lib/ansible/executor/task_executor.py b/lib/ansible/executor/task_executor.py index 9cc244a4022..df7daa607a2 100644 --- a/lib/ansible/executor/task_executor.py +++ b/lib/ansible/executor/task_executor.py @@ -474,7 +474,18 @@ class TaskExecutor: 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': @@ -529,74 +540,3 @@ class TaskExecutor: 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] - diff --git a/lib/ansible/playbook/play_context.py b/lib/ansible/playbook/play_context.py index 6edb5a10833..4e2e6a3a265 100644 --- a/lib/ansible/playbook/play_context.py +++ b/lib/ansible/playbook/play_context.py @@ -279,10 +279,16 @@ class PlayContext(Base): 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 @@ -296,6 +302,7 @@ class PlayContext(Base): 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 diff --git a/lib/ansible/vars/__init__.py b/lib/ansible/vars/__init__.py index da4a4b42ec4..d31fd43581d 100644 --- a/lib/ansible/vars/__init__.py +++ b/lib/ansible/vars/__init__.py @@ -34,6 +34,7 @@ except ImportError: 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 @@ -127,7 +128,7 @@ class VariableManager: 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 @@ -276,6 +277,38 @@ class VariableManager: 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: @@ -287,10 +320,8 @@ class VariableManager: 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: