summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBrian Coca <bcoca@users.noreply.github.com>2021-02-07 18:53:10 -0500
committerGitHub <noreply@github.com>2021-02-07 17:53:10 -0600
commit3ef061bdc4610bbf213f70bc70976fdc3005e2cc (patch)
tree1093fff0d51041f6bfdcd4693ec3cbe351b2a6d3
parent63ead90ab9d81f9b5ac3cdc08a6a3a932c1a81d7 (diff)
downloadansible-3ef061bdc4610bbf213f70bc70976fdc3005e2cc.tar.gz
Use a pty for local connections (#73023) (#73281)
Fixes #38696 Co-authored-by: James Cammarata <jimi@sngx.net> (cherry picked from commit 30d93995ddcf92636919c2238435064a8089e240)
-rw-r--r--changelogs/fragments/enable_su_on_local.yaml2
-rw-r--r--lib/ansible/plugins/connection/local.py27
-rw-r--r--test/integration/targets/become_su/aliases3
-rwxr-xr-xtest/integration/targets/become_su/runme.sh6
4 files changed, 36 insertions, 2 deletions
diff --git a/changelogs/fragments/enable_su_on_local.yaml b/changelogs/fragments/enable_su_on_local.yaml
new file mode 100644
index 0000000000..1f5cdb4bf9
--- /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 29505cc27e..85cb72b01c 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 0000000000..3a07aab32d
--- /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 0000000000..87a3511f65
--- /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" ]