Repository: ansible/ansible

Issue:
import_role with delegate_to does not run handlers on the delegated to host
##### ISSUE TYPE
<!--- Pick one below and delete the rest -->
 - Bug Report

##### COMPONENT NAME
import_role

##### ANSIBLE VERSION
<!--- Paste verbatim output from "ansible --version" between quotes below -->
```
ansible 2.6.0 (devel 8743b20c8e) last updated 2018/02/13 17:38:47 (GMT +200)
  config file = /home/nikos/Projects/ansible-demo/ansible.cfg
  configured module search path = ['/home/nikos/Projects/ansible-demo/ansible/lib/modules']
  ansible python module location = /home/nikos/Projects/ansible/lib/ansible
  executable location = /home/nikos/Projects/ansible/bin/ansible
  python version = 3.6.4 (default, Jan  5 2018, 02:35:40) [GCC 7.2.1 20171224]
```

##### CONFIGURATION
<!---
ANSIBLE_FORCE_COLOR(/home/nikos/Projects/ansible-demo/ansible.cfg) = True
ANSIBLE_NOCOWS(/home/nikos/Projects/ansible-demo/ansible.cfg) = True
ANSIBLE_PIPELINING(/home/nikos/Projects/ansible-demo/ansible.cfg) = True
ANY_ERRORS_FATAL(/home/nikos/Projects/ansible-demo/ansible.cfg) = True
COLOR_DEBUG(/home/nikos/Projects/ansible-demo/ansible.cfg) = magenta
DEFAULT_CALLBACK_PLUGIN_PATH(/home/nikos/Projects/ansible-demo/ansible.cfg) = ['/home/nikos/Projects/ansible-demo/ansible/lib/plugins/callback']
DEFAULT_CALLBACK_WHITELIST(/home/nikos/Projects/ansible-demo/ansible.cfg) = ['timer', 'log_plays_overwrite']
DEFAULT_EXECUTABLE(/home/nikos/Projects/ansible-demo/ansible.cfg) = /bin/bash
DEFAULT_FILTER_PLUGIN_PATH(/home/nikos/Projects/ansible-demo/ansible.cfg) = ['/home/nikos/Projects/ansible-demo/ansible/lib/plugins/filter']
DEFAULT_GATHERING(/home/nikos/Projects/ansible-demo/ansible.cfg) = explitic
DEFAULT_GATHER_SUBSET(/home/nikos/Projects/ansible-demo/ansible.cfg) = !hardware,!ohai,!facter
DEFAULT_HOST_LIST(/home/nikos/Projects/ansible-demo/ansible.cfg) = ['/home/nikos/Projects/ansible-demo/inventory.ini']
DEFAULT_INVENTORY_PLUGIN_PATH(/home/nikos/Projects/ansible-demo/ansible.cfg) = ['/home/nikos/Projects/ansible-demo/ansible/lib/plugins/inventory']
DEFAULT_LOOKUP_PLUGIN_PATH(/home/nikos/Projects/ansible-demo/ansible.cfg) = ['/home/nikos/Projects/ansible-demo/ansible/lib/plugins/lookup']
DEFAULT_MANAGED_STR(/home/nikos/Projects/ansible-demo/ansible.cfg) = Ansible managed (do not edit, changes may be overwritten)
DEFAULT_MODULE_PATH(/home/nikos/Projects/ansible-demo/ansible.cfg) = ['/home/nikos/Projects/ansible-demo/ansible/lib/modules']
DEFAULT_MODULE_UTILS_PATH(/home/nikos/Projects/ansible-demo/ansible.cfg) = ['/home/nikos/Projects/ansible-demo/ansible/lib/module_utils']
DEFAULT_ROLES_PATH(/home/nikos/Projects/ansible-demo/ansible.cfg) = ['/home/nikos/Projects/ansible-demo/roles']
DEFAULT_STDOUT_CALLBACK(/home/nikos/Projects/ansible-demo/ansible.cfg) = dense_with_book
DEFAULT_UNDEFINED_VAR_BEHAVIOR(/home/nikos/Projects/ansible-demo/ansible.cfg) = True
DEFAULT_VARS_PLUGIN_PATH(/home/nikos/Projects/ansible-demo/ansible.cfg) = ['/home/nikos/Projects/ansible-demo/ansible/lib/plugins/vars']
DEPRECATION_WARNINGS(/home/nikos/Projects/ansible-demo/ansible.cfg) = False
DISPLAY_ARGS_TO_STDOUT(/home/nikos/Projects/ansible-demo/ansible.cfg) = True
DISPLAY_SKIPPED_HOSTS(/home/nikos/Projects/ansible-demo/ansible.cfg) = False
HOST_KEY_CHECKING(/home/nikos/Projects/ansible-demo/ansible.cfg) = False
RETRY_FILES_ENABLED(/home/nikos/Projects/ansible-demo/ansible.cfg) = False
USE_PERSISTENT_CONNECTIONS(/home/nikos/Projects/ansible-demo/ansible.cfg) = True
-->

