Repository: ansible/ansible

Issue:
allow ansilbe_x_interpreter to be templated
##### SUMMARY
hardcoded interpreter paths worked, but templated values did not

fixes #18665


##### ISSUE TYPE
<!--- Pick one below and delete the rest: -->
 - Bugfix Pull Request


##### COMPONENT NAME
<!--- Name of the module/plugin/module/task -->
core
##### ANSIBLE VERSION
<!--- Paste verbatim output from "ansible --version" between quotes below -->
```
2.4/2.5
```




Discussion:
Running ansible with this branch I get:

```
TASK [command] ***********************************************************************************************************************************************
task path: /home/will/tmp/ansible/playbook.yml:7
An exception occurred during task execution. To see the full traceback, use -vvv. The error was: OSError: [Errno 2] No such file or directory
fatal: [localhost]: FAILED! => {"changed": false, "module_stderr": "Traceback (most recent call last):\n  File \"/home/will/.ansible/tmp/ansible-tmp-1512701457.16-116987303895134/command.py\", line 133, in <module>\n    exitcode = invoke_module(module, zipped_mod, ANSIBALLZ_PARAMS)\n  File \"/home/will/.ansible/tmp/ansible-tmp-1512701457.16-116987303895134/command.py\", line 37, in invoke_module\n    p = subprocess.Popen(['{{', 'ansible_playbook_python', '}}', module], env=os.environ, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE)\n  File \"/usr/lib64/python2.7/subprocess.py\", line 390, in __init__\n    errread, errwrite)\n  File \"/usr/lib64/python2.7/subprocess.py\", line 1025, in _execute_child\n    raise child_exception\nOSError: [Errno 2] No such file or directory\n", "module_stdout": "", "msg": "MODULE FAILURE", "rc": 0}
```
This is also a problem for setting `ansible_python_interpreter: "{{ ansible_playbook_python }}"` in inventory/host_vars/localhost.yml

I note that the failure is the same for that situation is the same as for play vars (both before this PR and after this PR), so hopefully fixing the above problem for play vars will fix it for inventory too.

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
@@ -435,7 +435,7 @@ def _slurp(path):
     return data
 
 
-def _get_shebang(interpreter, task_vars, args=tuple()):
+def _get_shebang(interpreter, task_vars, templar, args=tuple()):
     """
     Note not stellar API:
        Returns None instead of always returning a shebang line.  Doing it this
@@ -448,7 +448,7 @@ def _get_shebang(interpreter, task_vars, args=tuple()):
     if interpreter_config not in task_vars:
         return (None, interpreter)
 
-    interpreter = task_vars[interpreter_config].strip()
+    interpreter = templar.template(task_vars[interpreter_config].strip())
     shebang = u'#!' + interpreter
 
     if args:
@@ -598,7 +598,7 @@ def _is_binary(b_module_data):
     return bool(start.translate(None, textchars))
 
 
-def _find_module_utils(module_name, b_module_data, module_path, module_args, task_vars, module_compression, async_timeout, become,
+def _find_module_utils(module_name, b_module_data, module_path, module_args, task_vars, templar, module_compression, async_timeout, become,
                        become_method, become_user, become_password, environment):
     """
     Given the source of the module, convert it to a Jinja2 template to insert
@@ -732,7 +732,7 @@ def _find_module_utils(module_name, b_module_data, module_path, module_args, tas
                                        'Look at traceback for that process for debugging information.')
         zipdata = to_text(zipdata, errors='surrogate_or_strict')
 
-        shebang, interpreter = _get_shebang(u'/usr/bin/python', task_vars)
+        shebang, interpreter = _get_shebang(u'/usr/bin/python', task_vars, templar)
         if shebang is None:
             shebang = u'#!/usr/bin/python'
 
