Repository: ansible/ansible

Issue:
Issue with way su command is constructed for BSD when using su vs sudo
Example:
`ansible myhost -m ping -S`

_su_ command line would look like: 
`su root /bin/sh -c ....blah-blah-blah...`
This is extremely wrong.

The fact that the linux _su_ program is used _getopt_ for parsing of arguments. This leads to the fact that the second argument without options is simply ignored. For example:

```
$ su root /dev/bored/hands  -c "echo test"
test
```

I.e.
`su root /dev/bored/hands  -c "echo test"`
full identically in linux to
`su root -c "echo test"`

But it does not work for _su_ in other systems. For example in FreeBSD _su_ tries to run in the login shell that he was given. This is true for Linux and FreeBSD. But the version in Linux ignores bad argument, but in FreeBSD as it should pass thru to login shell. For example in FreeBSD with bash as root login shell:

```
$ su root /bin/sh -c "echo test"
/bin/sh: /bin/sh: cannot execute binary file
```

Login shell (bash in our example) takes string `'/bin/sh -c "echo test"'` and accordingly tries to run _/bin/sh_ as shell script. 

In summary, the right solution is to add the "-c" option after user login (_root_ in this case). Example:
`su root -c /bin/sh  -c "echo test"`
It will be correct. And most importantly - it will work on most systems.

IMPORTANT!! Important. This is not a wish. This CRITICAL error in the understanding the principles of _su_, leading to unusable on FreeBSD



Discussion:
Sorry. On our ingrian swamp was late at night, changed yues beguiled. Course will be the line of the form:
`su root -c '/bin/sh -c "echo test"'`
where the string after "-c" in quotes

Linking with #7503


Resolving patch:
diff --git a/lib/ansible/runner/connection_plugins/paramiko_ssh.py b/lib/ansible/runner/connection_plugins/paramiko_ssh.py
--- a/lib/ansible/runner/connection_plugins/paramiko_ssh.py
+++ b/lib/ansible/runner/connection_plugins/paramiko_ssh.py
@@ -31,6 +31,7 @@
 import logging
 import traceback
 import fcntl
+import re
 import sys
 from termios import tcflush, TCIFLUSH
 from binascii import hexlify
@@ -210,12 +211,17 @@ def exec_command(self, cmd, tmp_path, sudo_user=None, sudoable=False, executable
                 shcmd, prompt, success_key = utils.make_sudo_cmd(sudo_user, executable, cmd)
             elif self.runner.su or su:
                 shcmd, prompt, success_key = utils.make_su_cmd(su_user, executable, cmd)
+                prompt_re = re.compile(prompt)
             vvv("EXEC %s" % shcmd, host=self.host)
             sudo_output = ''
             try:
                 chan.exec_command(shcmd)
                 if self.runner.sudo_pass or self.runner.su_pass:
-                    while not sudo_output.endswith(prompt) and success_key not in sudo_output:
+                    while True:
+                        if success_key in sudo_output or \
+                            (self.runner.sudo_pass and sudo_output.endswith(prompt)) or \
+                            (self.runner.su_pass and prompt_re.match(sudo_output)):
+                            break
                         chunk = chan.recv(bufsize)
                         if not chunk:
                             if 'unknown user' in sudo_output:
diff --git a/lib/ansible/runner/connection_plugins/ssh.py b/lib/ansible/runner/connection_plugins/ssh.py
--- a/lib/ansible/runner/connection_plugins/ssh.py
+++ b/lib/ansible/runner/connection_plugins/ssh.py
@@ -17,6 +17,7 @@
 #
 
 import os
+import re
 import subprocess
 import shlex
 import pipes
@@ -268,6 +269,7 @@ def exec_command(self, cmd, tmp_path, sudo_user=None, sudoable=False, executable
 
         if su and su_user:
             sudocmd, prompt, success_key = utils.make_su_cmd(su_user, executable, cmd)
+            prompt_re = re.compile(prompt)
             ssh_cmd.append(sudocmd)
         elif not self.runner.sudo or not sudoable:
             prompt = None
@@ -308,7 +310,12 @@ def exec_command(self, cmd, tmp_path, sudo_user=None, sudoable=False, executable
             sudo_output = ''
             sudo_errput = ''
 
-            while not sudo_output.endswith(prompt) and success_key not in sudo_output:
+            while True:
+                if success_key in sudo_output or \
+                    (self.runner.sudo_pass and sudo_output.endswith(prompt)) or \
+                    (self.runner.su_pass and prompt_re.match(sudo_output)):
+                    break
+
                 rfd, wfd, efd = select.select([p.stdout, p.stderr], [],
                                               [p.stdout], self.runner.timeout)
                 if p.stderr in rfd:
diff --git a/lib/ansible/utils/__init__.py b/lib/ansible/utils/__init__.py
--- a/lib/ansible/utils/__init__.py
+++ b/lib/ansible/utils/__init__.py
@@ -952,9 +952,9 @@ def make_su_cmd(su_user, executable, cmd):
     """
     # TODO: work on this function
     randbits = ''.join(chr(random.randint(ord('a'), ord('z'))) for x in xrange(32))
-    prompt = 'assword: '
+    prompt = '[Pp]assword: ?$'
     success_key = 'SUDO-SUCCESS-%s' % randbits
-    sudocmd = '%s %s %s %s -c %s' % (
+    sudocmd = '%s %s %s -c "%s -c %s"' % (
         C.DEFAULT_SU_EXE, C.DEFAULT_SU_FLAGS, su_user, executable or '$SHELL',
         pipes.quote('echo %s; %s' % (success_key, cmd))
     )


Test patch:
diff --git a/test/units/TestUtils.py b/test/units/TestUtils.py
--- a/test/units/TestUtils.py
+++ b/test/units/TestUtils.py
@@ -3,6 +3,7 @@
 import unittest
 import os
 import os.path
+import re
 import tempfile
 import yaml
 import passlib.hash
@@ -511,8 +512,8 @@ def test_make_su_cmd(self):
         cmd = ansible.utils.make_su_cmd('root', '/bin/sh', '/bin/ls')
         self.assertTrue(isinstance(cmd, tuple))
         self.assertEqual(len(cmd), 3)
-        self.assertTrue(' root /bin/sh' in cmd[0])
-        self.assertTrue(cmd[1] == 'assword: ')
+        self.assertTrue(' root -c "/bin/sh' in cmd[0])
+        self.assertTrue(re.compile(cmd[1]))
         self.assertTrue('echo SUDO-SUCCESS-' in cmd[0] and cmd[2].startswith('SUDO-SUCCESS-'))
 
     def test_to_unicode(self):