Repository: ansible/ansible

Issue:
regression: delegated hosts are searched using ansible_host instead of inventory_hostname then results belong to the wrong host
##### ISSUE TYPE
 - Bug Report

##### COMPONENT NAME
`lib/ansible/plugins/strategy/__init__.py (StrategyBase)`

##### ANSIBLE VERSION
```
ansible-playbook 2.4.0 (devel 8ad04d6348) last updated 2017/06/15 15:42:47 (GMT +200)
```
```
ansible-playbook 2.3.1.0 (stable-2.3 5605091dc1) last updated 2017/06/15 12:47:10 (GMT +200)
```

##### SUMMARY
When a task is delegated on an host which have an `ansible_host` variable defined, the result host is defined using `ansible_host` instead of `inventory_hostname`. This is a regression introduced by e5cd675b38d49b22f1ecc929943d016fb7159856. This regression was backported in 2.3 (7e605f4e84d5280cb0987fe5c1ea7da0a12b127c).

I have created a [unit test](https://github.com/ansible/ansible/commit/c8ff896cf9158d86116c5ce9eb622e03c9f4c947) in order to test any potential fix and prevent future regression, this unit test is available [here](https://github.com/pilou-/ansible/tree/delegated_host_test_case). Please, add the 3 related commits to any fix attempt.

##### STEPS TO REPRODUCE
inventory (`hosts.yml`):
```yaml
all:
  hosts:
    Korutopi:
    Orfeo:
```

Hosts configuration:
```
$ cat host_vars/Korutopi/connection.yml
ansible_host: 10.10.10.10
```

Playbook `test.yml`:
```yaml
---
- hosts: Orfeo
  tasks:
    - name: 'Gather facts from server'
      setup:
        gather_subset: '!all'
      delegate_facts: True
      delegate_to: 'Korutopi'

    - name: 'Check timezone of the server'
      assert:
        that: "{{ hostvars['Korutopi']['ansible_date_time']['tz'] == 'UTC' }}"
        msg: "Server timezone is {{ hostvars['Korutopi']['ansible_date_time']['tz'] }}, expected timezone: UTC"
```

Note that `fact_caching` feature must be disabled while testing this playbook: otherwise the cached facts will interfere.

##### EXPECTED RESULTS
<!--- What did you expect to happen when running the steps above? -->
```
ansible-playbook -i hosts.yml -l Orfeo test.yml 

PLAY [Orfeo] *

TASK [Gathering Facts] *
ok: [Orfeo]

TASK [Gather facts from server] *
ok: [Orfeo -> 10.10.10.10]

TASK [Check timezone of the server] *
ok: [Orfeo] => {
    "changed": false, 
    "msg": "All assertions passed"
}

PLAY RECAP *
Orfeo                      : ok=3    changed=0    unreachable=0    failed=0 
```

##### ACTUAL RESULTS
```
PLAY [Orfeo] *

TASK [Gathering Facts] *
ok: [Orfeo]

TASK [Gather facts from server] *
ok: [Orfeo -> 10.10.10.10]

TASK [Check timezone of the server] *
fatal: [Orfeo]: FAILED! => {"failed": true, "msg": "the field 'args' has an invalid value, which appears to include a variable that is undefined. The error was: 'dict object' has no attribute 'ansible_date_time'\n\nThe error appears to have been in '/home/pilou/admin/ansible/test.yml': line 10, column 7, but may\nbe elsewhere in the file depending on the exact syntax problem.\n\nThe offending line appears to be:\n\n\n    - name: 'Check timezone of the server'\n      ^ here\n"}
	to retry, use: --limit @/home/pilou/admin/ansible/test.retry

PLAY RECAP ***************************************************************************************************************************************************
Orfeo                      : ok=2    changed=0    unreachable=0    failed=1  
```


Discussion:
cc @jimi-c 
`ansible_host` can be overriden by inventory when delegation is calculated, so added new field that should fix this:

```diff
diff --git a/lib/ansible/executor/task_executor.py b/lib/ansible/executor/task_executor.py
index ecdac5ee40..85e334b9a6 100644
--- a/lib/ansible/executor/task_executor.py
+++ b/lib/ansible/executor/task_executor.py
@@ -628,7 +628,7 @@ class TaskExecutor:
         delegated_vars = variables.get('ansible_delegated_vars', dict()).get(self._task.delegate_to, dict()).copy()
         if len(delegated_vars) > 0:
             result["_ansible_delegated_vars"] = dict()
-            for k in ('ansible_host', ):
+            for k in ('ansible_delegated_host', 'ansible_host' ):
                 result["_ansible_delegated_vars"][k] = delegated_vars.get(k)

         # and return
diff --git a/lib/ansible/plugins/strategy/__init__.py b/lib/ansible/plugins/strategy/__init__.py
index 2b10f87b7a..8cb918f50a 100644
--- a/lib/ansible/plugins/strategy/__init__.py
+++ b/lib/ansible/plugins/strategy/__init__.py
@@ -245,7 +245,7 @@ class StrategyBase:
         return host_list

     def get_delegated_hosts(self, result, task):
-        host_name = result.get('_ansible_delegated_vars', {}).get('ansible_host', None)
+        host_name = result.get('_ansible_delegated_vars', {}).get('ansible_delegated_host', None)
         if host_name is not None:
             actual_host = self._inventory.get_host(host_name)
             if actual_host is None:
diff --git a/lib/ansible/vars/manager.py b/lib/ansible/vars/manager.py
index 6747dff377..7dd4e205b0 100644
--- a/lib/ansible/vars/manager.py
+++ b/lib/ansible/vars/manager.py
@@ -524,7 +524,8 @@ class VariableManager:
                 new_port = 5986

             new_delegated_host_vars = dict(
-                ansible_host=delegated_host_name,
+                ansible_delegated_host=delegated_host_name,
+                ansible_host=delegated_host_name, #  not redundant as other sources can change ansible_host
                 ansible_port=new_port,
                 ansible_user=C.DEFAULT_REMOTE_USER,
                 ansible_connection=C.DEFAULT_TRANSPORT,

```

Resolving patch:
diff --git a/lib/ansible/executor/task_executor.py b/lib/ansible/executor/task_executor.py
--- a/lib/ansible/executor/task_executor.py
+++ b/lib/ansible/executor/task_executor.py
@@ -628,7 +628,7 @@ def _evaluate_failed_when_result(result):
         delegated_vars = variables.get('ansible_delegated_vars', dict()).get(self._task.delegate_to, dict()).copy()
         if len(delegated_vars) > 0:
             result["_ansible_delegated_vars"] = dict()
-            for k in ('ansible_host', ):
+            for k in ('ansible_delegated_host', 'ansible_host'):
                 result["_ansible_delegated_vars"][k] = delegated_vars.get(k)
 
         # and return
diff --git a/lib/ansible/plugins/strategy/__init__.py b/lib/ansible/plugins/strategy/__init__.py
--- a/lib/ansible/plugins/strategy/__init__.py
+++ b/lib/ansible/plugins/strategy/__init__.py
@@ -245,7 +245,7 @@ def get_task_hosts(self, iterator, task_host, task):
         return host_list
 
     def get_delegated_hosts(self, result, task):
-        host_name = result.get('_ansible_delegated_vars', {}).get('ansible_host', None)
+        host_name = result.get('_ansible_delegated_vars', {}).get('ansible_delegated_host', None)
         if host_name is not None:
             actual_host = self._inventory.get_host(host_name)
             if actual_host is None:
diff --git a/lib/ansible/vars/manager.py b/lib/ansible/vars/manager.py
--- a/lib/ansible/vars/manager.py
+++ b/lib/ansible/vars/manager.py
@@ -524,7 +524,8 @@ def _get_delegated_vars(self, play, task, existing_variables):
                 new_port = 5986
 
             new_delegated_host_vars = dict(
-                ansible_host=delegated_host_name,
+                ansible_delegated_host=delegated_host_name,
+                ansible_host=delegated_host_name,  # not redundant as other sources can change ansible_host
                 ansible_port=new_port,
                 ansible_user=C.DEFAULT_REMOTE_USER,
                 ansible_connection=C.DEFAULT_TRANSPORT,


Test patch: