Reformat code with black and isort.

This commit is contained in:
Felix Fontein
2025-10-06 18:34:59 +02:00
parent f45232635c
commit d65d37e9e9
132 changed files with 17581 additions and 14729 deletions
+214 -108
View File
@@ -9,6 +9,7 @@
from __future__ import annotations
DOCUMENTATION = r"""
author:
- Lorin Hochestein (!UNKNOWN)
@@ -114,26 +115,28 @@ options:
import fcntl
import os
import os.path
import re
import selectors
import subprocess
import re
from shlex import quote
from ansible.errors import AnsibleError, AnsibleFileNotFound, AnsibleConnectionFailure
from ansible.errors import AnsibleConnectionFailure, AnsibleError, AnsibleFileNotFound
from ansible.module_utils.common.process import get_bin_path
from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
from ansible.plugins.connection import ConnectionBase, BUFSIZE
from ansible.plugins.connection import BUFSIZE, ConnectionBase
from ansible.utils.display import Display
from ansible_collections.community.docker.plugins.module_utils.version import (
LooseVersion,
)
from ansible_collections.community.docker.plugins.module_utils.version import LooseVersion
display = Display()
class Connection(ConnectionBase):
''' Local docker based connections '''
"""Local docker based connections"""
transport = 'community.docker.docker'
transport = "community.docker.docker"
has_pipelining = True
def __init__(self, play_context, new_stdin, *args, **kwargs):
@@ -154,29 +157,31 @@ class Connection(ConnectionBase):
# Windows uses Powershell modules
if getattr(self._shell, "_IS_WINDOWS", False):
self.module_implementation_preferences = ('.ps1', '.exe', '')
self.module_implementation_preferences = (".ps1", ".exe", "")
if 'docker_command' in kwargs:
self.docker_cmd = kwargs['docker_command']
if "docker_command" in kwargs:
self.docker_cmd = kwargs["docker_command"]
else:
try:
self.docker_cmd = get_bin_path('docker')
self.docker_cmd = get_bin_path("docker")
except ValueError:
raise AnsibleError("docker command not found in PATH")
@staticmethod
def _sanitize_version(version):
version = re.sub('[^0-9a-zA-Z.]', '', version)
version = re.sub('^v', '', version)
version = re.sub("[^0-9a-zA-Z.]", "", version)
version = re.sub("^v", "", version)
return version
def _old_docker_version(self):
cmd_args = self._docker_args
old_version_subcommand = ['version']
old_version_subcommand = ["version"]
old_docker_cmd = [self.docker_cmd] + cmd_args + old_version_subcommand
p = subprocess.Popen(old_docker_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
p = subprocess.Popen(
old_docker_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
cmd_output, err = p.communicate()
return old_docker_cmd, to_native(cmd_output), err, p.returncode
@@ -185,10 +190,12 @@ class Connection(ConnectionBase):
# no result yet, must be newer Docker version
cmd_args = self._docker_args
new_version_subcommand = ['version', '--format', "'{{.Server.Version}}'"]
new_version_subcommand = ["version", "--format", "'{{.Server.Version}}'"]
new_docker_cmd = [self.docker_cmd] + cmd_args + new_version_subcommand
p = subprocess.Popen(new_docker_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
p = subprocess.Popen(
new_docker_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
cmd_output, err = p.communicate()
return new_docker_cmd, to_native(cmd_output), err, p.returncode
@@ -196,42 +203,49 @@ class Connection(ConnectionBase):
cmd, cmd_output, err, returncode = self._old_docker_version()
if returncode == 0:
for line in to_text(cmd_output, errors='surrogate_or_strict').split('\n'):
if line.startswith('Server version:'): # old docker versions
for line in to_text(cmd_output, errors="surrogate_or_strict").split("\n"):
if line.startswith("Server version:"): # old docker versions
return self._sanitize_version(line.split()[2])
cmd, cmd_output, err, returncode = self._new_docker_version()
if returncode:
raise AnsibleError(f'Docker version check ({to_native(cmd)}) failed: {to_native(err)}')
raise AnsibleError(
f"Docker version check ({to_native(cmd)}) failed: {to_native(err)}"
)
return self._sanitize_version(to_text(cmd_output, errors='surrogate_or_strict'))
return self._sanitize_version(to_text(cmd_output, errors="surrogate_or_strict"))
def _get_docker_remote_user(self):
""" Get the default user configured in the docker container """
container = self.get_option('remote_addr')
"""Get the default user configured in the docker container"""
container = self.get_option("remote_addr")
if container in self._container_user_cache:
return self._container_user_cache[container]
p = subprocess.Popen([self.docker_cmd, 'inspect', '--format', '{{.Config.User}}', container],
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
p = subprocess.Popen(
[self.docker_cmd, "inspect", "--format", "{{.Config.User}}", container],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
out, err = p.communicate()
out = to_text(out, errors='surrogate_or_strict')
out = to_text(out, errors="surrogate_or_strict")
if p.returncode != 0:
display.warning(f'unable to retrieve default user from docker container: {out} {to_text(err)}')
display.warning(
f"unable to retrieve default user from docker container: {out} {to_text(err)}"
)
self._container_user_cache[container] = None
return None
# The default exec user is root, unless it was changed in the Dockerfile with USER
user = out.strip() or 'root'
user = out.strip() or "root"
self._container_user_cache[container] = user
return user
def _build_exec_cmd(self, cmd):
""" Build the local docker exec command to run cmd on remote_host
"""Build the local docker exec command to run cmd on remote_host
If remote_user is available and is supported by the docker
version we are using, it will be provided to docker exec.
If remote_user is available and is supported by the docker
version we are using, it will be provided to docker exec.
"""
local_cmd = [self.docker_cmd]
@@ -239,34 +253,46 @@ class Connection(ConnectionBase):
if self._docker_args:
local_cmd += self._docker_args
local_cmd += [b'exec']
local_cmd += [b"exec"]
if self.remote_user is not None:
local_cmd += [b'-u', self.remote_user]
local_cmd += [b"-u", self.remote_user]
if self.get_option('extra_env'):
for k, v in self.get_option('extra_env').items():
for val, what in ((k, 'Key'), (v, 'Value')):
if self.get_option("extra_env"):
for k, v in self.get_option("extra_env").items():
for val, what in ((k, "Key"), (v, "Value")):
if not isinstance(val, str):
raise AnsibleConnectionFailure(
f'Non-string {what.lower()} found for extra_env option. Ambiguous env options must be '
f'wrapped in quotes to avoid them being interpreted. {what}: {val!r}'
f"Non-string {what.lower()} found for extra_env option. Ambiguous env options must be "
f"wrapped in quotes to avoid them being interpreted. {what}: {val!r}"
)
local_cmd += [b'-e', b"%s=%s" % (to_bytes(k, errors='surrogate_or_strict'), to_bytes(v, errors='surrogate_or_strict'))]
local_cmd += [
b"-e",
b"%s=%s"
% (
to_bytes(k, errors="surrogate_or_strict"),
to_bytes(v, errors="surrogate_or_strict"),
),
]
if self.get_option('working_dir') is not None:
local_cmd += [b'-w', to_bytes(self.get_option('working_dir'), errors='surrogate_or_strict')]
if self.docker_version != 'dev' and LooseVersion(self.docker_version) < LooseVersion('18.06'):
if self.get_option("working_dir") is not None:
local_cmd += [
b"-w",
to_bytes(self.get_option("working_dir"), errors="surrogate_or_strict"),
]
if self.docker_version != "dev" and LooseVersion(
self.docker_version
) < LooseVersion("18.06"):
# https://github.com/docker/cli/pull/732, first appeared in release 18.06.0
raise AnsibleConnectionFailure(
f'Providing the working directory requires Docker CLI version 18.06 or newer. You have Docker CLI version {self.docker_version}.'
f"Providing the working directory requires Docker CLI version 18.06 or newer. You have Docker CLI version {self.docker_version}."
)
if self.get_option('privileged'):
local_cmd += [b'--privileged']
if self.get_option("privileged"):
local_cmd += [b"--privileged"]
# -i is needed to keep stdin open which allows pipelining to work
local_cmd += [b'-i', self.get_option('remote_addr')] + cmd
local_cmd += [b"-i", self.get_option("remote_addr")] + cmd
return local_cmd
@@ -274,22 +300,23 @@ class Connection(ConnectionBase):
# TODO: this is mostly for backwards compatibility, play_context is used as fallback for older versions
# docker arguments
del self._docker_args[:]
extra_args = self.get_option('docker_extra_args') or getattr(self._play_context, 'docker_extra_args', '')
extra_args = self.get_option("docker_extra_args") or getattr(
self._play_context, "docker_extra_args", ""
)
if extra_args:
self._docker_args += extra_args.split(' ')
self._docker_args += extra_args.split(" ")
def _set_conn_data(self):
''' initialize for the connection, cannot do only in init since all data is not ready at that point '''
"""initialize for the connection, cannot do only in init since all data is not ready at that point"""
self._set_docker_args()
self.remote_user = self.get_option('remote_user')
self.remote_user = self.get_option("remote_user")
if self.remote_user is None and self._play_context.remote_user is not None:
self.remote_user = self._play_context.remote_user
# timeout, use unless default and pc is different, backwards compat
self.timeout = self.get_option('container_timeout')
self.timeout = self.get_option("container_timeout")
if self.timeout == 10 and self.timeout != self._play_context.timeout:
self.timeout = self._play_context.timeout
@@ -300,23 +327,33 @@ class Connection(ConnectionBase):
self._set_docker_args()
self._version = self._get_docker_version()
if self._version == 'dev':
display.warning('Docker version number is "dev". Will assume latest version.')
if self._version != 'dev' and LooseVersion(self._version) < LooseVersion('1.3'):
raise AnsibleError('docker connection type requires docker 1.3 or higher')
if self._version == "dev":
display.warning(
'Docker version number is "dev". Will assume latest version.'
)
if self._version != "dev" and LooseVersion(self._version) < LooseVersion(
"1.3"
):
raise AnsibleError(
"docker connection type requires docker 1.3 or higher"
)
return self._version
def _get_actual_user(self):
if self.remote_user is not None:
# An explicit user is provided
if self.docker_version == 'dev' or LooseVersion(self.docker_version) >= LooseVersion('1.7'):
if self.docker_version == "dev" or LooseVersion(
self.docker_version
) >= LooseVersion("1.7"):
# Support for specifying the exec user was added in docker 1.7
return self.remote_user
else:
self.remote_user = None
actual_user = self._get_docker_remote_user()
if actual_user != self.get_option('remote_user'):
display.warning(f'docker {self.docker_version} does not support remote_user, using container default: {self.actual_user or "?"}')
if actual_user != self.get_option("remote_user"):
display.warning(
f'docker {self.docker_version} does not support remote_user, using container default: {self.actual_user or "?"}'
)
return actual_user
elif self._display.verbosity > 2:
# Since we are not setting the actual_user, look it up so we have it for logging later
@@ -327,27 +364,30 @@ class Connection(ConnectionBase):
return None
def _connect(self, port=None):
""" Connect to the container. Nothing to do """
"""Connect to the container. Nothing to do"""
super(Connection, self)._connect()
if not self._connected:
self._set_conn_data()
actual_user = self._get_actual_user()
display.vvv(f"ESTABLISH DOCKER CONNECTION FOR USER: {actual_user or '?'}", host=self.get_option('remote_addr'))
display.vvv(
f"ESTABLISH DOCKER CONNECTION FOR USER: {actual_user or '?'}",
host=self.get_option("remote_addr"),
)
self._connected = True
def exec_command(self, cmd, in_data=None, sudoable=False):
""" Run a command on the docker host """
"""Run a command on the docker host"""
self._set_conn_data()
super(Connection, self).exec_command(cmd, in_data=in_data, sudoable=sudoable)
local_cmd = self._build_exec_cmd([self._play_context.executable, '-c', cmd])
local_cmd = self._build_exec_cmd([self._play_context.executable, "-c", cmd])
display.vvv(f"EXEC {to_text(local_cmd)}", host=self.get_option('remote_addr'))
display.vvv(f"EXEC {to_text(local_cmd)}", host=self.get_option("remote_addr"))
display.debug("opening command with Popen()")
local_cmd = [to_bytes(i, errors='surrogate_or_strict') for i in local_cmd]
local_cmd = [to_bytes(i, errors="surrogate_or_strict") for i in local_cmd]
p = subprocess.Popen(
local_cmd,
@@ -358,19 +398,32 @@ class Connection(ConnectionBase):
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)
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''
become_output = b""
try:
while not self.become.check_success(become_output) and not self.become.check_password_prompt(become_output):
while not self.become.check_success(
become_output
) and not self.become.check_password_prompt(become_output):
events = selector.select(self.timeout)
if not events:
stdout, stderr = p.communicate()
raise AnsibleError('timeout waiting for privilege escalation password prompt:\n' + to_native(become_output))
raise AnsibleError(
"timeout waiting for privilege escalation password prompt:\n"
+ to_native(become_output)
)
for key, event in events:
if key.fileobj == p.stdout:
@@ -380,16 +433,31 @@ class Connection(ConnectionBase):
if not chunk:
stdout, stderr = p.communicate()
raise AnsibleError('privilege output closed while waiting for password prompt:\n' + to_native(become_output))
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):
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')
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)
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"
)
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)
@@ -399,17 +467,18 @@ class Connection(ConnectionBase):
return (p.returncode, stdout, stderr)
def _prefix_login_path(self, remote_path):
''' Make sure that we put files into a standard path
"""Make sure that we put files into a standard path
If a path is relative, then we need to choose where to put it.
ssh chooses $HOME but we are not guaranteed that a home dir will
exist in any given chroot. So for now we are choosing "/" instead.
This also happens to be the former default.
If a path is relative, then we need to choose where to put it.
ssh chooses $HOME but we are not guaranteed that a home dir will
exist in any given chroot. So for now we are choosing "/" instead.
This also happens to be the former default.
Can revisit using $HOME instead if it is a problem
'''
Can revisit using $HOME instead if it is a problem
"""
if getattr(self._shell, "_IS_WINDOWS", False):
import ntpath
return ntpath.normpath(remote_path)
else:
if not remote_path.startswith(os.path.sep):
@@ -417,57 +486,79 @@ class Connection(ConnectionBase):
return os.path.normpath(remote_path)
def put_file(self, in_path, out_path):
""" Transfer a file from local to docker container """
"""Transfer a file from local to docker container"""
self._set_conn_data()
super(Connection, self).put_file(in_path, out_path)
display.vvv(f"PUT {in_path} TO {out_path}", host=self.get_option('remote_addr'))
display.vvv(f"PUT {in_path} TO {out_path}", host=self.get_option("remote_addr"))
out_path = self._prefix_login_path(out_path)
if not os.path.exists(to_bytes(in_path, errors='surrogate_or_strict')):
if not os.path.exists(to_bytes(in_path, errors="surrogate_or_strict")):
raise AnsibleFileNotFound(
f"file or module does not exist: {to_native(in_path)}")
f"file or module does not exist: {to_native(in_path)}"
)
out_path = quote(out_path)
# Older docker does not have native support for copying files into
# running containers, so we use docker exec to implement this
# Although docker version 1.8 and later provide support, the
# owner and group of the files are always set to root
with open(to_bytes(in_path, errors='surrogate_or_strict'), 'rb') as in_file:
with open(to_bytes(in_path, errors="surrogate_or_strict"), "rb") as in_file:
if not os.fstat(in_file.fileno()).st_size:
count = ' count=0'
count = " count=0"
else:
count = ''
args = self._build_exec_cmd([self._play_context.executable, "-c", f"dd of={out_path} bs={BUFSIZE}{count}"])
args = [to_bytes(i, errors='surrogate_or_strict') for i in args]
count = ""
args = self._build_exec_cmd(
[
self._play_context.executable,
"-c",
f"dd of={out_path} bs={BUFSIZE}{count}",
]
)
args = [to_bytes(i, errors="surrogate_or_strict") for i in args]
try:
p = subprocess.Popen(args, stdin=in_file, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
p = subprocess.Popen(
args, stdin=in_file, stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
except OSError:
raise AnsibleError("docker connection requires dd command in the container to put files")
raise AnsibleError(
"docker connection requires dd command in the container to put files"
)
stdout, stderr = p.communicate()
if p.returncode != 0:
raise AnsibleError(f"failed to transfer file {to_native(in_path)} to {to_native(out_path)}:\n{to_native(stdout)}\n{to_native(stderr)}")
raise AnsibleError(
f"failed to transfer file {to_native(in_path)} to {to_native(out_path)}:\n{to_native(stdout)}\n{to_native(stderr)}"
)
def fetch_file(self, in_path, out_path):
""" Fetch a file from container to local. """
"""Fetch a file from container to local."""
self._set_conn_data()
super(Connection, self).fetch_file(in_path, out_path)
display.vvv(f"FETCH {in_path} TO {out_path}", host=self.get_option('remote_addr'))
display.vvv(
f"FETCH {in_path} TO {out_path}", host=self.get_option("remote_addr")
)
in_path = self._prefix_login_path(in_path)
# out_path is the final file path, but docker takes a directory, not a
# file path
out_dir = os.path.dirname(out_path)
args = [self.docker_cmd, "cp", f"{self.get_option('remote_addr')}:{in_path}", out_dir]
args = [to_bytes(i, errors='surrogate_or_strict') for i in args]
args = [
self.docker_cmd,
"cp",
f"{self.get_option('remote_addr')}:{in_path}",
out_dir,
]
args = [to_bytes(i, errors="surrogate_or_strict") for i in args]
p = subprocess.Popen(args, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
p = subprocess.Popen(
args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
p.communicate()
if getattr(self._shell, "_IS_WINDOWS", False):
import ntpath
actual_out_path = ntpath.join(out_dir, ntpath.basename(in_path))
else:
actual_out_path = os.path.join(out_dir, os.path.basename(in_path))
@@ -475,25 +566,40 @@ class Connection(ConnectionBase):
if p.returncode != 0:
# Older docker does not have native support for fetching files command `cp`
# If `cp` fails, try to use `dd` instead
args = self._build_exec_cmd([self._play_context.executable, "-c", f"dd if={in_path} bs={BUFSIZE}"])
args = [to_bytes(i, errors='surrogate_or_strict') for i in args]
with open(to_bytes(actual_out_path, errors='surrogate_or_strict'), 'wb') as out_file:
args = self._build_exec_cmd(
[self._play_context.executable, "-c", f"dd if={in_path} bs={BUFSIZE}"]
)
args = [to_bytes(i, errors="surrogate_or_strict") for i in args]
with open(
to_bytes(actual_out_path, errors="surrogate_or_strict"), "wb"
) as out_file:
try:
p = subprocess.Popen(args, stdin=subprocess.PIPE,
stdout=out_file, stderr=subprocess.PIPE)
p = subprocess.Popen(
args,
stdin=subprocess.PIPE,
stdout=out_file,
stderr=subprocess.PIPE,
)
except OSError:
raise AnsibleError("docker connection requires dd command in the container to put files")
raise AnsibleError(
"docker connection requires dd command in the container to put files"
)
stdout, stderr = p.communicate()
if p.returncode != 0:
raise AnsibleError(f"failed to fetch file {in_path} to {out_path}:\n{stdout}\n{stderr}")
raise AnsibleError(
f"failed to fetch file {in_path} to {out_path}:\n{stdout}\n{stderr}"
)
# Rename if needed
if actual_out_path != out_path:
os.rename(to_bytes(actual_out_path, errors='strict'), to_bytes(out_path, errors='strict'))
os.rename(
to_bytes(actual_out_path, errors="strict"),
to_bytes(out_path, errors="strict"),
)
def close(self):
""" Terminate the connection. Nothing to do for Docker"""
"""Terminate the connection. Nothing to do for Docker"""
super(Connection, self).close()
self._connected = False
+149 -89
View File
@@ -4,6 +4,7 @@
from __future__ import annotations
DOCUMENTATION = r"""
author:
- Felix Fontein (@felixfontein)
@@ -107,11 +108,15 @@ options:
import os
import os.path
from ansible.errors import AnsibleFileNotFound, AnsibleConnectionFailure
from ansible.errors import AnsibleConnectionFailure, AnsibleFileNotFound
from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
from ansible.plugins.connection import ConnectionBase
from ansible.utils.display import Display
from ansible_collections.community.docker.plugins.module_utils._api.errors import (
APIError,
DockerException,
NotFound,
)
from ansible_collections.community.docker.plugins.module_utils.common_api import (
RequestException,
)
@@ -121,17 +126,16 @@ from ansible_collections.community.docker.plugins.module_utils.copy import (
fetch_file,
put_file,
)
from ansible_collections.community.docker.plugins.plugin_utils.socket_handler import (
DockerSocketHandler,
from ansible_collections.community.docker.plugins.module_utils.version import (
LooseVersion,
)
from ansible_collections.community.docker.plugins.plugin_utils.common_api import (
AnsibleDockerClient,
)
from ansible_collections.community.docker.plugins.plugin_utils.socket_handler import (
DockerSocketHandler,
)
from ansible_collections.community.docker.plugins.module_utils._api.errors import APIError, DockerException, NotFound
from ansible_collections.community.docker.plugins.module_utils.version import LooseVersion
MIN_DOCKER_API = None
@@ -140,23 +144,29 @@ display = Display()
class Connection(ConnectionBase):
''' Local docker based connections '''
"""Local docker based connections"""
transport = 'community.docker.docker_api'
transport = "community.docker.docker_api"
has_pipelining = True
def _call_client(self, callable, not_found_can_be_resource=False):
remote_addr = self.get_option('remote_addr')
remote_addr = self.get_option("remote_addr")
try:
return callable()
except NotFound as e:
if not_found_can_be_resource:
raise AnsibleConnectionFailure(f'Could not find container "{remote_addr}" or resource in it ({e})')
raise AnsibleConnectionFailure(
f'Could not find container "{remote_addr}" or resource in it ({e})'
)
else:
raise AnsibleConnectionFailure(f'Could not find container "{remote_addr}" ({e})')
raise AnsibleConnectionFailure(
f'Could not find container "{remote_addr}" ({e})'
)
except APIError as e:
if e.response is not None and e.response.status_code == 409:
raise AnsibleConnectionFailure(f'The container "{remote_addr}" has been paused ({e})')
raise AnsibleConnectionFailure(
f'The container "{remote_addr}" has been paused ({e})'
)
self.client.fail(
f'An unexpected Docker error occurred for container "{remote_addr}": {e}'
)
@@ -177,18 +187,23 @@ class Connection(ConnectionBase):
# Windows uses Powershell modules
if getattr(self._shell, "_IS_WINDOWS", False):
self.module_implementation_preferences = ('.ps1', '.exe', '')
self.module_implementation_preferences = (".ps1", ".exe", "")
self.actual_user = None
def _connect(self, port=None):
""" Connect to the container. Nothing to do """
"""Connect to the container. Nothing to do"""
super(Connection, self)._connect()
if not self._connected:
self.actual_user = self.get_option('remote_user')
display.vvv(f"ESTABLISH DOCKER CONNECTION FOR USER: {self.actual_user or '?'}", host=self.get_option('remote_addr'))
self.actual_user = self.get_option("remote_user")
display.vvv(
f"ESTABLISH DOCKER CONNECTION FOR USER: {self.actual_user or '?'}",
host=self.get_option("remote_addr"),
)
if self.client is None:
self.client = AnsibleDockerClient(self, min_docker_api_version=MIN_DOCKER_API)
self.client = AnsibleDockerClient(
self, min_docker_api_version=MIN_DOCKER_API
)
self._connected = True
if self.actual_user is None and display.verbosity > 2:
@@ -196,95 +211,125 @@ class Connection(ConnectionBase):
# Only do this if display verbosity is high enough that we'll need the value
# This saves overhead from calling into docker when we do not need to
display.vvv("Trying to determine actual user")
result = self._call_client(lambda: self.client.get_json('/containers/{0}/json', self.get_option('remote_addr')))
if result.get('Config'):
self.actual_user = result['Config'].get('User')
result = self._call_client(
lambda: self.client.get_json(
"/containers/{0}/json", self.get_option("remote_addr")
)
)
if result.get("Config"):
self.actual_user = result["Config"].get("User")
if self.actual_user is not None:
display.vvv(f"Actual user is '{self.actual_user}'")
def exec_command(self, cmd, in_data=None, sudoable=False):
""" Run a command on the docker host """
"""Run a command on the docker host"""
super(Connection, self).exec_command(cmd, in_data=in_data, sudoable=sudoable)
command = [self._play_context.executable, '-c', to_text(cmd)]
command = [self._play_context.executable, "-c", to_text(cmd)]
do_become = self.become and self.become.expect_prompt() and sudoable
stdin_part = f', with stdin ({len(in_data)} bytes)' if in_data is not None else ''
become_part = ', with become prompt' if do_become else ''
stdin_part = (
f", with stdin ({len(in_data)} bytes)" if in_data is not None else ""
)
become_part = ", with become prompt" if do_become else ""
display.vvv(
f"EXEC {to_text(command)}{stdin_part}{become_part}",
host=self.get_option('remote_addr')
host=self.get_option("remote_addr"),
)
need_stdin = True if (in_data is not None) or do_become else False
data = {
'Container': self.get_option('remote_addr'),
'User': self.get_option('remote_user') or '',
'Privileged': self.get_option('privileged'),
'Tty': False,
'AttachStdin': need_stdin,
'AttachStdout': True,
'AttachStderr': True,
'Cmd': command,
"Container": self.get_option("remote_addr"),
"User": self.get_option("remote_user") or "",
"Privileged": self.get_option("privileged"),
"Tty": False,
"AttachStdin": need_stdin,
"AttachStdout": True,
"AttachStderr": True,
"Cmd": command,
}
if 'detachKeys' in self.client._general_configs:
data['detachKeys'] = self.client._general_configs['detachKeys']
if "detachKeys" in self.client._general_configs:
data["detachKeys"] = self.client._general_configs["detachKeys"]
if self.get_option('extra_env'):
data['Env'] = []
for k, v in self.get_option('extra_env').items():
for val, what in ((k, 'Key'), (v, 'Value')):
if self.get_option("extra_env"):
data["Env"] = []
for k, v in self.get_option("extra_env").items():
for val, what in ((k, "Key"), (v, "Value")):
if not isinstance(val, str):
raise AnsibleConnectionFailure(
f'Non-string {what.lower()} found for extra_env option. Ambiguous env options must be '
f'wrapped in quotes to avoid them being interpreted. {what}: {val!r}'
f"Non-string {what.lower()} found for extra_env option. Ambiguous env options must be "
f"wrapped in quotes to avoid them being interpreted. {what}: {val!r}"
)
kk = to_text(k, errors='surrogate_or_strict')
vv = to_text(v, errors='surrogate_or_strict')
data['Env'].append(f'{kk}={vv}')
kk = to_text(k, errors="surrogate_or_strict")
vv = to_text(v, errors="surrogate_or_strict")
data["Env"].append(f"{kk}={vv}")
if self.get_option('working_dir') is not None:
data['WorkingDir'] = self.get_option('working_dir')
if self.client.docker_api_version < LooseVersion('1.35'):
if self.get_option("working_dir") is not None:
data["WorkingDir"] = self.get_option("working_dir")
if self.client.docker_api_version < LooseVersion("1.35"):
raise AnsibleConnectionFailure(
'Providing the working directory requires Docker API version 1.35 or newer.'
f' The Docker daemon the connection is using has API version {self.client.docker_api_version_str}.'
"Providing the working directory requires Docker API version 1.35 or newer."
f" The Docker daemon the connection is using has API version {self.client.docker_api_version_str}."
)
exec_data = self._call_client(lambda: self.client.post_json_to_json('/containers/{0}/exec', self.get_option('remote_addr'), data=data))
exec_id = exec_data['Id']
exec_data = self._call_client(
lambda: self.client.post_json_to_json(
"/containers/{0}/exec", self.get_option("remote_addr"), data=data
)
)
exec_id = exec_data["Id"]
data = {
'Tty': False,
'Detach': False
}
data = {"Tty": False, "Detach": False}
if need_stdin:
exec_socket = self._call_client(lambda: self.client.post_json_to_stream_socket('/exec/{0}/start', exec_id, data=data))
exec_socket = self._call_client(
lambda: self.client.post_json_to_stream_socket(
"/exec/{0}/start", exec_id, data=data
)
)
try:
with DockerSocketHandler(display, exec_socket, container=self.get_option('remote_addr')) as exec_socket_handler:
with DockerSocketHandler(
display, exec_socket, container=self.get_option("remote_addr")
) as exec_socket_handler:
if do_become:
become_output = [b'']
become_output = [b""]
def append_become_output(stream_id, data):
become_output[0] += data
exec_socket_handler.set_block_done_callback(append_become_output)
exec_socket_handler.set_block_done_callback(
append_become_output
)
while not self.become.check_success(become_output[0]) and not self.become.check_password_prompt(become_output[0]):
if not exec_socket_handler.select(self.get_option('container_timeout')):
while not self.become.check_success(
become_output[0]
) and not self.become.check_password_prompt(become_output[0]):
if not exec_socket_handler.select(
self.get_option("container_timeout")
):
stdout, stderr = exec_socket_handler.consume()
raise AnsibleConnectionFailure('timeout waiting for privilege escalation password prompt:\n' + to_native(become_output[0]))
raise AnsibleConnectionFailure(
"timeout waiting for privilege escalation password prompt:\n"
+ to_native(become_output[0])
)
if exec_socket_handler.is_eof():
raise AnsibleConnectionFailure('privilege output closed while waiting for password prompt:\n' + to_native(become_output[0]))
raise AnsibleConnectionFailure(
"privilege output closed while waiting for password prompt:\n"
+ to_native(become_output[0])
)
if not self.become.check_success(become_output[0]):
become_pass = self.become.get_option('become_pass', playcontext=self._play_context)
exec_socket_handler.write(to_bytes(become_pass, errors='surrogate_or_strict') + b'\n')
become_pass = self.become.get_option(
"become_pass", playcontext=self._play_context
)
exec_socket_handler.write(
to_bytes(become_pass, errors="surrogate_or_strict")
+ b"\n"
)
if in_data is not None:
exec_socket_handler.write(in_data)
@@ -293,25 +338,36 @@ class Connection(ConnectionBase):
finally:
exec_socket.close()
else:
stdout, stderr = self._call_client(lambda: self.client.post_json_to_stream(
'/exec/{0}/start', exec_id, stream=False, demux=True, tty=False, data=data))
stdout, stderr = self._call_client(
lambda: self.client.post_json_to_stream(
"/exec/{0}/start",
exec_id,
stream=False,
demux=True,
tty=False,
data=data,
)
)
result = self._call_client(lambda: self.client.get_json('/exec/{0}/json', exec_id))
result = self._call_client(
lambda: self.client.get_json("/exec/{0}/json", exec_id)
)
return result.get('ExitCode') or 0, stdout or b'', stderr or b''
return result.get("ExitCode") or 0, stdout or b"", stderr or b""
def _prefix_login_path(self, remote_path):
''' Make sure that we put files into a standard path
"""Make sure that we put files into a standard path
If a path is relative, then we need to choose where to put it.
ssh chooses $HOME but we are not guaranteed that a home dir will
exist in any given chroot. So for now we are choosing "/" instead.
This also happens to be the former default.
If a path is relative, then we need to choose where to put it.
ssh chooses $HOME but we are not guaranteed that a home dir will
exist in any given chroot. So for now we are choosing "/" instead.
This also happens to be the former default.
Can revisit using $HOME instead if it is a problem
'''
Can revisit using $HOME instead if it is a problem
"""
if getattr(self._shell, "_IS_WINDOWS", False):
import ntpath
return ntpath.normpath(remote_path)
else:
if not remote_path.startswith(os.path.sep):
@@ -319,21 +375,21 @@ class Connection(ConnectionBase):
return os.path.normpath(remote_path)
def put_file(self, in_path, out_path):
""" Transfer a file from local to docker container """
"""Transfer a file from local to docker container"""
super(Connection, self).put_file(in_path, out_path)
display.vvv(f"PUT {in_path} TO {out_path}", host=self.get_option('remote_addr'))
display.vvv(f"PUT {in_path} TO {out_path}", host=self.get_option("remote_addr"))
out_path = self._prefix_login_path(out_path)
if self.actual_user not in self.ids:
dummy, ids, dummy = self.exec_command(b'id -u && id -g')
remote_addr = self.get_option('remote_addr')
dummy, ids, dummy = self.exec_command(b"id -u && id -g")
remote_addr = self.get_option("remote_addr")
try:
user_id, group_id = ids.splitlines()
self.ids[self.actual_user] = int(user_id), int(group_id)
display.vvvv(
f'PUT: Determined uid={user_id} and gid={group_id} for user "{self.actual_user}"',
host=remote_addr
host=remote_addr,
)
except Exception as e:
raise AnsibleConnectionFailure(
@@ -345,7 +401,7 @@ class Connection(ConnectionBase):
self._call_client(
lambda: put_file(
self.client,
container=self.get_option('remote_addr'),
container=self.get_option("remote_addr"),
in_path=in_path,
out_path=out_path,
user_id=user_id,
@@ -361,9 +417,11 @@ class Connection(ConnectionBase):
raise AnsibleConnectionFailure(to_native(exc))
def fetch_file(self, in_path, out_path):
""" Fetch a file from container to local. """
"""Fetch a file from container to local."""
super(Connection, self).fetch_file(in_path, out_path)
display.vvv(f"FETCH {in_path} TO {out_path}", host=self.get_option('remote_addr'))
display.vvv(
f"FETCH {in_path} TO {out_path}", host=self.get_option("remote_addr")
)
in_path = self._prefix_login_path(in_path)
@@ -371,11 +429,13 @@ class Connection(ConnectionBase):
self._call_client(
lambda: fetch_file(
self.client,
container=self.get_option('remote_addr'),
container=self.get_option("remote_addr"),
in_path=in_path,
out_path=out_path,
follow_links=True,
log=lambda msg: display.vvvv(msg, host=self.get_option('remote_addr')),
log=lambda msg: display.vvvv(
msg, host=self.get_option("remote_addr")
),
),
not_found_can_be_resource=True,
)
@@ -385,7 +445,7 @@ class Connection(ConnectionBase):
raise AnsibleConnectionFailure(to_native(exc))
def close(self):
""" Terminate the connection. Nothing to do for Docker"""
"""Terminate the connection. Nothing to do for Docker"""
super(Connection, self).close()
self._connected = False
+58 -21
View File
@@ -7,6 +7,7 @@
from __future__ import annotations
DOCUMENTATION = r"""
name: nsenter
short_description: execute on host running controller container
@@ -39,11 +40,11 @@ notes:
PID namespace (C(--pid host))."
"""
import fcntl
import os
import pty
import selectors
import subprocess
import fcntl
import ansible.constants as C
from ansible.errors import AnsibleError
@@ -57,10 +58,9 @@ display = Display()
class Connection(ConnectionBase):
'''Connections to a container host using nsenter
'''
"""Connections to a container host using nsenter"""
transport = 'community.docker.nsenter'
transport = "community.docker.nsenter"
has_pipelining = False
def __init__(self, *args, **kwargs):
@@ -89,9 +89,11 @@ class Connection(ConnectionBase):
executable = C.DEFAULT_EXECUTABLE.split()[0] if C.DEFAULT_EXECUTABLE else None
if not os.path.exists(to_bytes(executable, errors='surrogate_or_strict')):
raise AnsibleError(f"failed to find the executable specified {executable}."
" Please verify if the executable exists and re-try.")
if not os.path.exists(to_bytes(executable, errors="surrogate_or_strict")):
raise AnsibleError(
f"failed to find the executable specified {executable}."
" Please verify if the executable exists and re-try."
)
# Rewrite the provided command to prefix it with nsenter
nsenter_cmd_parts = [
@@ -148,19 +150,32 @@ class Connection(ConnectionBase):
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)
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''
become_output = b""
try:
while not self.become.check_success(become_output) and not self.become.check_password_prompt(become_output):
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))
raise AnsibleError(
"timeout waiting for privilege escalation password prompt:\n"
+ to_native(become_output)
)
for key, event in events:
if key.fileobj == p.stdout:
@@ -170,20 +185,38 @@ class Connection(ConnectionBase):
if not chunk:
stdout, stderr = p.communicate()
raise AnsibleError('privilege output closed while waiting for password prompt:\n' + to_native(become_output))
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):
become_pass = self.become.get_option('become_pass', playcontext=self._play_context)
become_pass = self.become.get_option(
"become_pass", playcontext=self._play_context
)
if master is None:
p.stdin.write(to_bytes(become_pass, errors='surrogate_or_strict') + b'\n')
p.stdin.write(
to_bytes(become_pass, errors="surrogate_or_strict") + b"\n"
)
else:
os.write(master, 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)
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)
@@ -220,14 +253,18 @@ class Connection(ConnectionBase):
try:
rc, out, err = self.exec_command(cmd=["cat", in_path])
display.vvv(f"FETCH {in_path} TO {out_path}", host=self._play_context.remote_addr)
display.vvv(
f"FETCH {in_path} TO {out_path}", host=self._play_context.remote_addr
)
if rc != 0:
raise AnsibleError(f"failed to transfer file to {in_path}: {err}")
with open(to_bytes(out_path, errors='surrogate_or_strict'), 'wb') as out_file:
with open(
to_bytes(out_path, errors="surrogate_or_strict"), "wb"
) as out_file:
out_file.write(out)
except IOError as e:
raise AnsibleError(f"failed to transfer file to {to_native(out_path)}: {e}")
def close(self):
''' terminate the connection; nothing to do here '''
"""terminate the connection; nothing to do here"""
self._connected = False