Use a pty for local connections (#73023)

* Use a pty for local connections

Fixes #38696

Co-authored-by: James Cammarata <jimi@sngx.net>
This commit is contained in:
Brian Coca 2021-01-18 16:02:04 -05:00 committed by GitHub
parent d500e6ec45
commit 30d93995dd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 36 additions and 2 deletions

View file

@ -0,0 +1,2 @@
bugfixes:
- allow become method 'su' to work on 'local' connection by allocating a fake tty.

View file

@ -17,6 +17,7 @@ DOCUMENTATION = '''
''' '''
import os import os
import pty
import shutil import shutil
import subprocess import subprocess
import fcntl import fcntl
@ -79,15 +80,32 @@ class Connection(ConnectionBase):
else: else:
cmd = map(to_bytes, cmd) 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( p = subprocess.Popen(
cmd, cmd,
shell=isinstance(cmd, (text_type, binary_type)), shell=isinstance(cmd, (text_type, binary_type)),
executable=executable, executable=executable,
cwd=self.cwd, cwd=self.cwd,
stdin=subprocess.PIPE, stdin=stdin,
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=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()") display.debug("done running command with Popen()")
if self.become and self.become.expect_prompt() and sudoable: 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): if not self.become.check_success(become_output):
become_pass = self.become.get_option('become_pass', playcontext=self._play_context) 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.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) 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) stdout, stderr = p.communicate(in_data)
display.debug("done communicating") 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()") display.debug("done with local.exec_command()")
return (p.returncode, stdout, stderr) return (p.returncode, stdout, stderr)

View file

@ -0,0 +1,3 @@
destructive
shippable/posix/group1
skip/aix

View file

@ -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" ]