diff --git a/changelogs/fragments/enable_su_on_local.yaml b/changelogs/fragments/enable_su_on_local.yaml new file mode 100644 index 00000000000..1f5cdb4bf98 --- /dev/null +++ b/changelogs/fragments/enable_su_on_local.yaml @@ -0,0 +1,2 @@ +bugfixes: + - allow become method 'su' to work on 'local' connection by allocating a fake tty. diff --git a/lib/ansible/plugins/connection/local.py b/lib/ansible/plugins/connection/local.py index 2ababb061af..c6ec01d82d8 100644 --- a/lib/ansible/plugins/connection/local.py +++ b/lib/ansible/plugins/connection/local.py @@ -17,6 +17,7 @@ DOCUMENTATION = ''' ''' import os +import pty import shutil import subprocess import fcntl @@ -79,15 +80,32 @@ class Connection(ConnectionBase): else: cmd = map(to_bytes, cmd) + master = None + stdin = subprocess.PIPE + if sudoable and self.become and self.become.expect_prompt(): + # Create a pty if sudoable for privlege escalation that needs it. + # Falls back to using a standard pipe if this fails, which may + # cause the command to fail in certain situations where we are escalating + # privileges or the command otherwise needs a pty. + try: + master, stdin = pty.openpty() + except (IOError, OSError) as e: + display.debug("Unable to open pty: %s" % to_native(e)) + p = subprocess.Popen( cmd, shell=isinstance(cmd, (text_type, binary_type)), executable=executable, cwd=self.cwd, - stdin=subprocess.PIPE, + stdin=stdin, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) + + # if we created a master, we can close the other half of the pty now + if master is not None: + os.close(stdin) + display.debug("done running command with Popen()") if self.become and self.become.expect_prompt() and sudoable: @@ -120,7 +138,8 @@ class Connection(ConnectionBase): if not self.become.check_success(become_output): become_pass = self.become.get_option('become_pass', playcontext=self._play_context) - p.stdin.write(to_bytes(become_pass, errors='surrogate_or_strict') + b'\n') + os.write(master, to_bytes(become_pass, errors='surrogate_or_strict') + b'\n') + fcntl.fcntl(p.stdout, fcntl.F_SETFL, fcntl.fcntl(p.stdout, fcntl.F_GETFL) & ~os.O_NONBLOCK) fcntl.fcntl(p.stderr, fcntl.F_SETFL, fcntl.fcntl(p.stderr, fcntl.F_GETFL) & ~os.O_NONBLOCK) @@ -128,6 +147,10 @@ class Connection(ConnectionBase): stdout, stderr = p.communicate(in_data) display.debug("done communicating") + # finally, close the other half of the pty, if it was created + if master: + os.close(master) + display.debug("done with local.exec_command()") return (p.returncode, stdout, stderr) diff --git a/test/integration/targets/become_su/aliases b/test/integration/targets/become_su/aliases new file mode 100644 index 00000000000..3a07aab32d1 --- /dev/null +++ b/test/integration/targets/become_su/aliases @@ -0,0 +1,3 @@ +destructive +shippable/posix/group1 +skip/aix diff --git a/test/integration/targets/become_su/runme.sh b/test/integration/targets/become_su/runme.sh new file mode 100755 index 00000000000..87a3511f655 --- /dev/null +++ b/test/integration/targets/become_su/runme.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +set -eux + +# ensure we execute su with a pseudo terminal +[ "$(ansible -a whoami --become-method=su localhost --become)" != "su: requires a terminal to execute" ]