Repository: ansible/ansible

Issue:
ignore_errors always ignore failures when variable is used to control it
<!---
Verify first that your issue/request is not already reported on GitHub.
Also test if the latest release, and master branch are affected too.
-->

##### ISSUE TYPE
<!--- Pick one below and delete the rest: -->
 - Bug Report

##### COMPONENT NAME
<!--- Name of the module/plugin/task/feature -->
Ansible-core

##### ANSIBLE VERSION
<!--- Paste verbatim output from "ansible --version" between quotes below -->
```
ansible 2.4.1.0
  config file = None
  configured module search path = [u'/home/guenthgr/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/local/lib/python2.7/dist-packages/ansible
  executable location = /usr/local/bin/ansible
  python version = 2.7.13 (default, Jan 19 2017, 14:48:08) [GCC 6.3.0 20170118]
```

##### CONFIGURATION
`ansible-config dump --only-changed` returns nothing, but I just have a vanilla ansible.

##### OS / ENVIRONMENT
<!---
Mention the OS you are running Ansible from, and the OS you are
managing, or say "N/A" for anything that is not platform-specific.
Also mention the specific version of what you are trying to control,
e.g. if this is a network bug the version of firmware on the network device.
-->
4.9.0-3-amd64 #1 SMP Debian 4.9.30-2+deb9u5 (2017-09-19) x86_64 GNU/Linux

##### SUMMARY
When I use the 'ignore_errors' options on a task, this works if the boolean is hardcoded like:
```yaml
ignore_errors: false
```

But as soon as the option is set via a variable, that doesn't work any longer:

```yaml
ignore_errors: "{{ ignore_assertion_errors }}"
or
ignore_errors: ignore_assertion_errors
```
In the latter case, this is always handled as `true` so the assertion failures are ignored.

##### STEPS TO REPRODUCE
<!---
For bugs, show exactly how to reproduce the problem, using a minimal test-case.
For new features, show how the feature would be used.
-->

<!--- Paste example playbooks or commands between quotes below -->
```yaml
---
- hosts: 127.0.0.1
  vars:
    ignore_assertion_errors: false
  tasks:
    - name: With variable
      assert:
        that: item < 2
        msg: "{{item}} isn't < 2"
      ignore_errors: ignore_assertion_errors
      with_items:
        - 1
        - 2

    - name: With 'false' as hardcoded boolean
      assert:
        that: item < 2
        msg: "{{item}} isn't < 2"
      ignore_errors: false
      with_items:
        - 1
        - 2

```

<!--- You can also paste gist.github.com links for larger files -->

##### EXPECTED RESULTS
Expect the first task to fail. Second shouldn't even run.

##### ACTUAL RESULTS
<!--- What actually happened? If possible run with extra verbosity (-vvvv) -->
First task fails but the failure is ignored.
Second task than fails as expected.

<!--- Paste verbatim command output between quotes below -->
```
TASK [Gathering Facts] **********************************************************************************************************************************
ok: [127.0.0.1]

TASK [With variable] ************************************************************************************************************************************
ok: [127.0.0.1] => (item=1) => {
    "changed": false, 
    "failed": false, 
    "item": 1, 
    "msg": "All assertions passed"
}
failed: [127.0.0.1] (item=2) => {
    "assertion": "item < 2", 
    "changed": false, 
    "evaluated_to": false, 
    "failed": true, 
    "item": 2, 
    "msg": "2 isn't < 2"
}
...ignoring

TASK [With 'false' as hardcoded boolean] ****************************************************************************************************************
ok: [127.0.0.1] => (item=1) => {
    "changed": false, 
    "failed": false, <!--- What did you expect to happen when running the steps above? -->
    "item": 1, 
    "msg": "All assertions passed"
}
failed: [127.0.0.1] (item=2) => {
    "assertion": "item < 2", 
    "changed": false, 
    "evaluated_to": false, 
    "failed": true, 
    "item": 2, 
    "msg": "2 isn't < 2"
}

```



Discussion:
Possible Misunderstanding
=========================

Hi!

Thanks very much for your submission to Ansible.  It sincerely means a lot to us.

We believe the ticket you have filed is being somewhat misunderstood, as one thing works a little differently than stated.

You are not using a variable, you are using a string, which is always `True`.
This is what you want:
``` ignore_errors: "{{ignore_assertion_errors|bool}}"```

In the future, this might be a topic more well suited for the user list, which you can also post here if you'd like some more help with the above.
   
   * https://groups.google.com/forum/#!forum/ansible-project

Thank you once again for this and your interest in Ansible!
@bcoca but this also happens when I use:

```
vars:
    ignore_assertion_errors: false

tasks:
   ...
   ignore_errors: ignore_assertion_errors
```

I tried the following combinations:

```
ignore_errors: "{{ignore_assertion_errors}}"
ignore_errors: ignore_assertion_errors
ignore_errors: "{{ignore_assertion_errors|bool}}"
ignore_errors: ignore_assertion_errors|bool
```

Nothing works. 
@bcoca please repoen this. Even in the google mailing list, this can't be solved:

https://groups.google.com/forum/#!msg/ansible-project/EwpEF1Gfy-M/gSvplMK9AwAJ
verified, any value that is not a direct YAML boolean False is interpreted as True

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
@@ -100,9 +100,14 @@ def run(self):
                     for item in item_results:
                         if 'changed' in item and item['changed'] and not res.get('changed'):
                             res['changed'] = True
-                        if 'failed' in item and item['failed'] and not res.get('failed'):
-                            res['failed'] = True
-                            res['msg'] = 'One or more items failed'
+                        if 'failed' in item and item['failed']:
+                            item_ignore = item.pop('_ansible_ignore_errors')
+                            if not res.get('failed'):
+                                res['failed'] = True
+                                res['msg'] = 'One or more items failed'
+                                self._task.ignore_errors = item_ignore
+                            elif self._task.ignore_errors and not item_ignore:
+                                self._task.ignore_errors = item_ignore
 
                         # ensure to accumulate these
                         for array in ['warnings', 'deprecations']:
@@ -295,6 +300,7 @@ def _run_loop(self, items):
             (self._task, tmp_task) = (tmp_task, self._task)
             (self._play_context, tmp_play_context) = (tmp_play_context, self._play_context)
             res = self._execute(variables=task_vars)
+            task_fields = self._task.dump_attrs()
             (self._task, tmp_task) = (tmp_task, self._task)
             (self._play_context, tmp_play_context) = (tmp_play_context, self._play_context)
 
@@ -302,6 +308,7 @@ def _run_loop(self, items):
             # to the list of results
             res[loop_var] = item
             res['_ansible_item_result'] = True
+            res['_ansible_ignore_errors'] = task_fields.get('ignore_errors')
 
             if label is not None:
                 templar = Templar(loader=self._loader, shared_loader_obj=self._shared_loader_obj, variables=self._job_vars)
@@ -312,7 +319,7 @@ def _run_loop(self, items):
                     self._host.name,
                     self._task._uuid,
                     res,
-                    task_fields=self._task.dump_attrs(),
+                    task_fields=task_fields,
                 ),
                 block=False,
             )


Test patch: