diff --git a/ansible_mitogen/helpers.py b/ansible_mitogen/helpers.py index 3da41ef0..6b256b0c 100644 --- a/ansible_mitogen/helpers.py +++ b/ansible_mitogen/helpers.py @@ -26,6 +26,8 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import json +import os +import pwd import subprocess import time @@ -112,19 +114,41 @@ def run_module(module, raw_params=None, args=None): return json.dumps(e.dct) -def exec_command(cmd, in_data=''): +def get_user_shell(): """ - Run a command in subprocess, arranging for `in_data` to be supplied on its - standard input. + For commands executed directly via an SSH command-line, SSH looks up the + user's shell via getpwuid() and only defaults to /bin/sh if that field is + missing or empty. + """ + try: + pw_shell = pwd.getpwuid(os.geteuid()).pw_shell + except KeyError: + pw_shell = None + + return pw_shell or '/bin/sh' + +def exec_command(cmd, in_data='', chdir=None, shell=None): + """ + Run a command in a subprocess, emulating the argument handling behaviour of + SSH. + + :param bytes cmd: + String command line, passed to user's shell. + :param bytes in_data: + Optional standard input for the command. :return: (return code, stdout bytes, stderr bytes) """ - proc = subprocess.Popen(cmd, + assert isinstance(cmd, basestring) + + proc = subprocess.Popen( + args=[get_user_shell(), '-c', cmd], stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE, - shell=True) + cwd=chdir, + ) stdout, stderr = proc.communicate(in_data) return proc.returncode, stdout, stderr diff --git a/ansible_mitogen/mixins.py b/ansible_mitogen/mixins.py index 11335710..c3970a20 100644 --- a/ansible_mitogen/mixins.py +++ b/ansible_mitogen/mixins.py @@ -26,6 +26,7 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. from __future__ import absolute_import +import commands import os import pwd import shutil @@ -177,17 +178,24 @@ class ActionModuleMixin(ansible.plugins.action.ActionBase): def _low_level_execute_command(self, cmd, sudoable=True, in_data=None, executable=None, - encoding_errors='surrogate_then_replace'): + encoding_errors='surrogate_then_replace', + chdir=None): + if executable is None: # executable defaults to False + executable = self._play_context.executable + if executable: + cmd = executable + ' -c ' + commands.mkarg(cmd) + # replaces 57 lines # replaces 126 lines of make_become_cmd() rc, stdout, stderr = self.call( ansible_mitogen.helpers.exec_command, - cmd, - in_data, + cast(cmd), + cast(in_data), + chdir=cast(chdir), ) return { 'rc': rc, - 'stdout': to_text(stdout, encoding_errors), - 'stdout_lines': '\n'.split(to_text(stdout, encoding_errors)), + 'stdout': to_text(stdout, errors=encoding_errors), + 'stdout_lines': to_text(stdout, errors=encoding_errors).splitlines(), 'stderr': stderr, } diff --git a/examples/playbook/low_level_execute_command.yml b/examples/playbook/low_level_execute_command.yml new file mode 100644 index 00000000..10fb8d79 --- /dev/null +++ b/examples/playbook/low_level_execute_command.yml @@ -0,0 +1,32 @@ +--- + +# Verify the behaviour of _low_level_execute_command(). + +- hosts: all + gather_facts: false + tasks: + + # "echo -en" to test we actually hit bash shell too. + - name: Run raw module without sudo + raw: 'echo -en $((1 + 1))' + register: raw + + - name: Verify raw module output. + assert: + that: + - 'raw.rc == 0' + - 'raw.stdout_lines == ["2"]' + - 'raw.stdout == "2"' + + - name: Run raw module with sudo + become: true + raw: 'whoami' + register: raw + + # Can't test stdout because TTY inserts \r in Ansible version. + - debug: msg={{raw}} + - name: Verify raw module output. + assert: + that: + - 'raw.rc == 0' + - 'raw.stdout_lines == ["root"]'