##### OS / ENVIRONMENT
<!---
Arch Linux latest
-->

##### SUMMARY
I am running import_role against a just-above created instance on google cloud. The role runs fine on the distant host, but the role handlers (just a win_reboot task), runs against my localhost.

##### STEPS TO REPRODUCE
This is the tasks that I use:

<!--- Paste example playbooks or commands between quotes below -->
```yaml
- gce:
    name: custom-windows-image
    disk_auto_delete: no
    disk_size: "{{ win_server.disk_size }}"
    external_ip: ephemeral
    image_family: windows-2016
    ip_forward: yes
    machine_type: n1-highcpu-4
    metadata: {}
    # TODO change this to 'default'
    network: "{{ project_facts.network.name }}"
    subnetwork: "{{ win_server.subnet }}"
    zone: "{{ win_server.zone }}"
    tags: custom-windows-image
  register: win_instance
- command: >-
    gcloud beta compute reset-windows-password
      --quiet
      --user="{{ win_server.user }}"
      --zone="{{ win_server.zone }}"
      custom-windows-image
  register: win_instance_password
  until: win_instance_password.rc != 1
  failed_when: false
  retries: 5
- add_host: &winrm
    hostname: win_instance
    ansible_host: "{{ win_instance.instance_data[0].public_ip }}"
    ansible_connection: winrm
    ansible_port: 5986
    ansible_winrm_server_cert_validation: ignore
    ansible_winrm_transport: ntlm
    ansible_user: "{{ win_server.user }}"
    ansible_password: "{{ win_instance_password.stdout_lines[1] | regex_replace('password:[\\s]*([^s]*)', '\\1') }}"
- gce_firewall:
    name: custom-windows-image
    network: "{{ project_facts.network.name }}"
    allowed: udp:3389;tcp:3389;tcp:5986;icmp
    src_ranges: "{{ project_facts.local.address }}/32"
    target_tags: custom-windows-image
- wait_for_connection:
  delegate_to: custom-windows-image
  vars: *winrm
- import_role:
    name: ansible-windows-setup
    private: yes
  vars:
    windows_setup_hostname: "{{ win_server.name }}"
  delegate_to: win_instance
```

<!--- You can also paste gist.github.com links for larger files -->

##### EXPECTED RESULTS
The role handler should be executed against the windows instance and reboot it.

##### ACTUAL RESULTS
<!--- What actually happened? If possible run with extra verbosity (-vvvv) -->

