Allow hostvars delegation (#70331) (#70810)

* ensure hostvars are available on delegation
* also inventory_hostname must point to current host and not delegated one
* fix get_connection since it was still mixing original host vars and delegated ones
* also return connection vars for delegation and non delegation alike
* add test to ensure we have expected usage when directly assigning for non delegated host

(cherry picked from commit 84adaba6f5)
This commit is contained in:
Brian Coca 2020-07-22 21:29:07 -04:00 committed by GitHub
parent 7cdba7c923
commit 8c2754e6d3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 181 additions and 24 deletions

View file

@ -0,0 +1,4 @@
bugfixes:
- ensure delegated vars can resolve hostvars object and access vars from hostvars[inventory_hostname].
- fix issue with inventory_hostname and delegated host vars mixing on connection settings.
- add magic/connection vars updates from delegated host info.

View file

@ -38,6 +38,8 @@ from ansible.utils.vars import combine_vars, isidentifier
display = Display() display = Display()
RETURN_VARS = [x for x in C.MAGIC_VARIABLE_MAPPING.items() if 'become' not in x and '_pass' not in x]
__all__ = ['TaskExecutor'] __all__ = ['TaskExecutor']
@ -510,6 +512,10 @@ class TaskExecutor:
context_validation_error = None context_validation_error = None
try: try:
# TODO: remove play_context as this does not take delegation into account, task itself should hold values
# for connection/shell/become/terminal plugin options to finalize.
# Kept for now for backwards compatiblity and a few functions that are still exclusive to it.
# apply the given task's information to the connection info, # apply the given task's information to the connection info,
# which may override some fields already set by the play or # which may override some fields already set by the play or
# the options specified on the command line # the options specified on the command line
@ -528,7 +534,6 @@ class TaskExecutor:
# a certain subset of variables exist. # a certain subset of variables exist.
self._play_context.update_vars(variables) self._play_context.update_vars(variables)
# FIXME: update connection/shell plugin options
except AnsibleError as e: except AnsibleError as e:
# save the error, which we'll raise later if we don't end up # save the error, which we'll raise later if we don't end up
# skipping this task during the conditional evaluation step # skipping this task during the conditional evaluation step
@ -591,26 +596,28 @@ class TaskExecutor:
variable_params.update(self._task.args) variable_params.update(self._task.args)
self._task.args = variable_params self._task.args = variable_params
if self._task.delegate_to:
# use vars from delegated host (which already include task vars) instead of original host
cvars = variables.get('ansible_delegated_vars', {}).get(self._task.delegate_to, {})
orig_vars = templar.available_variables
else:
# just use normal host vars
cvars = orig_vars = variables
templar.available_variables = cvars
# get the connection and the handler for this execution # get the connection and the handler for this execution
if (not self._connection or if (not self._connection or
not getattr(self._connection, 'connected', False) or not getattr(self._connection, 'connected', False) or
self._play_context.remote_addr != self._connection._play_context.remote_addr): self._play_context.remote_addr != self._connection._play_context.remote_addr):
self._connection = self._get_connection(variables=variables, templar=templar) self._connection = self._get_connection(cvars, templar)
else: else:
# if connection is reused, its _play_context is no longer valid and needs # if connection is reused, its _play_context is no longer valid and needs
# to be replaced with the one templated above, in case other data changed # to be replaced with the one templated above, in case other data changed
self._connection._play_context = self._play_context self._connection._play_context = self._play_context
if self._task.delegate_to: plugin_vars = self._set_connection_options(cvars, templar)
# use vars from delegated host (which already include task vars) instead of original host templar.available_variables = orig_vars
delegated_vars = variables.get('ansible_delegated_vars', {}).get(self._task.delegate_to, {})
orig_vars = templar.available_variables
templar.available_variables = delegated_vars
plugin_vars = self._set_connection_options(delegated_vars, templar)
templar.available_variables = orig_vars
else:
# just use normal host vars
plugin_vars = self._set_connection_options(variables, templar)
# get handler # get handler
self._handler = self._get_action_handler(connection=self._connection, templar=templar) self._handler = self._get_action_handler(connection=self._connection, templar=templar)
@ -788,10 +795,17 @@ class TaskExecutor:
# add the delegated vars to the result, so we can reference them # add the delegated vars to the result, so we can reference them
# on the results side without having to do any further templating # on the results side without having to do any further templating
# also now add conneciton vars results when delegating
if self._task.delegate_to: if self._task.delegate_to:
result["_ansible_delegated_vars"] = {'ansible_delegated_host': self._task.delegate_to} result["_ansible_delegated_vars"] = {'ansible_delegated_host': self._task.delegate_to}
for k in plugin_vars: for k in plugin_vars + RETURN_VARS:
result["_ansible_delegated_vars"][k] = delegated_vars.get(k) if k in cvars and cvars[k] is not None:
result["_ansible_delegated_vars"][k] = cvars[k]
else:
for k in plugin_vars + RETURN_VARS:
if k in cvars and cvars[k] is not None:
result[k] = cvars[k]
# and return # and return
display.debug("attempt loop complete, returning result") display.debug("attempt loop complete, returning result")
return result return result
@ -877,17 +891,12 @@ class TaskExecutor:
"Use `ansible-doc -t become -l` to list available plugins." % name) "Use `ansible-doc -t become -l` to list available plugins." % name)
return become return become
def _get_connection(self, variables, templar): def _get_connection(self, cvars, templar):
''' '''
Reads the connection property for the host, and returns the Reads the connection property for the host, and returns the
correct connection object from the list of connection plugins correct connection object from the list of connection plugins
''' '''
if self._task.delegate_to is not None:
cvars = variables.get('ansible_delegated_vars', {}).get(self._task.delegate_to, {})
else:
cvars = variables
# use magic var if it exists, if not, let task inheritance do it's thing. # use magic var if it exists, if not, let task inheritance do it's thing.
if cvars.get('ansible_connection') is not None: if cvars.get('ansible_connection') is not None:
self._play_context.connection = templar.template(cvars['ansible_connection']) self._play_context.connection = templar.template(cvars['ansible_connection'])
@ -949,15 +958,14 @@ class TaskExecutor:
display.vvvv('attempting to start connection', host=self._play_context.remote_addr) display.vvvv('attempting to start connection', host=self._play_context.remote_addr)
display.vvvv('using connection plugin %s' % connection.transport, host=self._play_context.remote_addr) display.vvvv('using connection plugin %s' % connection.transport, host=self._play_context.remote_addr)
options = self._get_persistent_connection_options(connection, variables, templar) options = self._get_persistent_connection_options(connection, cvars, templar)
socket_path = start_connection(self._play_context, options, self._task._uuid) socket_path = start_connection(self._play_context, options, self._task._uuid)
display.vvvv('local domain socket path is %s' % socket_path, host=self._play_context.remote_addr) display.vvvv('local domain socket path is %s' % socket_path, host=self._play_context.remote_addr)
setattr(connection, '_socket_path', socket_path) setattr(connection, '_socket_path', socket_path)
return connection return connection
def _get_persistent_connection_options(self, connection, variables, templar): def _get_persistent_connection_options(self, connection, final_vars, templar):
final_vars = combine_vars(variables, variables.get('ansible_delegated_vars', dict()).get(self._task.delegate_to, dict()))
option_vars = C.config.get_plugin_vars('connection', connection._load_name) option_vars = C.config.get_plugin_vars('connection', connection._load_name)
plugin = connection._sub_plugin plugin = connection._sub_plugin

View file

@ -602,8 +602,9 @@ class VariableManager:
host=delegated_host, host=delegated_host,
task=task, task=task,
include_delegate_to=False, include_delegate_to=False,
include_hostvars=False, include_hostvars=True,
) )
delegated_host_vars[delegated_host_name]['inventory_hostname'] = vars_copy.get('inventory_hostname')
_ansible_loop_cache = None _ansible_loop_cache = None
if has_loop and cache_items: if has_loop and cache_items:

View file

@ -0,0 +1,76 @@
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
DOCUMENTATION = '''
connection: fakelocal
short_description: dont execute anything
description:
- This connection plugin just verifies parameters passed in
author: ansible (@core)
version_added: histerical
options:
password:
description: Authentication password for the C(remote_user). Can be supplied as CLI option.
vars:
- name: ansible_password
remote_user:
description:
- User name with which to login to the remote server, normally set by the remote_user keyword.
ini:
- section: defaults
key: remote_user
vars:
- name: ansible_user
'''
from ansible.errors import AnsibleConnectionFailure
from ansible.plugins.connection import ConnectionBase
from ansible.utils.display import Display
display = Display()
class Connection(ConnectionBase):
''' Local based connections '''
transport = 'fakelocal'
has_pipelining = True
def __init__(self, *args, **kwargs):
super(Connection, self).__init__(*args, **kwargs)
self.cwd = None
def _connect(self):
''' verify '''
if self.get_option('remote_user') == 'invaliduser' and self.get_option('password') == 'badpassword':
raise AnsibleConnectionFailure('Got invaliduser and badpassword')
if not self._connected:
display.vvv(u"ESTABLISH FAKELOCAL CONNECTION FOR USER: {0}".format(self._play_context.remote_user), host=self._play_context.remote_addr)
self._connected = True
return self
def exec_command(self, cmd, in_data=None, sudoable=True):
''' run a command on the local host '''
super(Connection, self).exec_command(cmd, in_data=in_data, sudoable=sudoable)
return 0, '{"msg": "ALL IS GOOD"}', ''
def put_file(self, in_path, out_path):
''' transfer a file from local to local '''
super(Connection, self).put_file(in_path, out_path)
def fetch_file(self, in_path, out_path):
''' fetch a file from local to local -- for compatibility '''
super(Connection, self).fetch_file(in_path, out_path)
def close(self):
''' terminate the connection; nothing to do here '''
self._connected = False

View file

@ -0,0 +1,64 @@
- name: ensure delegated host has hostvars available for resolving connection
hosts: testhost
gather_facts: false
tasks:
- name: ensure delegated host uses current host as inventory_hostname
assert:
that:
- inventory_hostname == ansible_delegated_vars['testhost5']['inventory_hostname']
delegate_to: testhost5
- name: Set info on inventory_hostname
set_fact:
login: invaliduser
mypass: badpassword
- name: test fakelocal
command: ls
ignore_unreachable: True
ignore_errors: True
remote_user: "{{ login }}"
vars:
ansible_password: "{{ mypass }}"
ansible_connection: fakelocal
register: badlogin
- name: ensure we skipped do to unreachable and not templating error
assert:
that:
- badlogin is unreachable
- name: delegate but try to use inventory_hostname data directly
command: ls
delegate_to: testhost5
ignore_unreachable: True
ignore_errors: True
remote_user: "{{ login }}"
vars:
ansible_password: "{{ mypass }}"
register: badlogin
- name: ensure we skipped do to unreachable and not templating error
assert:
that:
- badlogin is not unreachable
- badlogin is failed
- "'undefined' in badlogin['msg']"
- name: delegate ls to testhost5 as it uses ssh while testhost is local, but use vars from testhost
command: ls
remote_user: "{{ hostvars[inventory_hostname]['login'] }}"
delegate_to: testhost5
ignore_unreachable: True
ignore_errors: True
vars:
ansible_password: "{{ hostvars[inventory_hostname]['mypass'] }}"
register: badlogin
- name: ensure we skipped do to unreachable and not templating error
assert:
that:
- badlogin is unreachable
- badlogin is not failed
- "'undefined' not in badlogin['msg']"

View file

@ -3,6 +3,7 @@ testhost ansible_connection=local
testhost2 ansible_connection=local testhost2 ansible_connection=local
testhost3 ansible_ssh_host=127.0.0.3 testhost3 ansible_ssh_host=127.0.0.3
testhost4 ansible_ssh_host=127.0.0.4 testhost4 ansible_ssh_host=127.0.0.4
testhost5 ansible_connection=fakelocal
[all:vars] [all:vars]
ansible_python_interpreter="{{ ansible_playbook_python }}" ansible_python_interpreter="{{ ansible_playbook_python }}"

View file

@ -59,6 +59,7 @@ ansible-playbook test_delegate_to_loop_caching.yml -i inventory -v "$@"
# ensure we are using correct settings when delegating # ensure we are using correct settings when delegating
ANSIBLE_TIMEOUT=3 ansible-playbook delegate_vars_hanldling.yml -i inventory -v "$@" ANSIBLE_TIMEOUT=3 ansible-playbook delegate_vars_hanldling.yml -i inventory -v "$@"
ansible-playbook has_hostvars.yml -i inventory -v "$@"
# test ansible_x_interpreter # test ansible_x_interpreter
# python # python

View file

@ -18,6 +18,8 @@
register: setup_results register: setup_results
delegate_to: testhost4 delegate_to: testhost4
- debug: var=setup_results
- assert: - assert:
that: that:
- '"127.0.0.4" in setup_results.ansible_facts.ansible_env["SSH_CONNECTION"]' - '"127.0.0.4" in setup_results.ansible_facts.ansible_env["SSH_CONNECTION"]'