@@ -871,7 +871,7 @@ def _find_module_utils(module_name, b_module_data, module_path, module_args, tas
     return (b_module_data, module_style, shebang)
 
 
-def modify_module(module_name, module_path, module_args, task_vars=None, module_compression='ZIP_STORED', async_timeout=0, become=False,
+def modify_module(module_name, module_path, module_args, task_vars=None, templar=None, module_compression='ZIP_STORED', async_timeout=0, become=False,
                   become_method=None, become_user=None, become_password=None, environment=None):
     """
     Used to insert chunks of code into modules before transfer rather than
@@ -901,7 +901,7 @@ def modify_module(module_name, module_path, module_args, task_vars=None, module_
         # read in the module source
         b_module_data = f.read()
 
-    (b_module_data, module_style, shebang) = _find_module_utils(module_name, b_module_data, module_path, module_args, task_vars, module_compression,
+    (b_module_data, module_style, shebang) = _find_module_utils(module_name, b_module_data, module_path, module_args, task_vars, templar, module_compression,
                                                                 async_timeout=async_timeout, become=become, become_method=become_method,
                                                                 become_user=become_user, become_password=become_password,
                                                                 environment=environment)
@@ -916,7 +916,7 @@ def modify_module(module_name, module_path, module_args, task_vars=None, module_
             interpreter = args[0]
             interpreter = to_bytes(interpreter)
 
-            new_shebang = to_bytes(_get_shebang(interpreter, task_vars, args[1:])[0], errors='surrogate_or_strict', nonstring='passthru')
+            new_shebang = to_bytes(_get_shebang(interpreter, task_vars, templar, args[1:])[0], errors='surrogate_or_strict', nonstring='passthru')
             if new_shebang:
                 lines[0] = shebang = new_shebang
 
diff --git a/lib/ansible/plugins/action/__init__.py b/lib/ansible/plugins/action/__init__.py
--- a/lib/ansible/plugins/action/__init__.py
+++ b/lib/ansible/plugins/action/__init__.py
@@ -158,9 +158,12 @@ def _configure_module(self, module_name, module_args, task_vars=None):
         self._compute_environment_string(final_environment)
 
         (module_data, module_style, module_shebang) = modify_module(module_name, module_path, module_args,
-                                                                    task_vars=task_vars, module_compression=self._play_context.module_compression,
-                                                                    async_timeout=self._task.async_val, become=self._play_context.become,
-                                                                    become_method=self._play_context.become_method, become_user=self._play_context.become_user,
+                                                                    task_vars=task_vars, templar=self._templar,
+                                                                    module_compression=self._play_context.module_compression,
+                                                                    async_timeout=self._task.async_val,
+                                                                    become=self._play_context.become,
+                                                                    become_method=self._play_context.become_method,
+                                                                    become_user=self._play_context.become_user,
                                                                     become_password=self._play_context.become_pass,
                                                                     environment=final_environment)
 


Test patch:
diff --git a/test/units/executor/module_common/test_module_common.py b/test/units/executor/module_common/test_module_common.py
--- a/test/units/executor/module_common/test_module_common.py
+++ b/test/units/executor/module_common/test_module_common.py
@@ -93,26 +93,35 @@ def test_slurp_file_with_newlines(self, mocker):
         assert amc._slurp('some_file') == '#!/usr/bin/python\ndef test(args):\nprint("hi")\n'
 
 
+@pytest.fixture
+def templar():
+    class FakeTemplar(object):
+        def template(self, template_string, *args, **kwargs):
+            return template_string
+
+    return FakeTemplar()
+
+
 class TestGetShebang(object):
     """Note: We may want to change the API of this function in the future.  It isn't a great API"""
-    def test_no_interpreter_set(self):
-        assert amc._get_shebang(u'/usr/bin/python', {}) == (None, u'/usr/bin/python')
+    def test_no_interpreter_set(self, templar):
+        assert amc._get_shebang(u'/usr/bin/python', {}, templar) == (None, u'/usr/bin/python')
 
-    def test_non_python_interpreter(self):
-        assert amc._get_shebang(u'/usr/bin/ruby', {}) == (None, u'/usr/bin/ruby')
+    def test_non_python_interpreter(self, templar):
+        assert amc._get_shebang(u'/usr/bin/ruby', {}, templar) == (None, u'/usr/bin/ruby')
 
-    def test_interpreter_set_in_task_vars(self):
-        assert amc._get_shebang(u'/usr/bin/python', {u'ansible_python_interpreter': u'/usr/bin/pypy'}) == \
+    def test_interpreter_set_in_task_vars(self, templar):
+        assert amc._get_shebang(u'/usr/bin/python', {u'ansible_python_interpreter': u'/usr/bin/pypy'}, templar) == \
             (u'#!/usr/bin/pypy', u'/usr/bin/pypy')
 
-    def test_non_python_interpreter_in_task_vars(self):
-        assert amc._get_shebang(u'/usr/bin/ruby', {u'ansible_ruby_interpreter': u'/usr/local/bin/ruby'}) == \
+    def test_non_python_interpreter_in_task_vars(self, templar):
+        assert amc._get_shebang(u'/usr/bin/ruby', {u'ansible_ruby_interpreter': u'/usr/local/bin/ruby'}, templar) == \
             (u'#!/usr/local/bin/ruby', u'/usr/local/bin/ruby')
 
-    def test_with_args(self):
-        assert amc._get_shebang(u'/usr/bin/python', {u'ansible_python_interpreter': u'/usr/bin/python3'}, args=('-tt', '-OO')) == \
+    def test_with_args(self, templar):
+        assert amc._get_shebang(u'/usr/bin/python', {u'ansible_python_interpreter': u'/usr/bin/python3'}, templar, args=('-tt', '-OO')) == \
             (u'#!/usr/bin/python3 -tt -OO', u'/usr/bin/python3')
 
-    def test_python_via_env(self):
-        assert amc._get_shebang(u'/usr/bin/python', {u'ansible_python_interpreter': u'/usr/bin/env python'}) == \
+    def test_python_via_env(self, templar):
+        assert amc._get_shebang(u'/usr/bin/python', {u'ansible_python_interpreter': u'/usr/bin/env python'}, templar) == \
             (u'#!/usr/bin/env python', u'/usr/bin/env python')