<!--- Paste verbatim command output between quotes below -->
```
RUNNING HANDLER [ansible-windows-setup : Reboot windows] ************************************
<127.0.0.1> ESTABLISH LOCAL CONNECTION FOR USER: nikos
<127.0.0.1> EXEC (Get-WmiObject -ClassName Win32_OperatingSystem).LastBootUpTime
fatal: [localhost]: FAILED! => {
    "changed": false,
    "failed": true,
    "msg": "win_reboot: failed to get host uptime info, rc: 1, stdout: b'', stderr: b\"/bin/bash: -c: line 0: syntax error near unexpected token `.LastBootUpTime'\\n/bin/bash: -c: line 0: `(Get-WmiObject -ClassName Win32_OperatingSystem).LastBootUpTime'\\n\"",
    "reboot": false
}
META: ran handlers
```



Discussion:
Files identified in the description:
* [lib/ansible/modules/utilities/logic/import_role.py](https://github.com/ansible/ansible/blob/devel/lib/ansible/modules/utilities/logic/import_role.py)

If these files are inaccurate, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
More simplistic reproducer:

```
.
├── 36518.yml
└── roles
    └── foo
        ├── handlers
        │   └── main.yml
        └── tasks
            └── main.yml
```

36518.yml
```
---
- hosts: all
  gather_facts: false
  tasks:
    - import_role:
        name: foo
      delegate_to: localhost
```

roles/foo/handlers/main.yml
```
- name: foo handler
  command: hostname
```

roles/foo/tasks/main.yml
```
---
- command: hostname
  notify:
    - foo handler
```

The handler is not delegated.
@ansibot labels it as affects 2.6 but I understand this is also happens in 2.5 and 2.5.1 too

Resolving patch:
diff --git a/lib/ansible/playbook/helpers.py b/lib/ansible/playbook/helpers.py
--- a/lib/ansible/playbook/helpers.py
+++ b/lib/ansible/playbook/helpers.py
@@ -333,10 +333,10 @@ def load_list_of_tasks(ds, play, block=None, role=None, task_include=None, use_h
 
                     # uses compiled list from object
                     blocks, _ = ir.get_block_list(variable_manager=variable_manager, loader=loader)
-                    t = task_list.extend(blocks)
+                    task_list.extend(blocks)
                 else:
                     # passes task object itself for latter generation of list
-                    t = task_list.append(ir)
+                    task_list.append(ir)
             else:
                 if use_handlers:
                     t = Handler.load(task_ds, block=block, role=role, task_include=task_include, variable_manager=variable_manager, loader=loader)
diff --git a/lib/ansible/playbook/role_include.py b/lib/ansible/playbook/role_include.py
--- a/lib/ansible/playbook/role_include.py
+++ b/lib/ansible/playbook/role_include.py
@@ -96,6 +96,8 @@ def get_block_list(self, play=None, variable_manager=None, loader=None):
 
         # updated available handlers in play
         handlers = actual_role.get_handler_blocks(play=myplay)
+        for h in handlers:
+            h._parent = self
         myplay.handlers = myplay.handlers + handlers
         return blocks, handlers
 


Test patch:
diff --git a/test/integration/targets/include_import/role/test_import_role.yml b/test/integration/targets/include_import/role/test_import_role.yml
--- a/test/integration/targets/include_import/role/test_import_role.yml
+++ b/test/integration/targets/include_import/role/test_import_role.yml
@@ -136,3 +136,8 @@
         - name: Include role inside always
           import_role:
             name: role3
+
+    - name: Test delegate_to handler is delegated
+      import_role:
+        name: delegated_handler
+      delegate_to: localhost
diff --git a/test/integration/targets/include_import/roles/delegated_handler/handlers/main.yml b/test/integration/targets/include_import/roles/delegated_handler/handlers/main.yml
new file mode 100644
--- /dev/null
+++ b/test/integration/targets/include_import/roles/delegated_handler/handlers/main.yml
@@ -0,0 +1,4 @@
+- name: delegated assert handler
+  assert:
+    that:
+      - ansible_delegated_vars is defined
diff --git a/test/integration/targets/include_import/roles/delegated_handler/tasks/main.yml b/test/integration/targets/include_import/roles/delegated_handler/tasks/main.yml
new file mode 100644
--- /dev/null
+++ b/test/integration/targets/include_import/roles/delegated_handler/tasks/main.yml
@@ -0,0 +1,3 @@
+- command: "true"
+  notify:
+    - delegated assert handler