Repository: ansible/ansible

Issue:
ansible playbook failures if paths include spaces
<!--- Verify first that your issue/request is not already reported in GitHub -->
##### ISSUE TYPE

<!--- Pick one below and delete the rest: -->
- Bug Report
##### ANSIBLE VERSION

<!--- Paste verbatim output from “ansible --version” between quotes below -->

```
ansible --version
ansible 2.1.0.0
  config file =
  configured module search path = Default w/o overrides
```
##### CONFIGURATION

<!---
Mention any settings you have changed/added/removed in ansible.cfg
(or using the ANSIBLE_* environment variables).
-->

```
cat ansible.cfg
[defaults]

callback_whitelist = profile_tasks
retry_files_enabled = False
host_key_checking = False
inventory = environments/local
callback_plugins = callbacks

[ssh_connection]
scp_if_ssh = True
```
##### 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.
-->

N/A
##### SUMMARY

<!--- Explain the problem briefly -->

ansible playbook failures if paths include spaces
##### STEPS TO REPRODUCE

<!---
For bugs, show exactly how to reproduce the problem.
For new features, show how the feature would be used.
-->

<!--- Paste example playbooks or commands between quotes below -->

```
ansible-playbook -i environments/mac initdb.yml 
PLAY [ansible] *****************************************************************

TASK [setup] *******************************************************************
Tuesday 28 June 2016  10:47:49 +0200 (0:00:00.028)       0:00:00.028 **********
ok: [ansible]

TASK [include] *****************************************************************
Tuesday 28 June 2016  10:47:50 +0200 (0:00:00.497)       0:00:00.525 **********
included: /Users/lha/Development/hoang anh/openwhisk/ansible/tasks/initdb.yml for ansible

TASK [check if the immortal {{ db_auth }} db with {{ db_provider }} exists?] ***
Tuesday 28 June 2016  10:47:50 +0200 (0:00:00.057)       0:00:00.583 **********
fatal: [ansible]: FAILED! => {"failed": true, "msg": "{{ lookup('ini', 'db_password section=db_creds file={{ playbook_dir }}/db_local.ini') }}: need more than 1 value to unpack"}

PLAY RECAP *********************************************************************
ansible                    : ok=2    changed=0    unreachable=0    failed=1

Tuesday 28 June 2016  10:47:50 +0200 (0:00:00.024)       0:00:00.607 **********
===============================================================================
setup ------------------------------------------------------------------- 0.50s
include ----------------------------------------------------------------- 0.06s
check if the immortal {{ db_auth }} db with {{ db_provider }} exists? --- 0.02s
```

The problem arises from statements like these:

```
db_provider: "{{ lookup('ini', 'db_provider section=db_creds file={{ playbook_dir }}/db_local.ini') }}"
```

where file cannot handle a space in the path.

<!--- You can also paste gist.github.com links for larger files -->
##### EXPECTED RESULTS

<!--- What did you expect to happen when running the steps above? -->

It should handle spaces in the path
##### ACTUAL RESULTS

<!--- What actually happened? If possible run with extra verbosity (-vvvv) -->

But it doesn't

<!--- Paste verbatim command output between quotes below -->

```

```



Discussion:
Reproduced ...

```
###################################
v2.1.1.0-0.1.rc1
###################################
No config file found; using defaults
Loaded callback default of type stdout, v2.0

PLAYBOOK: site.yml *************************************************************
1 plays in dir with space/site.yml

PLAY [el6host] *****************************************************************

TASK [command] *****************************************************************
task path: /home/jtanner/workspace/issues/AP-16468/dir with space/site.yml:7
fatal: [el6host]: FAILED! => {"failed": true, "msg": "need more than 1 value to unpack"}

NO MORE HOSTS LEFT *************************************************************
        to retry, use: --limit @dir with space/site.retry

PLAY RECAP *********************************************************************
el6host                    : ok=0    changed=0    unreachable=0    failed=1   

## RC: 
[jtanner@fedmac AP-16468]$ tree .
.
├── dir\ with\ space
│   ├── db_local.ini
│   ├── site.retry
│   └── site.yml
├── inventory
├── report.sh
└── test.sh

1 directory, 6 files
[jtanner@fedmac AP-16468]$ cat dir\ with\ space/site.yml 
- hosts: el6host
  connection: local
  gather_facts: False
  tasks:
    #- shell: whoami
    # fatal: [ansible]: FAILED! => {"failed": true, "msg": "{{ lookup('ini', 'db_password section=db_creds file={{ playbook_dir }}/db_local.ini') }}: need more than 1 value to unpac
    - shell: cat "{{ lookup('ini', 'db_password section=db_creds file={{ playbook_dir }}/db_local.ini') }}"
[jtanner@fedmac AP-16468]$ cat dir\ with\ space/db_local.ini 
[db_creds]
db_password=foobar
```

This is the code in ini.py where the issue occurs ...

```
 61         for term in terms:
 62             params = term.split() #this is where it breaks
 63             key = params[0]
 64 
 65             paramvals = {
 66                 'file'     : 'ansible.ini',
 67                 're'       : False,
 68                 'default'  : None,
 69                 'section'  : "global",
 70                 'type'     : "ini",
 71             }
 72 
 73             # parameters specified?
 74             try:
 75                 for param in params[1:]:
 76                     name, value = param.split('=')
 77                     assert(name in paramvals)
 78                     paramvals[name] = value
 79             except (ValueError, AssertionError) as e:
 80                 raise AnsibleError(e)
```

The split of the parameters does not safely handle string values with spaces.

```
(Epdb) pp params
[u'db_password',
 u'section=db_creds',
 u'file=/home/jtanner/workspace/issues/AP-16468/dir',
 u'with',
 u'space/db_local.ini']
```

Prototype patch ...

```
diff --git a/lib/ansible/plugins/lookup/ini.py b/lib/ansible/plugins/lookup/ini.py
index 0e68816..98a1194 100644
--- a/lib/ansible/plugins/lookup/ini.py
+++ b/lib/ansible/plugins/lookup/ini.py
@@ -58,7 +58,12 @@ class LookupModule(LookupBase):

         ret = []
         for term in terms:
-            params = term.split()
+            params = []
+            for phrase in term.split():
+                if '=' in phrase or len(params) == 0:
+                    params.append(phrase)
+                else:
+                    params[-1] += ' ' + phrase
             key = params[0]

             paramvals = {
```


Resolving patch:
diff --git a/lib/ansible/plugins/lookup/ini.py b/lib/ansible/plugins/lookup/ini.py
--- a/lib/ansible/plugins/lookup/ini.py
+++ b/lib/ansible/plugins/lookup/ini.py
@@ -19,12 +19,41 @@
 
 from io import StringIO
 import os
-import ConfigParser
 import re
 
+try:
+    # python2
+    import ConfigParser as configparser
+except ImportError:
+    # python3
+    import configparser
+
 from ansible.errors import AnsibleError
 from ansible.plugins.lookup import LookupBase
 
+
+def _parse_params(term):
+    '''Safely split parameter term to preserve spaces'''
+
+    keys = ['key', 'section', 'file', 're']
+    params = {}
+    for k in keys:
+        params[k] = ''
+
+    thiskey = 'key'
+    for idp,phrase in enumerate(term.split()):
+        for k in keys:
+            if ('%s=' % k) in phrase:
+                thiskey = k
+        if idp == 0 or not params[thiskey]:
+            params[thiskey] = phrase
+        else:
+            params[thiskey] += ' ' + phrase
+
+    rparams = [params[x] for x in keys if params[x]]
+    return rparams
+
+
 class LookupModule(LookupBase):
 
     def read_properties(self, filename, key, dflt, is_regexp):
@@ -46,7 +75,7 @@ def get_value(self, key, section, dflt, is_regexp):
         # Retrieve a single value
         try:
             value = self.cp.get(section, key)
-        except ConfigParser.NoOptionError:
+        except configparser.NoOptionError:
             return dflt
         return value
 
@@ -54,11 +83,11 @@ def run(self, terms, variables=None, **kwargs):
 
         basedir = self.get_basedir(variables)
         self.basedir = basedir
-        self.cp      = ConfigParser.ConfigParser()
+        self.cp      = configparser.ConfigParser()
 
         ret = []
         for term in terms:
-            params = term.split()
+            params = _parse_params(term)
             key = params[0]
 
             paramvals = {


Test patch:
diff --git a/test/units/plugins/lookup/test_ini.py b/test/units/plugins/lookup/test_ini.py
new file mode 100644
--- /dev/null
+++ b/test/units/plugins/lookup/test_ini.py
@@ -0,0 +1,63 @@
+# -*- coding: utf-8 -*-
+# (c) 2015, Toshio Kuratomi <tkuratomi@ansible.com>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ansible.compat.tests import unittest
+from ansible.plugins.lookup.ini import _parse_params
+
+
+class TestINILookup(unittest.TestCase):
+
+    # Currently there isn't a new-style
+    old_style_params_data = (
+            # Simple case
+            dict(term=u'keyA section=sectionA file=/path/to/file',
+                expected=[u'keyA', u'section=sectionA', u'file=/path/to/file'],
+                ),
+            dict(term=u'keyB section=sectionB with space file=/path/with/embedded spaces and/file',
+                expected=[u'keyB', u'section=sectionB with space', u'file=/path/with/embedded spaces and/file'],
+                ),
+            dict(term=u'keyC section=sectionC file=/path/with/equals/cn=com.ansible',
+                expected=[u'keyC', u'section=sectionC', u'file=/path/with/equals/cn=com.ansible'],
+                ),
+            dict(term=u'keyD section=sectionD file=/path/with space and/equals/cn=com.ansible',
+                expected=[u'keyD', u'section=sectionD', u'file=/path/with space and/equals/cn=com.ansible'],
+                ),
+            dict(term=u'keyE section=sectionE file=/path/with/unicode/くらとみ/file',
+                expected=[u'keyE', u'section=sectionE', u'file=/path/with/unicode/くらとみ/file'],
+                ),
+            dict(term=u'keyF section=sectionF file=/path/with/utf 8 and spaces/くらとみ/file',
+                expected=[u'keyF', u'section=sectionF', u'file=/path/with/utf 8 and spaces/くらとみ/file'],
+                ),
+            )
+
+
+    def setUp(self):
+        pass
+
+    def tearDown(self):
+        pass
+
+    def test_parse_parameters(self):
+        for testcase in self.old_style_params_data:
+            #print(testcase)
+            params = _parse_params(testcase['term'])
+            self.assertEqual(params, testcase['expected'])