From 3fbe6e976cfcc7369d1aaf7750a76b521ff064c9 Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Tue, 21 May 2019 03:14:28 +0200 Subject: [PATCH] [WIP] [2.8] connection/docker: add support for privilege escalation (#56277) * connection/docker: add privilege escalation support (#55816) As described in #53385 (and #31759), the docker connection driver did not support privilege escalation. This commit is a shameless cut-and-paste of the privilege escalation support from the `local` connection plugin into the `docker` plugin. Closes: #53385 (cherry picked from commit 61e476b908801daa0fb01b3c16eab094d50abc63) * docker connection plugin: make privilege escalation code more similar to local.py (#56288) * Make more similar to local.py * Fix typo. (cherry picked from commit 708bda06aa5cceaea5ead1e881a52b5adbc4d424) --- .../53385-docker-privilege-escalation.yml | 4 ++ lib/ansible/plugins/connection/docker.py | 55 +++++++++++++++++-- 2 files changed, 55 insertions(+), 4 deletions(-) create mode 100644 changelogs/fragments/53385-docker-privilege-escalation.yml diff --git a/changelogs/fragments/53385-docker-privilege-escalation.yml b/changelogs/fragments/53385-docker-privilege-escalation.yml new file mode 100644 index 0000000000..f727d794d1 --- /dev/null +++ b/changelogs/fragments/53385-docker-privilege-escalation.yml @@ -0,0 +1,4 @@ +--- +bugfixes: + - Fix privilege escalation support for the docker connection plugin when + credentials need to be supplied (e.g. sudo with password). diff --git a/lib/ansible/plugins/connection/docker.py b/lib/ansible/plugins/connection/docker.py index 4ef3845a26..d6d3518630 100644 --- a/lib/ansible/plugins/connection/docker.py +++ b/lib/ansible/plugins/connection/docker.py @@ -39,6 +39,7 @@ DOCUMENTATION = """ """ import distutils.spawn +import fcntl import os import os.path import subprocess @@ -47,6 +48,7 @@ import re from distutils.version import LooseVersion import ansible.constants as C +from ansible.compat import selectors from ansible.errors import AnsibleError, AnsibleFileNotFound from ansible.module_utils.six.moves import shlex_quote from ansible.module_utils._text import to_bytes, to_native, to_text @@ -203,12 +205,57 @@ class Connection(ConnectionBase): local_cmd = self._build_exec_cmd([self._play_context.executable, '-c', cmd]) - display.vvv("EXEC %s" % (local_cmd,), host=self._play_context.remote_addr) - local_cmd = [to_bytes(i, errors='surrogate_or_strict') for i in local_cmd] - p = subprocess.Popen(local_cmd, shell=False, stdin=subprocess.PIPE, - stdout=subprocess.PIPE, stderr=subprocess.PIPE) + display.vvv(u"EXEC {0}".format(to_text(local_cmd)), host=self._play_context.remote_addr) + display.debug("opening command with Popen()") + local_cmd = [to_bytes(i, errors='surrogate_or_strict') for i in local_cmd] + + p = subprocess.Popen( + local_cmd, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + display.debug("done running command with Popen()") + + if self.become and self.become.expect_prompt() and sudoable: + 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) + selector = selectors.DefaultSelector() + selector.register(p.stdout, selectors.EVENT_READ) + selector.register(p.stderr, selectors.EVENT_READ) + + become_output = b'' + try: + while not self.become.check_success(become_output) and not self.become.check_password_prompt(become_output): + events = selector.select(self._play_context.timeout) + if not events: + stdout, stderr = p.communicate() + raise AnsibleError('timeout waiting for privilege escalation password prompt:\n' + to_native(become_output)) + + for key, event in events: + if key.fileobj == p.stdout: + chunk = p.stdout.read() + elif key.fileobj == p.stderr: + chunk = p.stderr.read() + + if not chunk: + stdout, stderr = p.communicate() + raise AnsibleError('privilege output closed while waiting for password prompt:\n' + to_native(become_output)) + become_output += chunk + finally: + selector.close() + + if not self.become.check_success(become_output): + p.stdin.write(to_bytes(self._play_context.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) + + display.debug("getting output with communicate()") stdout, stderr = p.communicate(in_data) + display.debug("done communicating") + + display.debug("done with docker.exec_command()") return (p.returncode, stdout, stderr) def _prefix_login_path(self, remote_path):