Repository: ansible/ansible

Issue:
CallbackModule.v2_runner_on_failed prevents tasks from running, git bisect done
Issue Type: Bug report
Ansible Version: ansible 2.0.0 (stable-2.0 204e27ca66)
Ansible Configuration: No changes from original
Environment: Ubuntu 14.04, hosts mixed Ubuntu 14.04/Debian wheezy.

Summary:
According to git bisect 984729016e3ea20e27c60fa5110cfca19580370f is responsible for introducing the error described below.  When I revert the changes introduced by this commit to lib/ansible/plugins/strategy/free.py, the error disappears.

Unfortunately, I do not understand the changes well enough to provide a patch, but I hope my report helps.

Steps To Reproduce:
Run a playbook with "strategy: free" and a few roles/includes.  I cannot provide further details at this point because this playbook is private.

Expected Results:
No error.

Actual Results:

```
[WARNING]: Error when using <bound method CallbackModule.v2_runner_on_failed of
</home/christoph/ansible-dev/lib/ansible/plugins/callback/default.CallbackModule
object at 0x7fec8bf56690>>: 'NoneType' object has no attribute 'loop'
```

preventing this task from running on the remaining hosts.



Discussion:
Hi @Christoph-D, could you please create a simple reproducer for this? I'm unable to reproduce it with the limited information you've provided.

I posted a test case on https://github.com/Christoph-D/ansible-issue-13370 .
It calls out to 8 remote hosts, but doesn't do anything except printing a debug message after including a role and then a yml file.

The output of "ansible-playbook site.yml" is in the file error.log.

I also observed that the error disappears when I remove the line "strategy: free".

I can reproduce with a simple include, but need at least 7 hosts
play.yml

```
- hosts: all
  gather_facts: False
  strategy: free
  tasks:
    - include: test.yml
```

test.yml

```
- debug: var=inventory_hostname
```

output:

```
 __________________________________________________ 
< TASK [debug msg=included {{inventory_hostname}}] >
 -------------------------------------------------- 
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

ok: [localhost] => {
    "changed": false, 
    "msg": "included localhost"
}
ok: [localhost2] => {
    "changed": false, 
    "msg": "included localhost2"
}
ok: [localhost3] => {
    "changed": false, 
    "msg": "included localhost3"
}
ok: [localhost4] => {
    "changed": false, 
    "msg": "included localhost4"
}
ok: [localhost5] => {
    "changed": false, 
    "msg": "included localhost5"
}
ok: [localhost6] => {
    "changed": false, 
    "msg": "included localhost6"
}
Unexpected Exception: 'NoneType' object has no attribute '_block'
```


Resolving patch:
diff --git a/lib/ansible/executor/play_iterator.py b/lib/ansible/executor/play_iterator.py
--- a/lib/ansible/executor/play_iterator.py
+++ b/lib/ansible/executor/play_iterator.py
@@ -399,6 +399,9 @@ def _insert_tasks_into_state(self, state, task_list):
         if state.fail_state != self.FAILED_NONE:
             return state
 
+        if not task_list:
+            return state
+
         if state.run_state == self.ITERATING_TASKS:
             if state.tasks_child_state:
                 state.tasks_child_state = self._insert_tasks_into_state(state.tasks_child_state, task_list)
diff --git a/lib/ansible/plugins/strategy/free.py b/lib/ansible/plugins/strategy/free.py
--- a/lib/ansible/plugins/strategy/free.py
+++ b/lib/ansible/plugins/strategy/free.py
@@ -145,9 +145,9 @@ def run(self, iterator, play_context):
                 return False
 
             if len(included_files) > 0:
+                all_blocks = dict((host, []) for host in hosts_left)
                 for included_file in included_files:
-                    # included hosts get the task list while those excluded get an equal-length
-                    # list of noop tasks, to make sure that they continue running in lock-step
+                    display.debug("collecting new blocks for %s" % included_file)
                     try:
                         new_blocks = self._load_included_file(included_file, iterator=iterator)
                     except AnsibleError as e:
@@ -156,18 +156,18 @@ def run(self, iterator, play_context):
                         display.warning(str(e))
                         continue
 
-                    display.debug("generating all_blocks data")
-                    all_blocks = dict((host, []) for host in hosts_left)
-                    display.debug("done generating all_blocks data")
                     for new_block in new_blocks:
                         task_vars = self._variable_manager.get_vars(loader=self._loader, play=iterator._play, task=included_file._task)
                         final_block = new_block.filter_tagged_tasks(play_context, task_vars)
                         for host in hosts_left:
                             if host in included_file._hosts:
                                 all_blocks[host].append(final_block)
+                    display.debug("done collecting new blocks for %s" % included_file)
 
+                display.debug("adding all collected blocks from %d included file(s) to iterator" % len(included_files))
                 for host in hosts_left:
                     iterator.add_tasks(host, all_blocks[host])
+                display.debug("done adding collected blocks to iterator")
 
             # pause briefly so we don't spin lock
             time.sleep(0.05)


Test patch: