Repository: ansible/ansible

Issue:
Not all spellings of boolean "false" in inventory are interpreted as false by  Jinja
##### ISSUE TYPE
<!--- Pick one below and delete the rest: -->
 - Bug Report


##### COMPONENT NAME
inventory
template
jinja

##### ANSIBLE VERSION
<!--- Paste verbatim output from “ansible --version” between quotes below -->
```
ansible 2.3.0.0
  configured module search path = Default w/o overrides
  python version = 2.7.5 (default, Nov  6 2016, 00:28:07) [GCC 4.8.5 20150623 (Red Hat 4.8.5-11)]
```

##### CONFIGURATION
none special

##### OS / ENVIRONMENT
CentOS 7 on x86_64

##### SUMMARY
Boolean variables set in the inventory are not respected by Jinjia as one would expect.  According to http://docs.ansible.com/ansible/YAMLSyntax.html e.g. the boolean value false can be written "false", "False", "FALSE" or "no".  But only the exact spelling "False" is seen as false by Jinjia, all other spellings are seen as true value.

This is similar to and maybe related to (or the same problem as)  #25778.

##### 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: all
  tasks:
  - shell: "echo \"The switch is {% if switch %}ON{% else %}OFF{% endif %} ({{ switch }})\""
    register: echo

  - debug:
      var: echo
```
set "switch" to various values in the inventory.

<!--- You can also paste gist.github.com links for larger files -->

##### EXPECTED RESULTS

switch=False: The switch is OFF (False)
switch=FALSE: The switch is **OFF** (FALSE)
switch=false: The switch is **OFF** (false)
switch=no: The switch is **OFF** (no)
switch=0: The switch is OFF (0)
switch=true: The switch is ON (true)
switch=blah: _(not sure what to expect here, probably an error message)_

##### ACTUAL RESULTS
echo results:

switch=False: The switch is OFF (False)
switch=FALSE: The switch is **ON** (FALSE)
switch=false: The switch is **ON** (false)
switch=no: The switch is **ON** (no)
switch=0: The switch is OFF (0)
switch=true: The switch is ON (true)
switch=blah: The switch is ON (blah)





Discussion:
@fthommen could you specify how you set `switch` value in the inventory (paste inventory content and filename) ?

I am not able to reproduce this issue defining the `switch` variable in an YAML file:

```
$ cat switch.yml
---
switch: FALSE

$ ansible-playbook -i localhost -e@switch.yml test.yml
[...]
"cmd": "echo \"The switch is OFF (False)\"",
[...]
```
I do it in the ansible inventory with switch=FALSE (http://docs.ansible.com/ansible/intro_inventory.html#host-variables)
See [25778](https://github.com/ansible/ansible/issues/25778#issuecomment-309014901):
> Variables which are not [behavioral inventory parameters](https://docs.ansible.com/ansible/intro_inventory.html#list-of-behavioral-inventory-parameters) and are defined in an INI-like inventory are interpreted as strings. This doesn't seem [documented](https://docs.ansible.com/ansible/intro_inventory.html), there is an hint in the note [here](https://docs.ansible.com/ansible/playbooks_variables.html#passing-variables-on-the-command-line).
Thanks @pilou- . Very pity and IMHO somehow inconsistent.  In this case - since that seems to be a design decision - please make this issue a documentation issue, not a bug report.
btw I then still wonder, why "False" is interpreted as a boolean....  I can't find the notion of a boolean type in the Jinja documentation at all
You need to specify type on 'consumption' we try to conserve it but both jinja2 and the ini format 'stringify everything'. 

This should give you more predictable results:
``` - shell: "echo \"The switch is {% if switch|bool %}ON{% else %}OFF{% endif %} ({{ switch }})\""```
@fthommen INI parser [tries `ast.literal_eval`](https://github.com/ansible/ansible/blob/43468b825d19c5990c07d19f5f269d8352f084f8/lib/ansible/plugins/inventory/ini.py#L346) for every value and 'stringify' when `literal_eval` fails.
^ note that ast evals use 'python booleans' unlike jinja2 and yaml which use an extensive list.

Resolving patch:
diff --git a/lib/ansible/plugins/inventory/ini.py b/lib/ansible/plugins/inventory/ini.py
--- a/lib/ansible/plugins/inventory/ini.py
+++ b/lib/ansible/plugins/inventory/ini.py
@@ -27,6 +27,9 @@
         - The C(children) modifier indicates that the section contains groups.
         - The C(vars) modifier indicates that the section contains variables assigned to members of the group.
         - Anything found outside a section is considered an 'ungrouped' host.
+        - Values passed in using the C(key=value) syntax are interpreted as Python literal structure (strings, numbers, tuples, lists, dicts,
+          booleans, None), alternatively as string. For example C(var=FALSE) would create a string equal to 'FALSE'. Do not rely on types set
+          during definition, always make sure you specify type with a filter when needed when consuming the variable.
     notes:
         - It takes the place of the previously hardcoded INI inventory.
         - To function it requires being whitelisted in configuration.


Test patch:
diff --git a/test/units/inventory/test_inventory.py b/test/units/inventory/test_inventory.py
--- a/test/units/inventory/test_inventory.py
+++ b/test/units/inventory/test_inventory.py
@@ -22,10 +22,10 @@
 import string
 
 from ansible.compat.tests import unittest
-from ansible.compat.tests.mock import patch
+from ansible.module_utils.six import string_types
+from ansible.module_utils._text import to_text
 
 from ansible.inventory.manager import InventoryManager, split_host_pattern
-from ansible.vars.manager import VariableManager
 
 from units.mock.loader import DictDataLoader
 
@@ -109,7 +109,7 @@ def test_ranges(self):
             )
 
 
-class InventoryDefaultGroup(unittest.TestCase):
+class IniInventory(unittest.TestCase):
 
     def test_empty_inventory(self):
         inventory = self._get_inventory('')
@@ -142,6 +142,20 @@ def test_ini_explicit_ungrouped(self):
             host5
             """)
 
+    def test_ini_variables_stringify(self):
+        values = ['string', 'no', 'No', 'false', 'FALSE', [], False, 0]
+
+        inventory_content = "host1 "
+        inventory_content += ' '.join(['var%s=%s' % (i, to_text(x)) for i, x in enumerate(values)])
+        inventory = self._get_inventory(inventory_content)
+
+        variables = inventory.get_host('host1').vars
+        for i in range(len(values)):
+            if isinstance(values[i], string_types):
+                self.assertIsInstance(variables['var%s' % i], string_types)
+            else:
+                self.assertIsInstance(variables['var%s' % i], type(values[i]))
+
     def _get_inventory(self, inventory_content):
 
         fake_loader = DictDataLoader({__file__: inventory_content})