Repository: ansible/ansible

Issue:
Including ansible_python_interpreter='/usr/bin/env python' in inventory leads to module failure (OSError: [Errno 2] No such file or directory)
##### ISSUE TYPE
- Bug Report
##### ANSIBLE VERSION

```
ansible 2.1.0.0 (stable-2.1 b6c59f89d2) last updated 2016/04/27 19:41:45 (GMT -400)
  lib/ansible/modules/core: (detached HEAD 6498f6e4d0) last updated 2016/04/27 19:42:05 (GMT -400)
  lib/ansible/modules/extras: (detached HEAD 156a8cd0b3) last updated 2016/04/27 19:42:10 (GMT -400)
  config file = /Users/jladd/.ansible.cfg
  configured module search path = Default w/o overrides
```
##### CONFIGURATION

```
[ssh_connection]
control_path = %(directory)s/%%h-%%r
```
##### OS / ENVIRONMENT

N/A
##### SUMMARY

Including ansible_python_interpreter='/usr/bin/env python' in inventory leads to module failure (OSError: [Errno 2] No such file or directory). Setting `ansible_python_interpreter` to specific path (e.g. `/usr/local/bin/python`) resolves issue.
##### STEPS TO REPRODUCE
1. Create `inventory` file:

```
[local]
127.0.0.1 ansible_connection=local ansible_python_interpreter='/usr/bin/env python'
```
1. Create playbook:

```

---
- hosts: all
  tasks:
    - shell: echo "testing.."
```
1. Run playbook: `ansible-playbook -i inventory playbook.yml`
   <!--- You can also paste gist.github.com links for larger files -->
##### EXPECTED RESULTS

