Repository: ansible/ansible

Issue:
Parsing issue in inventory files
<!---
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
 - Bug Report


##### COMPONENT NAME
<!--- Name of the module/plugin/task/feature -->

##### ANSIBLE VERSION
<!--- Paste verbatim output from "ansible --version" between quotes below -->
```
$ ansible --version
ansible 2.4.0.0
  config file = /home/user/.ansible.cfg
  configured module search path = [u'/home/user/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/lib/python2.7/site-packages/ansible
  executable location = /usr/bin/ansible
  python version = 2.7.14 (default, Sep 20 2017, 01:25:59) [GCC 7.2.0]
```

##### CONFIGURATION
```
$ ansible-config dump --only-changed
ANSIBLE_SSH_PIPELINING(/home/user/.ansible.cfg) = True
CACHE_PLUGIN(/home/user/.ansible.cfg) = jsonfile
CACHE_PLUGIN_CONNECTION(/home/user/.ansible.cfg) = ~/.facts_cache
CACHE_PLUGIN_TIMEOUT(/home/user/.ansible.cfg) = 7776000
COLOR_OK(/home/user/.ansible.cfg) = green
DEFAULT_FORKS(/home/user/.ansible.cfg) = 12
DEFAULT_GATHERING(/home/user/.ansible.cfg) = smart
DEFAULT_GATHER_SUBSET(/home/user/.ansible.cfg) = all
DEFAULT_HOST_LIST(/home/user/.ansible.cfg) = [u'/home/user/.inventory']
DEFAULT_LOCAL_TMP(/home/user/.ansible.cfg) = /tmp/.ansible/tmp/ansible-local-7970r3ZPEH
DEFAULT_MODULE_COMPRESSION(/home/user/.ansible.cfg) = ZIP_DEFLATED
DEFAULT_REMOTE_TMP(/home/user/.ansible.cfg) = ~/.ansible/tmp
DEFAULT_SCP_IF_SSH(/home/user/.ansible.cfg) = smart
DEFAULT_TIMEOUT(/home/user/.ansible.cfg) = 60
DEFAULT_VAULT_PASSWORD_FILE(/home/user/.ansible.cfg) = /home/user/.vault/passwfile
MERGE_MULTIPLE_CLI_TAGS(/home/user/.ansible.cfg) = True
RETRY_FILES_ENABLED(/home/user/.ansible.cfg) = True
RETRY_FILES_SAVE_PATH(/home/user/.ansible.cfg) = /tmp/.ansible-retry
```
##### OS / ENVIRONMENT
```
$ uname -rmo
4.13.5-1-ARCH x86_64 GNU/Linux
```

##### SUMMARY
When ansible is parsing the inventory file _ansible_sudo_pass_ which hold the password string stop working if the password start with a number and follow by # example _1#_, this make privilege escalation impossible .

```
fatal: [sever1]: FAILED! => {"failed": true, "msg": "Incorrect sudo password"}
```
<!--- Explain the problem briefly -->

##### STEPS TO REPRODUCE

```
/home/user/.inventory/host
server1     ansible_sudo_pass="3#thisanpasswd"
server2     ansible_sudo_pass="thisanotherpasswd"
server3    ansible_sudo_pass="u3#thisapasswd"


# ansible server1,server2,server3 -m debug -a"msg={{ ansible_sudo_pass }}"
```
##### EXPECTED RESULTS
```
$ ansible server1,server2,server3 -m debug -a"msg={{ ansible_sudo_pass }}"
ansible 2.4.0.0
  config file = /home/user/.ansible.cfg
  configured module search path = [u'/home/user/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/lib/python2.7/site-packages/ansible
  executable location = /usr/bin/ansible
  python version = 2.7.14 (default, Sep 20 2017, 01:25:59) [GCC 7.2.0]
Using /home/user/.ansible.cfg as config file
Parsed /home/user/.inventory/hosts inventory source with ini plugin
META: ran handlers
server1 | SUCCESS => {
    "msg": "3#thisanpasswd"
}
server2 | SUCCESS => {
    "msg": "thisanotherpasswd"
}
server3 | SUCCESS => {
    "msg": "u3#thisapasswd="
}
```
<!--- What did you expect to happen when running the steps above? -->

##### ACTUAL RESULTS
<!--- What actually happened? If possible run with extra verbosity (-vvvv) -->

<!--- Paste verbatim command output between quotes below -->
```
$ ansible server1,server2,server3 -m debug -a"msg={{ ansible_sudo_pass }}"
ansible 2.4.0.0
  config file = /home/user/.ansible.cfg
  configured module search path = [u'/home/user/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/lib/python2.7/site-packages/ansible
  executable location = /usr/bin/ansible
  python version = 2.7.14 (default, Sep 20 2017, 01:25:59) [GCC 7.2.0]
Using /home/user/.ansible.cfg as config file
Parsed /home/user/.inventory/hosts inventory source with ini plugin
META: ran handlers
server1 | SUCCESS => {
    "msg": 3
}
server2 | SUCCESS => {
    "msg": "thisanotherpasswd"
}
server3 | SUCCESS => {
    "msg": "u3#thisapasswd="
}
```



Discussion:
Run on 2.4 and 2.5:

```
$ cat hosts
[test]
localhost node="01" ansible_host=127.0.0.1 ansible_connection=local

---
- name: inventory import variable 
  hosts: all
  tasks:
  - debug: msg="{{ node }}"
...


$ ansible-playbook -i hosts recreate.yml  

PLAY [testing node variable] ***************************************************

TASK [Gathering Facts] *********************************************************
ok: [localhost]

TASK [debug] *******************************************************************
ok: [localhost] => {
    "msg": 1
}

PLAY RECAP *********************************************************************
localhost                  : ok=2    changed=0    unreachable=0    failed=0   
```

Thanks for the report! The reason why this is happening is that the value goes through https://github.com/ansible/ansible/blob/devel/lib/ansible/plugins/inventory/ini.py#L336 which converts `3#thisanpasswd` into `3`:
```
python -c "import ast; print ast.literal_eval('3#thisanpasswd')"
3
```

The bug might have been caused by https://github.com/ansible/ansible/pull/22893.

For the time being, the workaround seems to be to define the variable like so `ansible_sudo_pass="'3#thisanpasswd'"`.
Another workaround is to use the yaml format in inventory instead of ini, so:
```
all:
  hosts:
    localhost:
      ansible_sudo_pass: "3#thisanpasswd"
```

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
@@ -20,6 +20,9 @@
     notes:
         - It takes the place of the previously hardcoded INI inventory.
         - To function it requires being whitelisted in configuration.
+        - Variable values are processed by Python's ast.literal_eval function (U(https://docs.python.org/2/library/ast.html#ast.literal_eval))
+          which could cause the value to change in some cases. See the Examples for proper quoting to prevent changes. Another option would be
+          to use the yaml format for inventory source which processes the values correctly.
 '''
 
 EXAMPLES = '''
@@ -40,6 +43,7 @@
       [apache]
       tomcat1
       tomcat2 myvar=34 # host specific vars override group vars
+      tomcat3 mysecret="'03#pa33w0rd'" # proper quoting to prevent value changes
 
       [nginx]
       jenkins1


Test patch: