Repository: ansible/ansible

Issue:
Quoted text literals being converted to boolean by set_fact
##### SUMMARY
When setting a variable to a string in quotes or double quotes, ansible is not treating it like a string (as advertised) and treating it like a boolean.
(Advertised here:  https://docs.ansible.com/ansible/latest/reference_appendices/YAMLSyntax.html )
under the 'gotchas' section:
```
Boolean conversion is helpful, but this can be a problem when you want a literal yes or other boolean values as a string. In these cases just use quotes:

non_boolean: "yes"
other_string: "False"
```

##### ISSUE TYPE
 - Bug Report

##### COMPONENT NAME
When using the template feature specifically, but appears to happen at the level of the set_fact module actually.

##### ANSIBLE VERSION
```
ansible 2.5.0
  config file = /etc/ansible/ansible.cfg
  configured module search path = ['/root/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/local/lib/python3.6/dist-packages/ansible
  executable location = /usr/local/bin/ansible
  python version = 3.6.5 (default, Apr  1 2018, 05:46:30) [GCC 7.3.0]
```

##### CONFIGURATION
DEFAULT_EXECUTABLE(/etc/ansible/ansible.cfg) = /bin/bash
DEFAULT_HOST_LIST(/etc/ansible/ansible.cfg) = ['/etc/ansible/inventory/hosts']
DEFAULT_MANAGED_STR(/etc/ansible/ansible.cfg) = # This file is managed by Ansible, changes made here will be overwritten
HOST_KEY_CHECKING(/etc/ansible/ansible.cfg) = False
PERSISTENT_CONNECT_TIMEOUT(/etc/ansible/ansible.cfg) = 30

##### OS / ENVIRONMENT
"Ubuntu 18.04 LTS"
4.15.0-22-generic
x86_64 GNU/Linux

##### STEPS TO REPRODUCE

vars.yaml
```vars.yaml:
---
need_svcgssd: "no"
```

test.yaml
```test.yaml
---
# tasks file for test
- name: set a fact to yes or no
  tags: test
  changed_when: false
  set_fact:
    this_is_string: "yes"
    this_is_not_string: yes
    this_is_also_string: "{{ need_svcgssd }}"
    this_is_another_string: !!str "{% set thing = '' + need_svcgssd + '' %}{{ thing }}"
    this_is_more_strings: '{{ need_svcgssd + "" }}'

- name: echo the facts
  tags: test
  changed_when: false
  debug:
    msg: "fact this_is_string is: {{ this_is_string }}"

- name: echo the facts
  tags: test
  changed_when: false
  debug:
    msg: "fact this_is_not_string is: {{ this_is_not_string }}"

- name: echo the facts
  tags: test
  changed_when: false
  debug:
    msg: "fact this_is_also_string is: {{ this_is_also_string }}"

- name: echo the facts
  tags: test
  changed_when: false
  debug:
    msg: "fact this_is_another_string is: {{ this_is_another_string }}"

- name: echo the facts
  tags: test
  changed_when: false
  debug:
    msg: "fact this_is_more_strings is: {{ this_is_more_strings }}"

- name: without set_fact
  tags: test
  changed_when: false
  debug:
    msg: "fact need_svcgssd is: {{ need_svcgssd }}"

```

##### EXPECTED RESULTS
fact this_is_string is: yes
fact this_is_not_string is: True
fact this_is_also_string is: no
fact this_is_another_string is: no
fact this_is_more_strings is: no
fact need_svcgssd is: no

##### ACTUAL RESULTS
```
TASK [test : set a fact to yes or no] ***********************************************************************************************************************
ok: [server]

TASK [test : echo the facts] ***********************************************************************************************************************
ok: [server] => {
    "msg": "fact this_is_string is: True"
}

TASK [test : echo the facts] ***********************************************************************************************************************
ok: [server] => {
    "msg": "fact this_is_not_string is: True"
}

TASK [test : echo the facts] ***********************************************************************************************************************
ok: [server] => {
    "msg": "fact this_is_also_string is: False"
}

TASK [test : echo the facts] ***********************************************************************************************************************
ok: [server] => {
    "msg": "fact this_is_another_string is: False"
}

TASK [test : echo the facts] ***********************************************************************************************************************
ok: [server] => {
    "msg": "fact this_is_more_strings is: False"
}

TASK [test : without set_fact] ***********************************************************************************************************************
ok: [server] => {
    "msg": "fact need_svcgssd is: no"
}

```

I can't even find a workaround. Using ```var: !!str "no"``` doesn't change anything. In nfs-kernel-server, my values can't be "True" or "False", but literally "yes"  and "no".
 


Discussion:
Files identified in the description:
* [lib/ansible/modules/utilities/logic/set_fact.py](https://github.com/ansible/ansible/blob/devel/lib/ansible/modules/utilities/logic/set_fact.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 --->
cc @dagwieers
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
You can see from the last msg that ansible is properly setting the string to a literal by itself (need_svcgssd is no), but set_fact is not treating string literals like string literals. Everything changes when I try to set_fact.

Since I have more than one machine to play the role on, I'm setting the host's specific config via a dictionary where the hostname: {var1: "yes", var2: "--switch", var3: "no"} is overriding the default values via a set_fact with_items kind of thing. This is preventing me from having a role for this task that allows me to plug values into a list, dictionary, or otherwise.
This behavior has existed since the 2.0 release, and was explicitly added to the `set_fact` plugin in https://github.com/ansible/ansible/commit/a6d6a89ad1d460f8d29589128549e2bac1e54db8: https://github.com/ansible/ansible/blob/a6d6a89ad1d460f8d29589128549e2bac1e54db8/v2/ansible/plugins/action/set_fact.py#L33-L34

As such, we cannot change the default behavior.

Maybe we can make it conditional on `ANSIBLE_JINJA2_NATIVE` and not do this type casting when `ANSIBLE_JINJA2_NATIVE` is enabled.

```diff
diff --git a/lib/ansible/plugins/action/set_fact.py b/lib/ansible/plugins/action/set_fact.py
index 167220fe0b..d7fe573c1a 100644
--- a/lib/ansible/plugins/action/set_fact.py
+++ b/lib/ansible/plugins/action/set_fact.py
@@ -23,6 +23,8 @@ from ansible.module_utils.parsing.convert_bool import boolean
 from ansible.plugins.action import ActionBase
 from ansible.utils.vars import isidentifier
 
+import ansible.constants as C
+
 
 class ActionModule(ActionBase):
 
@@ -49,7 +51,7 @@ class ActionModule(ActionBase):
                                      "letters, numbers and underscores." % k)
                     return result
 
-                if isinstance(v, string_types) and v.lower() in ('true', 'false', 'yes', 'no'):
+                if not C.DEFAULT_JINJA2_NATIVE and isinstance(v, string_types) and v.lower() in ('true', 'false', 'yes', 'no'):
                     v = boolean(v, strict=False)
                 facts[k] = v
```
That may work to keep in in agreement with the YAML documentation.

Otherwise, why not respect the `!!str` declaration?
I do not immediately have the historical context of why this change was made, but my assumption is that it was changed to address the issue that jinja2 native types was introduced for, in that the interaction between jinja2 and ansible resulted in lossy typing.

We don't have full life cycle info of whether `!!str` was supplied for a variable, so there is no context for us to work off of.
Ah, got it.
I've stumbled upon the same behaviour recently as well and I consider it a **bug**. It is one of those unexpected 'gotchas' of Ansible that can break code in subtle ways.

It would be great if somebody could shed some more light on the exact problem with jinja2 types and maybe we can find a different solution for the problem.

Resolving patch:
diff --git a/lib/ansible/plugins/action/set_fact.py b/lib/ansible/plugins/action/set_fact.py
--- a/lib/ansible/plugins/action/set_fact.py
+++ b/lib/ansible/plugins/action/set_fact.py
@@ -23,6 +23,8 @@
 from ansible.plugins.action import ActionBase
 from ansible.utils.vars import isidentifier
 
+import ansible.constants as C
+
 
 class ActionModule(ActionBase):
 
@@ -49,7 +51,7 @@ def run(self, tmp=None, task_vars=None):
                                      "letters, numbers and underscores." % k)
                     return result
 
-                if isinstance(v, string_types) and v.lower() in ('true', 'false', 'yes', 'no'):
+                if not C.DEFAULT_JINJA2_NATIVE and isinstance(v, string_types) and v.lower() in ('true', 'false', 'yes', 'no'):
                     v = boolean(v, strict=False)
                 facts[k] = v
 


Test patch:
diff --git a/test/integration/targets/set_fact/runme.sh b/test/integration/targets/set_fact/runme.sh
--- a/test/integration/targets/set_fact/runme.sh
+++ b/test/integration/targets/set_fact/runme.sh
@@ -17,3 +17,7 @@ export ANSIBLE_CACHE_PLUGIN=jsonfile ANSIBLE_CACHE_PLUGIN_CONNECTION="${MYTMPDIR
 ansible-playbook -i ../../inventory "$@" set_fact_cached_1.yml
 ansible-playbook -i ../../inventory "$@" set_fact_cached_2.yml
 ansible-playbook -i ../../inventory --flush-cache "$@" set_fact_no_cache.yml
+
+# Test boolean conversions in set_fact
+ansible-playbook -v set_fact_bool_conv.yml
+ANSIBLE_JINJA2_NATIVE=1 ansible-playbook -v set_fact_bool_conv_jinja2_native.yml
diff --git a/test/integration/targets/set_fact/set_fact_bool_conv.yml b/test/integration/targets/set_fact/set_fact_bool_conv.yml
new file mode 100644
--- /dev/null
+++ b/test/integration/targets/set_fact/set_fact_bool_conv.yml
@@ -0,0 +1,20 @@
+- hosts: localhost
+  gather_facts: false
+  vars:
+    string_var: "no"
+  tasks:
+    - set_fact:
+        this_is_string: "yes"
+        this_is_not_string: yes
+        this_is_also_string: "{{ string_var }}"
+        this_is_another_string: !!str "{% set thing = '' + string_var + '' %}{{ thing }}"
+        this_is_more_strings: '{{ string_var + "" }}'
+
+    - assert:
+        that:
+          - string_var == 'no'
+          - this_is_string == True
+          - this_is_not_string == True
+          - this_is_also_string == False
+          - this_is_another_string == False
+          - this_is_more_strings == False
diff --git a/test/integration/targets/set_fact/set_fact_bool_conv_jinja2_native.yml b/test/integration/targets/set_fact/set_fact_bool_conv_jinja2_native.yml
new file mode 100644
--- /dev/null
+++ b/test/integration/targets/set_fact/set_fact_bool_conv_jinja2_native.yml
@@ -0,0 +1,20 @@
+- hosts: localhost
+  gather_facts: false
+  vars:
+    string_var: "no"
+  tasks:
+    - set_fact:
+        this_is_string: "yes"
+        this_is_not_string: yes
+        this_is_also_string: "{{ string_var }}"
+        this_is_another_string: !!str "{% set thing = '' + string_var + '' %}{{ thing }}"
+        this_is_more_strings: '{{ string_var + "" }}'
+
+    - assert:
+        that:
+          - string_var == 'no'
+          - this_is_string == 'yes'
+          - this_is_not_string == True
+          - this_is_also_string == 'no'
+          - this_is_another_string == 'no'
+          - this_is_more_strings == 'no'