Warning that `/usr/bin/env` is no longer supported for `ansible_python_interpreter`. (See http://docs.ansible.com/ansible/intro_inventory.html)
##### ACTUAL RESULTS

Seeing `MODULE FAILURE` and `OSError: [Errno 2] No such file or directory`.

Full output:

```
% ansible-playbook -vvvv -i inventory playbook.yml
Using /Users/jladd/.ansible.cfg as config file
Loaded callback default of type stdout, v2.0

PLAYBOOK: playbook.yml *********************************************************
1 plays in playbook.yml

PLAY [all] *********************************************************************

TASK [setup] *******************************************************************
<127.0.0.1> ESTABLISH LOCAL CONNECTION FOR USER: jladd
<127.0.0.1> EXEC /bin/sh -c '( umask 77 && mkdir -p "` echo $HOME/.ansible/tmp/ansible-tmp-1461804617.07-35816113603270 `" && echo "` echo $HOME/.ansible/tmp/ansible-tmp-1461804617.07-35816113603270 `" )'
<127.0.0.1> PUT /var/folders/9f/lfcc9zd96fngv4bpptdg86700000gn/T/tmp7VbvKW TO /Users/jladd/.ansible/tmp/ansible-tmp-1461804617.07-35816113603270/setup
<127.0.0.1> EXEC /bin/sh -c 'LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 LC_MESSAGES=en_US.UTF-8 /usr/bin/env python /Users/jladd/.ansible/tmp/ansible-tmp-1461804617.07-35816113603270/setup; rm -rf "/Users/jladd/.ansible/tmp/ansible-tmp-1461804617.07-35816113603270/" > /dev/null 2>&1'
An exception occurred during task execution. The full traceback is:
Traceback (most recent call last):
  File "/Users/jladd/.ansible/tmp/ansible-tmp-1461804617.07-35816113603270/setup", line 114, in <module>
    exitcode = invoke_module(module, zipped_mod, ZIPLOADER_PARAMS)
  File "/Users/jladd/.ansible/tmp/ansible-tmp-1461804617.07-35816113603270/setup", line 28, in invoke_module
    p = subprocess.Popen(['/usr/bin/env python', module], env=os.environ, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE)
  File "/usr/local/Cellar/python/2.7.11/Frameworks/Python.framework/Versions/2.7/lib/python2.7/subprocess.py", line 710, in __init__
    errread, errwrite)
  File "/usr/local/Cellar/python/2.7.11/Frameworks/Python.framework/Versions/2.7/lib/python2.7/subprocess.py", line 1335, in _execute_child
    raise child_exception
OSError: [Errno 2] No such file or directory

fatal: [127.0.0.1]: FAILED! => {"changed": false, "failed": true, "invocation": {"module_name": "setup"}, "module_stderr": "Traceback (most recent call last):\n  File \"/Users/jladd/.ansible/tmp/ansible-tmp-1461804617.07-35816113603270/setup\", line 114, in <module>\n    exitcode = invoke_module(module, zipped_mod, ZIPLOADER_PARAMS)\n  File \"/Users/jladd/.ansible/tmp/ansible-tmp-1461804617.07-35816113603270/setup\", line 28, in invoke_module\n    p = subprocess.Popen(['/usr/bin/env python', module], env=os.environ, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE)\n  File \"/usr/local/Cellar/python/2.7.11/Frameworks/Python.framework/Versions/2.7/lib/python2.7/subprocess.py\", line 710, in __init__\n    errread, errwrite)\n  File \"/usr/local/Cellar/python/2.7.11/Frameworks/Python.framework/Versions/2.7/lib/python2.7/subprocess.py\", line 1335, in _execute_child\n    raise child_exception\nOSError: [Errno 2] No such file or directory\n", "module_stdout": "", "msg": "MODULE FAILURE", "parsed": false}

NO MORE HOSTS LEFT *************************************************************
        to retry, use: --limit @playbook.retry

PLAY RECAP *********************************************************************
127.0.0.1                  : ok=0    changed=0    unreachable=0    failed=1
```



Discussion:
Confirmed that this was working in almost all versions prior to 2.1.x. Looks like the issue is inside the python code template used for ziploader. 

A quick patch ...

```
diff --git a/lib/ansible/executor/module_common.py b/lib/ansible/executor/module_common.py
index 2d10bd8..87d49c3 100644
--- a/lib/ansible/executor/module_common.py
+++ b/lib/ansible/executor/module_common.py
@@ -118,7 +118,10 @@ def invoke_module(module, modlib_path, json_params):
     else:
         os.environ['PYTHONPATH'] = modlib_path

-    p = subprocess.Popen(['%(interpreter)s', module], env=os.environ, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PI
+    safe_interpreter = '%(interpreter)s'.split()
+    safe_interpreter.append(module)
+
+    p = subprocess.Popen(safe_interpreter, env=os.environ, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE)
     (stdout, stderr) = p.communicate(json_params)

     if not isinstance(stderr, (bytes, unicode)):
```


Resolving patch:
diff --git a/lib/ansible/executor/module_common.py b/lib/ansible/executor/module_common.py
--- a/lib/ansible/executor/module_common.py
+++ b/lib/ansible/executor/module_common.py
@@ -118,7 +118,7 @@ def invoke_module(module, modlib_path, json_params):
     else:
         os.environ['PYTHONPATH'] = modlib_path
 
-    p = subprocess.Popen(['%(interpreter)s', module], env=os.environ, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE)
+    p = subprocess.Popen([%(interpreter)s, module], env=os.environ, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE)
     (stdout, stderr) = p.communicate(json_params)
 
     if not isinstance(stderr, (bytes, unicode)):
@@ -215,7 +215,7 @@ def debug(command, zipped_mod, json_params):
         else:
             os.environ['PYTHONPATH'] = basedir
 
-        p = subprocess.Popen(['%(interpreter)s', script_path, args_path], env=os.environ, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE)
+        p = subprocess.Popen([%(interpreter)s, script_path, args_path], env=os.environ, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE)
         (stdout, stderr) = p.communicate()
 
         if not isinstance(stderr, (bytes, unicode)):
@@ -372,12 +372,12 @@ def _get_shebang(interpreter, task_vars, args=tuple()):
        file rather than trust that we reformatted what they already have
        correctly.
     """
-    interpreter_config = u'ansible_%s_interpreter' % os.path.basename(interpreter)
+    interpreter_config = u'ansible_%s_interpreter' % os.path.basename(interpreter).strip()
 
     if interpreter_config not in task_vars:
         return (None, interpreter)
 
-    interpreter = task_vars[interpreter_config]
+    interpreter = task_vars[interpreter_config].strip()
     shebang = u'#!' + interpreter
 
     if args:
@@ -601,6 +601,17 @@ def _find_snippet_imports(module_name, module_data, module_path, module_args, ta
         shebang, interpreter = _get_shebang(u'/usr/bin/python', task_vars)
         if shebang is None:
             shebang = u'#!/usr/bin/python'
+
+        executable = interpreter.split(u' ', 1)
+        if len(executable) == 2 and executable[0].endswith(u'env'):
+            # Handle /usr/bin/env python style interpreter settings
+            interpreter = u"'{0}', '{1}'".format(*executable)
+        else:
+            # Still have to enclose the parts of the interpreter in quotes
+            # because we're substituting it into the template as a python
+            # string
+            interpreter = u"'{0}'".format(interpreter)
+
         output.write(to_bytes(ACTIVE_ZIPLOADER_TEMPLATE % dict(
             zipdata=zipdata,
             ansible_module=module_name,


Test patch: