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
+12 -8
View File
@@ -9,8 +9,9 @@ import base64
from ansible import constants as C
from ansible.plugins.action import ActionBase
from ansible.utils.vars import merge_hash
from ansible_collections.community.docker.plugins.module_utils._scramble import unscramble
from ansible_collections.community.docker.plugins.module_utils._scramble import (
unscramble,
)
class ActionModule(ActionBase):
@@ -24,15 +25,18 @@ class ActionModule(ActionBase):
result = super(ActionModule, self).run(tmp, task_vars)
del tmp # tmp no longer has any effect
self._task.args['_max_file_size_for_diff'] = C.MAX_FILE_SIZE_FOR_DIFF
self._task.args["_max_file_size_for_diff"] = C.MAX_FILE_SIZE_FOR_DIFF
result = merge_hash(result, self._execute_module(task_vars=task_vars, wrap_async=self._task.async_val))
result = merge_hash(
result,
self._execute_module(task_vars=task_vars, wrap_async=self._task.async_val),
)
if 'diff' in result and result['diff'].get('scrambled_diff'):
if "diff" in result and result["diff"].get("scrambled_diff"):
# Scrambling is not done for security, but to avoid no_log screwing up the diff
diff = result['diff']
key = base64.b64decode(diff.pop('scrambled_diff'))
for k in ('before', 'after'):
diff = result["diff"]
key = base64.b64decode(diff.pop("scrambled_diff"))
for k in ("before", "after"):
if k in diff:
diff[k] = unscramble(diff[k], key)
+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
+6 -6
View File
@@ -34,7 +34,7 @@ attributes:
"""
# Should be used together with the standard fragment
INFO_MODULE = r'''
INFO_MODULE = r"""
options: {}
attributes:
check_mode:
@@ -45,9 +45,9 @@ attributes:
support: N/A
details:
- This action does not modify state.
'''
"""
ACTIONGROUP_DOCKER = r'''
ACTIONGROUP_DOCKER = r"""
options: {}
attributes:
action_group:
@@ -56,7 +56,7 @@ attributes:
membership:
- community.docker.docker
- docker
'''
"""
CONN = r"""
options: {}
@@ -77,7 +77,7 @@ attributes:
"""
# Should be used together with the standard fragment and the FACTS fragment
FACTS_MODULE = r'''
FACTS_MODULE = r"""
options: {}
attributes:
check_mode:
@@ -90,7 +90,7 @@ attributes:
- This action does not modify state.
facts:
support: full
'''
"""
FILES = r"""
options: {}
+2 -2
View File
@@ -74,8 +74,8 @@ notes:
"""
# The following needs to be kept in sync with the compose_v2 module utils
MINIMUM_VERSION = r'''
MINIMUM_VERSION = r"""
options: {}
requirements:
- "Docker CLI with Docker compose plugin 2.18.0 or later"
'''
"""
+10 -10
View File
@@ -123,7 +123,7 @@ notes:
# For plugins: allow to define common options with Ansible variables
VAR_NAMES = r'''
VAR_NAMES = r"""
options:
docker_host:
vars:
@@ -154,11 +154,11 @@ options:
validate_certs:
vars:
- name: ansible_docker_validate_certs
'''
"""
# Additional, more specific stuff for minimal Docker SDK for Python version < 2.0
DOCKER_PY_1_DOCUMENTATION = r'''
DOCKER_PY_1_DOCUMENTATION = r"""
options: {}
notes:
- This module uses the L(Docker SDK for Python,https://docker-py.readthedocs.io/en/stable/) to
@@ -170,12 +170,12 @@ requirements:
modules should *not* be installed at the same time. Also note that when both modules are
installed and one of them is uninstalled, the other might no longer function and a reinstall
of it is required."
'''
"""
# Additional, more specific stuff for minimal Docker SDK for Python version >= 2.0.
# Note that Docker SDK for Python >= 2.0 requires Python 2.7 or newer.
DOCKER_PY_2_DOCUMENTATION = r'''
DOCKER_PY_2_DOCUMENTATION = r"""
options: {}
notes:
- This module uses the L(Docker SDK for Python,https://docker-py.readthedocs.io/en/stable/) to
@@ -186,10 +186,10 @@ requirements:
Python module has been superseded by L(docker,https://pypi.org/project/docker/)
(see L(here,https://github.com/docker/docker-py/issues/1310) for details).
This module does *not* work with docker-py."
'''
"""
# Docker doc fragment when using the vendored API access code
API_DOCUMENTATION = r'''
API_DOCUMENTATION = r"""
options:
docker_host:
description:
@@ -302,10 +302,10 @@ requirements:
- pywin32 (when using named pipes on Windows 32)
- paramiko (when using SSH with O(use_ssh_client=false))
- pyOpenSSL (when using TLS)
'''
"""
# Docker doc fragment when using the Docker CLI
CLI_DOCUMENTATION = r'''
CLI_DOCUMENTATION = r"""
options:
docker_cli:
description:
@@ -403,4 +403,4 @@ notes:
U(https://docs.docker.com/machine/reference/env/) for more details.
- This module does B(not) use the L(Docker SDK for Python,https://docker-py.readthedocs.io/en/stable/) to
communicate with the Docker daemon. It directly calls the Docker CLI program.
'''
"""
+92 -77
View File
@@ -107,7 +107,7 @@ options:
version_added: 3.5.0
"""
EXAMPLES = '''
EXAMPLES = """
---
# Minimal example using local Docker daemon
plugin: community.docker.docker_containers
@@ -167,13 +167,16 @@ filters:
inventory_hostname.startswith("a")
# Exclude all containers that did not match any of the above filters
- exclude: true
'''
"""
import re
from ansible.errors import AnsibleError
from ansible.plugins.inventory import BaseInventoryPlugin, Constructable
from ansible_collections.community.docker.plugins.module_utils._api.errors import (
APIError,
DockerException,
)
from ansible_collections.community.docker.plugins.module_utils.common_api import (
RequestException,
)
@@ -183,64 +186,66 @@ from ansible_collections.community.docker.plugins.module_utils.util import (
from ansible_collections.community.docker.plugins.plugin_utils.common_api import (
AnsibleDockerClient,
)
from ansible_collections.community.docker.plugins.module_utils._api.errors import APIError, DockerException
from ansible_collections.community.docker.plugins.plugin_utils.unsafe import make_unsafe
from ansible_collections.community.library_inventory_filtering_v1.plugins.plugin_utils.inventory_filter import parse_filters, filter_host
from ansible_collections.community.library_inventory_filtering_v1.plugins.plugin_utils.inventory_filter import (
filter_host,
parse_filters,
)
MIN_DOCKER_API = None
class InventoryModule(BaseInventoryPlugin, Constructable):
''' Host inventory parser for ansible using Docker daemon as source. '''
"""Host inventory parser for ansible using Docker daemon as source."""
NAME = 'community.docker.docker_containers'
NAME = "community.docker.docker_containers"
def _slugify(self, value):
slug = re.sub(r'[^\w-]', '_', value).lower().lstrip('_')
return f'docker_{slug}'
slug = re.sub(r"[^\w-]", "_", value).lower().lstrip("_")
return f"docker_{slug}"
def _populate(self, client):
strict = self.get_option('strict')
strict = self.get_option("strict")
ssh_port = self.get_option('private_ssh_port')
default_ip = self.get_option('default_ip')
hostname = self.get_option('docker_host')
verbose_output = self.get_option('verbose_output')
connection_type = self.get_option('connection_type')
add_legacy_groups = self.get_option('add_legacy_groups')
ssh_port = self.get_option("private_ssh_port")
default_ip = self.get_option("default_ip")
hostname = self.get_option("docker_host")
verbose_output = self.get_option("verbose_output")
connection_type = self.get_option("connection_type")
add_legacy_groups = self.get_option("add_legacy_groups")
try:
params = {
'limit': -1,
'all': 1,
'size': 0,
'trunc_cmd': 0,
'since': None,
'before': None,
"limit": -1,
"all": 1,
"size": 0,
"trunc_cmd": 0,
"since": None,
"before": None,
}
containers = client.get_json('/containers/json', params=params)
containers = client.get_json("/containers/json", params=params)
except APIError as exc:
raise AnsibleError(f"Error listing containers: {exc}")
if add_legacy_groups:
self.inventory.add_group('running')
self.inventory.add_group('stopped')
self.inventory.add_group("running")
self.inventory.add_group("stopped")
extra_facts = {}
if self.get_option('configure_docker_daemon'):
if self.get_option("configure_docker_daemon"):
for option_name, var_name in DOCKER_COMMON_ARGS_VARS.items():
value = self.get_option(option_name)
if value is not None:
extra_facts[var_name] = value
filters = parse_filters(self.get_option('filters'))
filters = parse_filters(self.get_option("filters"))
for container in containers:
id = container.get('Id')
id = container.get("Id")
short_id = id[:13]
try:
name = container.get('Names', list())[0].lstrip('/')
name = container.get("Names", list())[0].lstrip("/")
full_name = name
except IndexError:
name = short_id
@@ -253,66 +258,72 @@ class InventoryModule(BaseInventoryPlugin, Constructable):
full_facts = dict()
try:
inspect = client.get_json('/containers/{0}/json', id)
inspect = client.get_json("/containers/{0}/json", id)
except APIError as exc:
raise AnsibleError(f"Error inspecting container {name} - {exc}")
state = inspect.get('State') or dict()
config = inspect.get('Config') or dict()
labels = config.get('Labels') or dict()
state = inspect.get("State") or dict()
config = inspect.get("Config") or dict()
labels = config.get("Labels") or dict()
running = state.get('Running')
running = state.get("Running")
groups = []
# Add container to groups
image_name = config.get('Image')
image_name = config.get("Image")
if image_name and add_legacy_groups:
groups.append(f'image_{image_name}')
groups.append(f"image_{image_name}")
stack_name = labels.get('com.docker.stack.namespace')
stack_name = labels.get("com.docker.stack.namespace")
if stack_name:
full_facts['docker_stack'] = stack_name
full_facts["docker_stack"] = stack_name
if add_legacy_groups:
groups.append(f'stack_{stack_name}')
groups.append(f"stack_{stack_name}")
service_name = labels.get('com.docker.swarm.service.name')
service_name = labels.get("com.docker.swarm.service.name")
if service_name:
full_facts['docker_service'] = service_name
full_facts["docker_service"] = service_name
if add_legacy_groups:
groups.append(f'service_{service_name}')
groups.append(f"service_{service_name}")
ansible_connection = None
if connection_type == 'ssh':
if connection_type == "ssh":
# Figure out ssh IP and Port
try:
# Lookup the public facing port Nat'ed to ssh port.
network_settings = inspect.get('NetworkSettings') or {}
port_settings = network_settings.get('Ports') or {}
port = port_settings.get(f'{ssh_port}/tcp')[0]
network_settings = inspect.get("NetworkSettings") or {}
port_settings = network_settings.get("Ports") or {}
port = port_settings.get(f"{ssh_port}/tcp")[0]
except (IndexError, AttributeError, TypeError):
port = dict()
try:
ip = default_ip if port['HostIp'] == '0.0.0.0' else port['HostIp']
ip = default_ip if port["HostIp"] == "0.0.0.0" else port["HostIp"]
except KeyError:
ip = ''
ip = ""
facts.update(dict(
ansible_ssh_host=ip,
ansible_ssh_port=port.get('HostPort', 0),
))
elif connection_type == 'docker-cli':
facts.update(dict(
ansible_host=full_name,
))
ansible_connection = 'community.docker.docker'
elif connection_type == 'docker-api':
facts.update(dict(
ansible_host=full_name,
))
facts.update(
dict(
ansible_ssh_host=ip,
ansible_ssh_port=port.get("HostPort", 0),
)
)
elif connection_type == "docker-cli":
facts.update(
dict(
ansible_host=full_name,
)
)
ansible_connection = "community.docker.docker"
elif connection_type == "docker-api":
facts.update(
dict(
ansible_host=full_name,
)
)
facts.update(extra_facts)
ansible_connection = 'community.docker.docker_api'
ansible_connection = "community.docker.docker_api"
full_facts.update(facts)
for key, value in inspect.items():
@@ -323,8 +334,8 @@ class InventoryModule(BaseInventoryPlugin, Constructable):
if ansible_connection:
for d in (facts, full_facts):
if 'ansible_connection' not in d:
d['ansible_connection'] = ansible_connection
if "ansible_connection" not in d:
d["ansible_connection"] = ansible_connection
if not filter_host(self, name, full_facts, filters):
continue
@@ -342,11 +353,17 @@ class InventoryModule(BaseInventoryPlugin, Constructable):
# Use constructed if applicable
# Composed variables
self._set_composite_vars(self.get_option('compose'), full_facts, name, strict=strict)
self._set_composite_vars(
self.get_option("compose"), full_facts, name, strict=strict
)
# Complex groups based on jinja2 conditionals, hosts that meet the conditional are added to group
self._add_host_to_composed_groups(self.get_option('groups'), full_facts, name, strict=strict)
self._add_host_to_composed_groups(
self.get_option("groups"), full_facts, name, strict=strict
)
# Create groups based on variable values and add the corresponding hosts to it
self._add_host_to_keyed_groups(self.get_option('keyed_groups'), full_facts, name, strict=strict)
self._add_host_to_keyed_groups(
self.get_option("keyed_groups"), full_facts, name, strict=strict
)
# We need to do this last since we also add a group called `name`.
# When we do this before a set_variable() call, the variables are assigned
@@ -362,15 +379,15 @@ class InventoryModule(BaseInventoryPlugin, Constructable):
self.inventory.add_host(name, group=hostname)
if running is True:
self.inventory.add_host(name, group='running')
self.inventory.add_host(name, group="running")
else:
self.inventory.add_host(name, group='stopped')
self.inventory.add_host(name, group="stopped")
def verify_file(self, path):
"""Return the possibly of a file being consumable by this plugin."""
return (
super(InventoryModule, self).verify_file(path) and
path.endswith(('docker.yaml', 'docker.yml')))
return super(InventoryModule, self).verify_file(path) and path.endswith(
("docker.yaml", "docker.yml")
)
def _create_client(self):
return AnsibleDockerClient(self, min_docker_api_version=MIN_DOCKER_API)
@@ -382,10 +399,8 @@ class InventoryModule(BaseInventoryPlugin, Constructable):
try:
self._populate(client)
except DockerException as e:
raise AnsibleError(
f'An unexpected Docker error occurred: {e}'
)
raise AnsibleError(f"An unexpected Docker error occurred: {e}")
except RequestException as e:
raise AnsibleError(
f'An unexpected requests error occurred when trying to talk to the Docker daemon: {e}'
f"An unexpected requests error occurred when trying to talk to the Docker daemon: {e}"
)
+96 -54
View File
@@ -5,6 +5,7 @@
from __future__ import annotations
DOCUMENTATION = r"""
name: docker_machine
author: Ximon Eighteen (@ximon18)
@@ -59,7 +60,7 @@ options:
version_added: 3.5.0
"""
EXAMPLES = '''
EXAMPLES = """
---
# Minimal example
plugin: community.docker.docker_machine
@@ -96,57 +97,63 @@ keyed_groups:
plugin: community.docker.docker_machine
compose:
ansible_ssh_common_args: '"-o StrictHostKeyChecking=accept-new"'
'''
from ansible.errors import AnsibleError
from ansible.module_utils.common.text.converters import to_native
from ansible.module_utils.common.text.converters import to_text
from ansible.module_utils.common.process import get_bin_path
from ansible.plugins.inventory import BaseInventoryPlugin, Constructable, Cacheable
from ansible.utils.display import Display
from ansible_collections.community.docker.plugins.plugin_utils.unsafe import make_unsafe
from ansible_collections.community.library_inventory_filtering_v1.plugins.plugin_utils.inventory_filter import parse_filters, filter_host
"""
import json
import re
import subprocess
from ansible.errors import AnsibleError
from ansible.module_utils.common.process import get_bin_path
from ansible.module_utils.common.text.converters import to_native, to_text
from ansible.plugins.inventory import BaseInventoryPlugin, Cacheable, Constructable
from ansible.utils.display import Display
from ansible_collections.community.docker.plugins.plugin_utils.unsafe import make_unsafe
from ansible_collections.community.library_inventory_filtering_v1.plugins.plugin_utils.inventory_filter import (
filter_host,
parse_filters,
)
display = Display()
class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
''' Host inventory parser for ansible using Docker machine as source. '''
"""Host inventory parser for ansible using Docker machine as source."""
NAME = 'community.docker.docker_machine'
NAME = "community.docker.docker_machine"
DOCKER_MACHINE_PATH = None
def _run_command(self, args):
if not self.DOCKER_MACHINE_PATH:
try:
self.DOCKER_MACHINE_PATH = get_bin_path('docker-machine')
self.DOCKER_MACHINE_PATH = get_bin_path("docker-machine")
except ValueError as e:
raise AnsibleError(to_native(e))
command = [self.DOCKER_MACHINE_PATH]
command.extend(args)
display.debug(f'Executing command {command}')
display.debug(f"Executing command {command}")
try:
result = subprocess.check_output(command)
except subprocess.CalledProcessError as e:
display.warning(f'Exception {type(e).__name__} caught while executing command {command}, this was the original exception: {e}')
display.warning(
f"Exception {type(e).__name__} caught while executing command {command}, this was the original exception: {e}"
)
raise e
return to_text(result).strip()
def _get_docker_daemon_variables(self, machine_name):
'''
"""
Capture settings from Docker Machine that would be needed to connect to the remote Docker daemon installed on
the Docker Machine remote host. Note: passing '--shell=sh' is a workaround for 'Error: Unknown shell'.
'''
"""
try:
env_lines = self._run_command(['env', '--shell=sh', machine_name]).splitlines()
env_lines = self._run_command(
["env", "--shell=sh", machine_name]
).splitlines()
except subprocess.CalledProcessError:
# This can happen when the machine is created but provisioning is incomplete
return []
@@ -174,9 +181,9 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
def _get_machine_names(self):
# Filter out machines that are not in the Running state as we probably cannot do anything useful actions
# with them.
ls_command = ['ls', '-q']
if self.get_option('running_required'):
ls_command.extend(['--filter', 'state=Running'])
ls_command = ["ls", "-q"]
if self.get_option("running_required"):
ls_command.extend(["--filter", "state=Running"])
try:
ls_lines = self._run_command(ls_command)
@@ -187,7 +194,7 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
def _inspect_docker_machine_host(self, node):
try:
inspect_lines = self._run_command(['inspect', node])
inspect_lines = self._run_command(["inspect", node])
except subprocess.CalledProcessError:
return None
@@ -195,7 +202,7 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
def _ip_addr_docker_machine_host(self, node):
try:
ip_addr = self._run_command(['ip', node])
ip_addr = self._run_command(["ip", node])
except subprocess.CalledProcessError:
return None
@@ -203,19 +210,21 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
def _should_skip_host(self, machine_name, env_var_tuples, daemon_env):
if not env_var_tuples:
warning_prefix = f'Unable to fetch Docker daemon env vars from Docker Machine for host {machine_name}'
if daemon_env in ('require', 'require-silently'):
if daemon_env == 'require':
display.warning(f'{warning_prefix}: host will be skipped')
warning_prefix = f"Unable to fetch Docker daemon env vars from Docker Machine for host {machine_name}"
if daemon_env in ("require", "require-silently"):
if daemon_env == "require":
display.warning(f"{warning_prefix}: host will be skipped")
return True
else: # 'optional', 'optional-silently'
if daemon_env == 'optional':
display.warning(f'{warning_prefix}: host will lack dm_DOCKER_xxx variables')
if daemon_env == "optional":
display.warning(
f"{warning_prefix}: host will lack dm_DOCKER_xxx variables"
)
return False
def _populate(self):
daemon_env = self.get_option('daemon_env')
filters = parse_filters(self.get_option('filters'))
daemon_env = self.get_option("daemon_env")
filters = parse_filters(self.get_option("filters"))
try:
for node in self._get_machine_names():
node_attrs = self._inspect_docker_machine_host(node)
@@ -224,13 +233,13 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
unsafe_node_attrs = make_unsafe(node_attrs)
machine_name = unsafe_node_attrs['Driver']['MachineName']
machine_name = unsafe_node_attrs["Driver"]["MachineName"]
if not filter_host(self, machine_name, unsafe_node_attrs, filters):
continue
# query `docker-machine env` to obtain remote Docker daemon connection settings in the form of commands
# that could be used to set environment variables to influence a local Docker client:
if daemon_env == 'skip':
if daemon_env == "skip":
env_var_tuples = []
else:
env_var_tuples = self._get_docker_daemon_variables(machine_name)
@@ -243,49 +252,82 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
# check for valid ip address from inspect output, else explicitly use ip command to find host ip address
# this works around an issue seen with Google Compute Platform where the IP address was not available
# via the 'inspect' subcommand but was via the 'ip' subcomannd.
if unsafe_node_attrs['Driver']['IPAddress']:
ip_addr = unsafe_node_attrs['Driver']['IPAddress']
if unsafe_node_attrs["Driver"]["IPAddress"]:
ip_addr = unsafe_node_attrs["Driver"]["IPAddress"]
else:
ip_addr = self._ip_addr_docker_machine_host(node)
# set standard Ansible remote host connection settings to details captured from `docker-machine`
# see: https://docs.ansible.com/ansible/latest/user_guide/intro_inventory.html
self.inventory.set_variable(machine_name, 'ansible_host', make_unsafe(ip_addr))
self.inventory.set_variable(machine_name, 'ansible_port', unsafe_node_attrs['Driver']['SSHPort'])
self.inventory.set_variable(machine_name, 'ansible_user', unsafe_node_attrs['Driver']['SSHUser'])
self.inventory.set_variable(machine_name, 'ansible_ssh_private_key_file', unsafe_node_attrs['Driver']['SSHKeyPath'])
self.inventory.set_variable(
machine_name, "ansible_host", make_unsafe(ip_addr)
)
self.inventory.set_variable(
machine_name, "ansible_port", unsafe_node_attrs["Driver"]["SSHPort"]
)
self.inventory.set_variable(
machine_name, "ansible_user", unsafe_node_attrs["Driver"]["SSHUser"]
)
self.inventory.set_variable(
machine_name,
"ansible_ssh_private_key_file",
unsafe_node_attrs["Driver"]["SSHKeyPath"],
)
# set variables based on Docker Machine tags
tags = unsafe_node_attrs['Driver'].get('Tags') or ''
self.inventory.set_variable(machine_name, 'dm_tags', make_unsafe(tags))
tags = unsafe_node_attrs["Driver"].get("Tags") or ""
self.inventory.set_variable(machine_name, "dm_tags", make_unsafe(tags))
# set variables based on Docker Machine env variables
for kv in env_var_tuples:
self.inventory.set_variable(machine_name, f'dm_{kv[0]}', make_unsafe(kv[1]))
self.inventory.set_variable(
machine_name, f"dm_{kv[0]}", make_unsafe(kv[1])
)
if self.get_option('verbose_output'):
self.inventory.set_variable(machine_name, 'docker_machine_node_attributes', unsafe_node_attrs)
if self.get_option("verbose_output"):
self.inventory.set_variable(
machine_name,
"docker_machine_node_attributes",
unsafe_node_attrs,
)
# Use constructed if applicable
strict = self.get_option('strict')
strict = self.get_option("strict")
# Composed variables
self._set_composite_vars(self.get_option('compose'), unsafe_node_attrs, machine_name, strict=strict)
self._set_composite_vars(
self.get_option("compose"),
unsafe_node_attrs,
machine_name,
strict=strict,
)
# Complex groups based on jinja2 conditionals, hosts that meet the conditional are added to group
self._add_host_to_composed_groups(self.get_option('groups'), unsafe_node_attrs, machine_name, strict=strict)
self._add_host_to_composed_groups(
self.get_option("groups"),
unsafe_node_attrs,
machine_name,
strict=strict,
)
# Create groups based on variable values and add the corresponding hosts to it
self._add_host_to_keyed_groups(self.get_option('keyed_groups'), unsafe_node_attrs, machine_name, strict=strict)
self._add_host_to_keyed_groups(
self.get_option("keyed_groups"),
unsafe_node_attrs,
machine_name,
strict=strict,
)
except Exception as e:
raise AnsibleError(f'Unable to fetch hosts from Docker Machine, this was the original exception: {e}') from e
raise AnsibleError(
f"Unable to fetch hosts from Docker Machine, this was the original exception: {e}"
) from e
def verify_file(self, path):
"""Return the possibility of a file being consumable by this plugin."""
return (
super(InventoryModule, self).verify_file(path) and
path.endswith(('docker_machine.yaml', 'docker_machine.yml')))
return super(InventoryModule, self).verify_file(path) and path.endswith(
("docker_machine.yaml", "docker_machine.yml")
)
def parse(self, inventory, loader, path, cache=True):
super(InventoryModule, self).parse(inventory, loader, path, cache)
+122 -71
View File
@@ -6,6 +6,7 @@
from __future__ import annotations
DOCUMENTATION = r"""
name: docker_swarm
author:
@@ -103,7 +104,7 @@ options:
version_added: 3.5.0
"""
EXAMPLES = '''
EXAMPLES = """
---
# Minimal example using local docker
plugin: community.docker.docker_swarm
@@ -146,126 +147,176 @@ keyed_groups:
# hint: labels containing special characters will be converted to safe names
- key: 'Spec.Labels'
prefix: label
'''
"""
from ansible.errors import AnsibleError
from ansible_collections.community.docker.plugins.module_utils.common import get_connect_params
from ansible_collections.community.docker.plugins.module_utils.util import update_tls_hostname
from ansible.plugins.inventory import BaseInventoryPlugin, Constructable
from ansible.parsing.utils.addresses import parse_address
from ansible.plugins.inventory import BaseInventoryPlugin, Constructable
from ansible_collections.community.docker.plugins.module_utils.common import (
get_connect_params,
)
from ansible_collections.community.docker.plugins.module_utils.util import (
update_tls_hostname,
)
from ansible_collections.community.docker.plugins.plugin_utils.unsafe import make_unsafe
from ansible_collections.community.library_inventory_filtering_v1.plugins.plugin_utils.inventory_filter import parse_filters, filter_host
from ansible_collections.community.library_inventory_filtering_v1.plugins.plugin_utils.inventory_filter import (
filter_host,
parse_filters,
)
try:
import docker
HAS_DOCKER = True
except ImportError:
HAS_DOCKER = False
class InventoryModule(BaseInventoryPlugin, Constructable):
''' Host inventory parser for ansible using Docker swarm as source. '''
"""Host inventory parser for ansible using Docker swarm as source."""
NAME = 'community.docker.docker_swarm'
NAME = "community.docker.docker_swarm"
def _fail(self, msg):
raise AnsibleError(msg)
def _populate(self):
raw_params = dict(
docker_host=self.get_option('docker_host'),
tls=self.get_option('tls'),
tls_verify=self.get_option('validate_certs'),
key_path=self.get_option('client_key'),
cacert_path=self.get_option('ca_path'),
cert_path=self.get_option('client_cert'),
tls_hostname=self.get_option('tls_hostname'),
api_version=self.get_option('api_version'),
timeout=self.get_option('timeout'),
use_ssh_client=self.get_option('use_ssh_client'),
docker_host=self.get_option("docker_host"),
tls=self.get_option("tls"),
tls_verify=self.get_option("validate_certs"),
key_path=self.get_option("client_key"),
cacert_path=self.get_option("ca_path"),
cert_path=self.get_option("client_cert"),
tls_hostname=self.get_option("tls_hostname"),
api_version=self.get_option("api_version"),
timeout=self.get_option("timeout"),
use_ssh_client=self.get_option("use_ssh_client"),
debug=None,
)
update_tls_hostname(raw_params)
connect_params = get_connect_params(raw_params, fail_function=self._fail)
self.client = docker.DockerClient(**connect_params)
self.inventory.add_group('all')
self.inventory.add_group('manager')
self.inventory.add_group('worker')
self.inventory.add_group('leader')
self.inventory.add_group('nonleaders')
self.inventory.add_group("all")
self.inventory.add_group("manager")
self.inventory.add_group("worker")
self.inventory.add_group("leader")
self.inventory.add_group("nonleaders")
filters = parse_filters(self.get_option('filters'))
filters = parse_filters(self.get_option("filters"))
if self.get_option('include_host_uri'):
if self.get_option('include_host_uri_port'):
host_uri_port = str(self.get_option('include_host_uri_port'))
elif self.get_option('tls') or self.get_option('validate_certs'):
host_uri_port = '2376'
if self.get_option("include_host_uri"):
if self.get_option("include_host_uri_port"):
host_uri_port = str(self.get_option("include_host_uri_port"))
elif self.get_option("tls") or self.get_option("validate_certs"):
host_uri_port = "2376"
else:
host_uri_port = '2375'
host_uri_port = "2375"
try:
self.nodes = self.client.nodes.list()
for node in self.nodes:
node_attrs = self.client.nodes.get(node.id).attrs
unsafe_node_attrs = make_unsafe(node_attrs)
if not filter_host(self, unsafe_node_attrs['ID'], unsafe_node_attrs, filters):
if not filter_host(
self, unsafe_node_attrs["ID"], unsafe_node_attrs, filters
):
continue
self.inventory.add_host(unsafe_node_attrs['ID'])
self.inventory.add_host(unsafe_node_attrs['ID'], group=unsafe_node_attrs['Spec']['Role'])
self.inventory.set_variable(unsafe_node_attrs['ID'], 'ansible_host',
unsafe_node_attrs['Status']['Addr'])
if self.get_option('include_host_uri'):
self.inventory.set_variable(unsafe_node_attrs['ID'], 'ansible_host_uri',
make_unsafe('tcp://' + unsafe_node_attrs['Status']['Addr'] + ':' + host_uri_port))
if self.get_option('verbose_output'):
self.inventory.set_variable(unsafe_node_attrs['ID'], 'docker_swarm_node_attributes', unsafe_node_attrs)
if 'ManagerStatus' in unsafe_node_attrs:
if unsafe_node_attrs['ManagerStatus'].get('Leader'):
self.inventory.add_host(unsafe_node_attrs["ID"])
self.inventory.add_host(
unsafe_node_attrs["ID"], group=unsafe_node_attrs["Spec"]["Role"]
)
self.inventory.set_variable(
unsafe_node_attrs["ID"],
"ansible_host",
unsafe_node_attrs["Status"]["Addr"],
)
if self.get_option("include_host_uri"):
self.inventory.set_variable(
unsafe_node_attrs["ID"],
"ansible_host_uri",
make_unsafe(
"tcp://"
+ unsafe_node_attrs["Status"]["Addr"]
+ ":"
+ host_uri_port
),
)
if self.get_option("verbose_output"):
self.inventory.set_variable(
unsafe_node_attrs["ID"],
"docker_swarm_node_attributes",
unsafe_node_attrs,
)
if "ManagerStatus" in unsafe_node_attrs:
if unsafe_node_attrs["ManagerStatus"].get("Leader"):
# This is workaround of bug in Docker when in some cases the Leader IP is 0.0.0.0
# Check moby/moby#35437 for details
swarm_leader_ip = parse_address(node_attrs['ManagerStatus']['Addr'])[0] or \
unsafe_node_attrs['Status']['Addr']
if self.get_option('include_host_uri'):
self.inventory.set_variable(unsafe_node_attrs['ID'], 'ansible_host_uri',
make_unsafe('tcp://' + swarm_leader_ip + ':' + host_uri_port))
self.inventory.set_variable(unsafe_node_attrs['ID'], 'ansible_host', make_unsafe(swarm_leader_ip))
self.inventory.add_host(unsafe_node_attrs['ID'], group='leader')
swarm_leader_ip = (
parse_address(node_attrs["ManagerStatus"]["Addr"])[0]
or unsafe_node_attrs["Status"]["Addr"]
)
if self.get_option("include_host_uri"):
self.inventory.set_variable(
unsafe_node_attrs["ID"],
"ansible_host_uri",
make_unsafe(
"tcp://" + swarm_leader_ip + ":" + host_uri_port
),
)
self.inventory.set_variable(
unsafe_node_attrs["ID"],
"ansible_host",
make_unsafe(swarm_leader_ip),
)
self.inventory.add_host(unsafe_node_attrs["ID"], group="leader")
else:
self.inventory.add_host(unsafe_node_attrs['ID'], group='nonleaders')
self.inventory.add_host(
unsafe_node_attrs["ID"], group="nonleaders"
)
else:
self.inventory.add_host(unsafe_node_attrs['ID'], group='nonleaders')
self.inventory.add_host(unsafe_node_attrs["ID"], group="nonleaders")
# Use constructed if applicable
strict = self.get_option('strict')
strict = self.get_option("strict")
# Composed variables
self._set_composite_vars(self.get_option('compose'),
unsafe_node_attrs,
unsafe_node_attrs['ID'],
strict=strict)
self._set_composite_vars(
self.get_option("compose"),
unsafe_node_attrs,
unsafe_node_attrs["ID"],
strict=strict,
)
# Complex groups based on jinja2 conditionals, hosts that meet the conditional are added to group
self._add_host_to_composed_groups(self.get_option('groups'),
unsafe_node_attrs,
unsafe_node_attrs['ID'],
strict=strict)
self._add_host_to_composed_groups(
self.get_option("groups"),
unsafe_node_attrs,
unsafe_node_attrs["ID"],
strict=strict,
)
# Create groups based on variable values and add the corresponding hosts to it
self._add_host_to_keyed_groups(self.get_option('keyed_groups'),
unsafe_node_attrs,
unsafe_node_attrs['ID'],
strict=strict)
self._add_host_to_keyed_groups(
self.get_option("keyed_groups"),
unsafe_node_attrs,
unsafe_node_attrs["ID"],
strict=strict,
)
except Exception as e:
raise AnsibleError(f'Unable to fetch hosts from Docker swarm API, this was the original exception: {e}')
raise AnsibleError(
f"Unable to fetch hosts from Docker swarm API, this was the original exception: {e}"
)
def verify_file(self, path):
"""Return the possibly of a file being consumable by this plugin."""
return (
super(InventoryModule, self).verify_file(path) and
path.endswith(('docker_swarm.yaml', 'docker_swarm.yml')))
return super(InventoryModule, self).verify_file(path) and path.endswith(
("docker_swarm.yaml", "docker_swarm.yml")
)
def parse(self, inventory, loader, path, cache=True):
if not HAS_DOCKER:
raise AnsibleError('The Docker swarm dynamic inventory plugin requires the Docker SDK for Python: '
'https://github.com/docker/docker-py.')
raise AnsibleError(
"The Docker swarm dynamic inventory plugin requires the Docker SDK for Python: "
"https://github.com/docker/docker-py."
)
super(InventoryModule, self).parse(inventory, loader, path, cache)
self._read_config_data(path)
self._populate()
+17 -8
View File
@@ -18,8 +18,13 @@ URLLIB3_IMPORT_ERROR = None
try:
from requests import Session # noqa: F401, pylint: disable=unused-import
from requests.adapters import HTTPAdapter # noqa: F401, pylint: disable=unused-import
from requests.exceptions import HTTPError, InvalidSchema # noqa: F401, pylint: disable=unused-import
from requests.adapters import ( # noqa: F401, pylint: disable=unused-import
HTTPAdapter,
)
from requests.exceptions import ( # noqa: F401, pylint: disable=unused-import
HTTPError,
InvalidSchema,
)
except ImportError:
REQUESTS_IMPORT_ERROR = traceback.format_exc()
@@ -38,11 +43,15 @@ except ImportError:
try:
from requests.packages import urllib3 # pylint: disable=unused-import
from requests.packages.urllib3 import connection as urllib3_connection # pylint: disable=unused-import
from requests.packages.urllib3 import (
connection as urllib3_connection, # pylint: disable=unused-import
)
except ImportError:
try:
import urllib3 # pylint: disable=unused-import
from urllib3 import connection as urllib3_connection # pylint: disable=unused-import
from urllib3 import (
connection as urllib3_connection, # pylint: disable=unused-import
)
except ImportError:
URLLIB3_IMPORT_ERROR = traceback.format_exc()
@@ -77,11 +86,11 @@ def fail_on_missing_imports():
from .errors import MissingRequirementException
raise MissingRequirementException(
'You have to install requests',
'requests', REQUESTS_IMPORT_ERROR)
"You have to install requests", "requests", REQUESTS_IMPORT_ERROR
)
if URLLIB3_IMPORT_ERROR is not None:
from .errors import MissingRequirementException
raise MissingRequirementException(
'You have to install urllib3',
'urllib3', URLLIB3_IMPORT_ERROR)
"You have to install urllib3", "urllib3", URLLIB3_IMPORT_ERROR
)
+214 -148
View File
@@ -16,37 +16,45 @@ from functools import partial
from urllib.parse import quote
from .. import auth
from .._import_helper import fail_on_missing_imports
from .._import_helper import HTTPError as _HTTPError
from .._import_helper import InvalidSchema as _InvalidSchema
from .._import_helper import Session as _Session
from ..constants import (DEFAULT_NUM_POOLS, DEFAULT_NUM_POOLS_SSH,
DEFAULT_MAX_POOL_SIZE, DEFAULT_TIMEOUT_SECONDS,
DEFAULT_USER_AGENT, IS_WINDOWS_PLATFORM,
MINIMUM_DOCKER_API_VERSION, STREAM_HEADER_SIZE_BYTES,
DEFAULT_DATA_CHUNK_SIZE)
from ..errors import (DockerException, InvalidVersion, TLSParameterError, MissingRequirementException,
create_api_error_from_http_exception)
from .._import_helper import fail_on_missing_imports
from ..constants import (
DEFAULT_DATA_CHUNK_SIZE,
DEFAULT_MAX_POOL_SIZE,
DEFAULT_NUM_POOLS,
DEFAULT_NUM_POOLS_SSH,
DEFAULT_TIMEOUT_SECONDS,
DEFAULT_USER_AGENT,
IS_WINDOWS_PLATFORM,
MINIMUM_DOCKER_API_VERSION,
STREAM_HEADER_SIZE_BYTES,
)
from ..errors import (
DockerException,
InvalidVersion,
MissingRequirementException,
TLSParameterError,
create_api_error_from_http_exception,
)
from ..tls import TLSConfig
from ..transport.npipeconn import NpipeHTTPAdapter
from ..transport.npipesocket import PYWIN32_IMPORT_ERROR
from ..transport.unixconn import UnixHTTPAdapter
from ..transport.sshconn import SSHHTTPAdapter, PARAMIKO_IMPORT_ERROR
from ..transport.sshconn import PARAMIKO_IMPORT_ERROR, SSHHTTPAdapter
from ..transport.ssladapter import SSLHTTPAdapter
from ..utils import config, utils, json_stream
from ..transport.unixconn import UnixHTTPAdapter
from ..utils import config, json_stream, utils
from ..utils.decorators import check_resource, update_headers
from ..utils.proxy import ProxyConfig
from ..utils.socket import consume_socket_output, demux_adaptor, frames_iter
from .daemon import DaemonApiMixin
log = logging.getLogger(__name__)
class APIClient(
_Session,
DaemonApiMixin):
class APIClient(_Session, DaemonApiMixin):
"""
A low-level client for the Docker Engine API.
@@ -85,139 +93,155 @@ class APIClient(
to save in the pool.
"""
__attrs__ = _Session.__attrs__ + ['_auth_configs',
'_general_configs',
'_version',
'base_url',
'timeout']
__attrs__ = _Session.__attrs__ + [
"_auth_configs",
"_general_configs",
"_version",
"base_url",
"timeout",
]
def __init__(self, base_url=None, version=None,
timeout=DEFAULT_TIMEOUT_SECONDS, tls=False,
user_agent=DEFAULT_USER_AGENT, num_pools=None,
credstore_env=None, use_ssh_client=False,
max_pool_size=DEFAULT_MAX_POOL_SIZE):
def __init__(
self,
base_url=None,
version=None,
timeout=DEFAULT_TIMEOUT_SECONDS,
tls=False,
user_agent=DEFAULT_USER_AGENT,
num_pools=None,
credstore_env=None,
use_ssh_client=False,
max_pool_size=DEFAULT_MAX_POOL_SIZE,
):
super(APIClient, self).__init__()
fail_on_missing_imports()
if tls and not base_url:
raise TLSParameterError(
'If using TLS, the base_url argument must be provided.'
"If using TLS, the base_url argument must be provided."
)
self.base_url = base_url
self.timeout = timeout
self.headers['User-Agent'] = user_agent
self.headers["User-Agent"] = user_agent
self._general_configs = config.load_general_config()
proxy_config = self._general_configs.get('proxies', {})
proxy_config = self._general_configs.get("proxies", {})
try:
proxies = proxy_config[base_url]
except KeyError:
proxies = proxy_config.get('default', {})
proxies = proxy_config.get("default", {})
self._proxy_configs = ProxyConfig.from_dict(proxies)
self._auth_configs = auth.load_config(
config_dict=self._general_configs, credstore_env=credstore_env,
config_dict=self._general_configs,
credstore_env=credstore_env,
)
self.credstore_env = credstore_env
base_url = utils.parse_host(
base_url, IS_WINDOWS_PLATFORM, tls=bool(tls)
)
base_url = utils.parse_host(base_url, IS_WINDOWS_PLATFORM, tls=bool(tls))
# SSH has a different default for num_pools to all other adapters
num_pools = num_pools or DEFAULT_NUM_POOLS_SSH if \
base_url.startswith('ssh://') else DEFAULT_NUM_POOLS
num_pools = (
num_pools or DEFAULT_NUM_POOLS_SSH
if base_url.startswith("ssh://")
else DEFAULT_NUM_POOLS
)
if base_url.startswith('http+unix://'):
if base_url.startswith("http+unix://"):
self._custom_adapter = UnixHTTPAdapter(
base_url, timeout, pool_connections=num_pools,
max_pool_size=max_pool_size
base_url,
timeout,
pool_connections=num_pools,
max_pool_size=max_pool_size,
)
self.mount('http+docker://', self._custom_adapter)
self._unmount('http://', 'https://')
self.mount("http+docker://", self._custom_adapter)
self._unmount("http://", "https://")
# host part of URL should be unused, but is resolved by requests
# module in proxy_bypass_macosx_sysconf()
self.base_url = 'http+docker://localhost'
elif base_url.startswith('npipe://'):
self.base_url = "http+docker://localhost"
elif base_url.startswith("npipe://"):
if not IS_WINDOWS_PLATFORM:
raise DockerException(
'The npipe:// protocol is only supported on Windows'
"The npipe:// protocol is only supported on Windows"
)
if PYWIN32_IMPORT_ERROR is not None:
raise MissingRequirementException(
'Install pypiwin32 package to enable npipe:// support',
'pywin32',
PYWIN32_IMPORT_ERROR)
"Install pypiwin32 package to enable npipe:// support",
"pywin32",
PYWIN32_IMPORT_ERROR,
)
self._custom_adapter = NpipeHTTPAdapter(
base_url, timeout, pool_connections=num_pools,
max_pool_size=max_pool_size
base_url,
timeout,
pool_connections=num_pools,
max_pool_size=max_pool_size,
)
self.mount('http+docker://', self._custom_adapter)
self.base_url = 'http+docker://localnpipe'
elif base_url.startswith('ssh://'):
self.mount("http+docker://", self._custom_adapter)
self.base_url = "http+docker://localnpipe"
elif base_url.startswith("ssh://"):
if PARAMIKO_IMPORT_ERROR is not None and not use_ssh_client:
raise MissingRequirementException(
'Install paramiko package to enable ssh:// support',
'paramiko',
PARAMIKO_IMPORT_ERROR)
"Install paramiko package to enable ssh:// support",
"paramiko",
PARAMIKO_IMPORT_ERROR,
)
self._custom_adapter = SSHHTTPAdapter(
base_url, timeout, pool_connections=num_pools,
max_pool_size=max_pool_size, shell_out=use_ssh_client
base_url,
timeout,
pool_connections=num_pools,
max_pool_size=max_pool_size,
shell_out=use_ssh_client,
)
self.mount('http+docker://ssh', self._custom_adapter)
self._unmount('http://', 'https://')
self.base_url = 'http+docker://ssh'
self.mount("http+docker://ssh", self._custom_adapter)
self._unmount("http://", "https://")
self.base_url = "http+docker://ssh"
else:
# Use SSLAdapter for the ability to specify SSL version
if isinstance(tls, TLSConfig):
tls.configure_client(self)
elif tls:
self._custom_adapter = SSLHTTPAdapter(
pool_connections=num_pools)
self.mount('https://', self._custom_adapter)
self._custom_adapter = SSLHTTPAdapter(pool_connections=num_pools)
self.mount("https://", self._custom_adapter)
self.base_url = base_url
# version detection needs to be after unix adapter mounting
if version is None or (isinstance(version, str) and version.lower() == 'auto'):
if version is None or (isinstance(version, str) and version.lower() == "auto"):
self._version = self._retrieve_server_version()
else:
self._version = version
if not isinstance(self._version, str):
raise DockerException(
f'Version parameter must be a string or None. Found {type(version).__name__}'
f"Version parameter must be a string or None. Found {type(version).__name__}"
)
if utils.version_lt(self._version, MINIMUM_DOCKER_API_VERSION):
raise InvalidVersion(
f'API versions below {MINIMUM_DOCKER_API_VERSION} are no longer supported by this library.'
f"API versions below {MINIMUM_DOCKER_API_VERSION} are no longer supported by this library."
)
def _retrieve_server_version(self):
try:
version_result = self.version(api_version=False)
except Exception as e:
raise DockerException(
f'Error while fetching server API version: {e}'
)
raise DockerException(f"Error while fetching server API version: {e}")
try:
return version_result["ApiVersion"]
except KeyError:
raise DockerException(
'Invalid response from docker daemon: key "ApiVersion"'
' is missing.'
'Invalid response from docker daemon: key "ApiVersion"' " is missing."
)
except Exception as e:
raise DockerException(
f'Error while fetching server API version: {e}. Response seems to be broken.'
f"Error while fetching server API version: {e}. Response seems to be broken."
)
def _set_request_timeout(self, kwargs):
"""Prepare the kwargs for an HTTP request by inserting the timeout
parameter, if not already present."""
kwargs.setdefault('timeout', self.timeout)
kwargs.setdefault("timeout", self.timeout)
return kwargs
@update_headers
@@ -244,16 +268,16 @@ class APIClient(
for arg in args:
if not isinstance(arg, str):
raise ValueError(
f'Expected a string but found {arg} ({type(arg)}) instead'
f"Expected a string but found {arg} ({type(arg)}) instead"
)
quote_f = partial(quote, safe="/:")
args = map(quote_f, args)
if kwargs.get('versioned_api', True):
return f'{self.base_url}/v{self._version}{pathfmt.format(*args)}'
if kwargs.get("versioned_api", True):
return f"{self.base_url}/v{self._version}{pathfmt.format(*args)}"
else:
return f'{self.base_url}{pathfmt.format(*args)}'
return f"{self.base_url}{pathfmt.format(*args)}"
def _raise_for_status(self, response):
"""Raises stored :class:`APIError`, if one occurred."""
@@ -264,7 +288,7 @@ class APIClient(
def _result(self, response, json=False, binary=False):
if json and binary:
raise AssertionError('json and binary must not be both True')
raise AssertionError("json and binary must not be both True")
self._raise_for_status(response)
if json:
@@ -284,23 +308,19 @@ class APIClient(
elif data is not None:
data2 = data
if 'headers' not in kwargs:
kwargs['headers'] = {}
kwargs['headers']['Content-Type'] = 'application/json'
if "headers" not in kwargs:
kwargs["headers"] = {}
kwargs["headers"]["Content-Type"] = "application/json"
return self._post(url, data=json.dumps(data2), **kwargs)
def _attach_params(self, override=None):
return override or {
'stdout': 1,
'stderr': 1,
'stream': 1
}
return override or {"stdout": 1, "stderr": 1, "stream": 1}
def _get_raw_response_socket(self, response):
self._raise_for_status(response)
if self.base_url == "http+docker://localnpipe":
sock = response.raw._fp.fp.raw.sock
elif self.base_url.startswith('http+docker://ssh'):
elif self.base_url.startswith("http+docker://ssh"):
sock = response.raw._fp.fp.channel
else:
sock = response.raw._fp.fp.raw
@@ -348,8 +368,8 @@ class APIClient(
while True:
if buf_length - walker < STREAM_HEADER_SIZE_BYTES:
break
header = buf[walker:walker + STREAM_HEADER_SIZE_BYTES]
dummy, length = struct.unpack_from('>BxxxL', header)
header = buf[walker : walker + STREAM_HEADER_SIZE_BYTES]
dummy, length = struct.unpack_from(">BxxxL", header)
start = walker + STREAM_HEADER_SIZE_BYTES
end = start + length
walker = end
@@ -368,7 +388,7 @@ class APIClient(
header = response.raw.read(STREAM_HEADER_SIZE_BYTES)
if not header:
break
dummy, length = struct.unpack('>BxxxL', header)
dummy, length = struct.unpack(">BxxxL", header)
if not length:
continue
data = response.raw.read(length)
@@ -377,7 +397,7 @@ class APIClient(
yield data
def _stream_raw_result(self, response, chunk_size=1, decode=True):
''' Stream result for TTY-enabled container and raw binary data'''
"""Stream result for TTY-enabled container and raw binary data"""
self._raise_for_status(response)
# Disable timeout on the underlying socket to prevent
@@ -413,7 +433,7 @@ class APIClient(
response.close()
def _disable_socket_timeout(self, socket):
""" Depending on the combination of python version and whether we are
"""Depending on the combination of python version and whether we are
connecting over http or https, we might need to access _sock, which
may or may not exist; or we may need to just settimeout on socket
itself, which also may or may not have settimeout on it. To avoid
@@ -423,15 +443,15 @@ class APIClient(
you run the risk of changing a socket that was non-blocking to
blocking, for example when using gevent.
"""
sockets = [socket, getattr(socket, '_sock', None)]
sockets = [socket, getattr(socket, "_sock", None)]
for s in sockets:
if not hasattr(s, 'settimeout'):
if not hasattr(s, "settimeout"):
continue
timeout = -1
if hasattr(s, 'gettimeout'):
if hasattr(s, "gettimeout"):
timeout = s.gettimeout()
# Do not change the timeout if it is already disabled.
@@ -440,10 +460,10 @@ class APIClient(
s.settimeout(None)
@check_resource('container')
@check_resource("container")
def _check_is_tty(self, container):
cont = self.inspect_container(container)
return cont['Config']['Tty']
return cont["Config"]["Tty"]
def _get_result(self, container, stream, res):
return self._get_result_tty(stream, res, self._check_is_tty(container))
@@ -452,17 +472,18 @@ class APIClient(
# We should also use raw streaming (without keep-alive)
# if we are dealing with a tty-enabled container.
if is_tty:
return self._stream_raw_result(res) if stream else \
self._result(res, binary=True)
return (
self._stream_raw_result(res)
if stream
else self._result(res, binary=True)
)
self._raise_for_status(res)
sep = b''
sep = b""
if stream:
return self._multiplexed_response_stream_helper(res)
else:
return sep.join(
list(self._multiplexed_buffer_helper(res))
)
return sep.join(list(self._multiplexed_buffer_helper(res)))
def _unmount(self, *args):
for proto in args:
@@ -498,15 +519,13 @@ class APIClient(
)
def _set_auth_headers(self, headers):
log.debug('Looking for auth config')
log.debug("Looking for auth config")
# If we do not have any auth data so far, try reloading the config
# file one more time in case anything showed up in there.
if not self._auth_configs or self._auth_configs.is_empty:
log.debug("No auth config in memory - loading from filesystem")
self._auth_configs = auth.load_config(
credstore_env=self.credstore_env
)
self._auth_configs = auth.load_config(credstore_env=self.credstore_env)
# Send the full auth configuration (if any exists), since the build
# could use any (or all) of the registries.
@@ -514,87 +533,134 @@ class APIClient(
auth_data = self._auth_configs.get_all_credentials()
# See https://github.com/docker/docker-py/issues/1683
if (auth.INDEX_URL not in auth_data and
auth.INDEX_NAME in auth_data):
if auth.INDEX_URL not in auth_data and auth.INDEX_NAME in auth_data:
auth_data[auth.INDEX_URL] = auth_data.get(auth.INDEX_NAME, {})
log.debug(
'Sending auth config (%s)',
', '.join(repr(k) for k in auth_data.keys())
"Sending auth config (%s)", ", ".join(repr(k) for k in auth_data.keys())
)
if auth_data:
headers['X-Registry-Config'] = auth.encode_header(
auth_data
)
headers["X-Registry-Config"] = auth.encode_header(auth_data)
else:
log.debug('No auth config found')
log.debug("No auth config found")
def get_binary(self, pathfmt, *args, **kwargs):
return self._result(self._get(self._url(pathfmt, *args, versioned_api=True), **kwargs), binary=True)
return self._result(
self._get(self._url(pathfmt, *args, versioned_api=True), **kwargs),
binary=True,
)
def get_json(self, pathfmt, *args, **kwargs):
return self._result(self._get(self._url(pathfmt, *args, versioned_api=True), **kwargs), json=True)
return self._result(
self._get(self._url(pathfmt, *args, versioned_api=True), **kwargs),
json=True,
)
def get_text(self, pathfmt, *args, **kwargs):
return self._result(self._get(self._url(pathfmt, *args, versioned_api=True), **kwargs))
return self._result(
self._get(self._url(pathfmt, *args, versioned_api=True), **kwargs)
)
def get_raw_stream(self, pathfmt, *args, **kwargs):
chunk_size = kwargs.pop('chunk_size', DEFAULT_DATA_CHUNK_SIZE)
res = self._get(self._url(pathfmt, *args, versioned_api=True), stream=True, **kwargs)
chunk_size = kwargs.pop("chunk_size", DEFAULT_DATA_CHUNK_SIZE)
res = self._get(
self._url(pathfmt, *args, versioned_api=True), stream=True, **kwargs
)
self._raise_for_status(res)
return self._stream_raw_result(res, chunk_size, False)
def delete_call(self, pathfmt, *args, **kwargs):
self._raise_for_status(self._delete(self._url(pathfmt, *args, versioned_api=True), **kwargs))
self._raise_for_status(
self._delete(self._url(pathfmt, *args, versioned_api=True), **kwargs)
)
def delete_json(self, pathfmt, *args, **kwargs):
return self._result(self._delete(self._url(pathfmt, *args, versioned_api=True), **kwargs), json=True)
return self._result(
self._delete(self._url(pathfmt, *args, versioned_api=True), **kwargs),
json=True,
)
def post_call(self, pathfmt, *args, **kwargs):
self._raise_for_status(self._post(self._url(pathfmt, *args, versioned_api=True), **kwargs))
self._raise_for_status(
self._post(self._url(pathfmt, *args, versioned_api=True), **kwargs)
)
def post_json(self, pathfmt, *args, **kwargs):
data = kwargs.pop('data', None)
self._raise_for_status(self._post_json(self._url(pathfmt, *args, versioned_api=True), data, **kwargs))
data = kwargs.pop("data", None)
self._raise_for_status(
self._post_json(
self._url(pathfmt, *args, versioned_api=True), data, **kwargs
)
)
def post_json_to_binary(self, pathfmt, *args, **kwargs):
data = kwargs.pop('data', None)
return self._result(self._post_json(self._url(pathfmt, *args, versioned_api=True), data, **kwargs), binary=True)
data = kwargs.pop("data", None)
return self._result(
self._post_json(
self._url(pathfmt, *args, versioned_api=True), data, **kwargs
),
binary=True,
)
def post_json_to_json(self, pathfmt, *args, **kwargs):
data = kwargs.pop('data', None)
return self._result(self._post_json(self._url(pathfmt, *args, versioned_api=True), data, **kwargs), json=True)
data = kwargs.pop("data", None)
return self._result(
self._post_json(
self._url(pathfmt, *args, versioned_api=True), data, **kwargs
),
json=True,
)
def post_json_to_text(self, pathfmt, *args, **kwargs):
data = kwargs.pop('data', None)
data = kwargs.pop("data", None)
def post_json_to_stream_socket(self, pathfmt, *args, **kwargs):
data = kwargs.pop('data', None)
headers = (kwargs.pop('headers', None) or {}).copy()
headers.update({
'Connection': 'Upgrade',
'Upgrade': 'tcp',
})
data = kwargs.pop("data", None)
headers = (kwargs.pop("headers", None) or {}).copy()
headers.update(
{
"Connection": "Upgrade",
"Upgrade": "tcp",
}
)
return self._get_raw_response_socket(
self._post_json(self._url(pathfmt, *args, versioned_api=True), data, headers=headers, stream=True, **kwargs))
self._post_json(
self._url(pathfmt, *args, versioned_api=True),
data,
headers=headers,
stream=True,
**kwargs,
)
)
def post_json_to_stream(self, pathfmt, *args, **kwargs):
data = kwargs.pop('data', None)
headers = (kwargs.pop('headers', None) or {}).copy()
headers.update({
'Connection': 'Upgrade',
'Upgrade': 'tcp',
})
stream = kwargs.pop('stream', False)
demux = kwargs.pop('demux', False)
tty = kwargs.pop('tty', False)
data = kwargs.pop("data", None)
headers = (kwargs.pop("headers", None) or {}).copy()
headers.update(
{
"Connection": "Upgrade",
"Upgrade": "tcp",
}
)
stream = kwargs.pop("stream", False)
demux = kwargs.pop("demux", False)
tty = kwargs.pop("tty", False)
return self._read_from_socket(
self._post_json(self._url(pathfmt, *args, versioned_api=True), data, headers=headers, stream=True, **kwargs),
self._post_json(
self._url(pathfmt, *args, versioned_api=True),
data,
headers=headers,
stream=True,
**kwargs,
),
stream,
tty=tty,
demux=demux
demux=demux,
)
def post_to_json(self, pathfmt, *args, **kwargs):
return self._result(self._post(self._url(pathfmt, *args, versioned_api=True), **kwargs), json=True)
return self._result(
self._post(self._url(pathfmt, *args, versioned_api=True), **kwargs),
json=True,
)
+19 -15
View File
@@ -16,7 +16,7 @@ from ..utils.decorators import minimum_version
class DaemonApiMixin(object):
@minimum_version('1.25')
@minimum_version("1.25")
def df(self):
"""
Get data usage information.
@@ -29,7 +29,7 @@ class DaemonApiMixin(object):
:py:class:`docker.errors.APIError`
If the server returns an error.
"""
url = self._url('/system/df')
url = self._url("/system/df")
return self._result(self._get(url), True)
def info(self):
@@ -46,8 +46,15 @@ class DaemonApiMixin(object):
"""
return self._result(self._get(self._url("/info")), True)
def login(self, username, password=None, email=None, registry=None,
reauth=False, dockercfg_path=None):
def login(
self,
username,
password=None,
email=None,
registry=None,
reauth=False,
dockercfg_path=None,
):
"""
Authenticate with a registry. Similar to the ``docker login`` command.
@@ -80,25 +87,22 @@ class DaemonApiMixin(object):
dockercfg_path, credstore_env=self.credstore_env
)
elif not self._auth_configs or self._auth_configs.is_empty:
self._auth_configs = auth.load_config(
credstore_env=self.credstore_env
)
self._auth_configs = auth.load_config(credstore_env=self.credstore_env)
authcfg = self._auth_configs.resolve_authconfig(registry)
# If we found an existing auth config for this registry and username
# combination, we can return it immediately unless reauth is requested.
if authcfg and authcfg.get('username', None) == username \
and not reauth:
if authcfg and authcfg.get("username", None) == username and not reauth:
return authcfg
req_data = {
'username': username,
'password': password,
'email': email,
'serveraddress': registry,
"username": username,
"password": password,
"email": email,
"serveraddress": registry,
}
response = self._post_json(self._url('/auth'), data=req_data)
response = self._post_json(self._url("/auth"), data=req_data)
if response.status_code == 200:
self._auth_configs.add_auth(registry or auth.INDEX_NAME, req_data)
return self._result(response, json=True)
@@ -115,7 +119,7 @@ class DaemonApiMixin(object):
:py:class:`docker.errors.APIError`
If the server returns an error.
"""
return self._result(self._get(self._url('/_ping'))) == 'OK'
return self._result(self._get(self._url("/_ping"))) == "OK"
def version(self, api_version=True):
"""
+81 -89
View File
@@ -14,44 +14,43 @@ import json
import logging
from . import errors
from .credentials.errors import CredentialsNotFound, StoreError
from .credentials.store import Store
from .credentials.errors import StoreError, CredentialsNotFound
from .utils import config
INDEX_NAME = 'docker.io'
INDEX_URL = f'https://index.{INDEX_NAME}/v1/'
TOKEN_USERNAME = '<token>'
INDEX_NAME = "docker.io"
INDEX_URL = f"https://index.{INDEX_NAME}/v1/"
TOKEN_USERNAME = "<token>"
log = logging.getLogger(__name__)
def resolve_repository_name(repo_name):
if '://' in repo_name:
if "://" in repo_name:
raise errors.InvalidRepository(
f'Repository name cannot contain a scheme ({repo_name})'
f"Repository name cannot contain a scheme ({repo_name})"
)
index_name, remote_name = split_repo_name(repo_name)
if index_name[0] == '-' or index_name[-1] == '-':
if index_name[0] == "-" or index_name[-1] == "-":
raise errors.InvalidRepository(
f'Invalid index name ({index_name}). Cannot begin or end with a hyphen.'
f"Invalid index name ({index_name}). Cannot begin or end with a hyphen."
)
return resolve_index_name(index_name), remote_name
def resolve_index_name(index_name):
index_name = convert_to_hostname(index_name)
if index_name == 'index.' + INDEX_NAME:
if index_name == "index." + INDEX_NAME:
index_name = INDEX_NAME
return index_name
def get_config_header(client, registry):
log.debug('Looking for auth config')
log.debug("Looking for auth config")
if not client._auth_configs or client._auth_configs.is_empty:
log.debug(
"No auth config in memory - loading from filesystem"
)
log.debug("No auth config in memory - loading from filesystem")
client._auth_configs = load_config(credstore_env=client.credstore_env)
authcfg = resolve_authconfig(
client._auth_configs, registry, credstore_env=client.credstore_env
@@ -60,18 +59,18 @@ def get_config_header(client, registry):
# specific registry as we can have a readonly pull. Just
# put the header if we can.
if authcfg:
log.debug('Found auth config')
log.debug("Found auth config")
# auth_config needs to be a dict in the format used by
# auth.py username , password, serveraddress, email
return encode_header(authcfg)
log.debug('No auth config found')
log.debug("No auth config found")
return None
def split_repo_name(repo_name):
parts = repo_name.split('/', 1)
parts = repo_name.split("/", 1)
if len(parts) == 1 or (
'.' not in parts[0] and ':' not in parts[0] and parts[0] != 'localhost'
"." not in parts[0] and ":" not in parts[0] and parts[0] != "localhost"
):
# This is a docker index repo (ex: username/foobar or ubuntu)
return INDEX_NAME, repo_name
@@ -86,8 +85,8 @@ def get_credential_store(authconfig, registry):
class AuthConfig(dict):
def __init__(self, dct, credstore_env=None):
if 'auths' not in dct:
dct['auths'] = {}
if "auths" not in dct:
dct["auths"] = {}
self.update(dct)
self._credstore_env = credstore_env
self._stores = {}
@@ -109,39 +108,42 @@ class AuthConfig(dict):
conf = {}
for registry, entry in entries.items():
if not isinstance(entry, dict):
log.debug('Config entry for key %s is not auth config', registry)
log.debug("Config entry for key %s is not auth config", registry)
# We sometimes fall back to parsing the whole config as if it
# was the auth config by itself, for legacy purposes. In that
# case, we fail silently and return an empty conf if any of the
# keys is not formatted properly.
if raise_on_error:
raise errors.InvalidConfigFile(
f'Invalid configuration for registry {registry}'
f"Invalid configuration for registry {registry}"
)
return {}
if 'identitytoken' in entry:
log.debug('Found an IdentityToken entry for registry %s', registry)
conf[registry] = {
'IdentityToken': entry['identitytoken']
}
if "identitytoken" in entry:
log.debug("Found an IdentityToken entry for registry %s", registry)
conf[registry] = {"IdentityToken": entry["identitytoken"]}
continue # Other values are irrelevant if we have a token
if 'auth' not in entry:
if "auth" not in entry:
# Starting with engine v1.11 (API 1.23), an empty dictionary is
# a valid value in the auths config.
# https://github.com/docker/compose/issues/3265
log.debug('Auth data for %s is absent. Client might be using a credentials store instead.', registry)
log.debug(
"Auth data for %s is absent. Client might be using a credentials store instead.",
registry,
)
conf[registry] = {}
continue
username, password = decode_auth(entry['auth'])
log.debug('Found entry (registry=%s, username=%s)', repr(registry), repr(username))
username, password = decode_auth(entry["auth"])
log.debug(
"Found entry (registry=%s, username=%s)", repr(registry), repr(username)
)
conf[registry] = {
'username': username,
'password': password,
'email': entry.get('email'),
'serveraddress': registry,
"username": username,
"password": password,
"email": entry.get("email"),
"serveraddress": registry,
}
return conf
@@ -171,19 +173,17 @@ class AuthConfig(dict):
return cls(_load_legacy_config(config_file), credstore_env)
res = {}
if config_dict.get('auths'):
if config_dict.get("auths"):
log.debug("Found 'auths' section")
res.update({
'auths': cls.parse_auth(
config_dict.pop('auths'), raise_on_error=True
)
})
if config_dict.get('credsStore'):
res.update(
{"auths": cls.parse_auth(config_dict.pop("auths"), raise_on_error=True)}
)
if config_dict.get("credsStore"):
log.debug("Found 'credsStore' section")
res.update({'credsStore': config_dict.pop('credsStore')})
if config_dict.get('credHelpers'):
res.update({"credsStore": config_dict.pop("credsStore")})
if config_dict.get("credHelpers"):
log.debug("Found 'credHelpers' section")
res.update({'credHelpers': config_dict.pop('credHelpers')})
res.update({"credHelpers": config_dict.pop("credHelpers")})
if res:
return cls(res, credstore_env)
@@ -191,25 +191,23 @@ class AuthConfig(dict):
"Could not find auth-related section ; attempting to interpret "
"as auth-only file"
)
return cls({'auths': cls.parse_auth(config_dict)}, credstore_env)
return cls({"auths": cls.parse_auth(config_dict)}, credstore_env)
@property
def auths(self):
return self.get('auths', {})
return self.get("auths", {})
@property
def creds_store(self):
return self.get('credsStore', None)
return self.get("credsStore", None)
@property
def cred_helpers(self):
return self.get('credHelpers', {})
return self.get("credHelpers", {})
@property
def is_empty(self):
return (
not self.auths and not self.creds_store and not self.cred_helpers
)
return not self.auths and not self.creds_store and not self.cred_helpers
def resolve_authconfig(self, registry=None):
"""
@@ -226,7 +224,7 @@ class AuthConfig(dict):
cfg = self._resolve_authconfig_credstore(registry, store_name)
if cfg is not None:
return cfg
log.debug('No entry in credstore - fetching from auth dict')
log.debug("No entry in credstore - fetching from auth dict")
# Default to the public index server
registry = resolve_index_name(registry) if registry else INDEX_NAME
@@ -254,29 +252,27 @@ class AuthConfig(dict):
try:
data = store.get(registry)
res = {
'ServerAddress': registry,
"ServerAddress": registry,
}
if data['Username'] == TOKEN_USERNAME:
res['IdentityToken'] = data['Secret']
if data["Username"] == TOKEN_USERNAME:
res["IdentityToken"] = data["Secret"]
else:
res.update({
'Username': data['Username'],
'Password': data['Secret'],
})
res.update(
{
"Username": data["Username"],
"Password": data["Secret"],
}
)
return res
except CredentialsNotFound:
log.debug('No entry found')
log.debug("No entry found")
return None
except StoreError as e:
raise errors.DockerException(
f'Credentials store error: {e}'
)
raise errors.DockerException(f"Credentials store error: {e}")
def _get_store_instance(self, name):
if name not in self._stores:
self._stores[name] = Store(
name, environment=self._credstore_env
)
self._stores[name] = Store(name, environment=self._credstore_env)
return self._stores[name]
def get_credential_store(self, registry):
@@ -291,22 +287,18 @@ class AuthConfig(dict):
# Retrieve all credentials from the default store
store = self._get_store_instance(self.creds_store)
for k in store.list().keys():
auth_data[k] = self._resolve_authconfig_credstore(
k, self.creds_store
)
auth_data[k] = self._resolve_authconfig_credstore(k, self.creds_store)
auth_data[convert_to_hostname(k)] = auth_data[k]
# credHelpers entries take priority over all others
for reg, store_name in self.cred_helpers.items():
auth_data[reg] = self._resolve_authconfig_credstore(
reg, store_name
)
auth_data[reg] = self._resolve_authconfig_credstore(reg, store_name)
auth_data[convert_to_hostname(reg)] = auth_data[reg]
return auth_data
def add_auth(self, reg, data):
self['auths'][reg] = data
self["auths"][reg] = data
def resolve_authconfig(authconfig, registry=None, credstore_env=None):
@@ -316,19 +308,19 @@ def resolve_authconfig(authconfig, registry=None, credstore_env=None):
def convert_to_hostname(url):
return url.replace('http://', '').replace('https://', '').split('/', 1)[0]
return url.replace("http://", "").replace("https://", "").split("/", 1)[0]
def decode_auth(auth):
if isinstance(auth, str):
auth = auth.encode('ascii')
auth = auth.encode("ascii")
s = base64.b64decode(auth)
login, pwd = s.split(b':', 1)
return login.decode('utf8'), pwd.decode('utf8')
login, pwd = s.split(b":", 1)
return login.decode("utf8"), pwd.decode("utf8")
def encode_header(auth):
auth_json = json.dumps(auth).encode('ascii')
auth_json = json.dumps(auth).encode("ascii")
return base64.urlsafe_b64encode(auth_json)
@@ -358,22 +350,22 @@ def _load_legacy_config(config_file):
data = []
with open(config_file) as f:
for line in f.readlines():
data.append(line.strip().split(' = ')[1])
data.append(line.strip().split(" = ")[1])
if len(data) < 2:
# Not enough data
raise errors.InvalidConfigFile(
'Invalid or empty configuration file!'
)
raise errors.InvalidConfigFile("Invalid or empty configuration file!")
username, password = decode_auth(data[0])
return {'auths': {
INDEX_NAME: {
'username': username,
'password': password,
'email': data[1],
'serveraddress': INDEX_URL,
return {
"auths": {
INDEX_NAME: {
"username": username,
"password": password,
"email": data[1],
"serveraddress": INDEX_URL,
}
}
}}
}
except Exception as e:
log.debug(e)
pass
+7 -13
View File
@@ -11,26 +11,20 @@ from __future__ import annotations
import sys
MINIMUM_DOCKER_API_VERSION = '1.21'
MINIMUM_DOCKER_API_VERSION = "1.21"
DEFAULT_TIMEOUT_SECONDS = 60
STREAM_HEADER_SIZE_BYTES = 8
CONTAINER_LIMITS_KEYS = [
'memory', 'memswap', 'cpushares', 'cpusetcpus'
]
CONTAINER_LIMITS_KEYS = ["memory", "memswap", "cpushares", "cpusetcpus"]
DEFAULT_HTTP_HOST = "127.0.0.1"
DEFAULT_UNIX_SOCKET = "http+unix:///var/run/docker.sock"
DEFAULT_NPIPE = 'npipe:////./pipe/docker_engine'
DEFAULT_NPIPE = "npipe:////./pipe/docker_engine"
BYTE_UNITS = {
'b': 1,
'k': 1024,
'm': 1024 * 1024,
'g': 1024 * 1024 * 1024
}
BYTE_UNITS = {"b": 1, "k": 1024, "m": 1024 * 1024, "g": 1024 * 1024 * 1024}
IS_WINDOWS_PLATFORM = (sys.platform == 'win32')
WINDOWS_LONGPATH_PREFIX = '\\\\?\\'
IS_WINDOWS_PLATFORM = sys.platform == "win32"
WINDOWS_LONGPATH_PREFIX = "\\\\?\\"
DEFAULT_USER_AGENT = "ansible-community.docker"
DEFAULT_NUM_POOLS = 25
+22 -14
View File
@@ -13,7 +13,6 @@ import json
import os
from .. import errors
from .config import (
METAFILE,
get_current_context_name,
@@ -25,9 +24,11 @@ from .context import Context
def create_default_context():
host = None
if os.environ.get('DOCKER_HOST'):
host = os.environ.get('DOCKER_HOST')
return Context("default", "swarm", host, description="Current DOCKER_HOST based configuration")
if os.environ.get("DOCKER_HOST"):
host = os.environ.get("DOCKER_HOST")
return Context(
"default", "swarm", host, description="Current DOCKER_HOST based configuration"
)
class ContextAPI(object):
@@ -35,6 +36,7 @@ class ContextAPI(object):
Contains methods for context management:
create, list, remove, get, inspect.
"""
DEFAULT_CONTEXT = None
@classmethod
@@ -47,8 +49,14 @@ class ContextAPI(object):
@classmethod
def create_context(
cls, name, orchestrator=None, host=None, tls_cfg=None,
default_namespace=None, skip_tls_verify=False):
cls,
name,
orchestrator=None,
host=None,
tls_cfg=None,
default_namespace=None,
skip_tls_verify=False,
):
"""Creates a new context.
Returns:
(Context): a Context object.
@@ -79,8 +87,7 @@ class ContextAPI(object):
if not name:
raise errors.MissingContextParameter("name")
if name == "default":
raise errors.ContextException(
'"default" is a reserved context name')
raise errors.ContextException('"default" is a reserved context name')
ctx = Context.load_context(name)
if ctx:
raise errors.ContextAlreadyExists(name)
@@ -89,9 +96,12 @@ class ContextAPI(object):
endpoint = orchestrator
ctx = Context(name, orchestrator)
ctx.set_endpoint(
endpoint, host, tls_cfg,
endpoint,
host,
tls_cfg,
skip_tls_verify=skip_tls_verify,
def_namespace=default_namespace)
def_namespace=default_namespace,
)
ctx.save()
return ctx
@@ -173,8 +183,7 @@ class ContextAPI(object):
err = write_context_name_to_docker_config(name)
if err:
raise errors.ContextException(
f'Failed to set current context: {err}')
raise errors.ContextException(f"Failed to set current context: {err}")
@classmethod
def remove_context(cls, name):
@@ -200,8 +209,7 @@ class ContextAPI(object):
if not name:
raise errors.MissingContextParameter("name")
if name == "default":
raise errors.ContextException(
'context "default" cannot be removed')
raise errors.ContextException('context "default" cannot be removed')
ctx = Context.load_context(name)
if not ctx:
raise errors.ContextNotFound(name)
+10 -6
View File
@@ -17,19 +17,23 @@ from ..constants import DEFAULT_UNIX_SOCKET, IS_WINDOWS_PLATFORM
from ..utils.config import find_config_file, get_default_config_file
from ..utils.utils import parse_host
METAFILE = "meta.json"
def get_current_context_name_with_source():
if os.environ.get('DOCKER_HOST'):
if os.environ.get("DOCKER_HOST"):
return "default", "DOCKER_HOST environment variable set"
if os.environ.get('DOCKER_CONTEXT'):
return os.environ['DOCKER_CONTEXT'], "DOCKER_CONTEXT environment variable set"
if os.environ.get("DOCKER_CONTEXT"):
return os.environ["DOCKER_CONTEXT"], "DOCKER_CONTEXT environment variable set"
docker_cfg_path = find_config_file()
if docker_cfg_path:
try:
with open(docker_cfg_path) as f:
return json.load(f).get("currentContext", "default"), f"configuration file {docker_cfg_path}"
return (
json.load(f).get("currentContext", "default"),
f"configuration file {docker_cfg_path}",
)
except Exception:
pass
return "default", "fallback value"
@@ -40,7 +44,7 @@ def get_current_context_name():
def write_context_name_to_docker_config(name=None):
if name == 'default':
if name == "default":
name = None
docker_cfg_path = find_config_file()
config = {}
@@ -67,7 +71,7 @@ def write_context_name_to_docker_config(name=None):
def get_context_id(name):
return hashlib.sha256(name.encode('utf-8')).hexdigest()
return hashlib.sha256(name.encode("utf-8")).hexdigest()
def get_context_dir():
+56 -39
View File
@@ -15,7 +15,6 @@ from shutil import copyfile, rmtree
from ..errors import ContextException
from ..tls import TLSConfig
from .config import (
get_context_host,
get_meta_dir,
@@ -30,8 +29,16 @@ IN_MEMORY = "IN MEMORY"
class Context(object):
"""A context."""
def __init__(self, name, orchestrator=None, host=None, endpoints=None,
skip_tls_verify=False, tls=False, description=None):
def __init__(
self,
name,
orchestrator=None,
host=None,
endpoints=None,
skip_tls_verify=False,
tls=False,
description=None,
):
if not name:
raise Exception("Name not provided")
self.name = name
@@ -45,9 +52,11 @@ class Context(object):
if not endpoints:
# set default docker endpoint if no endpoint is set
default_endpoint = "docker" if (
not orchestrator or orchestrator == "swarm"
) else orchestrator
default_endpoint = (
"docker"
if (not orchestrator or orchestrator == "swarm")
else orchestrator
)
self.endpoints = {
default_endpoint: {
@@ -69,17 +78,24 @@ class Context(object):
if k != "docker":
continue
self.endpoints[k]["Host"] = v.get("Host", get_context_host(
host, skip_tls_verify or tls))
self.endpoints[k]["SkipTLSVerify"] = bool(v.get(
"SkipTLSVerify", skip_tls_verify))
self.endpoints[k]["Host"] = v.get(
"Host", get_context_host(host, skip_tls_verify or tls)
)
self.endpoints[k]["SkipTLSVerify"] = bool(
v.get("SkipTLSVerify", skip_tls_verify)
)
def set_endpoint(
self, name="docker", host=None, tls_cfg=None,
skip_tls_verify=False, def_namespace=None):
self,
name="docker",
host=None,
tls_cfg=None,
skip_tls_verify=False,
def_namespace=None,
):
self.endpoints[name] = {
"Host": get_context_host(host, not skip_tls_verify or tls_cfg is not None),
"SkipTLSVerify": skip_tls_verify
"SkipTLSVerify": skip_tls_verify,
}
if def_namespace:
self.endpoints[name]["DefaultNamespace"] = def_namespace
@@ -98,7 +114,8 @@ class Context(object):
meta["Name"],
orchestrator=meta["Metadata"].get("StackOrchestrator", None),
endpoints=meta.get("Endpoints", None),
description=meta["Metadata"].get('Description'))
description=meta["Metadata"].get("Description"),
)
instance.context_type = meta["Metadata"].get("Type", None)
instance._load_certs()
instance.meta_path = get_meta_dir(name)
@@ -127,9 +144,11 @@ class Context(object):
if k != "docker":
continue
metadata["Endpoints"][k]["Host"] = v.get(
"Host", get_context_host(None, False))
"Host", get_context_host(None, False)
)
metadata["Endpoints"][k]["SkipTLSVerify"] = bool(
v.get("SkipTLSVerify", True))
v.get("SkipTLSVerify", True)
)
return metadata
@@ -152,10 +171,14 @@ class Context(object):
if all([cert, key]) or ca_cert:
verify = None
if endpoint == "docker" and not self.endpoints["docker"].get(
"SkipTLSVerify", False):
"SkipTLSVerify", False
):
verify = True
certs[endpoint] = TLSConfig(
client_cert=(cert, key) if cert and key else None, ca_cert=ca_cert, verify=verify)
client_cert=(cert, key) if cert and key else None,
ca_cert=ca_cert,
verify=verify,
)
self.tls_cfg = certs
self.tls_path = tls_dir
@@ -173,15 +196,20 @@ class Context(object):
ca_file = tls.ca_cert
if ca_file:
copyfile(ca_file, os.path.join(
tls_dir, endpoint, os.path.basename(ca_file)))
copyfile(
ca_file, os.path.join(tls_dir, endpoint, os.path.basename(ca_file))
)
if tls.cert:
cert_file, key_file = tls.cert
copyfile(cert_file, os.path.join(
tls_dir, endpoint, os.path.basename(cert_file)))
copyfile(key_file, os.path.join(
tls_dir, endpoint, os.path.basename(key_file)))
copyfile(
cert_file,
os.path.join(tls_dir, endpoint, os.path.basename(cert_file)),
)
copyfile(
key_file,
os.path.join(tls_dir, endpoint, os.path.basename(key_file)),
)
self.meta_path = get_meta_dir(self.name)
self.tls_path = get_tls_dir(self.name)
@@ -230,11 +258,7 @@ class Context(object):
meta = {}
if self.orchestrator:
meta = {"StackOrchestrator": self.orchestrator}
return {
"Name": self.name,
"Metadata": meta,
"Endpoints": self.endpoints
}
return {"Name": self.name, "Metadata": meta, "Endpoints": self.endpoints}
@property
def TLSConfig(self):
@@ -250,16 +274,9 @@ class Context(object):
certs = {}
for endpoint, tls in self.tls_cfg.items():
cert, key = tls.cert
certs[endpoint] = list(
map(os.path.basename, [tls.ca_cert, cert, key]))
return {
"TLSMaterial": certs
}
certs[endpoint] = list(map(os.path.basename, [tls.ca_cert, cert, key]))
return {"TLSMaterial": certs}
@property
def Storage(self):
return {
"Storage": {
"MetadataPath": self.meta_path,
"TLSPath": self.tls_path
}}
return {"Storage": {"MetadataPath": self.meta_path, "TLSPath": self.tls_path}}
@@ -9,7 +9,8 @@
from __future__ import annotations
PROGRAM_PREFIX = 'docker-credential-'
DEFAULT_LINUX_STORE = 'secretservice'
DEFAULT_OSX_STORE = 'osxkeychain'
DEFAULT_WIN32_STORE = 'wincred'
PROGRAM_PREFIX = "docker-credential-"
DEFAULT_LINUX_STORE = "secretservice"
DEFAULT_OSX_STORE = "osxkeychain"
DEFAULT_WIN32_STORE = "wincred"
@@ -23,11 +23,9 @@ class InitializationError(StoreError):
def process_store_error(cpe, program):
message = cpe.output.decode('utf-8')
if 'credentials not found in native keychain' in message:
return CredentialsNotFound(
f'No matching credentials in {program}'
)
message = cpe.output.decode("utf-8")
if "credentials not found in native keychain" in message:
return CredentialsNotFound(f"No matching credentials in {program}")
return StoreError(
f'Credentials store {program} exited with "{cpe.output.decode("utf-8").strip()}".'
)
+30 -33
View File
@@ -13,84 +13,81 @@ import errno
import json
import subprocess
from . import constants
from . import errors
from .utils import create_environment_dict
from .utils import find_executable
from . import constants, errors
from .utils import create_environment_dict, find_executable
class Store(object):
def __init__(self, program, environment=None):
""" Create a store object that acts as an interface to
perform the basic operations for storing, retrieving
and erasing credentials using `program`.
"""Create a store object that acts as an interface to
perform the basic operations for storing, retrieving
and erasing credentials using `program`.
"""
self.program = constants.PROGRAM_PREFIX + program
self.exe = find_executable(self.program)
self.environment = environment
if self.exe is None:
raise errors.InitializationError(
f'{self.program} not installed or not available in PATH'
f"{self.program} not installed or not available in PATH"
)
def get(self, server):
""" Retrieve credentials for `server`. If no credentials are found,
a `StoreError` will be raised.
"""Retrieve credentials for `server`. If no credentials are found,
a `StoreError` will be raised.
"""
if not isinstance(server, bytes):
server = server.encode('utf-8')
data = self._execute('get', server)
result = json.loads(data.decode('utf-8'))
server = server.encode("utf-8")
data = self._execute("get", server)
result = json.loads(data.decode("utf-8"))
# docker-credential-pass will return an object for inexistent servers
# whereas other helpers will exit with returncode != 0. For
# consistency, if no significant data is returned,
# raise CredentialsNotFound
if result['Username'] == '' and result['Secret'] == '':
if result["Username"] == "" and result["Secret"] == "":
raise errors.CredentialsNotFound(
f'No matching credentials in {self.program}'
f"No matching credentials in {self.program}"
)
return result
def store(self, server, username, secret):
""" Store credentials for `server`. Raises a `StoreError` if an error
occurs.
"""Store credentials for `server`. Raises a `StoreError` if an error
occurs.
"""
data_input = json.dumps({
'ServerURL': server,
'Username': username,
'Secret': secret
}).encode('utf-8')
return self._execute('store', data_input)
data_input = json.dumps(
{"ServerURL": server, "Username": username, "Secret": secret}
).encode("utf-8")
return self._execute("store", data_input)
def erase(self, server):
""" Erase credentials for `server`. Raises a `StoreError` if an error
occurs.
"""Erase credentials for `server`. Raises a `StoreError` if an error
occurs.
"""
if not isinstance(server, bytes):
server = server.encode('utf-8')
self._execute('erase', server)
server = server.encode("utf-8")
self._execute("erase", server)
def list(self):
""" List stored credentials. Requires v0.4.0+ of the helper.
"""
data = self._execute('list', None)
return json.loads(data.decode('utf-8'))
"""List stored credentials. Requires v0.4.0+ of the helper."""
data = self._execute("list", None)
return json.loads(data.decode("utf-8"))
def _execute(self, subcmd, data_input):
output = None
env = create_environment_dict(self.environment)
try:
output = subprocess.check_output(
[self.exe, subcmd], input=data_input, env=env,
[self.exe, subcmd],
input=data_input,
env=env,
)
except subprocess.CalledProcessError as e:
raise errors.process_store_error(e, self.program)
except OSError as e:
if e.errno == errno.ENOENT:
raise errors.StoreError(
f'{self.program} not installed or not available in PATH'
f"{self.program} not installed or not available in PATH"
)
else:
raise errors.StoreError(
@@ -10,7 +10,6 @@
from __future__ import annotations
import os
from shutil import which
+23 -18
View File
@@ -9,10 +9,10 @@
from __future__ import annotations
from ._import_helper import HTTPError as _HTTPError
from ansible.module_utils.common.text.converters import to_native
from ._import_helper import HTTPError as _HTTPError
class DockerException(Exception):
"""
@@ -29,15 +29,16 @@ def create_api_error_from_http_exception(e):
"""
response = e.response
try:
explanation = response.json()['message']
explanation = response.json()["message"]
except ValueError:
explanation = to_native((response.content or '').strip())
explanation = to_native((response.content or "").strip())
cls = APIError
if response.status_code == 404:
if explanation and ('No such image' in str(explanation) or
'not found: does not exist or no pull access'
in str(explanation) or
'repository does not exist' in str(explanation)):
if explanation and (
"No such image" in str(explanation)
or "not found: does not exist or no pull access" in str(explanation)
or "repository does not exist" in str(explanation)
):
cls = ImageNotFound
else:
cls = NotFound
@@ -48,6 +49,7 @@ class APIError(_HTTPError, DockerException):
"""
An HTTP error from the API.
"""
def __init__(self, message, response=None, explanation=None):
# requests 1.2 supports response as a keyword argument, but
# requests 1.1 does not
@@ -59,10 +61,10 @@ class APIError(_HTTPError, DockerException):
message = super(APIError, self).__str__()
if self.is_client_error():
message = f'{self.response.status_code} Client Error for {self.response.url}: {self.response.reason}'
message = f"{self.response.status_code} Client Error for {self.response.url}: {self.response.reason}"
elif self.is_server_error():
message = f'{self.response.status_code} Server Error for {self.response.url}: {self.response.reason}'
message = f"{self.response.status_code} Server Error for {self.response.url}: {self.response.reason}"
if self.explanation:
message = f'{message} ("{self.explanation}")'
@@ -121,10 +123,12 @@ class TLSParameterError(DockerException):
self.msg = msg
def __str__(self):
return self.msg + (". TLS configurations should map the Docker CLI "
"client configurations. See "
"https://docs.docker.com/engine/articles/https/ "
"for API details.")
return self.msg + (
". TLS configurations should map the Docker CLI "
"client configurations. See "
"https://docs.docker.com/engine/articles/https/ "
"for API details."
)
class NullResource(DockerException, ValueError):
@@ -135,6 +139,7 @@ class ContainerError(DockerException):
"""
Represents a container that has exited with a non-zero exit code.
"""
def __init__(self, container, exit_status, command, image, stderr):
self.container = container
self.exit_status = exit_status
@@ -171,8 +176,8 @@ def create_unexpected_kwargs_error(name, kwargs):
text.append("got an unexpected keyword argument ")
else:
text.append("got unexpected keyword arguments ")
text.append(', '.join(quoted_kwargs))
return TypeError(''.join(text))
text.append(", ".join(quoted_kwargs))
return TypeError("".join(text))
class MissingContextParameter(DockerException):
@@ -196,7 +201,7 @@ class ContextException(DockerException):
self.msg = msg
def __str__(self):
return (self.msg)
return self.msg
class ContextNotFound(DockerException):
@@ -214,4 +219,4 @@ class MissingRequirementException(DockerException):
self.import_exception = import_exception
def __str__(self):
return (self.msg)
return self.msg
+23 -13
View File
@@ -31,13 +31,20 @@ class TLSConfig(object):
.. _`SSL version`:
https://docs.python.org/3.5/library/ssl.html#ssl.PROTOCOL_TLSv1
"""
cert = None
ca_cert = None
verify = None
ssl_version = None
def __init__(self, client_cert=None, ca_cert=None, verify=None,
ssl_version=None, assert_hostname=None):
def __init__(
self,
client_cert=None,
ca_cert=None,
verify=None,
ssl_version=None,
assert_hostname=None,
):
# Argument compatibility/mapping with
# https://docs.docker.com/engine/articles/https/
# This diverges from the Docker CLI in that users can specify 'tls'
@@ -61,15 +68,15 @@ class TLSConfig(object):
tls_cert, tls_key = client_cert
except ValueError:
raise errors.TLSParameterError(
'client_cert must be a tuple of'
' (client certificate, key file)'
"client_cert must be a tuple of" " (client certificate, key file)"
)
if not (tls_cert and tls_key) or (not os.path.isfile(tls_cert) or
not os.path.isfile(tls_key)):
if not (tls_cert and tls_key) or (
not os.path.isfile(tls_cert) or not os.path.isfile(tls_key)
):
raise errors.TLSParameterError(
'Path to a certificate and key files must be provided'
' through the client_cert param'
"Path to a certificate and key files must be provided"
" through the client_cert param"
)
self.cert = (tls_cert, tls_key)
@@ -78,7 +85,7 @@ class TLSConfig(object):
self.ca_cert = ca_cert
if self.verify and self.ca_cert and not os.path.isfile(self.ca_cert):
raise errors.TLSParameterError(
'Invalid CA certificate provided for `ca_cert`.'
"Invalid CA certificate provided for `ca_cert`."
)
def configure_client(self, client):
@@ -95,7 +102,10 @@ class TLSConfig(object):
if self.cert:
client.cert = self.cert
client.mount('https://', SSLHTTPAdapter(
ssl_version=self.ssl_version,
assert_hostname=self.assert_hostname,
))
client.mount(
"https://",
SSLHTTPAdapter(
ssl_version=self.ssl_version,
assert_hostname=self.assert_hostname,
),
)
@@ -15,7 +15,7 @@ from .._import_helper import HTTPAdapter as _HTTPAdapter
class BaseHTTPAdapter(_HTTPAdapter):
def close(self):
super(BaseHTTPAdapter, self).close()
if hasattr(self, 'pools'):
if hasattr(self, "pools"):
self.pools.clear()
# Hotfix for requests 2.32.0 and 2.32.1: its commit
@@ -23,7 +23,7 @@ class BaseHTTPAdapter(_HTTPAdapter):
# changes requests.adapters.HTTPAdapter to no longer call get_connection() from
# send(), but instead call _get_connection().
def _get_connection(self, request, *args, **kwargs):
return self.get_connection(request.url, kwargs.get('proxies'))
return self.get_connection(request.url, kwargs.get("proxies"))
# Fix for requests 2.32.2+:
# https://github.com/psf/requests/commit/c98e4d133ef29c46a9b68cd783087218a8075e05
@@ -13,18 +13,16 @@ from queue import Empty
from .. import constants
from .._import_helper import HTTPAdapter, urllib3, urllib3_connection
from .basehttpadapter import BaseHTTPAdapter
from .npipesocket import NpipeSocket
RecentlyUsedContainer = urllib3._collections.RecentlyUsedContainer
class NpipeHTTPConnection(urllib3_connection.HTTPConnection, object):
def __init__(self, npipe_path, timeout=60):
super(NpipeHTTPConnection, self).__init__(
'localhost', timeout=timeout
)
super(NpipeHTTPConnection, self).__init__("localhost", timeout=timeout)
self.npipe_path = npipe_path
self.timeout = timeout
@@ -38,15 +36,13 @@ class NpipeHTTPConnection(urllib3_connection.HTTPConnection, object):
class NpipeHTTPConnectionPool(urllib3.connectionpool.HTTPConnectionPool):
def __init__(self, npipe_path, timeout=60, maxsize=10):
super(NpipeHTTPConnectionPool, self).__init__(
'localhost', timeout=timeout, maxsize=maxsize
"localhost", timeout=timeout, maxsize=maxsize
)
self.npipe_path = npipe_path
self.timeout = timeout
def _new_conn(self):
return NpipeHTTPConnection(
self.npipe_path, self.timeout
)
return NpipeHTTPConnection(self.npipe_path, self.timeout)
# When re-using connections, urllib3 tries to call select() on our
# NpipeSocket instance, causing a crash. To circumvent this, we override
@@ -63,8 +59,7 @@ class NpipeHTTPConnectionPool(urllib3.connectionpool.HTTPConnectionPool):
if self.block:
raise urllib3.exceptions.EmptyPoolError(
self,
"Pool reached maximum size and no more "
"connections are allowed."
"Pool reached maximum size and no more " "connections are allowed.",
)
pass # Oh well, we'll create a new connection then
@@ -73,15 +68,21 @@ class NpipeHTTPConnectionPool(urllib3.connectionpool.HTTPConnectionPool):
class NpipeHTTPAdapter(BaseHTTPAdapter):
__attrs__ = HTTPAdapter.__attrs__ + ['npipe_path',
'pools',
'timeout',
'max_pool_size']
__attrs__ = HTTPAdapter.__attrs__ + [
"npipe_path",
"pools",
"timeout",
"max_pool_size",
]
def __init__(self, base_url, timeout=60,
pool_connections=constants.DEFAULT_NUM_POOLS,
max_pool_size=constants.DEFAULT_MAX_POOL_SIZE):
self.npipe_path = base_url.replace('npipe://', '')
def __init__(
self,
base_url,
timeout=60,
pool_connections=constants.DEFAULT_NUM_POOLS,
max_pool_size=constants.DEFAULT_MAX_POOL_SIZE,
):
self.npipe_path = base_url.replace("npipe://", "")
self.timeout = timeout
self.max_pool_size = max_pool_size
self.pools = RecentlyUsedContainer(
@@ -96,8 +97,7 @@ class NpipeHTTPAdapter(BaseHTTPAdapter):
return pool
pool = NpipeHTTPConnectionPool(
self.npipe_path, self.timeout,
maxsize=self.max_pool_size
self.npipe_path, self.timeout, maxsize=self.max_pool_size
)
self.pools[url] = pool
@@ -14,18 +14,19 @@ import io
import time
import traceback
PYWIN32_IMPORT_ERROR = None
try:
import pywintypes
import win32api
import win32event
import win32file
import win32pipe
import pywintypes
import win32event
import win32api
except ImportError:
PYWIN32_IMPORT_ERROR = traceback.format_exc()
cERROR_PIPE_BUSY = 0xe7
cERROR_PIPE_BUSY = 0xE7
cSECURITY_SQOS_PRESENT = 0x100000
cSECURITY_ANONYMOUS = 0
@@ -36,18 +37,17 @@ def check_closed(f):
@functools.wraps(f)
def wrapped(self, *args, **kwargs):
if self._closed:
raise RuntimeError(
'Can not reuse socket after connection was closed.'
)
raise RuntimeError("Can not reuse socket after connection was closed.")
return f(self, *args, **kwargs)
return wrapped
class NpipeSocket(object):
""" Partial implementation of the socket API over windows named pipes.
This implementation is only designed to be used as a client socket,
and server-specific methods (bind, listen, accept...) are not
implemented.
"""Partial implementation of the socket API over windows named pipes.
This implementation is only designed to be used as a client socket,
and server-specific methods (bind, listen, accept...) are not
implemented.
"""
def __init__(self, handle=None):
@@ -74,10 +74,12 @@ class NpipeSocket(object):
0,
None,
win32file.OPEN_EXISTING,
(cSECURITY_ANONYMOUS
(
cSECURITY_ANONYMOUS
| cSECURITY_SQOS_PRESENT
| win32file.FILE_FLAG_OVERLAPPED),
0
| win32file.FILE_FLAG_OVERLAPPED
),
0,
)
except win32pipe.error as e:
# See Remarks:
@@ -87,7 +89,7 @@ class NpipeSocket(object):
# before we got to it. Wait for availability and attempt to
# connect again.
retry_count = retry_count + 1
if (retry_count < MAXIMUM_RETRY_COUNT):
if retry_count < MAXIMUM_RETRY_COUNT:
time.sleep(1)
return self.connect(address, retry_count)
raise e
@@ -126,7 +128,7 @@ class NpipeSocket(object):
raise NotImplementedError()
def makefile(self, mode=None, bufsize=None):
if mode.strip('b') != 'r':
if mode.strip("b") != "r":
raise NotImplementedError()
rawio = NpipeFileIOBase(self)
if bufsize is None or bufsize <= 0:
@@ -158,9 +160,7 @@ class NpipeSocket(object):
overlapped = pywintypes.OVERLAPPED()
overlapped.hEvent = event
err, data = win32file.ReadFile(
self._handle,
readbuf[:nbytes] if nbytes else readbuf,
overlapped
self._handle, readbuf[:nbytes] if nbytes else readbuf, overlapped
)
wait_result = win32event.WaitForSingleObject(event, self._timeout)
if wait_result == win32event.WAIT_TIMEOUT:
@@ -204,7 +204,7 @@ class NpipeSocket(object):
# Blocking mode
self._timeout = win32event.INFINITE
elif not isinstance(value, (float, int)) or value < 0:
raise ValueError('Timeout value out of range')
raise ValueError("Timeout value out of range")
else:
# Timeout mode - Value converted to milliseconds
self._timeout = int(value * 1000)
+56 -47
View File
@@ -18,10 +18,10 @@ import traceback
from queue import Empty
from urllib.parse import urlparse
from .basehttpadapter import BaseHTTPAdapter
from .. import constants
from .._import_helper import HTTPAdapter, urllib3, urllib3_connection
from .basehttpadapter import BaseHTTPAdapter
PARAMIKO_IMPORT_ERROR = None
try:
@@ -35,51 +35,54 @@ RecentlyUsedContainer = urllib3._collections.RecentlyUsedContainer
class SSHSocket(socket.socket):
def __init__(self, host):
super(SSHSocket, self).__init__(
socket.AF_INET, socket.SOCK_STREAM)
super(SSHSocket, self).__init__(socket.AF_INET, socket.SOCK_STREAM)
self.host = host
self.port = None
self.user = None
if ':' in self.host:
self.host, self.port = self.host.split(':')
if '@' in self.host:
self.user, self.host = self.host.split('@')
if ":" in self.host:
self.host, self.port = self.host.split(":")
if "@" in self.host:
self.user, self.host = self.host.split("@")
self.proc = None
def connect(self, **kwargs):
args = ['ssh']
args = ["ssh"]
if self.user:
args = args + ['-l', self.user]
args = args + ["-l", self.user]
if self.port:
args = args + ['-p', self.port]
args = args + ["-p", self.port]
args = args + ['--', self.host, 'docker system dial-stdio']
args = args + ["--", self.host, "docker system dial-stdio"]
preexec_func = None
if not constants.IS_WINDOWS_PLATFORM:
def f():
signal.signal(signal.SIGINT, signal.SIG_IGN)
preexec_func = f
env = dict(os.environ)
# drop LD_LIBRARY_PATH and SSL_CERT_FILE
env.pop('LD_LIBRARY_PATH', None)
env.pop('SSL_CERT_FILE', None)
env.pop("LD_LIBRARY_PATH", None)
env.pop("SSL_CERT_FILE", None)
self.proc = subprocess.Popen(
args,
env=env,
stdout=subprocess.PIPE,
stdin=subprocess.PIPE,
preexec_fn=preexec_func)
preexec_fn=preexec_func,
)
def _write(self, data):
if not self.proc or self.proc.stdin.closed:
raise Exception('SSH subprocess not initiated.'
'connect() must be called first.')
raise Exception(
"SSH subprocess not initiated." "connect() must be called first."
)
written = self.proc.stdin.write(data)
self.proc.stdin.flush()
return written
@@ -92,8 +95,9 @@ class SSHSocket(socket.socket):
def recv(self, n):
if not self.proc:
raise Exception('SSH subprocess not initiated.'
'connect() must be called first.')
raise Exception(
"SSH subprocess not initiated." "connect() must be called first."
)
return self.proc.stdout.read(n)
def makefile(self, mode):
@@ -106,16 +110,14 @@ class SSHSocket(socket.socket):
def close(self):
if not self.proc or self.proc.stdin.closed:
return
self.proc.stdin.write(b'\n\n')
self.proc.stdin.write(b"\n\n")
self.proc.stdin.flush()
self.proc.terminate()
class SSHConnection(urllib3_connection.HTTPConnection, object):
def __init__(self, ssh_transport=None, timeout=60, host=None):
super(SSHConnection, self).__init__(
'localhost', timeout=timeout
)
super(SSHConnection, self).__init__("localhost", timeout=timeout)
self.ssh_transport = ssh_transport
self.timeout = timeout
self.ssh_host = host
@@ -124,7 +126,7 @@ class SSHConnection(urllib3_connection.HTTPConnection, object):
if self.ssh_transport:
sock = self.ssh_transport.open_session()
sock.settimeout(self.timeout)
sock.exec_command('docker system dial-stdio')
sock.exec_command("docker system dial-stdio")
else:
sock = SSHSocket(self.ssh_host)
sock.settimeout(self.timeout)
@@ -134,11 +136,11 @@ class SSHConnection(urllib3_connection.HTTPConnection, object):
class SSHConnectionPool(urllib3.connectionpool.HTTPConnectionPool):
scheme = 'ssh'
scheme = "ssh"
def __init__(self, ssh_client=None, timeout=60, maxsize=10, host=None):
super(SSHConnectionPool, self).__init__(
'localhost', timeout=timeout, maxsize=maxsize
"localhost", timeout=timeout, maxsize=maxsize
)
self.ssh_transport = None
self.timeout = timeout
@@ -164,8 +166,7 @@ class SSHConnectionPool(urllib3.connectionpool.HTTPConnectionPool):
if self.block:
raise urllib3.exceptions.EmptyPoolError(
self,
"Pool reached maximum size and no more "
"connections are allowed."
"Pool reached maximum size and no more " "connections are allowed.",
)
pass # Oh well, we'll create a new connection then
@@ -175,21 +176,29 @@ class SSHConnectionPool(urllib3.connectionpool.HTTPConnectionPool):
class SSHHTTPAdapter(BaseHTTPAdapter):
__attrs__ = HTTPAdapter.__attrs__ + [
'pools', 'timeout', 'ssh_client', 'ssh_params', 'max_pool_size'
"pools",
"timeout",
"ssh_client",
"ssh_params",
"max_pool_size",
]
def __init__(self, base_url, timeout=60,
pool_connections=constants.DEFAULT_NUM_POOLS,
max_pool_size=constants.DEFAULT_MAX_POOL_SIZE,
shell_out=False):
def __init__(
self,
base_url,
timeout=60,
pool_connections=constants.DEFAULT_NUM_POOLS,
max_pool_size=constants.DEFAULT_MAX_POOL_SIZE,
shell_out=False,
):
self.ssh_client = None
if not shell_out:
self._create_paramiko_client(base_url)
self._connect()
self.ssh_host = base_url
if base_url.startswith('ssh://'):
self.ssh_host = base_url[len('ssh://'):]
if base_url.startswith("ssh://"):
self.ssh_host = base_url[len("ssh://") :]
self.timeout = timeout
self.max_pool_size = max_pool_size
@@ -213,18 +222,18 @@ class SSHHTTPAdapter(BaseHTTPAdapter):
with open(ssh_config_file) as f:
conf.parse(f)
host_config = conf.lookup(base_url.hostname)
if 'proxycommand' in host_config:
if "proxycommand" in host_config:
self.ssh_params["sock"] = paramiko.ProxyCommand(
host_config['proxycommand']
host_config["proxycommand"]
)
if 'hostname' in host_config:
self.ssh_params['hostname'] = host_config['hostname']
if base_url.port is None and 'port' in host_config:
self.ssh_params['port'] = host_config['port']
if base_url.username is None and 'user' in host_config:
self.ssh_params['username'] = host_config['user']
if 'identityfile' in host_config:
self.ssh_params['key_filename'] = host_config['identityfile']
if "hostname" in host_config:
self.ssh_params["hostname"] = host_config["hostname"]
if base_url.port is None and "port" in host_config:
self.ssh_params["port"] = host_config["port"]
if base_url.username is None and "user" in host_config:
self.ssh_params["username"] = host_config["user"]
if "identityfile" in host_config:
self.ssh_params["key_filename"] = host_config["identityfile"]
self.ssh_client.load_system_host_keys()
self.ssh_client.set_missing_host_key_policy(paramiko.RejectPolicy())
@@ -239,7 +248,7 @@ class SSHHTTPAdapter(BaseHTTPAdapter):
ssh_client=self.ssh_client,
timeout=self.timeout,
maxsize=self.max_pool_size,
host=self.ssh_host
host=self.ssh_host,
)
with self.pools.lock:
pool = self.pools.get(url)
@@ -254,7 +263,7 @@ class SSHHTTPAdapter(BaseHTTPAdapter):
ssh_client=self.ssh_client,
timeout=self.timeout,
maxsize=self.max_pool_size,
host=self.ssh_host
host=self.ssh_host,
)
self.pools[url] = pool
@@ -9,12 +9,15 @@
from __future__ import annotations
""" Resolves OpenSSL issues in some servers:
https://lukasa.co.uk/2013/01/Choosing_SSL_Version_In_Requests/
https://github.com/kennethreitz/requests/pull/799
"""
from ansible_collections.community.docker.plugins.module_utils.version import LooseVersion
from ansible_collections.community.docker.plugins.module_utils.version import (
LooseVersion,
)
from .._import_helper import HTTPAdapter, urllib3
from .basehttpadapter import BaseHTTPAdapter
@@ -24,9 +27,9 @@ PoolManager = urllib3.poolmanager.PoolManager
class SSLHTTPAdapter(BaseHTTPAdapter):
'''An HTTPS Transport Adapter that uses an arbitrary SSL version.'''
"""An HTTPS Transport Adapter that uses an arbitrary SSL version."""
__attrs__ = HTTPAdapter.__attrs__ + ['assert_hostname', 'ssl_version']
__attrs__ = HTTPAdapter.__attrs__ + ["assert_hostname", "ssl_version"]
def __init__(self, ssl_version=None, assert_hostname=None, **kwargs):
self.ssl_version = ssl_version
@@ -35,14 +38,14 @@ class SSLHTTPAdapter(BaseHTTPAdapter):
def init_poolmanager(self, connections, maxsize, block=False):
kwargs = {
'num_pools': connections,
'maxsize': maxsize,
'block': block,
"num_pools": connections,
"maxsize": maxsize,
"block": block,
}
if self.assert_hostname is not None:
kwargs['assert_hostname'] = self.assert_hostname
kwargs["assert_hostname"] = self.assert_hostname
if self.ssl_version and self.can_override_ssl_version():
kwargs['ssl_version'] = self.ssl_version
kwargs["ssl_version"] = self.ssl_version
self.poolmanager = PoolManager(**kwargs)
@@ -55,14 +58,17 @@ class SSLHTTPAdapter(BaseHTTPAdapter):
But we still need to take care of when there is a proxy poolmanager
"""
conn = super(SSLHTTPAdapter, self).get_connection(*args, **kwargs)
if self.assert_hostname is not None and conn.assert_hostname != self.assert_hostname:
if (
self.assert_hostname is not None
and conn.assert_hostname != self.assert_hostname
):
conn.assert_hostname = self.assert_hostname
return conn
def can_override_ssl_version(self):
urllib_ver = urllib3.__version__.split('-')[0]
urllib_ver = urllib3.__version__.split("-")[0]
if urllib_ver is None:
return False
if urllib_ver == 'dev':
if urllib_ver == "dev":
return True
return LooseVersion(urllib_ver) > LooseVersion('1.5')
return LooseVersion(urllib_ver) > LooseVersion("1.5")
+22 -22
View File
@@ -11,10 +11,9 @@ from __future__ import annotations
import socket
from .basehttpadapter import BaseHTTPAdapter
from .. import constants
from .._import_helper import HTTPAdapter, urllib3, urllib3_connection
from .basehttpadapter import BaseHTTPAdapter
RecentlyUsedContainer = urllib3._collections.RecentlyUsedContainer
@@ -23,9 +22,7 @@ RecentlyUsedContainer = urllib3._collections.RecentlyUsedContainer
class UnixHTTPConnection(urllib3_connection.HTTPConnection, object):
def __init__(self, base_url, unix_socket, timeout=60):
super(UnixHTTPConnection, self).__init__(
'localhost', timeout=timeout
)
super(UnixHTTPConnection, self).__init__("localhost", timeout=timeout)
self.base_url = base_url
self.unix_socket = unix_socket
self.timeout = timeout
@@ -39,7 +36,7 @@ class UnixHTTPConnection(urllib3_connection.HTTPConnection, object):
def putheader(self, header, *values):
super(UnixHTTPConnection, self).putheader(header, *values)
if header == 'Connection' and 'Upgrade' in values:
if header == "Connection" and "Upgrade" in values:
self.disable_buffering = True
def response_class(self, sock, *args, **kwargs):
@@ -52,31 +49,35 @@ class UnixHTTPConnection(urllib3_connection.HTTPConnection, object):
class UnixHTTPConnectionPool(urllib3.connectionpool.HTTPConnectionPool):
def __init__(self, base_url, socket_path, timeout=60, maxsize=10):
super(UnixHTTPConnectionPool, self).__init__(
'localhost', timeout=timeout, maxsize=maxsize
"localhost", timeout=timeout, maxsize=maxsize
)
self.base_url = base_url
self.socket_path = socket_path
self.timeout = timeout
def _new_conn(self):
return UnixHTTPConnection(
self.base_url, self.socket_path, self.timeout
)
return UnixHTTPConnection(self.base_url, self.socket_path, self.timeout)
class UnixHTTPAdapter(BaseHTTPAdapter):
__attrs__ = HTTPAdapter.__attrs__ + ['pools',
'socket_path',
'timeout',
'max_pool_size']
__attrs__ = HTTPAdapter.__attrs__ + [
"pools",
"socket_path",
"timeout",
"max_pool_size",
]
def __init__(self, socket_url, timeout=60,
pool_connections=constants.DEFAULT_NUM_POOLS,
max_pool_size=constants.DEFAULT_MAX_POOL_SIZE):
socket_path = socket_url.replace('http+unix://', '')
if not socket_path.startswith('/'):
socket_path = '/' + socket_path
def __init__(
self,
socket_url,
timeout=60,
pool_connections=constants.DEFAULT_NUM_POOLS,
max_pool_size=constants.DEFAULT_MAX_POOL_SIZE,
):
socket_path = socket_url.replace("http+unix://", "")
if not socket_path.startswith("/"):
socket_path = "/" + socket_path
self.socket_path = socket_path
self.timeout = timeout
self.max_pool_size = max_pool_size
@@ -92,8 +93,7 @@ class UnixHTTPAdapter(BaseHTTPAdapter):
return pool
pool = UnixHTTPConnectionPool(
url, self.socket_path, self.timeout,
maxsize=self.max_pool_size
url, self.socket_path, self.timeout, maxsize=self.max_pool_size
)
self.pools[url] = pool
+8 -8
View File
@@ -12,7 +12,6 @@ from __future__ import annotations
import socket
from .._import_helper import urllib3
from ..errors import DockerException
@@ -56,26 +55,27 @@ class CancellableStream(object):
sock_fp = self._response.raw._fp.fp
if hasattr(sock_fp, 'raw'):
if hasattr(sock_fp, "raw"):
sock_raw = sock_fp.raw
if hasattr(sock_raw, 'sock'):
if hasattr(sock_raw, "sock"):
sock = sock_raw.sock
elif hasattr(sock_raw, '_sock'):
elif hasattr(sock_raw, "_sock"):
sock = sock_raw._sock
elif hasattr(sock_fp, 'channel'):
elif hasattr(sock_fp, "channel"):
# We are working with a paramiko (SSH) channel, which does not
# support cancelable streams with the current implementation
raise DockerException(
'Cancellable streams not supported for the SSH protocol'
"Cancellable streams not supported for the SSH protocol"
)
else:
sock = sock_fp._sock
if hasattr(urllib3.contrib, 'pyopenssl') and isinstance(
sock, urllib3.contrib.pyopenssl.WrappedSocket):
if hasattr(urllib3.contrib, "pyopenssl") and isinstance(
sock, urllib3.contrib.pyopenssl.WrappedSocket
):
sock = sock.socket
sock.shutdown(socket.SHUT_RDWR)
+39 -49
View File
@@ -16,11 +16,11 @@ import re
import tarfile
import tempfile
from . import fnmatch
from ..constants import IS_WINDOWS_PLATFORM, WINDOWS_LONGPATH_PREFIX
from . import fnmatch
_SEP = re.compile('/|\\\\') if IS_WINDOWS_PLATFORM else re.compile('/')
_SEP = re.compile("/|\\\\") if IS_WINDOWS_PLATFORM else re.compile("/")
def tar(path, exclude=None, dockerfile=None, fileobj=None, gzip=False):
@@ -29,16 +29,19 @@ def tar(path, exclude=None, dockerfile=None, fileobj=None, gzip=False):
dockerfile = dockerfile or (None, None)
extra_files = []
if dockerfile[1] is not None:
dockerignore_contents = '\n'.join(
(exclude or ['.dockerignore']) + [dockerfile[0]]
dockerignore_contents = "\n".join(
(exclude or [".dockerignore"]) + [dockerfile[0]]
)
extra_files = [
('.dockerignore', dockerignore_contents),
(".dockerignore", dockerignore_contents),
dockerfile,
]
return create_archive(
files=sorted(exclude_paths(root, exclude, dockerfile=dockerfile[0])),
root=root, fileobj=fileobj, gzip=gzip, extra_files=extra_files
root=root,
fileobj=fileobj,
gzip=gzip,
extra_files=extra_files,
)
@@ -52,9 +55,9 @@ def exclude_paths(root, patterns, dockerfile=None):
"""
if dockerfile is None:
dockerfile = 'Dockerfile'
dockerfile = "Dockerfile"
patterns.append('!' + dockerfile)
patterns.append("!" + dockerfile)
pm = PatternMatcher(patterns)
return set(pm.walk(root))
@@ -64,19 +67,16 @@ def build_file_list(root):
for dirname, dirnames, fnames in os.walk(root):
for filename in fnames + dirnames:
longpath = os.path.join(dirname, filename)
files.append(
longpath.replace(root, '', 1).lstrip('/')
)
files.append(longpath.replace(root, "", 1).lstrip("/"))
return files
def create_archive(root, files=None, fileobj=None, gzip=False,
extra_files=None):
def create_archive(root, files=None, fileobj=None, gzip=False, extra_files=None):
extra_files = extra_files or []
if not fileobj:
fileobj = tempfile.NamedTemporaryFile()
t = tarfile.open(mode='w:gz' if gzip else 'w', fileobj=fileobj)
t = tarfile.open(mode="w:gz" if gzip else "w", fileobj=fileobj)
if files is None:
files = build_file_list(root)
extra_names = set(e[0] for e in extra_files)
@@ -103,19 +103,17 @@ def create_archive(root, files=None, fileobj=None, gzip=False,
if i.isfile():
try:
with open(full_path, 'rb') as f:
with open(full_path, "rb") as f:
t.addfile(i, f)
except IOError:
raise IOError(
f'Can not read file in context: {full_path}'
)
raise IOError(f"Can not read file in context: {full_path}")
else:
# Directories, FIFOs, symlinks... do not need to be read.
t.addfile(i, None)
for name, contents in extra_files:
info = tarfile.TarInfo(name)
contents_encoded = contents.encode('utf-8')
contents_encoded = contents.encode("utf-8")
info.size = len(contents_encoded)
t.addfile(info, io.BytesIO(contents_encoded))
@@ -126,15 +124,15 @@ def create_archive(root, files=None, fileobj=None, gzip=False,
def mkbuildcontext(dockerfile):
f = tempfile.NamedTemporaryFile()
t = tarfile.open(mode='w', fileobj=f)
t = tarfile.open(mode="w", fileobj=f)
if isinstance(dockerfile, io.StringIO):
raise TypeError('Please use io.BytesIO to create in-memory Dockerfiles')
raise TypeError("Please use io.BytesIO to create in-memory Dockerfiles")
elif isinstance(dockerfile, io.BytesIO):
dfinfo = tarfile.TarInfo('Dockerfile')
dfinfo = tarfile.TarInfo("Dockerfile")
dfinfo.size = len(dockerfile.getvalue())
dockerfile.seek(0)
else:
dfinfo = t.gettarinfo(fileobj=dockerfile, arcname='Dockerfile')
dfinfo = t.gettarinfo(fileobj=dockerfile, arcname="Dockerfile")
t.addfile(dfinfo, dockerfile)
t.close()
f.seek(0)
@@ -142,12 +140,12 @@ def mkbuildcontext(dockerfile):
def split_path(p):
return [pt for pt in re.split(_SEP, p) if pt and pt != '.']
return [pt for pt in re.split(_SEP, p) if pt and pt != "."]
def normalize_slashes(p):
if IS_WINDOWS_PLATFORM:
return '/'.join(split_path(p))
return "/".join(split_path(p))
return p
@@ -160,10 +158,8 @@ def walk(root, patterns, default=True):
# https://github.com/moby/moby/blob/master/pkg/fileutils/fileutils.go
class PatternMatcher(object):
def __init__(self, patterns):
self.patterns = list(filter(
lambda p: p.dirs, [Pattern(p) for p in patterns]
))
self.patterns.append(Pattern('!.dockerignore'))
self.patterns = list(filter(lambda p: p.dirs, [Pattern(p) for p in patterns]))
self.patterns.append(Pattern("!.dockerignore"))
def matches(self, filepath):
matched = False
@@ -173,10 +169,10 @@ class PatternMatcher(object):
for pattern in self.patterns:
negative = pattern.exclusion
match = pattern.match(filepath)
if not match and parent_path != '':
if not match and parent_path != "":
if len(pattern.dirs) <= len(parent_path_dirs):
match = pattern.match(
os.path.sep.join(parent_path_dirs[:len(pattern.dirs)])
os.path.sep.join(parent_path_dirs[: len(pattern.dirs)])
)
if match:
@@ -187,10 +183,8 @@ class PatternMatcher(object):
def walk(self, root):
def rec_walk(current_dir):
for f in os.listdir(current_dir):
fpath = os.path.join(
os.path.relpath(current_dir, root), f
)
if fpath.startswith('.' + os.path.sep):
fpath = os.path.join(os.path.relpath(current_dir, root), f)
if fpath.startswith("." + os.path.sep):
fpath = fpath[2:]
match = self.matches(fpath)
if not match:
@@ -210,8 +204,7 @@ class PatternMatcher(object):
for pat in self.patterns:
if not pat.exclusion:
continue
if pat.cleaned_pattern.startswith(
normalize_slashes(fpath)):
if pat.cleaned_pattern.startswith(normalize_slashes(fpath)):
skip = False
break
if skip:
@@ -224,12 +217,12 @@ class PatternMatcher(object):
class Pattern(object):
def __init__(self, pattern_str):
self.exclusion = False
if pattern_str.startswith('!'):
if pattern_str.startswith("!"):
self.exclusion = True
pattern_str = pattern_str[1:]
self.dirs = self.normalize(pattern_str)
self.cleaned_pattern = '/'.join(self.dirs)
self.cleaned_pattern = "/".join(self.dirs)
@classmethod
def normalize(cls, p):
@@ -249,7 +242,7 @@ class Pattern(object):
i = 0
split = split_path(p)
while i < len(split):
if split[i] == '..':
if split[i] == "..":
del split[i]
if i > 0:
del split[i - 1]
@@ -269,17 +262,14 @@ def process_dockerfile(dockerfile, path):
abs_dockerfile = dockerfile
if not os.path.isabs(dockerfile):
abs_dockerfile = os.path.join(path, dockerfile)
if IS_WINDOWS_PLATFORM and path.startswith(
WINDOWS_LONGPATH_PREFIX):
abs_dockerfile = f'{WINDOWS_LONGPATH_PREFIX}{os.path.normpath(abs_dockerfile[len(WINDOWS_LONGPATH_PREFIX):])}'
if (os.path.splitdrive(path)[0] != os.path.splitdrive(abs_dockerfile)[0] or
os.path.relpath(abs_dockerfile, path).startswith('..')):
if IS_WINDOWS_PLATFORM and path.startswith(WINDOWS_LONGPATH_PREFIX):
abs_dockerfile = f"{WINDOWS_LONGPATH_PREFIX}{os.path.normpath(abs_dockerfile[len(WINDOWS_LONGPATH_PREFIX):])}"
if os.path.splitdrive(path)[0] != os.path.splitdrive(abs_dockerfile)[
0
] or os.path.relpath(abs_dockerfile, path).startswith(".."):
# Dockerfile not in context - read data to insert into tar later
with open(abs_dockerfile) as df:
return (
f'.dockerfile.{random.getrandbits(160):x}',
df.read()
)
return (f".dockerfile.{random.getrandbits(160):x}", df.read())
# Dockerfile is inside the context - return path relative to context root
if dockerfile == abs_dockerfile:
+17 -11
View File
@@ -15,8 +15,9 @@ import os
from ..constants import IS_WINDOWS_PLATFORM
DOCKER_CONFIG_FILENAME = os.path.join('.docker', 'config.json')
LEGACY_DOCKER_CONFIG_FILENAME = '.dockercfg'
DOCKER_CONFIG_FILENAME = os.path.join(".docker", "config.json")
LEGACY_DOCKER_CONFIG_FILENAME = ".dockercfg"
log = logging.getLogger(__name__)
@@ -27,12 +28,17 @@ def get_default_config_file():
def find_config_file(config_path=None):
homedir = home_dir()
paths = list(filter(None, [
config_path, # 1
config_path_from_environment(), # 2
os.path.join(homedir, DOCKER_CONFIG_FILENAME), # 3
os.path.join(homedir, LEGACY_DOCKER_CONFIG_FILENAME), # 4
]))
paths = list(
filter(
None,
[
config_path, # 1
config_path_from_environment(), # 2
os.path.join(homedir, DOCKER_CONFIG_FILENAME), # 3
os.path.join(homedir, LEGACY_DOCKER_CONFIG_FILENAME), # 4
],
)
)
log.debug("Trying paths: %s", repr(paths))
@@ -47,7 +53,7 @@ def find_config_file(config_path=None):
def config_path_from_environment():
config_dir = os.environ.get('DOCKER_CONFIG')
config_dir = os.environ.get("DOCKER_CONFIG")
if not config_dir:
return None
return os.path.join(config_dir, os.path.basename(DOCKER_CONFIG_FILENAME))
@@ -59,9 +65,9 @@ def home_dir():
client - use %USERPROFILE% on Windows, $HOME/getuid on POSIX.
"""
if IS_WINDOWS_PLATFORM:
return os.environ.get('USERPROFILE', '')
return os.environ.get("USERPROFILE", "")
else:
return os.path.expanduser('~')
return os.path.expanduser("~")
def load_general_config(config_path=None):
+12 -9
View File
@@ -22,13 +22,13 @@ def check_resource(resource_name):
if resource_id is None and kwargs.get(resource_name):
resource_id = kwargs.pop(resource_name)
if isinstance(resource_id, dict):
resource_id = resource_id.get('Id', resource_id.get('ID'))
resource_id = resource_id.get("Id", resource_id.get("ID"))
if not resource_id:
raise errors.NullResource(
'Resource ID was not provided'
)
raise errors.NullResource("Resource ID was not provided")
return f(self, resource_id, *args, **kwargs)
return wrapped
return decorator
@@ -38,19 +38,22 @@ def minimum_version(version):
def wrapper(self, *args, **kwargs):
if utils.version_lt(self._version, version):
raise errors.InvalidVersion(
f'{f.__name__} is not available for version < {version}'
f"{f.__name__} is not available for version < {version}"
)
return f(self, *args, **kwargs)
return wrapper
return decorator
def update_headers(f):
def inner(self, *args, **kwargs):
if 'HttpHeaders' in self._general_configs:
if not kwargs.get('headers'):
kwargs['headers'] = self._general_configs['HttpHeaders']
if "HttpHeaders" in self._general_configs:
if not kwargs.get("headers"):
kwargs["headers"] = self._general_configs["HttpHeaders"]
else:
kwargs['headers'].update(self._general_configs['HttpHeaders'])
kwargs["headers"].update(self._general_configs["HttpHeaders"])
return f(self, *args, **kwargs)
return inner
+23 -21
View File
@@ -9,6 +9,7 @@
from __future__ import annotations
"""Filename matching with shell patterns.
fnmatch(FILENAME, PATTERN) matches according to the local convention.
@@ -23,6 +24,7 @@ corresponding to PATTERN. (It does not compile it.)
import re
__all__ = ["fnmatch", "fnmatchcase", "translate"]
_cache = {}
@@ -77,50 +79,50 @@ def translate(pat):
There is no way to quote meta-characters.
"""
i, n = 0, len(pat)
res = '^'
res = "^"
while i < n:
c = pat[i]
i = i + 1
if c == '*':
if i < n and pat[i] == '*':
if c == "*":
if i < n and pat[i] == "*":
# is some flavor of "**"
i = i + 1
# Treat **/ as ** so eat the "/"
if i < n and pat[i] == '/':
if i < n and pat[i] == "/":
i = i + 1
if i >= n:
# is "**EOF" - to align with .gitignore just accept all
res = res + '.*'
res = res + ".*"
else:
# is "**"
# Note that this allows for any # of /'s (even 0) because
# the .* will eat everything, even /'s
res = res + '(.*/)?'
res = res + "(.*/)?"
else:
# is "*" so map it to anything but "/"
res = res + '[^/]*'
elif c == '?':
res = res + "[^/]*"
elif c == "?":
# "?" is any char except "/"
res = res + '[^/]'
elif c == '[':
res = res + "[^/]"
elif c == "[":
j = i
if j < n and pat[j] == '!':
if j < n and pat[j] == "!":
j = j + 1
if j < n and pat[j] == ']':
if j < n and pat[j] == "]":
j = j + 1
while j < n and pat[j] != ']':
while j < n and pat[j] != "]":
j = j + 1
if j >= n:
res = res + '\\['
res = res + "\\["
else:
stuff = pat[i:j].replace('\\', '\\\\')
stuff = pat[i:j].replace("\\", "\\\\")
i = j + 1
if stuff[0] == '!':
stuff = '^' + stuff[1:]
elif stuff[0] == '^':
stuff = '\\' + stuff
res = f'{res}[{stuff}]'
if stuff[0] == "!":
stuff = "^" + stuff[1:]
elif stuff[0] == "^":
stuff = "\\" + stuff
res = f"{res}[{stuff}]"
else:
res = res + re.escape(c)
return res + '$'
return res + "$"
@@ -27,7 +27,7 @@ def stream_as_text(stream):
"""
for data in stream:
if not isinstance(data, str):
data = data.decode('utf-8', 'replace')
data = data.decode("utf-8", "replace")
yield data
@@ -38,7 +38,7 @@ def json_splitter(buffer):
buffer = buffer.strip()
try:
obj, index = json_decoder.raw_decode(buffer)
rest = buffer[json.decoder.WHITESPACE.match(buffer, index).end():]
rest = buffer[json.decoder.WHITESPACE.match(buffer, index).end() :]
return obj, rest
except ValueError:
return None
@@ -52,11 +52,11 @@ def json_stream(stream):
return split_buffer(stream, json_splitter, json_decoder.decode)
def line_splitter(buffer, separator='\n'):
def line_splitter(buffer, separator="\n"):
index = buffer.find(str(separator))
if index == -1:
return None
return buffer[:index + 1], buffer[index + 1:]
return buffer[: index + 1], buffer[index + 1 :]
def split_buffer(stream, splitter=None, decoder=lambda a: a):
@@ -67,7 +67,7 @@ def split_buffer(stream, splitter=None, decoder=lambda a: a):
of the input.
"""
splitter = splitter or line_splitter
buffered = ''
buffered = ""
for data in stream_as_text(stream):
buffered += data
+16 -14
View File
@@ -11,6 +11,7 @@ from __future__ import annotations
import re
PORT_SPEC = re.compile(
"^" # Match full string
"(" # External part
@@ -49,23 +50,25 @@ def build_port_bindings(ports):
def _raise_invalid_port(port):
raise ValueError(f'Invalid port "{port}", should be '
'[[remote_ip:]remote_port[-remote_port]:]'
'port[/protocol]')
raise ValueError(
f'Invalid port "{port}", should be '
"[[remote_ip:]remote_port[-remote_port]:]"
"port[/protocol]"
)
def port_range(start, end, proto, randomly_available_port=False):
if not start:
return start
if not end:
return [f'{start}{proto}']
return [f"{start}{proto}"]
if randomly_available_port:
return [f'{start}-{end}{proto}']
return [f'{port}{proto}' for port in range(int(start), int(end) + 1)]
return [f"{start}-{end}{proto}"]
return [f"{port}{proto}" for port in range(int(start), int(end) + 1)]
def split_port(port):
if hasattr(port, 'legacy_repr'):
if hasattr(port, "legacy_repr"):
# This is the worst hack, but it prevents a bug in Compose 1.14.0
# https://github.com/docker/docker-py/issues/1668
# TODO: remove once fixed in Compose stable
@@ -76,19 +79,18 @@ def split_port(port):
_raise_invalid_port(port)
parts = match.groupdict()
host = parts['host']
proto = parts['proto'] or ''
internal = port_range(parts['int'], parts['int_end'], proto)
external = port_range(
parts['ext'], parts['ext_end'], '', len(internal) == 1)
host = parts["host"]
proto = parts["proto"] or ""
internal = port_range(parts["int"], parts["int_end"], proto)
external = port_range(parts["ext"], parts["ext_end"], "", len(internal) == 1)
if host is None:
if external is not None and len(internal) != len(external):
raise ValueError('Port ranges don\'t match in length')
raise ValueError("Port ranges don't match in length")
return internal, external
else:
if not external:
external = [None] * len(internal)
elif len(internal) != len(external):
raise ValueError('Port ranges don\'t match in length')
raise ValueError("Port ranges don't match in length")
return internal, [(host, ext_port) for ext_port in external]
+22 -21
View File
@@ -13,62 +13,63 @@ from .utils import format_environment
class ProxyConfig(dict):
'''
"""
Hold the client's proxy configuration
'''
"""
@property
def http(self):
return self.get('http')
return self.get("http")
@property
def https(self):
return self.get('https')
return self.get("https")
@property
def ftp(self):
return self.get('ftp')
return self.get("ftp")
@property
def no_proxy(self):
return self.get('no_proxy')
return self.get("no_proxy")
@staticmethod
def from_dict(config):
'''
"""
Instantiate a new ProxyConfig from a dictionary that represents a
client configuration, as described in `the documentation`_.
.. _the documentation:
https://docs.docker.com/network/proxy/#configure-the-docker-client
'''
"""
return ProxyConfig(
http=config.get('httpProxy'),
https=config.get('httpsProxy'),
ftp=config.get('ftpProxy'),
no_proxy=config.get('noProxy'),
http=config.get("httpProxy"),
https=config.get("httpsProxy"),
ftp=config.get("ftpProxy"),
no_proxy=config.get("noProxy"),
)
def get_environment(self):
'''
"""
Return a dictionary representing the environment variables used to
set the proxy settings.
'''
"""
env = {}
if self.http:
env['http_proxy'] = env['HTTP_PROXY'] = self.http
env["http_proxy"] = env["HTTP_PROXY"] = self.http
if self.https:
env['https_proxy'] = env['HTTPS_PROXY'] = self.https
env["https_proxy"] = env["HTTPS_PROXY"] = self.https
if self.ftp:
env['ftp_proxy'] = env['FTP_PROXY'] = self.ftp
env["ftp_proxy"] = env["FTP_PROXY"] = self.ftp
if self.no_proxy:
env['no_proxy'] = env['NO_PROXY'] = self.no_proxy
env["no_proxy"] = env["NO_PROXY"] = self.no_proxy
return env
def inject_proxy_environment(self, environment):
'''
"""
Given a list of strings representing environment variables, prepend the
environment variables corresponding to the proxy settings.
'''
"""
if not self:
return environment
@@ -80,4 +81,4 @@ class ProxyConfig(dict):
return proxy_env + environment
def __str__(self):
return f'ProxyConfig(http={self.http}, https={self.https}, ftp={self.ftp}, no_proxy={self.no_proxy})'
return f"ProxyConfig(http={self.http}, https={self.https}, ftp={self.ftp}, no_proxy={self.no_proxy})"
+13 -11
View File
@@ -48,22 +48,24 @@ def read(socket, n=4096):
poll.poll()
try:
if hasattr(socket, 'recv'):
if hasattr(socket, "recv"):
return socket.recv(n)
if isinstance(socket, getattr(pysocket, 'SocketIO')):
if isinstance(socket, getattr(pysocket, "SocketIO")):
return socket.read(n)
return os.read(socket.fileno(), n)
except EnvironmentError as e:
if e.errno not in recoverable_errors:
raise
except Exception as e:
is_pipe_ended = (isinstance(socket, NpipeSocket) and
len(e.args) > 0 and
e.args[0] == NPIPE_ENDED)
is_pipe_ended = (
isinstance(socket, NpipeSocket)
and len(e.args) > 0
and e.args[0] == NPIPE_ENDED
)
if is_pipe_ended:
# npipes do not support duplex sockets, so we interpret
# a PIPE_ENDED error as a close operation (0-length read).
return ''
return ""
raise
@@ -72,7 +74,7 @@ def read_exactly(socket, n):
Reads exactly n bytes from socket
Raises SocketError if there is not enough data
"""
data = b''
data = b""
while len(data) < n:
next_data = read(socket, n - len(data))
if not next_data:
@@ -93,7 +95,7 @@ def next_frame_header(socket):
except SocketError:
return (-1, -1)
stream, actual = struct.unpack('>BxxxL', data)
stream, actual = struct.unpack(">BxxxL", data)
return (stream, actual)
@@ -160,7 +162,7 @@ def consume_socket_output(frames, demux=False):
if demux is False:
# If the streams are multiplexed, the generator returns strings, that
# we just need to concatenate.
return b''.join(frames)
return b"".join(frames)
# If the streams are demultiplexed, the generator yields tuples
# (stdout, stderr)
@@ -169,7 +171,7 @@ def consume_socket_output(frames, demux=False):
# It is guaranteed that for each frame, one and only one stream
# is not None.
if frame == (None, None):
raise AssertionError(f'frame must be (None, None), but got {frame}')
raise AssertionError(f"frame must be (None, None), but got {frame}")
if frame[0] is not None:
if out[0] is None:
out[0] = frame[0]
@@ -193,4 +195,4 @@ def demux_adaptor(stream_id, data):
elif stream_id == STDERR:
return (None, data)
else:
raise ValueError(f'{stream_id} is not a valid stream')
raise ValueError(f"{stream_id} is not a valid stream")
+147 -150
View File
@@ -16,40 +16,44 @@ import os
import os.path
import shlex
import string
from ansible_collections.community.docker.plugins.module_utils.version import StrictVersion
from urllib.parse import urlparse, urlunparse
from ansible_collections.community.docker.plugins.module_utils.version import (
StrictVersion,
)
from .. import errors
from ..constants import DEFAULT_HTTP_HOST
from ..constants import DEFAULT_UNIX_SOCKET
from ..constants import DEFAULT_NPIPE
from ..constants import BYTE_UNITS
from ..constants import (
BYTE_UNITS,
DEFAULT_HTTP_HOST,
DEFAULT_NPIPE,
DEFAULT_UNIX_SOCKET,
)
from ..tls import TLSConfig
from urllib.parse import urlparse, urlunparse
URLComponents = collections.namedtuple(
'URLComponents',
'scheme netloc url params query fragment',
"URLComponents",
"scheme netloc url params query fragment",
)
def create_ipam_pool(*args, **kwargs):
raise errors.DeprecatedMethod(
'utils.create_ipam_pool has been removed. Please use a '
'docker.types.IPAMPool object instead.'
"utils.create_ipam_pool has been removed. Please use a "
"docker.types.IPAMPool object instead."
)
def create_ipam_config(*args, **kwargs):
raise errors.DeprecatedMethod(
'utils.create_ipam_config has been removed. Please use a '
'docker.types.IPAMConfig object instead.'
"utils.create_ipam_config has been removed. Please use a "
"docker.types.IPAMConfig object instead."
)
def decode_json_header(header):
data = base64.b64decode(header).decode('utf-8')
data = base64.b64decode(header).decode("utf-8")
return json.loads(data)
@@ -84,29 +88,29 @@ def version_gte(v1, v2):
def _convert_port_binding(binding):
result = {'HostIp': '', 'HostPort': ''}
result = {"HostIp": "", "HostPort": ""}
if isinstance(binding, tuple):
if len(binding) == 2:
result['HostPort'] = binding[1]
result['HostIp'] = binding[0]
result["HostPort"] = binding[1]
result["HostIp"] = binding[0]
elif isinstance(binding[0], str):
result['HostIp'] = binding[0]
result["HostIp"] = binding[0]
else:
result['HostPort'] = binding[0]
result["HostPort"] = binding[0]
elif isinstance(binding, dict):
if 'HostPort' in binding:
result['HostPort'] = binding['HostPort']
if 'HostIp' in binding:
result['HostIp'] = binding['HostIp']
if "HostPort" in binding:
result["HostPort"] = binding["HostPort"]
if "HostIp" in binding:
result["HostIp"] = binding["HostIp"]
else:
raise ValueError(binding)
else:
result['HostPort'] = binding
result["HostPort"] = binding
if result['HostPort'] is None:
result['HostPort'] = ''
if result["HostPort"] is None:
result["HostPort"] = ""
else:
result['HostPort'] = str(result['HostPort'])
result["HostPort"] = str(result["HostPort"])
return result
@@ -115,8 +119,8 @@ def convert_port_bindings(port_bindings):
result = {}
for k, v in port_bindings.items():
key = str(k)
if '/' not in key:
key += '/tcp'
if "/" not in key:
key += "/tcp"
if isinstance(v, list):
result[key] = [_convert_port_binding(binding) for binding in v]
else:
@@ -131,46 +135,44 @@ def convert_volume_binds(binds):
result = []
for k, v in binds.items():
if isinstance(k, bytes):
k = k.decode('utf-8')
k = k.decode("utf-8")
if isinstance(v, dict):
if 'ro' in v and 'mode' in v:
raise ValueError(
f'Binding cannot contain both "ro" and "mode": {v!r}'
)
if "ro" in v and "mode" in v:
raise ValueError(f'Binding cannot contain both "ro" and "mode": {v!r}')
bind = v['bind']
bind = v["bind"]
if isinstance(bind, bytes):
bind = bind.decode('utf-8')
bind = bind.decode("utf-8")
if 'ro' in v:
mode = 'ro' if v['ro'] else 'rw'
elif 'mode' in v:
mode = v['mode']
if "ro" in v:
mode = "ro" if v["ro"] else "rw"
elif "mode" in v:
mode = v["mode"]
else:
mode = 'rw'
mode = "rw"
# NOTE: this is only relevant for Linux hosts
# (does not apply in Docker Desktop)
propagation_modes = [
'rshared',
'shared',
'rslave',
'slave',
'rprivate',
'private',
"rshared",
"shared",
"rslave",
"slave",
"rprivate",
"private",
]
if 'propagation' in v and v['propagation'] in propagation_modes:
if "propagation" in v and v["propagation"] in propagation_modes:
if mode:
mode = ','.join([mode, v['propagation']])
mode = ",".join([mode, v["propagation"]])
else:
mode = v['propagation']
mode = v["propagation"]
result.append(f'{k}:{bind}:{mode}')
result.append(f"{k}:{bind}:{mode}")
else:
if isinstance(v, bytes):
v = v.decode('utf-8')
result.append(f'{k}:{v}:rw')
v = v.decode("utf-8")
result.append(f"{k}:{v}:rw")
return result
@@ -180,7 +182,7 @@ def convert_tmpfs_mounts(tmpfs):
if not isinstance(tmpfs, list):
raise ValueError(
f'Expected tmpfs value to be either a list or a dict, found: {type(tmpfs).__name__}'
f"Expected tmpfs value to be either a list or a dict, found: {type(tmpfs).__name__}"
)
result = {}
@@ -205,22 +207,22 @@ def convert_service_networks(networks):
if not networks:
return networks
if not isinstance(networks, list):
raise TypeError('networks parameter must be a list.')
raise TypeError("networks parameter must be a list.")
result = []
for n in networks:
if isinstance(n, str):
n = {'Target': n}
n = {"Target": n}
result.append(n)
return result
def parse_repository_tag(repo_name):
parts = repo_name.rsplit('@', 1)
parts = repo_name.rsplit("@", 1)
if len(parts) == 2:
return tuple(parts)
parts = repo_name.rsplit(':', 1)
if len(parts) == 2 and '/' not in parts[1]:
parts = repo_name.rsplit(":", 1)
if len(parts) == 2 and "/" not in parts[1]:
return tuple(parts)
return repo_name, None
@@ -229,86 +231,81 @@ def parse_host(addr, is_win32=False, tls=False):
# Sensible defaults
if not addr and is_win32:
return DEFAULT_NPIPE
if not addr or addr.strip() == 'unix://':
if not addr or addr.strip() == "unix://":
return DEFAULT_UNIX_SOCKET
addr = addr.strip()
parsed_url = urlparse(addr)
proto = parsed_url.scheme
if not proto or any(x not in string.ascii_letters + '+' for x in proto):
if not proto or any(x not in string.ascii_letters + "+" for x in proto):
# https://bugs.python.org/issue754016
parsed_url = urlparse('//' + addr, 'tcp')
proto = 'tcp'
parsed_url = urlparse("//" + addr, "tcp")
proto = "tcp"
if proto == 'fd':
raise errors.DockerException('fd protocol is not implemented')
if proto == "fd":
raise errors.DockerException("fd protocol is not implemented")
# These protos are valid aliases for our library but not for the
# official spec
if proto == 'http' or proto == 'https':
tls = proto == 'https'
proto = 'tcp'
elif proto == 'http+unix':
proto = 'unix'
if proto == "http" or proto == "https":
tls = proto == "https"
proto = "tcp"
elif proto == "http+unix":
proto = "unix"
if proto not in ('tcp', 'unix', 'npipe', 'ssh'):
raise errors.DockerException(
f"Invalid bind address protocol: {addr}"
)
if proto not in ("tcp", "unix", "npipe", "ssh"):
raise errors.DockerException(f"Invalid bind address protocol: {addr}")
if proto == 'tcp' and not parsed_url.netloc:
if proto == "tcp" and not parsed_url.netloc:
# "tcp://" is exceptionally disallowed by convention;
# omitting a hostname for other protocols is fine
raise errors.DockerException(
f'Invalid bind address format: {addr}'
)
raise errors.DockerException(f"Invalid bind address format: {addr}")
if any([
parsed_url.params, parsed_url.query, parsed_url.fragment,
parsed_url.password
]):
raise errors.DockerException(
f'Invalid bind address format: {addr}'
)
if any(
[parsed_url.params, parsed_url.query, parsed_url.fragment, parsed_url.password]
):
raise errors.DockerException(f"Invalid bind address format: {addr}")
if parsed_url.path and proto == 'ssh':
if parsed_url.path and proto == "ssh":
raise errors.DockerException(
f'Invalid bind address format: no path allowed for this protocol: {addr}'
f"Invalid bind address format: no path allowed for this protocol: {addr}"
)
else:
path = parsed_url.path
if proto == 'unix' and parsed_url.hostname is not None:
if proto == "unix" and parsed_url.hostname is not None:
# For legacy reasons, we consider unix://path
# to be valid and equivalent to unix:///path
path = '/'.join((parsed_url.hostname, path))
path = "/".join((parsed_url.hostname, path))
netloc = parsed_url.netloc
if proto in ('tcp', 'ssh'):
if proto in ("tcp", "ssh"):
port = parsed_url.port or 0
if port <= 0:
port = 22 if proto == 'ssh' else (2375 if tls else 2376)
netloc = f'{parsed_url.netloc}:{port}'
port = 22 if proto == "ssh" else (2375 if tls else 2376)
netloc = f"{parsed_url.netloc}:{port}"
if not parsed_url.hostname:
netloc = f'{DEFAULT_HTTP_HOST}:{port}'
netloc = f"{DEFAULT_HTTP_HOST}:{port}"
# Rewrite schemes to fit library internals (requests adapters)
if proto == 'tcp':
if proto == "tcp":
proto = f"http{'s' if tls else ''}"
elif proto == 'unix':
proto = 'http+unix'
elif proto == "unix":
proto = "http+unix"
if proto in ('http+unix', 'npipe'):
return f"{proto}://{path}".rstrip('/')
return urlunparse(URLComponents(
scheme=proto,
netloc=netloc,
url=path,
params='',
query='',
fragment='',
)).rstrip('/')
if proto in ("http+unix", "npipe"):
return f"{proto}://{path}".rstrip("/")
return urlunparse(
URLComponents(
scheme=proto,
netloc=netloc,
url=path,
params="",
query="",
fragment="",
)
).rstrip("/")
def parse_devices(devices):
@@ -318,10 +315,8 @@ def parse_devices(devices):
device_list.append(device)
continue
if not isinstance(device, str):
raise errors.DockerException(
f'Invalid device type {type(device)}'
)
device_mapping = device.split(':')
raise errors.DockerException(f"Invalid device type {type(device)}")
device_mapping = device.split(":")
if device_mapping:
path_on_host = device_mapping[0]
if len(device_mapping) > 1:
@@ -331,27 +326,29 @@ def parse_devices(devices):
if len(device_mapping) > 2:
permissions = device_mapping[2]
else:
permissions = 'rwm'
device_list.append({
'PathOnHost': path_on_host,
'PathInContainer': path_in_container,
'CgroupPermissions': permissions
})
permissions = "rwm"
device_list.append(
{
"PathOnHost": path_on_host,
"PathInContainer": path_in_container,
"CgroupPermissions": permissions,
}
)
return device_list
def kwargs_from_env(ssl_version=None, assert_hostname=None, environment=None):
if not environment:
environment = os.environ
host = environment.get('DOCKER_HOST')
host = environment.get("DOCKER_HOST")
# empty string for cert path is the same as unset.
cert_path = environment.get('DOCKER_CERT_PATH') or None
cert_path = environment.get("DOCKER_CERT_PATH") or None
# empty string for tls verify counts as "false".
# Any value or 'unset' counts as true.
tls_verify = environment.get('DOCKER_TLS_VERIFY')
if tls_verify == '':
tls_verify = environment.get("DOCKER_TLS_VERIFY")
if tls_verify == "":
tls_verify = False
else:
tls_verify = tls_verify is not None
@@ -360,23 +357,25 @@ def kwargs_from_env(ssl_version=None, assert_hostname=None, environment=None):
params = {}
if host:
params['base_url'] = host
params["base_url"] = host
if not enable_tls:
return params
if not cert_path:
cert_path = os.path.join(os.path.expanduser('~'), '.docker')
cert_path = os.path.join(os.path.expanduser("~"), ".docker")
if not tls_verify and assert_hostname is None:
# assert_hostname is a subset of TLS verification,
# so if it is not set already then set it to false.
assert_hostname = False
params['tls'] = TLSConfig(
client_cert=(os.path.join(cert_path, 'cert.pem'),
os.path.join(cert_path, 'key.pem')),
ca_cert=os.path.join(cert_path, 'ca.pem'),
params["tls"] = TLSConfig(
client_cert=(
os.path.join(cert_path, "cert.pem"),
os.path.join(cert_path, "key.pem"),
),
ca_cert=os.path.join(cert_path, "ca.pem"),
verify=tls_verify,
ssl_version=ssl_version,
assert_hostname=assert_hostname,
@@ -389,13 +388,12 @@ def convert_filters(filters):
result = {}
for k, v in filters.items():
if isinstance(v, bool):
v = 'true' if v else 'false'
v = "true" if v else "false"
if not isinstance(v, list):
v = [v, ]
result[k] = [
str(item) if not isinstance(item, str) else item
for item in v
]
v = [
v,
]
result[k] = [str(item) if not isinstance(item, str) else item for item in v]
return json.dumps(result)
@@ -415,7 +413,7 @@ def parse_bytes(s):
# without a units part. Assuming that the units are bytes.
if suffix.isdigit():
digits_part = s
suffix = 'b'
suffix = "b"
else:
digits_part = s[:-1]
@@ -424,14 +422,14 @@ def parse_bytes(s):
digits = float(digits_part)
except ValueError:
raise errors.DockerException(
f'Failed converting the string value for memory ({digits_part}) to an integer.'
f"Failed converting the string value for memory ({digits_part}) to an integer."
)
# Reconvert to long for the final result
s = int(digits * units[suffix])
else:
raise errors.DockerException(
f'The specified value for memory ({s}) should specify the units. The postfix should be one of the `b` `k` `m` `g` characters'
f"The specified value for memory ({s}) should specify the units. The postfix should be one of the `b` `k` `m` `g` characters"
)
return s
@@ -441,7 +439,7 @@ def normalize_links(links):
if isinstance(links, dict):
links = links.items()
return [f'{k}:{v}' if v else k for k, v in sorted(links)]
return [f"{k}:{v}" if v else k for k, v in sorted(links)]
def parse_env_file(env_file):
@@ -451,22 +449,24 @@ def parse_env_file(env_file):
"""
environment = {}
with open(env_file, 'r') as f:
with open(env_file, "r") as f:
for line in f:
if line[0] == '#':
if line[0] == "#":
continue
line = line.strip()
if not line:
continue
parse_line = line.split('=', 1)
parse_line = line.split("=", 1)
if len(parse_line) == 2:
k, v = parse_line
environment[k] = v
else:
raise errors.DockerException(f'Invalid line in environment file {env_file}:\n{line}')
raise errors.DockerException(
f"Invalid line in environment file {env_file}:\n{line}"
)
return environment
@@ -480,26 +480,23 @@ def format_environment(environment):
if value is None:
return key
if isinstance(value, bytes):
value = value.decode('utf-8')
value = value.decode("utf-8")
return f"{key}={value}"
return f'{key}={value}'
return [format_env(*var) for var in environment.items()]
def format_extra_hosts(extra_hosts, task=False):
# Use format dictated by Swarm API if container is part of a task
if task:
return [
f'{v} {k}' for k, v in sorted(extra_hosts.items())
]
return [f"{v} {k}" for k, v in sorted(extra_hosts.items())]
return [
f'{k}:{v}' for k, v in sorted(extra_hosts.items())
]
return [f"{k}:{v}" for k, v in sorted(extra_hosts.items())]
def create_host_config(self, *args, **kwargs):
raise errors.DeprecatedMethod(
'utils.create_host_config has been removed. Please use a '
'docker.types.HostConfig object instead.'
"utils.create_host_config has been removed. Please use a "
"docker.types.HostConfig object instead."
)
+46 -44
View File
@@ -29,44 +29,44 @@ class _Mode(object):
_ESCAPE_DICT = {
'"': '"',
'\\': '\\',
"\\": "\\",
"'": "'",
'/': '/',
'b': '\b',
'f': '\f',
'n': '\n',
'r': '\r',
't': '\t',
"/": "/",
"b": "\b",
"f": "\f",
"n": "\n",
"r": "\r",
"t": "\t",
}
_HEX_DICT = {
'0': 0,
'1': 1,
'2': 2,
'3': 3,
'4': 4,
'5': 5,
'6': 6,
'7': 7,
'8': 8,
'9': 9,
'a': 0xA,
'b': 0xB,
'c': 0xC,
'd': 0xD,
'e': 0xE,
'f': 0xF,
'A': 0xA,
'B': 0xB,
'C': 0xC,
'D': 0xD,
'E': 0xE,
'F': 0xF,
"0": 0,
"1": 1,
"2": 2,
"3": 3,
"4": 4,
"5": 5,
"6": 6,
"7": 7,
"8": 8,
"9": 9,
"a": 0xA,
"b": 0xB,
"c": 0xC,
"d": 0xD,
"e": 0xE,
"f": 0xF,
"A": 0xA,
"B": 0xB,
"C": 0xC,
"D": 0xD,
"E": 0xE,
"F": 0xF,
}
def _is_ident(cur):
return cur > ' ' and cur not in ('"', '=')
return cur > " " and cur not in ('"', "=")
class _Parser(object):
@@ -89,16 +89,18 @@ class _Parser(object):
def parse_unicode_sequence(self):
if self.index + 6 > self.length:
raise InvalidLogFmt('Not enough space for unicode escape')
if self.line[self.index:self.index + 2] != '\\u':
raise InvalidLogFmt('Invalid unicode escape start')
raise InvalidLogFmt("Not enough space for unicode escape")
if self.line[self.index : self.index + 2] != "\\u":
raise InvalidLogFmt("Invalid unicode escape start")
v = 0
for i in range(self.index + 2, self.index + 6):
v <<= 4
try:
v += _HEX_DICT[self.line[self.index]]
except KeyError:
raise InvalidLogFmt(f'Invalid unicode escape digit {self.line[self.index]!r}')
raise InvalidLogFmt(
f"Invalid unicode escape digit {self.line[self.index]!r}"
)
self.index += 6
return chr(v)
@@ -111,8 +113,8 @@ def parse_line(line, logrus_mode=False):
mode = _Mode.GARBAGE
def handle_kv(has_no_value=False):
k = ''.join(key)
v = None if has_no_value else ''.join(value)
k = "".join(key)
v = None if has_no_value else "".join(value)
result[k] = v
del key[:]
del value[:]
@@ -128,7 +130,7 @@ def parse_line(line, logrus_mode=False):
key.append(cur)
parser.next()
return _Mode.KEY
elif cur == '=':
elif cur == "=":
parser.next()
return _Mode.EQUAL
else:
@@ -162,16 +164,16 @@ def parse_line(line, logrus_mode=False):
return _Mode.GARBAGE
def parse_quoted_value(cur):
if cur == '\\':
if cur == "\\":
parser.next()
if parser.done():
raise InvalidLogFmt('Unterminated escape sequence in quoted string')
raise InvalidLogFmt("Unterminated escape sequence in quoted string")
cur = parser.cur()
if cur in _ESCAPE_DICT:
value.append(_ESCAPE_DICT[cur])
elif cur != 'u':
elif cur != "u":
es = f"\\{cur}"
raise InvalidLogFmt(f'Unknown escape sequence {es!r}')
raise InvalidLogFmt(f"Unknown escape sequence {es!r}")
else:
parser.prev()
value.append(parser.parse_unicode_sequence())
@@ -181,8 +183,8 @@ def parse_line(line, logrus_mode=False):
handle_kv()
parser.next()
return _Mode.GARBAGE
elif cur < ' ':
raise InvalidLogFmt('Control characters in quoted string are not allowed')
elif cur < " ":
raise InvalidLogFmt("Control characters in quoted string are not allowed")
else:
value.append(cur)
parser.next()
@@ -204,5 +206,5 @@ def parse_line(line, logrus_mode=False):
elif mode == _Mode.IDENT_VALUE:
handle_kv()
elif mode == _Mode.QUOTED_VALUE:
raise InvalidLogFmt('Unterminated quoted string')
raise InvalidLogFmt("Unterminated quoted string")
return result
+92 -32
View File
@@ -13,36 +13,74 @@ from __future__ import annotations
import re
_VALID_STR = re.compile('^[A-Za-z0-9_-]+$')
_VALID_STR = re.compile("^[A-Za-z0-9_-]+$")
def _validate_part(string, part, part_name):
if not part:
raise ValueError(f'Invalid platform string "{string}": {part_name} is empty')
if not _VALID_STR.match(part):
raise ValueError(f'Invalid platform string "{string}": {part_name} has invalid characters')
raise ValueError(
f'Invalid platform string "{string}": {part_name} has invalid characters'
)
return part
# See https://github.com/containerd/containerd/blob/main/platforms/database.go#L32-L38
_KNOWN_OS = (
"aix", "android", "darwin", "dragonfly", "freebsd", "hurd", "illumos", "ios", "js",
"linux", "nacl", "netbsd", "openbsd", "plan9", "solaris", "windows", "zos",
"aix",
"android",
"darwin",
"dragonfly",
"freebsd",
"hurd",
"illumos",
"ios",
"js",
"linux",
"nacl",
"netbsd",
"openbsd",
"plan9",
"solaris",
"windows",
"zos",
)
# See https://github.com/containerd/containerd/blob/main/platforms/database.go#L54-L60
_KNOWN_ARCH = (
"386", "amd64", "amd64p32", "arm", "armbe", "arm64", "arm64be", "ppc64", "ppc64le",
"loong64", "mips", "mipsle", "mips64", "mips64le", "mips64p32", "mips64p32le",
"ppc", "riscv", "riscv64", "s390", "s390x", "sparc", "sparc64", "wasm",
"386",
"amd64",
"amd64p32",
"arm",
"armbe",
"arm64",
"arm64be",
"ppc64",
"ppc64le",
"loong64",
"mips",
"mipsle",
"mips64",
"mips64le",
"mips64p32",
"mips64p32le",
"ppc",
"riscv",
"riscv64",
"s390",
"s390x",
"sparc",
"sparc64",
"wasm",
)
def _normalize_os(os_str):
# See normalizeOS() in https://github.com/containerd/containerd/blob/main/platforms/database.go
os_str = os_str.lower()
if os_str == 'macos':
os_str = 'darwin'
if os_str == "macos":
os_str = "darwin"
return os_str
@@ -94,9 +132,9 @@ class _Platform(object):
self.variant = variant
if variant is not None:
if arch is None:
raise ValueError('If variant is given, architecture must be given too')
raise ValueError("If variant is given, architecture must be given too")
if os is None:
raise ValueError('If variant is given, os must be given too')
raise ValueError("If variant is given, os must be given too")
@classmethod
def parse_platform_string(cls, string, daemon_os=None, daemon_arch=None):
@@ -104,40 +142,48 @@ class _Platform(object):
if string is None:
return cls()
if not string:
raise ValueError('Platform string must be non-empty')
parts = string.split('/', 2)
raise ValueError("Platform string must be non-empty")
parts = string.split("/", 2)
arch = None
variant = None
if len(parts) == 1:
_validate_part(string, string, 'OS/architecture')
_validate_part(string, string, "OS/architecture")
# The part is either OS or architecture
os = _normalize_os(string)
if os in _KNOWN_OS:
if daemon_arch is not None:
arch, variant = _normalize_arch(daemon_arch, '')
arch, variant = _normalize_arch(daemon_arch, "")
return cls(os=os, arch=arch, variant=variant)
arch, variant = _normalize_arch(os, '')
arch, variant = _normalize_arch(os, "")
if arch in _KNOWN_ARCH:
return cls(
os=_normalize_os(daemon_os) if daemon_os else None,
arch=arch or None,
variant=variant or None,
)
raise ValueError(f'Invalid platform string "{string}": unknown OS or architecture')
os = _validate_part(string, parts[0], 'OS')
raise ValueError(
f'Invalid platform string "{string}": unknown OS or architecture'
)
os = _validate_part(string, parts[0], "OS")
if not os:
raise ValueError(f'Invalid platform string "{string}": OS is empty')
arch = _validate_part(string, parts[1], 'architecture') if len(parts) > 1 else None
arch = (
_validate_part(string, parts[1], "architecture") if len(parts) > 1 else None
)
if arch is not None and not arch:
raise ValueError(f'Invalid platform string "{string}": architecture is empty')
variant = _validate_part(string, parts[2], 'variant') if len(parts) > 2 else None
raise ValueError(
f'Invalid platform string "{string}": architecture is empty'
)
variant = (
_validate_part(string, parts[2], "variant") if len(parts) > 2 else None
)
if variant is not None and not variant:
raise ValueError(f'Invalid platform string "{string}": variant is empty')
arch, variant = _normalize_arch(arch, variant or '')
if len(parts) == 2 and arch == 'arm' and variant == 'v7':
arch, variant = _normalize_arch(arch, variant or "")
if len(parts) == 2 and arch == "arm" and variant == "v7":
variant = None
if len(parts) == 3 and arch == 'arm64' and variant == '':
variant = 'v8'
if len(parts) == 3 and arch == "arm64" and variant == "":
variant = "v8"
return cls(os=_normalize_os(os), arch=arch, variant=variant or None)
def __str__(self):
@@ -152,27 +198,41 @@ class _Platform(object):
parts = [self.arch]
else:
parts = []
return '/'.join(parts)
return "/".join(parts)
def __repr__(self):
return f'_Platform(os={self.os!r}, arch={self.arch!r}, variant={self.variant!r})'
return (
f"_Platform(os={self.os!r}, arch={self.arch!r}, variant={self.variant!r})"
)
def __eq__(self, other):
return self.os == other.os and self.arch == other.arch and self.variant == other.variant
return (
self.os == other.os
and self.arch == other.arch
and self.variant == other.variant
)
def normalize_platform_string(string, daemon_os=None, daemon_arch=None):
return str(_Platform.parse_platform_string(string, daemon_os=daemon_os, daemon_arch=daemon_arch))
return str(
_Platform.parse_platform_string(
string, daemon_os=daemon_os, daemon_arch=daemon_arch
)
)
def compose_platform_string(os=None, arch=None, variant=None, daemon_os=None, daemon_arch=None):
def compose_platform_string(
os=None, arch=None, variant=None, daemon_os=None, daemon_arch=None
):
if os is None and daemon_os is not None:
os = _normalize_os(daemon_os)
if arch is None and daemon_arch is not None:
arch, variant = _normalize_arch(daemon_arch, variant or '')
arch, variant = _normalize_arch(daemon_arch, variant or "")
variant = variant or None
return str(_Platform(os=os, arch=arch, variant=variant or None))
def compare_platform_strings(string1, string2):
return _Platform.parse_platform_string(string1) == _Platform.parse_platform_string(string2)
return _Platform.parse_platform_string(string1) == _Platform.parse_platform_string(
string2
)
+9 -9
View File
@@ -11,32 +11,32 @@ from ansible.module_utils.common.text.converters import to_bytes, to_native, to_
def generate_insecure_key():
'''Do NOT use this for cryptographic purposes!'''
"""Do NOT use this for cryptographic purposes!"""
while True:
# Generate a one-byte key. Right now the functions below do not use more
# than one byte, so this is sufficient.
key = bytes([random.randint(0, 255)])
# Return anything that is not zero
if key != b'\x00':
if key != b"\x00":
return key
def scramble(value, key):
'''Do NOT use this for cryptographic purposes!'''
"""Do NOT use this for cryptographic purposes!"""
if len(key) < 1:
raise ValueError('Key must be at least one byte')
raise ValueError("Key must be at least one byte")
value = to_bytes(value)
k = key[0]
value = bytes([k ^ b for b in value])
return '=S=' + to_native(base64.b64encode(value))
return "=S=" + to_native(base64.b64encode(value))
def unscramble(value, key):
'''Do NOT use this for cryptographic purposes!'''
"""Do NOT use this for cryptographic purposes!"""
if len(key) < 1:
raise ValueError('Key must be at least one byte')
if not value.startswith('=S='):
raise ValueError('Value does not start with indicator')
raise ValueError("Key must be at least one byte")
if not value.startswith("=S="):
raise ValueError("Value does not start with indicator")
value = base64.b64decode(value[3:])
k = key[0]
value = bytes([k ^ b for b in value])
+277 -158
View File
@@ -4,7 +4,6 @@
from __future__ import annotations
import abc
import os
import platform
@@ -14,9 +13,11 @@ import traceback
from collections.abc import Mapping, Sequence
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
from ansible.module_utils.parsing.convert_bool import BOOLEANS_TRUE, BOOLEANS_FALSE
from ansible.module_utils.parsing.convert_bool import BOOLEANS_FALSE, BOOLEANS_TRUE
from ansible_collections.community.docker.plugins.module_utils.version import (
LooseVersion,
)
from ansible_collections.community.docker.plugins.module_utils.version import LooseVersion
HAS_DOCKER_PY = True
HAS_DOCKER_PY_2 = False
@@ -26,15 +27,16 @@ HAS_DOCKER_TRACEBACK = None
try:
from requests.exceptions import SSLError
from docker import __version__ as docker_version
from docker import auth
from docker.errors import APIError, NotFound, TLSParameterError
from docker.tls import TLSConfig
from docker import auth
if LooseVersion(docker_version) >= LooseVersion('3.0.0'):
if LooseVersion(docker_version) >= LooseVersion("3.0.0"):
HAS_DOCKER_PY_3 = True
from docker import APIClient as Client
elif LooseVersion(docker_version) >= LooseVersion('2.0.0'):
elif LooseVersion(docker_version) >= LooseVersion("2.0.0"):
HAS_DOCKER_PY_2 = True
from docker import APIClient as Client
else:
@@ -52,6 +54,7 @@ except ImportError as exc:
try:
# docker (Docker SDK for Python >= 2.0.0)
import docker.models # noqa: F401, pylint: disable=unused-import
HAS_DOCKER_MODELS = True
except ImportError:
HAS_DOCKER_MODELS = False
@@ -59,13 +62,16 @@ except ImportError:
try:
# docker-py (Docker SDK for Python < 2.0.0)
import docker.ssladapter # noqa: F401, pylint: disable=unused-import
HAS_DOCKER_SSLADAPTER = True
except ImportError:
HAS_DOCKER_SSLADAPTER = False
try:
from requests.exceptions import RequestException # noqa: F401, pylint: disable=unused-import
from requests.exceptions import ( # noqa: F401, pylint: disable=unused-import
RequestException,
)
except ImportError:
# Either Docker SDK for Python is no longer using requests, or Docker SDK for Python is not around either,
# or Docker SDK for Python's dependency requests is missing. In any case, define an exception
@@ -73,30 +79,59 @@ except ImportError:
class RequestException(Exception):
pass
from ansible_collections.community.docker.plugins.module_utils.util import (
BYTE_SUFFIXES, # TODO: remove
)
from ansible_collections.community.docker.plugins.module_utils.util import (
DEFAULT_DOCKER_REGISTRY, # TODO: remove
)
from ansible_collections.community.docker.plugins.module_utils.util import (
DEFAULT_TLS_HOSTNAME, # TODO: remove
)
from ansible_collections.community.docker.plugins.module_utils.util import (
DOCKER_COMMON_ARGS_VARS, # TODO: remove
)
from ansible_collections.community.docker.plugins.module_utils.util import (
DifferenceTracker, # TODO: remove
)
from ansible_collections.community.docker.plugins.module_utils.util import (
DockerBaseClass, # TODO: remove
)
from ansible_collections.community.docker.plugins.module_utils.util import (
clean_dict_booleans_for_docker_api, # TODO: remove
)
from ansible_collections.community.docker.plugins.module_utils.util import (
compare_dict_allow_more_present, # TODO: remove
)
from ansible_collections.community.docker.plugins.module_utils.util import (
compare_generic, # TODO: remove
)
from ansible_collections.community.docker.plugins.module_utils.util import (
convert_duration_to_nanosecond, # TODO: remove
)
from ansible_collections.community.docker.plugins.module_utils.util import (
is_image_name_id, # TODO: remove
)
from ansible_collections.community.docker.plugins.module_utils.util import (
is_valid_tag, # TODO: remove
)
from ansible_collections.community.docker.plugins.module_utils.util import (
omit_none_from_dict, # TODO: remove
)
from ansible_collections.community.docker.plugins.module_utils.util import (
parse_healthcheck, # TODO: remove
)
from ansible_collections.community.docker.plugins.module_utils.util import ( # noqa: F401, pylint: disable=unused-import
DEFAULT_DOCKER_HOST,
DEFAULT_TIMEOUT_SECONDS,
DEFAULT_TLS,
DEFAULT_TLS_VERIFY,
DEFAULT_TLS_HOSTNAME, # TODO: remove
DEFAULT_TIMEOUT_SECONDS,
DOCKER_COMMON_ARGS,
DOCKER_COMMON_ARGS_VARS, # TODO: remove
DOCKER_MUTUALLY_EXCLUSIVE,
DOCKER_REQUIRED_TOGETHER,
DEFAULT_DOCKER_REGISTRY, # TODO: remove
BYTE_SUFFIXES, # TODO: remove
is_image_name_id, # TODO: remove
is_valid_tag, # TODO: remove
sanitize_result,
DockerBaseClass, # TODO: remove
update_tls_hostname,
compare_dict_allow_more_present, # TODO: remove
compare_generic, # TODO: remove
DifferenceTracker, # TODO: remove
clean_dict_booleans_for_docker_api, # TODO: remove
convert_duration_to_nanosecond, # TODO: remove
parse_healthcheck, # TODO: remove
omit_none_from_dict, # TODO: remove
)
@@ -120,8 +155,10 @@ if not HAS_DOCKER_PY:
def _get_tls_config(fail_function, **kwargs):
if 'assert_hostname' in kwargs and LooseVersion(docker_version) >= LooseVersion('7.0.0b1'):
assert_hostname = kwargs.pop('assert_hostname')
if "assert_hostname" in kwargs and LooseVersion(docker_version) >= LooseVersion(
"7.0.0b1"
):
assert_hostname = kwargs.pop("assert_hostname")
if assert_hostname is not None:
fail_function(
"tls_hostname is not compatible with Docker SDK for Python 7.0.0+. You are using"
@@ -139,51 +176,55 @@ def _get_tls_config(fail_function, **kwargs):
def is_using_tls(auth):
return auth['tls_verify'] or auth['tls']
return auth["tls_verify"] or auth["tls"]
def get_connect_params(auth, fail_function):
if is_using_tls(auth):
auth['docker_host'] = auth['docker_host'].replace('tcp://', 'https://')
auth["docker_host"] = auth["docker_host"].replace("tcp://", "https://")
result = dict(
base_url=auth['docker_host'],
version=auth['api_version'],
timeout=auth['timeout'],
base_url=auth["docker_host"],
version=auth["api_version"],
timeout=auth["timeout"],
)
if auth['tls_verify']:
if auth["tls_verify"]:
# TLS with verification
tls_config = dict(
verify=True,
assert_hostname=auth['tls_hostname'],
assert_hostname=auth["tls_hostname"],
fail_function=fail_function,
)
if auth['cert_path'] and auth['key_path']:
tls_config['client_cert'] = (auth['cert_path'], auth['key_path'])
if auth['cacert_path']:
tls_config['ca_cert'] = auth['cacert_path']
result['tls'] = _get_tls_config(**tls_config)
elif auth['tls']:
if auth["cert_path"] and auth["key_path"]:
tls_config["client_cert"] = (auth["cert_path"], auth["key_path"])
if auth["cacert_path"]:
tls_config["ca_cert"] = auth["cacert_path"]
result["tls"] = _get_tls_config(**tls_config)
elif auth["tls"]:
# TLS without verification
tls_config = dict(
verify=False,
fail_function=fail_function,
)
if auth['cert_path'] and auth['key_path']:
tls_config['client_cert'] = (auth['cert_path'], auth['key_path'])
result['tls'] = _get_tls_config(**tls_config)
if auth["cert_path"] and auth["key_path"]:
tls_config["client_cert"] = (auth["cert_path"], auth["key_path"])
result["tls"] = _get_tls_config(**tls_config)
if auth.get('use_ssh_client'):
if LooseVersion(docker_version) < LooseVersion('4.4.0'):
fail_function("use_ssh_client=True requires Docker SDK for Python 4.4.0 or newer")
result['use_ssh_client'] = True
if auth.get("use_ssh_client"):
if LooseVersion(docker_version) < LooseVersion("4.4.0"):
fail_function(
"use_ssh_client=True requires Docker SDK for Python 4.4.0 or newer"
)
result["use_ssh_client"] = True
# No TLS
return result
DOCKERPYUPGRADE_SWITCH_TO_DOCKER = "Try `pip uninstall docker-py` followed by `pip install docker`."
DOCKERPYUPGRADE_SWITCH_TO_DOCKER = (
"Try `pip uninstall docker-py` followed by `pip install docker`."
)
DOCKERPYUPGRADE_UPGRADE_DOCKER = "Use `pip install --upgrade docker` to upgrade."
DOCKERPYUPGRADE_RECOMMEND_DOCKER = "Use `pip install --upgrade docker-py` to upgrade."
@@ -192,17 +233,19 @@ class AnsibleDockerClientBase(Client):
def __init__(self, min_docker_version=None, min_docker_api_version=None):
if min_docker_version is None:
min_docker_version = MIN_DOCKER_VERSION
NEEDS_DOCKER_PY2 = (LooseVersion(min_docker_version) >= LooseVersion('2.0.0'))
NEEDS_DOCKER_PY2 = LooseVersion(min_docker_version) >= LooseVersion("2.0.0")
self.docker_py_version = LooseVersion(docker_version)
if HAS_DOCKER_MODELS and HAS_DOCKER_SSLADAPTER:
self.fail("Cannot have both the docker-py and docker python modules (old and new version of Docker "
"SDK for Python) installed together as they use the same namespace and cause a corrupt "
"installation. Please uninstall both packages, and re-install only the docker-py or docker "
f"python module (for {platform.node()}'s Python {sys.executable}). It is recommended to install the docker module. Please "
"note that simply uninstalling one of the modules can leave the other module in a broken "
"state.")
self.fail(
"Cannot have both the docker-py and docker python modules (old and new version of Docker "
"SDK for Python) installed together as they use the same namespace and cause a corrupt "
"installation. Please uninstall both packages, and re-install only the docker-py or docker "
f"python module (for {platform.node()}'s Python {sys.executable}). It is recommended to install the docker module. Please "
"note that simply uninstalling one of the modules can leave the other module in a broken "
"state."
)
if not HAS_DOCKER_PY:
msg = missing_required_lib("Docker SDK for Python: docker>=5.0.0")
@@ -218,13 +261,15 @@ class AnsibleDockerClientBase(Client):
# The minimal required version is < 2.0 (and the current version as well).
# Advertise docker (instead of docker-py).
msg += DOCKERPYUPGRADE_RECOMMEND_DOCKER
elif docker_version < LooseVersion('2.0'):
elif docker_version < LooseVersion("2.0"):
msg += DOCKERPYUPGRADE_SWITCH_TO_DOCKER
else:
msg += DOCKERPYUPGRADE_UPGRADE_DOCKER
self.fail(msg)
self._connect_params = get_connect_params(self.auth_params, fail_function=self.fail)
self._connect_params = get_connect_params(
self.auth_params, fail_function=self.fail
)
try:
super(AnsibleDockerClientBase, self).__init__(**self._connect_params)
@@ -235,9 +280,11 @@ class AnsibleDockerClientBase(Client):
self.fail(f"Error connecting: {exc}")
self.docker_api_version = LooseVersion(self.docker_api_version_str)
min_docker_api_version = min_docker_api_version or '1.25'
min_docker_api_version = min_docker_api_version or "1.25"
if self.docker_api_version < LooseVersion(min_docker_api_version):
self.fail(f'Docker API version is {self.docker_api_version_str}. Minimum version required is {min_docker_api_version}.')
self.fail(
f"Docker API version is {self.docker_api_version_str}. Minimum version required is {min_docker_api_version}."
)
def log(self, msg, pretty_print=False):
pass
@@ -253,16 +300,16 @@ class AnsibleDockerClientBase(Client):
pass
@staticmethod
def _get_value(param_name, param_value, env_variable, default_value, type='str'):
def _get_value(param_name, param_value, env_variable, default_value, type="str"):
if param_value is not None:
# take module parameter value
if type == 'bool':
if type == "bool":
if param_value in BOOLEANS_TRUE:
return True
if param_value in BOOLEANS_FALSE:
return False
return bool(param_value)
if type == 'int':
if type == "int":
return int(param_value)
return param_value
@@ -270,19 +317,19 @@ class AnsibleDockerClientBase(Client):
env_value = os.environ.get(env_variable)
if env_value is not None:
# take the env variable value
if param_name == 'cert_path':
return os.path.join(env_value, 'cert.pem')
if param_name == 'cacert_path':
return os.path.join(env_value, 'ca.pem')
if param_name == 'key_path':
return os.path.join(env_value, 'key.pem')
if type == 'bool':
if param_name == "cert_path":
return os.path.join(env_value, "cert.pem")
if param_name == "cacert_path":
return os.path.join(env_value, "ca.pem")
if param_name == "key_path":
return os.path.join(env_value, "key.pem")
if type == "bool":
if env_value in BOOLEANS_TRUE:
return True
if env_value in BOOLEANS_FALSE:
return False
return bool(env_value)
if type == 'int':
if type == "int":
return int(env_value)
return env_value
@@ -298,7 +345,7 @@ class AnsibleDockerClientBase(Client):
# Get authentication credentials.
# Precedence: module parameters-> environment variables-> defaults.
self.log('Getting credentials')
self.log("Getting credentials")
client_params = self._get_params()
@@ -307,21 +354,56 @@ class AnsibleDockerClientBase(Client):
params[key] = client_params.get(key)
result = dict(
docker_host=self._get_value('docker_host', params['docker_host'], 'DOCKER_HOST',
DEFAULT_DOCKER_HOST, type='str'),
tls_hostname=self._get_value('tls_hostname', params['tls_hostname'],
'DOCKER_TLS_HOSTNAME', None, type='str'),
api_version=self._get_value('api_version', params['api_version'], 'DOCKER_API_VERSION',
'auto', type='str'),
cacert_path=self._get_value('cacert_path', params['ca_path'], 'DOCKER_CERT_PATH', None, type='str'),
cert_path=self._get_value('cert_path', params['client_cert'], 'DOCKER_CERT_PATH', None, type='str'),
key_path=self._get_value('key_path', params['client_key'], 'DOCKER_CERT_PATH', None, type='str'),
tls=self._get_value('tls', params['tls'], 'DOCKER_TLS', DEFAULT_TLS, type='bool'),
tls_verify=self._get_value('validate_certs', params['validate_certs'], 'DOCKER_TLS_VERIFY',
DEFAULT_TLS_VERIFY, type='bool'),
timeout=self._get_value('timeout', params['timeout'], 'DOCKER_TIMEOUT',
DEFAULT_TIMEOUT_SECONDS, type='int'),
use_ssh_client=self._get_value('use_ssh_client', params['use_ssh_client'], None, False, type='bool'),
docker_host=self._get_value(
"docker_host",
params["docker_host"],
"DOCKER_HOST",
DEFAULT_DOCKER_HOST,
type="str",
),
tls_hostname=self._get_value(
"tls_hostname",
params["tls_hostname"],
"DOCKER_TLS_HOSTNAME",
None,
type="str",
),
api_version=self._get_value(
"api_version",
params["api_version"],
"DOCKER_API_VERSION",
"auto",
type="str",
),
cacert_path=self._get_value(
"cacert_path", params["ca_path"], "DOCKER_CERT_PATH", None, type="str"
),
cert_path=self._get_value(
"cert_path", params["client_cert"], "DOCKER_CERT_PATH", None, type="str"
),
key_path=self._get_value(
"key_path", params["client_key"], "DOCKER_CERT_PATH", None, type="str"
),
tls=self._get_value(
"tls", params["tls"], "DOCKER_TLS", DEFAULT_TLS, type="bool"
),
tls_verify=self._get_value(
"validate_certs",
params["validate_certs"],
"DOCKER_TLS_VERIFY",
DEFAULT_TLS_VERIFY,
type="bool",
),
timeout=self._get_value(
"timeout",
params["timeout"],
"DOCKER_TIMEOUT",
DEFAULT_TIMEOUT_SECONDS,
type="int",
),
use_ssh_client=self._get_value(
"use_ssh_client", params["use_ssh_client"], None, False, type="bool"
),
)
update_tls_hostname(result)
@@ -331,11 +413,13 @@ class AnsibleDockerClientBase(Client):
def _handle_ssl_error(self, error):
match = re.match(r"hostname.*doesn\'t match (\'.*\')", str(error))
if match:
hostname = self.auth_params['tls_hostname']
self.fail(f"You asked for verification that Docker daemons certificate's hostname matches {hostname}. "
f"The actual certificate's hostname is {match.group(1)}. Most likely you need to set DOCKER_TLS_HOSTNAME "
f"or pass `tls_hostname` with a value of {match.group(1)}. You may also use TLS without verification by "
"setting the `tls` parameter to true.")
hostname = self.auth_params["tls_hostname"]
self.fail(
f"You asked for verification that Docker daemons certificate's hostname matches {hostname}. "
f"The actual certificate's hostname is {match.group(1)}. Most likely you need to set DOCKER_TLS_HOSTNAME "
f"or pass `tls_hostname` with a value of {match.group(1)}. You may also use TLS without verification by "
"setting the `tls` parameter to true."
)
self.fail(f"SSL Exception: {error}")
def get_container_by_id(self, container_id):
@@ -350,27 +434,30 @@ class AnsibleDockerClientBase(Client):
self.fail(f"Error inspecting container: {exc}")
def get_container(self, name=None):
'''
"""
Lookup a container and return the inspection results.
'''
"""
if name is None:
return None
search_name = name
if not name.startswith('/'):
search_name = '/' + name
if not name.startswith("/"):
search_name = "/" + name
result = None
try:
for container in self.containers(all=True):
self.log(f"testing container: {container['Names']}")
if isinstance(container['Names'], list) and search_name in container['Names']:
if (
isinstance(container["Names"], list)
and search_name in container["Names"]
):
result = container
break
if container['Id'].startswith(name):
if container["Id"].startswith(name):
result = container
break
if container['Id'] == name:
if container["Id"] == name:
result = container
break
except SSLError as exc:
@@ -381,12 +468,12 @@ class AnsibleDockerClientBase(Client):
if result is None:
return None
return self.get_container_by_id(result['Id'])
return self.get_container_by_id(result["Id"])
def get_network(self, name=None, network_id=None):
'''
"""
Lookup a network and return the inspection results.
'''
"""
if name is None and network_id is None:
return None
@@ -396,10 +483,10 @@ class AnsibleDockerClientBase(Client):
try:
for network in self.networks():
self.log(f"testing network: {network['Name']}")
if name == network['Name']:
if name == network["Name"]:
result = network
break
if network['Id'].startswith(name):
if network["Id"].startswith(name):
result = network
break
except SSLError as exc:
@@ -408,7 +495,7 @@ class AnsibleDockerClientBase(Client):
self.fail(f"Error retrieving network list: {exc}")
if result is not None:
network_id = result['Id']
network_id = result["Id"]
if network_id is not None:
try:
@@ -423,9 +510,9 @@ class AnsibleDockerClientBase(Client):
return result
def find_image(self, name, tag):
'''
"""
Lookup an image (by name and tag) and return the inspection results.
'''
"""
if not name:
return None
@@ -434,14 +521,14 @@ class AnsibleDockerClientBase(Client):
if not images:
# In API <= 1.20 seeing 'docker.io/<name>' as the name of images pulled from docker hub
registry, repo_name = auth.resolve_repository_name(name)
if registry == 'docker.io':
if registry == "docker.io":
# If docker.io is explicitly there in name, the image
# is not found in some cases (#41509)
self.log(f"Check for docker.io image: {repo_name}")
images = self._image_lookup(repo_name, tag)
if not images and repo_name.startswith('library/'):
if not images and repo_name.startswith("library/"):
# Sometimes library/xxx images are not found
lookup = repo_name[len('library/'):]
lookup = repo_name[len("library/") :]
self.log(f"Check for docker.io image: {lookup}")
images = self._image_lookup(lookup, tag)
if not images:
@@ -451,7 +538,7 @@ class AnsibleDockerClientBase(Client):
lookup = f"{registry}/{repo_name}"
self.log(f"Check for docker.io image: {lookup}")
images = self._image_lookup(lookup, tag)
if not images and '/' not in repo_name:
if not images and "/" not in repo_name:
# This seems to be happening with podman-docker
# (https://github.com/ansible-collections/community.docker/issues/291)
lookup = f"{registry}/library/{repo_name}"
@@ -463,7 +550,7 @@ class AnsibleDockerClientBase(Client):
if len(images) == 1:
try:
inspection = self.inspect_image(images[0]['Id'])
inspection = self.inspect_image(images[0]["Id"])
except NotFound:
self.log(f"Image {name}:{tag} not found.")
return None
@@ -475,9 +562,9 @@ class AnsibleDockerClientBase(Client):
return None
def find_image_by_id(self, image_id, accept_missing_image=False):
'''
"""
Lookup an image (by ID) and return the inspection results.
'''
"""
if not image_id:
return None
@@ -494,11 +581,11 @@ class AnsibleDockerClientBase(Client):
return inspection
def _image_lookup(self, name, tag):
'''
"""
Including a tag in the name parameter sent to the Docker SDK for Python images method
does not work consistently. Instead, get the result set for name and manually check
if the tag exists.
'''
"""
try:
response = self.images(name=name)
except Exception as exc:
@@ -509,33 +596,35 @@ class AnsibleDockerClientBase(Client):
lookup_digest = f"{name}@{tag}"
images = []
for image in response:
tags = image.get('RepoTags')
digests = image.get('RepoDigests')
tags = image.get("RepoTags")
digests = image.get("RepoDigests")
if (tags and lookup in tags) or (digests and lookup_digest in digests):
images = [image]
break
return images
def pull_image(self, name, tag="latest", platform=None):
'''
"""
Pull an image
'''
"""
kwargs = dict(
tag=tag,
stream=True,
decode=True,
)
if platform is not None:
kwargs['platform'] = platform
kwargs["platform"] = platform
self.log(f"Pulling image {name}:{tag}")
old_tag = self.find_image(name, tag)
try:
for line in self.pull(name, **kwargs):
self.log(line, pretty_print=True)
if line.get('error'):
if line.get('errorDetail'):
error_detail = line.get('errorDetail')
self.fail(f"Error pulling {name} - code: {error_detail.get('code')} message: {error_detail.get('message')}")
if line.get("error"):
if line.get("errorDetail"):
error_detail = line.get("errorDetail")
self.fail(
f"Error pulling {name} - code: {error_detail.get('code')} message: {error_detail.get('message')}"
)
else:
self.fail(f"Error pulling {name} - {line.get('error')}")
except Exception as exc:
@@ -546,27 +635,43 @@ class AnsibleDockerClientBase(Client):
return new_tag, old_tag == new_tag
def inspect_distribution(self, image, **kwargs):
'''
"""
Get image digest by directly calling the Docker API when running Docker SDK < 4.0.0
since prior versions did not support accessing private repositories.
'''
if self.docker_py_version < LooseVersion('4.0.0'):
"""
if self.docker_py_version < LooseVersion("4.0.0"):
registry = auth.resolve_repository_name(image)[0]
header = auth.get_config_header(self, registry)
if header:
return self._result(self._get(
self._url('/distribution/{0}/json', image),
headers={'X-Registry-Auth': header}
), json=True)
return super(AnsibleDockerClientBase, self).inspect_distribution(image, **kwargs)
return self._result(
self._get(
self._url("/distribution/{0}/json", image),
headers={"X-Registry-Auth": header},
),
json=True,
)
return super(AnsibleDockerClientBase, self).inspect_distribution(
image, **kwargs
)
class AnsibleDockerClient(AnsibleDockerClientBase):
def __init__(self, argument_spec=None, supports_check_mode=False, mutually_exclusive=None,
required_together=None, required_if=None, required_one_of=None, required_by=None,
min_docker_version=None, min_docker_api_version=None, option_minimal_versions=None,
option_minimal_versions_ignore_params=None, fail_results=None):
def __init__(
self,
argument_spec=None,
supports_check_mode=False,
mutually_exclusive=None,
required_together=None,
required_if=None,
required_one_of=None,
required_by=None,
min_docker_version=None,
min_docker_api_version=None,
option_minimal_versions=None,
option_minimal_versions_ignore_params=None,
fail_results=None,
):
# Modules can put information in here which will always be returned
# in case client.fail() is called.
@@ -598,22 +703,27 @@ class AnsibleDockerClient(AnsibleDockerClientBase):
required_by=required_by or {},
)
self.debug = self.module.params.get('debug')
self.debug = self.module.params.get("debug")
self.check_mode = self.module.check_mode
super(AnsibleDockerClient, self).__init__(
min_docker_version=min_docker_version,
min_docker_api_version=min_docker_api_version)
min_docker_api_version=min_docker_api_version,
)
if option_minimal_versions is not None:
self._get_minimal_versions(option_minimal_versions, option_minimal_versions_ignore_params)
self._get_minimal_versions(
option_minimal_versions, option_minimal_versions_ignore_params
)
def fail(self, msg, **kwargs):
self.fail_results.update(kwargs)
self.module.fail_json(msg=msg, **sanitize_result(self.fail_results))
def deprecate(self, msg, version=None, date=None, collection_name=None):
self.module.deprecate(msg, version=version, date=date, collection_name=collection_name)
self.module.deprecate(
msg, version=version, date=date, collection_name=collection_name
)
def _get_params(self):
return self.module.params
@@ -631,26 +741,33 @@ class AnsibleDockerClient(AnsibleDockerClientBase):
# Test whether option is supported, and store result
support_docker_py = True
support_docker_api = True
if 'docker_py_version' in data:
support_docker_py = self.docker_py_version >= LooseVersion(data['docker_py_version'])
if 'docker_api_version' in data:
support_docker_api = self.docker_api_version >= LooseVersion(data['docker_api_version'])
data['supported'] = support_docker_py and support_docker_api
if "docker_py_version" in data:
support_docker_py = self.docker_py_version >= LooseVersion(
data["docker_py_version"]
)
if "docker_api_version" in data:
support_docker_api = self.docker_api_version >= LooseVersion(
data["docker_api_version"]
)
data["supported"] = support_docker_py and support_docker_api
# Fail if option is not supported but used
if not data['supported']:
if not data["supported"]:
# Test whether option is specified
if 'detect_usage' in data:
used = data['detect_usage'](self)
if "detect_usage" in data:
used = data["detect_usage"](self)
else:
used = self.module.params.get(option) is not None
if used and 'default' in self.module.argument_spec[option]:
used = self.module.params[option] != self.module.argument_spec[option]['default']
if used and "default" in self.module.argument_spec[option]:
used = (
self.module.params[option]
!= self.module.argument_spec[option]["default"]
)
if used:
# If the option is used, compose error message.
if 'usage_msg' in data:
usg = data['usage_msg']
if "usage_msg" in data:
usg = data["usage_msg"]
else:
usg = f'set {option} option'
usg = f"set {option} option"
if not support_docker_api:
msg = f"Docker API version is {self.docker_api_version_str}. Minimum version required is {data['docker_api_version']} to {usg}."
elif not support_docker_py:
@@ -658,19 +775,21 @@ class AnsibleDockerClient(AnsibleDockerClientBase):
f"Docker SDK for Python version is {docker_version} ({platform.node()}'s Python {sys.executable})."
f" Minimum version required is {data['docker_py_version']} to {usg}. "
)
if LooseVersion(data['docker_py_version']) < LooseVersion('2.0.0'):
if LooseVersion(data["docker_py_version"]) < LooseVersion(
"2.0.0"
):
msg += DOCKERPYUPGRADE_RECOMMEND_DOCKER
elif self.docker_py_version < LooseVersion('2.0.0'):
elif self.docker_py_version < LooseVersion("2.0.0"):
msg += DOCKERPYUPGRADE_SWITCH_TO_DOCKER
else:
msg += DOCKERPYUPGRADE_UPGRADE_DOCKER
else:
# should not happen
msg = f'Cannot {usg} with your configuration.'
msg = f"Cannot {usg} with your configuration."
self.fail(msg)
def report_warnings(self, result, warnings_key=None):
'''
"""
Checks result of client operation for warnings, and if present, outputs them.
warnings_key should be a list of keys used to crawl the result dictionary.
@@ -681,15 +800,15 @@ class AnsibleDockerClient(AnsibleDockerClientBase):
In most cases (if warnings are returned at all), warnings_key should be
['Warnings'] or ['Warning']. The default value (if not specified) is ['Warnings'].
'''
"""
if warnings_key is None:
warnings_key = ['Warnings']
warnings_key = ["Warnings"]
for key in warnings_key:
if not isinstance(result, Mapping):
return
result = result.get(key)
if isinstance(result, Sequence):
for warning in result:
self.module.warn(f'Docker warning: {warning}')
self.module.warn(f"Docker warning: {warning}")
elif isinstance(result, str) and result:
self.module.warn(f'Docker warning: {result}')
self.module.warn(f"Docker warning: {result}")
+235 -142
View File
@@ -5,30 +5,37 @@
from __future__ import annotations
import abc
import os
import re
from collections.abc import Mapping, Sequence
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
from ansible.module_utils.parsing.convert_bool import BOOLEANS_TRUE, BOOLEANS_FALSE
from ansible.module_utils.parsing.convert_bool import BOOLEANS_FALSE, BOOLEANS_TRUE
from ansible_collections.community.docker.plugins.module_utils.version import (
LooseVersion,
)
from ansible_collections.community.docker.plugins.module_utils.version import LooseVersion
try:
from requests.exceptions import RequestException, SSLError # noqa: F401, pylint: disable=unused-import
from requests.exceptions import ( # noqa: F401, pylint: disable=unused-import
RequestException,
SSLError,
)
except ImportError:
# Define an exception class RequestException so that our code does not break.
class RequestException(Exception):
pass
from ansible_collections.community.docker.plugins.module_utils._api import auth
from ansible_collections.community.docker.plugins.module_utils._api.api.client import APIClient as Client
from ansible_collections.community.docker.plugins.module_utils._api.api.client import (
APIClient as Client,
)
from ansible_collections.community.docker.plugins.module_utils._api.errors import (
APIError,
NotFound,
MissingRequirementException,
NotFound,
TLSParameterError,
)
from ansible_collections.community.docker.plugins.module_utils._api.tls import TLSConfig
@@ -36,19 +43,26 @@ from ansible_collections.community.docker.plugins.module_utils._api.utils.utils
convert_filters,
parse_repository_tag,
)
from ansible_collections.community.docker.plugins.module_utils.util import (
DEFAULT_DOCKER_REGISTRY, # TODO: remove
)
from ansible_collections.community.docker.plugins.module_utils.util import (
DEFAULT_TLS_HOSTNAME, # TODO: remove
)
from ansible_collections.community.docker.plugins.module_utils.util import (
is_image_name_id, # TODO: remove
)
from ansible_collections.community.docker.plugins.module_utils.util import (
is_valid_tag, # TODO: remove
)
from ansible_collections.community.docker.plugins.module_utils.util import ( # noqa: F401, pylint: disable=unused-import
DEFAULT_DOCKER_HOST,
DEFAULT_TIMEOUT_SECONDS,
DEFAULT_TLS,
DEFAULT_TLS_VERIFY,
DEFAULT_TLS_HOSTNAME, # TODO: remove
DEFAULT_TIMEOUT_SECONDS,
DOCKER_COMMON_ARGS,
DOCKER_MUTUALLY_EXCLUSIVE,
DOCKER_REQUIRED_TOGETHER,
DEFAULT_DOCKER_REGISTRY, # TODO: remove
is_image_name_id, # TODO: remove
is_valid_tag, # TODO: remove
sanitize_result,
update_tls_hostname,
)
@@ -63,43 +77,45 @@ def _get_tls_config(fail_function, **kwargs):
def is_using_tls(auth_data):
return auth_data['tls_verify'] or auth_data['tls']
return auth_data["tls_verify"] or auth_data["tls"]
def get_connect_params(auth_data, fail_function):
if is_using_tls(auth_data):
auth_data['docker_host'] = auth_data['docker_host'].replace('tcp://', 'https://')
auth_data["docker_host"] = auth_data["docker_host"].replace(
"tcp://", "https://"
)
result = dict(
base_url=auth_data['docker_host'],
version=auth_data['api_version'],
timeout=auth_data['timeout'],
base_url=auth_data["docker_host"],
version=auth_data["api_version"],
timeout=auth_data["timeout"],
)
if auth_data['tls_verify']:
if auth_data["tls_verify"]:
# TLS with verification
tls_config = dict(
verify=True,
assert_hostname=auth_data['tls_hostname'],
assert_hostname=auth_data["tls_hostname"],
fail_function=fail_function,
)
if auth_data['cert_path'] and auth_data['key_path']:
tls_config['client_cert'] = (auth_data['cert_path'], auth_data['key_path'])
if auth_data['cacert_path']:
tls_config['ca_cert'] = auth_data['cacert_path']
result['tls'] = _get_tls_config(**tls_config)
elif auth_data['tls']:
if auth_data["cert_path"] and auth_data["key_path"]:
tls_config["client_cert"] = (auth_data["cert_path"], auth_data["key_path"])
if auth_data["cacert_path"]:
tls_config["ca_cert"] = auth_data["cacert_path"]
result["tls"] = _get_tls_config(**tls_config)
elif auth_data["tls"]:
# TLS without verification
tls_config = dict(
verify=False,
fail_function=fail_function,
)
if auth_data['cert_path'] and auth_data['key_path']:
tls_config['client_cert'] = (auth_data['cert_path'], auth_data['key_path'])
result['tls'] = _get_tls_config(**tls_config)
if auth_data["cert_path"] and auth_data["key_path"]:
tls_config["client_cert"] = (auth_data["cert_path"], auth_data["key_path"])
result["tls"] = _get_tls_config(**tls_config)
if auth_data.get('use_ssh_client'):
result['use_ssh_client'] = True
if auth_data.get("use_ssh_client"):
result["use_ssh_client"] = True
# No TLS
return result
@@ -107,22 +123,28 @@ def get_connect_params(auth_data, fail_function):
class AnsibleDockerClientBase(Client):
def __init__(self, min_docker_api_version=None):
self._connect_params = get_connect_params(self.auth_params, fail_function=self.fail)
self._connect_params = get_connect_params(
self.auth_params, fail_function=self.fail
)
try:
super(AnsibleDockerClientBase, self).__init__(**self._connect_params)
self.docker_api_version_str = self.api_version
except MissingRequirementException as exc:
self.fail(missing_required_lib(exc.requirement), exception=exc.import_exception)
self.fail(
missing_required_lib(exc.requirement), exception=exc.import_exception
)
except APIError as exc:
self.fail(f"Docker API error: {exc}")
except Exception as exc:
self.fail(f"Error connecting: {exc}")
self.docker_api_version = LooseVersion(self.docker_api_version_str)
min_docker_api_version = min_docker_api_version or '1.25'
min_docker_api_version = min_docker_api_version or "1.25"
if self.docker_api_version < LooseVersion(min_docker_api_version):
self.fail(f'Docker API version is {self.docker_api_version_str}. Minimum version required is {min_docker_api_version}.')
self.fail(
f"Docker API version is {self.docker_api_version_str}. Minimum version required is {min_docker_api_version}."
)
def log(self, msg, pretty_print=False):
pass
@@ -138,16 +160,16 @@ class AnsibleDockerClientBase(Client):
pass
@staticmethod
def _get_value(param_name, param_value, env_variable, default_value, type='str'):
def _get_value(param_name, param_value, env_variable, default_value, type="str"):
if param_value is not None:
# take module parameter value
if type == 'bool':
if type == "bool":
if param_value in BOOLEANS_TRUE:
return True
if param_value in BOOLEANS_FALSE:
return False
return bool(param_value)
if type == 'int':
if type == "int":
return int(param_value)
return param_value
@@ -155,19 +177,19 @@ class AnsibleDockerClientBase(Client):
env_value = os.environ.get(env_variable)
if env_value is not None:
# take the env variable value
if param_name == 'cert_path':
return os.path.join(env_value, 'cert.pem')
if param_name == 'cacert_path':
return os.path.join(env_value, 'ca.pem')
if param_name == 'key_path':
return os.path.join(env_value, 'key.pem')
if type == 'bool':
if param_name == "cert_path":
return os.path.join(env_value, "cert.pem")
if param_name == "cacert_path":
return os.path.join(env_value, "ca.pem")
if param_name == "key_path":
return os.path.join(env_value, "key.pem")
if type == "bool":
if env_value in BOOLEANS_TRUE:
return True
if env_value in BOOLEANS_FALSE:
return False
return bool(env_value)
if type == 'int':
if type == "int":
return int(env_value)
return env_value
@@ -183,7 +205,7 @@ class AnsibleDockerClientBase(Client):
# Get authentication credentials.
# Precedence: module parameters-> environment variables-> defaults.
self.log('Getting credentials')
self.log("Getting credentials")
client_params = self._get_params()
@@ -192,44 +214,86 @@ class AnsibleDockerClientBase(Client):
params[key] = client_params.get(key)
result = dict(
docker_host=self._get_value('docker_host', params['docker_host'], 'DOCKER_HOST',
DEFAULT_DOCKER_HOST, type='str'),
tls_hostname=self._get_value('tls_hostname', params['tls_hostname'],
'DOCKER_TLS_HOSTNAME', None, type='str'),
api_version=self._get_value('api_version', params['api_version'], 'DOCKER_API_VERSION',
'auto', type='str'),
cacert_path=self._get_value('cacert_path', params['ca_path'], 'DOCKER_CERT_PATH', None, type='str'),
cert_path=self._get_value('cert_path', params['client_cert'], 'DOCKER_CERT_PATH', None, type='str'),
key_path=self._get_value('key_path', params['client_key'], 'DOCKER_CERT_PATH', None, type='str'),
tls=self._get_value('tls', params['tls'], 'DOCKER_TLS', DEFAULT_TLS, type='bool'),
tls_verify=self._get_value('validate_certs', params['validate_certs'], 'DOCKER_TLS_VERIFY',
DEFAULT_TLS_VERIFY, type='bool'),
timeout=self._get_value('timeout', params['timeout'], 'DOCKER_TIMEOUT',
DEFAULT_TIMEOUT_SECONDS, type='int'),
use_ssh_client=self._get_value('use_ssh_client', params['use_ssh_client'], None, False, type='bool'),
docker_host=self._get_value(
"docker_host",
params["docker_host"],
"DOCKER_HOST",
DEFAULT_DOCKER_HOST,
type="str",
),
tls_hostname=self._get_value(
"tls_hostname",
params["tls_hostname"],
"DOCKER_TLS_HOSTNAME",
None,
type="str",
),
api_version=self._get_value(
"api_version",
params["api_version"],
"DOCKER_API_VERSION",
"auto",
type="str",
),
cacert_path=self._get_value(
"cacert_path", params["ca_path"], "DOCKER_CERT_PATH", None, type="str"
),
cert_path=self._get_value(
"cert_path", params["client_cert"], "DOCKER_CERT_PATH", None, type="str"
),
key_path=self._get_value(
"key_path", params["client_key"], "DOCKER_CERT_PATH", None, type="str"
),
tls=self._get_value(
"tls", params["tls"], "DOCKER_TLS", DEFAULT_TLS, type="bool"
),
tls_verify=self._get_value(
"validate_certs",
params["validate_certs"],
"DOCKER_TLS_VERIFY",
DEFAULT_TLS_VERIFY,
type="bool",
),
timeout=self._get_value(
"timeout",
params["timeout"],
"DOCKER_TIMEOUT",
DEFAULT_TIMEOUT_SECONDS,
type="int",
),
use_ssh_client=self._get_value(
"use_ssh_client", params["use_ssh_client"], None, False, type="bool"
),
)
def depr(*args, **kwargs):
self.deprecate(*args, **kwargs)
update_tls_hostname(result, old_behavior=True, deprecate_function=depr, uses_tls=is_using_tls(result))
update_tls_hostname(
result,
old_behavior=True,
deprecate_function=depr,
uses_tls=is_using_tls(result),
)
return result
def _handle_ssl_error(self, error):
match = re.match(r"hostname.*doesn\'t match (\'.*\')", str(error))
if match:
hostname = self.auth_params['tls_hostname']
self.fail(f"You asked for verification that Docker daemons certificate's hostname matches {hostname}. "
f"The actual certificate's hostname is {match.group(1)}. Most likely you need to set DOCKER_TLS_HOSTNAME "
f"or pass `tls_hostname` with a value of {match.group(1)}. You may also use TLS without verification by "
"setting the `tls` parameter to true.")
hostname = self.auth_params["tls_hostname"]
self.fail(
f"You asked for verification that Docker daemons certificate's hostname matches {hostname}. "
f"The actual certificate's hostname is {match.group(1)}. Most likely you need to set DOCKER_TLS_HOSTNAME "
f"or pass `tls_hostname` with a value of {match.group(1)}. You may also use TLS without verification by "
"setting the `tls` parameter to true."
)
self.fail(f"SSL Exception: {error}")
def get_container_by_id(self, container_id):
try:
self.log(f"Inspecting container Id {container_id}")
result = self.get_json('/containers/{0}/json', container_id)
result = self.get_json("/containers/{0}/json", container_id)
self.log("Completed container inspection")
return result
except NotFound as dummy:
@@ -238,34 +302,37 @@ class AnsibleDockerClientBase(Client):
self.fail(f"Error inspecting container: {exc}")
def get_container(self, name=None):
'''
"""
Lookup a container and return the inspection results.
'''
"""
if name is None:
return None
search_name = name
if not name.startswith('/'):
search_name = '/' + name
if not name.startswith("/"):
search_name = "/" + name
result = None
try:
params = {
'limit': -1,
'all': 1,
'size': 0,
'trunc_cmd': 0,
"limit": -1,
"all": 1,
"size": 0,
"trunc_cmd": 0,
}
containers = self.get_json("/containers/json", params=params)
for container in containers:
self.log(f"testing container: {container['Names']}")
if isinstance(container['Names'], list) and search_name in container['Names']:
if (
isinstance(container["Names"], list)
and search_name in container["Names"]
):
result = container
break
if container['Id'].startswith(name):
if container["Id"].startswith(name):
result = container
break
if container['Id'] == name:
if container["Id"] == name:
result = container
break
except SSLError as exc:
@@ -276,12 +343,12 @@ class AnsibleDockerClientBase(Client):
if result is None:
return None
return self.get_container_by_id(result['Id'])
return self.get_container_by_id(result["Id"])
def get_network(self, name=None, network_id=None):
'''
"""
Lookup a network and return the inspection results.
'''
"""
if name is None and network_id is None:
return None
@@ -292,10 +359,10 @@ class AnsibleDockerClientBase(Client):
networks = self.get_json("/networks")
for network in networks:
self.log(f"testing network: {network['Name']}")
if name == network['Name']:
if name == network["Name"]:
result = network
break
if network['Id'].startswith(name):
if network["Id"].startswith(name):
result = network
break
except SSLError as exc:
@@ -304,12 +371,12 @@ class AnsibleDockerClientBase(Client):
self.fail(f"Error retrieving network list: {exc}")
if result is not None:
network_id = result['Id']
network_id = result["Id"]
if network_id is not None:
try:
self.log(f"Inspecting network Id {network_id}")
result = self.get_json('/networks/{0}', network_id)
result = self.get_json("/networks/{0}", network_id)
self.log("Completed network inspection")
except NotFound as dummy:
return None
@@ -319,21 +386,21 @@ class AnsibleDockerClientBase(Client):
return result
def _image_lookup(self, name, tag):
'''
"""
Including a tag in the name parameter sent to the Docker SDK for Python images method
does not work consistently. Instead, get the result set for name and manually check
if the tag exists.
'''
"""
try:
params = {
'only_ids': 0,
'all': 0,
"only_ids": 0,
"all": 0,
}
if LooseVersion(self.api_version) < LooseVersion('1.25'):
if LooseVersion(self.api_version) < LooseVersion("1.25"):
# only use "filter" on API 1.24 and under, as it is deprecated
params['filter'] = name
params["filter"] = name
else:
params['filters'] = convert_filters({'reference': name})
params["filters"] = convert_filters({"reference": name})
images = self.get_json("/images/json", params=params)
except Exception as exc:
self.fail(f"Error searching for image {name} - {exc}")
@@ -343,17 +410,17 @@ class AnsibleDockerClientBase(Client):
response = images
images = []
for image in response:
tags = image.get('RepoTags')
digests = image.get('RepoDigests')
tags = image.get("RepoTags")
digests = image.get("RepoDigests")
if (tags and lookup in tags) or (digests and lookup_digest in digests):
images = [image]
break
return images
def find_image(self, name, tag):
'''
"""
Lookup an image (by name and tag) and return the inspection results.
'''
"""
if not name:
return None
@@ -362,14 +429,14 @@ class AnsibleDockerClientBase(Client):
if not images:
# In API <= 1.20 seeing 'docker.io/<name>' as the name of images pulled from docker hub
registry, repo_name = auth.resolve_repository_name(name)
if registry == 'docker.io':
if registry == "docker.io":
# If docker.io is explicitly there in name, the image
# is not found in some cases (#41509)
self.log(f"Check for docker.io image: {repo_name}")
images = self._image_lookup(repo_name, tag)
if not images and repo_name.startswith('library/'):
if not images and repo_name.startswith("library/"):
# Sometimes library/xxx images are not found
lookup = repo_name[len('library/'):]
lookup = repo_name[len("library/") :]
self.log(f"Check for docker.io image: {lookup}")
images = self._image_lookup(lookup, tag)
if not images:
@@ -379,7 +446,7 @@ class AnsibleDockerClientBase(Client):
lookup = f"{registry}/{repo_name}"
self.log(f"Check for docker.io image: {lookup}")
images = self._image_lookup(lookup, tag)
if not images and '/' not in repo_name:
if not images and "/" not in repo_name:
# This seems to be happening with podman-docker
# (https://github.com/ansible-collections/community.docker/issues/291)
lookup = f"{registry}/library/{repo_name}"
@@ -391,7 +458,7 @@ class AnsibleDockerClientBase(Client):
if len(images) == 1:
try:
return self.get_json('/images/{0}/json', images[0]['Id'])
return self.get_json("/images/{0}/json", images[0]["Id"])
except NotFound:
self.log(f"Image {name}:{tag} not found.")
return None
@@ -402,15 +469,15 @@ class AnsibleDockerClientBase(Client):
return None
def find_image_by_id(self, image_id, accept_missing_image=False):
'''
"""
Lookup an image (by ID) and return the inspection results.
'''
"""
if not image_id:
return None
self.log(f"Find image {image_id} (by ID)")
try:
return self.get_json('/images/{0}/json', image_id)
return self.get_json("/images/{0}/json", image_id)
except NotFound as exc:
if not accept_missing_image:
self.fail(f"Error inspecting image ID {image_id} - {exc}")
@@ -420,37 +487,42 @@ class AnsibleDockerClientBase(Client):
self.fail(f"Error inspecting image ID {image_id} - {exc}")
def pull_image(self, name, tag="latest", platform=None):
'''
"""
Pull an image
'''
"""
self.log(f"Pulling image {name}:{tag}")
old_tag = self.find_image(name, tag)
try:
repository, image_tag = parse_repository_tag(name)
registry, repo_name = auth.resolve_repository_name(repository)
params = {
'tag': tag or image_tag or 'latest',
'fromImage': repository,
"tag": tag or image_tag or "latest",
"fromImage": repository,
}
if platform is not None:
params['platform'] = platform
params["platform"] = platform
headers = {}
header = auth.get_config_header(self, registry)
if header:
headers['X-Registry-Auth'] = header
headers["X-Registry-Auth"] = header
response = self._post(
self._url('/images/create'), params=params, headers=headers,
stream=True, timeout=None
self._url("/images/create"),
params=params,
headers=headers,
stream=True,
timeout=None,
)
self._raise_for_status(response)
for line in self._stream_helper(response, decode=True):
self.log(line, pretty_print=True)
if line.get('error'):
if line.get('errorDetail'):
error_detail = line.get('errorDetail')
self.fail(f"Error pulling {name} - code: {error_detail.get('code')} message: {error_detail.get('message')}")
if line.get("error"):
if line.get("errorDetail"):
error_detail = line.get("errorDetail")
self.fail(
f"Error pulling {name} - code: {error_detail.get('code')} message: {error_detail.get('message')}"
)
else:
self.fail(f"Error pulling {name} - {line.get('error')}")
except Exception as exc:
@@ -463,10 +535,20 @@ class AnsibleDockerClientBase(Client):
class AnsibleDockerClient(AnsibleDockerClientBase):
def __init__(self, argument_spec=None, supports_check_mode=False, mutually_exclusive=None,
required_together=None, required_if=None, required_one_of=None, required_by=None,
min_docker_api_version=None, option_minimal_versions=None,
option_minimal_versions_ignore_params=None, fail_results=None):
def __init__(
self,
argument_spec=None,
supports_check_mode=False,
mutually_exclusive=None,
required_together=None,
required_if=None,
required_one_of=None,
required_by=None,
min_docker_api_version=None,
option_minimal_versions=None,
option_minimal_versions_ignore_params=None,
fail_results=None,
):
# Modules can put information in here which will always be returned
# in case client.fail() is called.
@@ -498,20 +580,26 @@ class AnsibleDockerClient(AnsibleDockerClientBase):
required_by=required_by or {},
)
self.debug = self.module.params.get('debug')
self.debug = self.module.params.get("debug")
self.check_mode = self.module.check_mode
super(AnsibleDockerClient, self).__init__(min_docker_api_version=min_docker_api_version)
super(AnsibleDockerClient, self).__init__(
min_docker_api_version=min_docker_api_version
)
if option_minimal_versions is not None:
self._get_minimal_versions(option_minimal_versions, option_minimal_versions_ignore_params)
self._get_minimal_versions(
option_minimal_versions, option_minimal_versions_ignore_params
)
def fail(self, msg, **kwargs):
self.fail_results.update(kwargs)
self.module.fail_json(msg=msg, **sanitize_result(self.fail_results))
def deprecate(self, msg, version=None, date=None, collection_name=None):
self.module.deprecate(msg, version=version, date=date, collection_name=collection_name)
self.module.deprecate(
msg, version=version, date=date, collection_name=collection_name
)
def _get_params(self):
return self.module.params
@@ -528,33 +616,38 @@ class AnsibleDockerClient(AnsibleDockerClientBase):
for option, data in self.option_minimal_versions.items():
# Test whether option is supported, and store result
support_docker_api = True
if 'docker_api_version' in data:
support_docker_api = self.docker_api_version >= LooseVersion(data['docker_api_version'])
data['supported'] = support_docker_api
if "docker_api_version" in data:
support_docker_api = self.docker_api_version >= LooseVersion(
data["docker_api_version"]
)
data["supported"] = support_docker_api
# Fail if option is not supported but used
if not data['supported']:
if not data["supported"]:
# Test whether option is specified
if 'detect_usage' in data:
used = data['detect_usage'](self)
if "detect_usage" in data:
used = data["detect_usage"](self)
else:
used = self.module.params.get(option) is not None
if used and 'default' in self.module.argument_spec[option]:
used = self.module.params[option] != self.module.argument_spec[option]['default']
if used and "default" in self.module.argument_spec[option]:
used = (
self.module.params[option]
!= self.module.argument_spec[option]["default"]
)
if used:
# If the option is used, compose error message.
if 'usage_msg' in data:
usg = data['usage_msg']
if "usage_msg" in data:
usg = data["usage_msg"]
else:
usg = f'set {option} option'
usg = f"set {option} option"
if not support_docker_api:
msg = f"Docker API version is {self.docker_api_version_str}. Minimum version required is {data['docker_api_version']} to {usg}."
else:
# should not happen
msg = f'Cannot {usg} with your configuration.'
msg = f"Cannot {usg} with your configuration."
self.fail(msg)
def report_warnings(self, result, warnings_key=None):
'''
"""
Checks result of client operation for warnings, and if present, outputs them.
warnings_key should be a list of keys used to crawl the result dictionary.
@@ -565,15 +658,15 @@ class AnsibleDockerClient(AnsibleDockerClientBase):
In most cases (if warnings are returned at all), warnings_key should be
['Warnings'] or ['Warning']. The default value (if not specified) is ['Warnings'].
'''
"""
if warnings_key is None:
warnings_key = ['Warnings']
warnings_key = ["Warnings"]
for key in warnings_key:
if not isinstance(result, Mapping):
return
result = result.get(key)
if isinstance(result, Sequence):
for warning in result:
self.module.warn(f'Docker warning: {warning}')
self.module.warn(f"Docker warning: {warning}")
elif isinstance(result, str) and result:
self.module.warn(f'Docker warning: {result}')
self.module.warn(f"Docker warning: {result}")
+133 -77
View File
@@ -4,7 +4,6 @@
from __future__ import annotations
import abc
import json
import shlex
@@ -12,11 +11,9 @@ import shlex
from ansible.module_utils.basic import AnsibleModule, env_fallback
from ansible.module_utils.common.process import get_bin_path
from ansible.module_utils.common.text.converters import to_native
from ansible_collections.community.docker.plugins.module_utils.version import LooseVersion
from ansible_collections.community.docker.plugins.module_utils._api.auth import resolve_repository_name
from ansible_collections.community.docker.plugins.module_utils._api.auth import (
resolve_repository_name,
)
from ansible_collections.community.docker.plugins.module_utils.util import ( # noqa: F401, pylint: disable=unused-import
DEFAULT_DOCKER_HOST,
DEFAULT_TLS,
@@ -25,20 +22,35 @@ from ansible_collections.community.docker.plugins.module_utils.util import ( #
DOCKER_REQUIRED_TOGETHER,
sanitize_result,
)
from ansible_collections.community.docker.plugins.module_utils.version import (
LooseVersion,
)
DOCKER_COMMON_ARGS = dict(
docker_cli=dict(type='path'),
docker_host=dict(type='str', fallback=(env_fallback, ['DOCKER_HOST']), aliases=['docker_url']),
tls_hostname=dict(type='str', fallback=(env_fallback, ['DOCKER_TLS_HOSTNAME'])),
api_version=dict(type='str', default='auto', fallback=(env_fallback, ['DOCKER_API_VERSION']), aliases=['docker_api_version']),
ca_path=dict(type='path', aliases=['ca_cert', 'tls_ca_cert', 'cacert_path']),
client_cert=dict(type='path', aliases=['tls_client_cert', 'cert_path']),
client_key=dict(type='path', aliases=['tls_client_key', 'key_path']),
tls=dict(type='bool', default=DEFAULT_TLS, fallback=(env_fallback, ['DOCKER_TLS'])),
validate_certs=dict(type='bool', default=DEFAULT_TLS_VERIFY, fallback=(env_fallback, ['DOCKER_TLS_VERIFY']), aliases=['tls_verify']),
docker_cli=dict(type="path"),
docker_host=dict(
type="str", fallback=(env_fallback, ["DOCKER_HOST"]), aliases=["docker_url"]
),
tls_hostname=dict(type="str", fallback=(env_fallback, ["DOCKER_TLS_HOSTNAME"])),
api_version=dict(
type="str",
default="auto",
fallback=(env_fallback, ["DOCKER_API_VERSION"]),
aliases=["docker_api_version"],
),
ca_path=dict(type="path", aliases=["ca_cert", "tls_ca_cert", "cacert_path"]),
client_cert=dict(type="path", aliases=["tls_client_cert", "cert_path"]),
client_key=dict(type="path", aliases=["tls_client_key", "key_path"]),
tls=dict(type="bool", default=DEFAULT_TLS, fallback=(env_fallback, ["DOCKER_TLS"])),
validate_certs=dict(
type="bool",
default=DEFAULT_TLS_VERIFY,
fallback=(env_fallback, ["DOCKER_TLS_VERIFY"]),
aliases=["tls_verify"],
),
# debug=dict(type='bool', default=False),
cli_context=dict(type='str'),
cli_context=dict(type="str"),
)
@@ -47,55 +59,71 @@ class DockerException(Exception):
class AnsibleDockerClientBase(object):
def __init__(self, common_args, min_docker_api_version=None, needs_api_version=True):
def __init__(
self, common_args, min_docker_api_version=None, needs_api_version=True
):
self._environment = {}
if common_args['tls_hostname']:
self._environment['DOCKER_TLS_HOSTNAME'] = common_args['tls_hostname']
if common_args['api_version'] and common_args['api_version'] != 'auto':
self._environment['DOCKER_API_VERSION'] = common_args['api_version']
self._cli = common_args.get('docker_cli')
if common_args["tls_hostname"]:
self._environment["DOCKER_TLS_HOSTNAME"] = common_args["tls_hostname"]
if common_args["api_version"] and common_args["api_version"] != "auto":
self._environment["DOCKER_API_VERSION"] = common_args["api_version"]
self._cli = common_args.get("docker_cli")
if self._cli is None:
try:
self._cli = get_bin_path('docker')
self._cli = get_bin_path("docker")
except ValueError:
self.fail('Cannot find docker CLI in path. Please provide it explicitly with the docker_cli parameter')
self.fail(
"Cannot find docker CLI in path. Please provide it explicitly with the docker_cli parameter"
)
self._cli_base = [self._cli]
docker_host = common_args['docker_host']
if not docker_host and not common_args['cli_context']:
docker_host = common_args["docker_host"]
if not docker_host and not common_args["cli_context"]:
docker_host = DEFAULT_DOCKER_HOST
if docker_host:
self._cli_base.extend(['--host', docker_host])
if common_args['validate_certs']:
self._cli_base.append('--tlsverify')
elif common_args['tls']:
self._cli_base.append('--tls')
if common_args['ca_path']:
self._cli_base.extend(['--tlscacert', common_args['ca_path']])
if common_args['client_cert']:
self._cli_base.extend(['--tlscert', common_args['client_cert']])
if common_args['client_key']:
self._cli_base.extend(['--tlskey', common_args['client_key']])
if common_args['cli_context']:
self._cli_base.extend(['--context', common_args['cli_context']])
self._cli_base.extend(["--host", docker_host])
if common_args["validate_certs"]:
self._cli_base.append("--tlsverify")
elif common_args["tls"]:
self._cli_base.append("--tls")
if common_args["ca_path"]:
self._cli_base.extend(["--tlscacert", common_args["ca_path"]])
if common_args["client_cert"]:
self._cli_base.extend(["--tlscert", common_args["client_cert"]])
if common_args["client_key"]:
self._cli_base.extend(["--tlskey", common_args["client_key"]])
if common_args["cli_context"]:
self._cli_base.extend(["--context", common_args["cli_context"]])
# `--format json` was only added as a shorthand for `--format {{ json . }}` in Docker 23.0
dummy, self._version, dummy = self.call_cli_json('version', '--format', '{{ json . }}', check_rc=True)
dummy, self._version, dummy = self.call_cli_json(
"version", "--format", "{{ json . }}", check_rc=True
)
self._info = None
if needs_api_version:
if not isinstance(self._version.get('Server'), dict) or not isinstance(self._version['Server'].get('ApiVersion'), str):
self.fail('Cannot determine Docker Daemon information. Are you maybe using podman instead of docker?')
self.docker_api_version_str = to_native(self._version['Server']['ApiVersion'])
if not isinstance(self._version.get("Server"), dict) or not isinstance(
self._version["Server"].get("ApiVersion"), str
):
self.fail(
"Cannot determine Docker Daemon information. Are you maybe using podman instead of docker?"
)
self.docker_api_version_str = to_native(
self._version["Server"]["ApiVersion"]
)
self.docker_api_version = LooseVersion(self.docker_api_version_str)
min_docker_api_version = min_docker_api_version or '1.25'
min_docker_api_version = min_docker_api_version or "1.25"
if self.docker_api_version < LooseVersion(min_docker_api_version):
self.fail(f'Docker API version is {self.docker_api_version_str}. Minimum version required is {min_docker_api_version}.')
self.fail(
f"Docker API version is {self.docker_api_version_str}. Minimum version required is {min_docker_api_version}."
)
else:
self.docker_api_version_str = None
self.docker_api_version = None
if min_docker_api_version is not None:
self.fail('Internal error: cannot have needs_api_version=False with min_docker_api_version not None')
self.fail(
"Internal error: cannot have needs_api_version=False with min_docker_api_version not None"
)
def log(self, msg, pretty_print=False):
pass
@@ -113,7 +141,7 @@ class AnsibleDockerClientBase(object):
return self._cli_base + list(args)
def _compose_cmd_str(self, args):
return ' '.join(shlex.quote(a) for a in self._compose_cmd(args))
return " ".join(shlex.quote(a) for a in self._compose_cmd(args))
@abc.abstractmethod
def call_cli(self, *args, check_rc=False, data=None, cwd=None, environ_update=None):
@@ -121,19 +149,21 @@ class AnsibleDockerClientBase(object):
# def call_cli_json(self, *args, check_rc=False, data=None, cwd=None, environ_update=None, warn_on_stderr=False):
def call_cli_json(self, *args, **kwargs):
warn_on_stderr = kwargs.pop('warn_on_stderr', False)
warn_on_stderr = kwargs.pop("warn_on_stderr", False)
rc, stdout, stderr = self.call_cli(*args, **kwargs)
if warn_on_stderr and stderr:
self.warn(to_native(stderr))
try:
data = json.loads(stdout)
except Exception as exc:
self.fail(f'Error while parsing JSON output of {self._compose_cmd_str(args)}: {exc}\nJSON output: {to_native(stdout)}')
self.fail(
f"Error while parsing JSON output of {self._compose_cmd_str(args)}: {exc}\nJSON output: {to_native(stdout)}"
)
return rc, data, stderr
# def call_cli_json_stream(self, *args, check_rc=False, data=None, cwd=None, environ_update=None, warn_on_stderr=False):
def call_cli_json_stream(self, *args, **kwargs):
warn_on_stderr = kwargs.pop('warn_on_stderr', False)
warn_on_stderr = kwargs.pop("warn_on_stderr", False)
rc, stdout, stderr = self.call_cli(*args, **kwargs)
if warn_on_stderr and stderr:
self.warn(to_native(stderr))
@@ -141,10 +171,12 @@ class AnsibleDockerClientBase(object):
try:
for line in stdout.splitlines():
line = line.strip()
if line.startswith(b'{'):
if line.startswith(b"{"):
result.append(json.loads(line))
except Exception as exc:
self.fail(f'Error while parsing JSON output of {self._compose_cmd_str(args)}: {exc}\nJSON output: {to_native(stdout)}')
self.fail(
f"Error while parsing JSON output of {self._compose_cmd_str(args)}: {exc}\nJSON output: {to_native(stdout)}"
)
return rc, result, stderr
@abc.abstractmethod
@@ -161,26 +193,36 @@ class AnsibleDockerClientBase(object):
def get_cli_info(self):
if self._info is None:
dummy, self._info, dummy = self.call_cli_json('info', '--format', '{{ json . }}', check_rc=True)
dummy, self._info, dummy = self.call_cli_json(
"info", "--format", "{{ json . }}", check_rc=True
)
return self._info
def get_client_plugin_info(self, component):
cli_info = self.get_cli_info()
if not isinstance(cli_info.get('ClientInfo'), dict):
self.fail('Cannot determine Docker client information. Are you maybe using podman instead of docker?')
for plugin in cli_info['ClientInfo'].get('Plugins') or []:
if plugin.get('Name') == component:
if not isinstance(cli_info.get("ClientInfo"), dict):
self.fail(
"Cannot determine Docker client information. Are you maybe using podman instead of docker?"
)
for plugin in cli_info["ClientInfo"].get("Plugins") or []:
if plugin.get("Name") == component:
return plugin
return None
def _image_lookup(self, name, tag):
'''
"""
Including a tag in the name parameter sent to the Docker SDK for Python images method
does not work consistently. Instead, get the result set for name and manually check
if the tag exists.
'''
"""
dummy, images, dummy = self.call_cli_json_stream(
'image', 'ls', '--format', '{{ json . }}', '--no-trunc', '--filter', f'reference={name}',
"image",
"ls",
"--format",
"{{ json . }}",
"--no-trunc",
"--filter",
f"reference={name}",
check_rc=True,
)
if tag:
@@ -189,15 +231,15 @@ class AnsibleDockerClientBase(object):
response = images
images = []
for image in response:
if image.get('Tag') == tag or image.get('Digest') == tag:
if image.get("Tag") == tag or image.get("Digest") == tag:
images = [image]
break
return images
def find_image(self, name, tag):
'''
"""
Lookup an image (by name and tag) and return the inspection results.
'''
"""
if not name:
return None
@@ -206,14 +248,14 @@ class AnsibleDockerClientBase(object):
if not images:
# In API <= 1.20 seeing 'docker.io/<name>' as the name of images pulled from docker hub
registry, repo_name = resolve_repository_name(name)
if registry == 'docker.io':
if registry == "docker.io":
# If docker.io is explicitly there in name, the image
# is not found in some cases (#41509)
self.log(f"Check for docker.io image: {repo_name}")
images = self._image_lookup(repo_name, tag)
if not images and repo_name.startswith('library/'):
if not images and repo_name.startswith("library/"):
# Sometimes library/xxx images are not found
lookup = repo_name[len('library/'):]
lookup = repo_name[len("library/") :]
self.log(f"Check for docker.io image: {lookup}")
images = self._image_lookup(lookup, tag)
if not images:
@@ -223,7 +265,7 @@ class AnsibleDockerClientBase(object):
lookup = f"{registry}/{repo_name}"
self.log(f"Check for docker.io image: {lookup}")
images = self._image_lookup(lookup, tag)
if not images and '/' not in repo_name:
if not images and "/" not in repo_name:
# This seems to be happening with podman-docker
# (https://github.com/ansible-collections/community.docker/issues/291)
lookup = f"{registry}/library/{repo_name}"
@@ -234,7 +276,7 @@ class AnsibleDockerClientBase(object):
self.fail(f"Daemon returned more than one result for {name}:{tag}")
if len(images) == 1:
rc, image, stderr = self.call_cli_json('image', 'inspect', images[0]['ID'])
rc, image, stderr = self.call_cli_json("image", "inspect", images[0]["ID"])
if not image:
self.log(f"Image {name}:{tag} not found.")
return None
@@ -246,14 +288,14 @@ class AnsibleDockerClientBase(object):
return None
def find_image_by_id(self, image_id, accept_missing_image=False):
'''
"""
Lookup an image (by ID) and return the inspection results.
'''
"""
if not image_id:
return None
self.log(f"Find image {image_id} (by ID)")
rc, image, stderr = self.call_cli_json('image', 'inspect', image_id)
rc, image, stderr = self.call_cli_json("image", "inspect", image_id)
if not image:
if not accept_missing_image:
self.fail(f"Error inspecting image ID {image_id} - {to_native(stderr)}")
@@ -265,9 +307,19 @@ class AnsibleDockerClientBase(object):
class AnsibleModuleDockerClient(AnsibleDockerClientBase):
def __init__(self, argument_spec=None, supports_check_mode=False, mutually_exclusive=None,
required_together=None, required_if=None, required_one_of=None, required_by=None,
min_docker_api_version=None, fail_results=None, needs_api_version=True):
def __init__(
self,
argument_spec=None,
supports_check_mode=False,
mutually_exclusive=None,
required_together=None,
required_if=None,
required_one_of=None,
required_by=None,
min_docker_api_version=None,
fail_results=None,
needs_api_version=True,
):
# Modules can put information in here which will always be returned
# in case client.fail() is called.
@@ -279,7 +331,7 @@ class AnsibleModuleDockerClient(AnsibleDockerClientBase):
merged_arg_spec.update(argument_spec)
self.arg_spec = merged_arg_spec
mutually_exclusive_params = [('docker_host', 'cli_context')]
mutually_exclusive_params = [("docker_host", "cli_context")]
mutually_exclusive_params += DOCKER_MUTUALLY_EXCLUSIVE
if mutually_exclusive:
mutually_exclusive_params += mutually_exclusive
@@ -305,7 +357,9 @@ class AnsibleModuleDockerClient(AnsibleDockerClientBase):
common_args = dict((k, self.module.params[k]) for k in DOCKER_COMMON_ARGS)
super(AnsibleModuleDockerClient, self).__init__(
common_args, min_docker_api_version=min_docker_api_version, needs_api_version=needs_api_version,
common_args,
min_docker_api_version=min_docker_api_version,
needs_api_version=needs_api_version,
)
def call_cli(self, *args, check_rc=False, data=None, cwd=None, environ_update=None):
@@ -333,4 +387,6 @@ class AnsibleModuleDockerClient(AnsibleDockerClientBase):
self.module.warn(msg)
def deprecate(self, msg, version=None, date=None, collection_name=None):
self.module.deprecate(msg, version=version, date=date, collection_name=collection_name)
self.module.deprecate(
msg, version=version, date=date, collection_name=collection_name
)
File diff suppressed because it is too large Load Diff
+174 -84
View File
@@ -15,8 +15,10 @@ import stat
import tarfile
from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
from ansible_collections.community.docker.plugins.module_utils._api.errors import APIError, NotFound
from ansible_collections.community.docker.plugins.module_utils._api.errors import (
APIError,
NotFound,
)
class DockerFileCopyError(Exception):
@@ -34,52 +36,64 @@ class DockerFileNotFound(DockerFileCopyError):
def _put_archive(client, container, path, data):
# data can also be file object for streaming. This is because _put uses requests's put().
# See https://requests.readthedocs.io/en/latest/user/advanced/#streaming-uploads
url = client._url('/containers/{0}/archive', container)
res = client._put(url, params={'path': path}, data=data)
url = client._url("/containers/{0}/archive", container)
res = client._put(url, params={"path": path}, data=data)
client._raise_for_status(res)
return res.status_code == 200
def _symlink_tar_creator(b_in_path, file_stat, out_file, user_id, group_id, mode=None, user_name=None):
def _symlink_tar_creator(
b_in_path, file_stat, out_file, user_id, group_id, mode=None, user_name=None
):
if not stat.S_ISLNK(file_stat.st_mode):
raise DockerUnexpectedError('stat information is not for a symlink')
raise DockerUnexpectedError("stat information is not for a symlink")
bio = io.BytesIO()
with tarfile.open(fileobj=bio, mode='w|', dereference=False, encoding='utf-8') as tar:
with tarfile.open(
fileobj=bio, mode="w|", dereference=False, encoding="utf-8"
) as tar:
# Note that without both name (bytes) and arcname (unicode), this either fails for
# Python 2.7, Python 3.5/3.6, or Python 3.7+. Only when passing both (in this
# form) it works with Python 2.7, 3.5, 3.6, and 3.7 up to 3.11
tarinfo = tar.gettarinfo(b_in_path, arcname=to_text(out_file))
tarinfo.uid = user_id
tarinfo.uname = ''
tarinfo.uname = ""
if user_name:
tarinfo.uname = user_name
tarinfo.gid = group_id
tarinfo.gname = ''
tarinfo.gname = ""
tarinfo.mode &= 0o700
if mode is not None:
tarinfo.mode = mode
if not tarinfo.issym():
raise DockerUnexpectedError('stat information is not for a symlink')
raise DockerUnexpectedError("stat information is not for a symlink")
tar.addfile(tarinfo)
return bio.getvalue()
def _symlink_tar_generator(b_in_path, file_stat, out_file, user_id, group_id, mode=None, user_name=None):
yield _symlink_tar_creator(b_in_path, file_stat, out_file, user_id, group_id, mode, user_name)
def _symlink_tar_generator(
b_in_path, file_stat, out_file, user_id, group_id, mode=None, user_name=None
):
yield _symlink_tar_creator(
b_in_path, file_stat, out_file, user_id, group_id, mode, user_name
)
def _regular_file_tar_generator(b_in_path, file_stat, out_file, user_id, group_id, mode=None, user_name=None):
def _regular_file_tar_generator(
b_in_path, file_stat, out_file, user_id, group_id, mode=None, user_name=None
):
if not stat.S_ISREG(file_stat.st_mode):
raise DockerUnexpectedError('stat information is not for a regular file')
raise DockerUnexpectedError("stat information is not for a regular file")
tarinfo = tarfile.TarInfo()
tarinfo.name = os.path.splitdrive(to_text(out_file))[1].replace(os.sep, '/').lstrip('/')
tarinfo.name = (
os.path.splitdrive(to_text(out_file))[1].replace(os.sep, "/").lstrip("/")
)
tarinfo.mode = (file_stat.st_mode & 0o700) if mode is None else mode
tarinfo.uid = user_id
tarinfo.gid = group_id
tarinfo.size = file_stat.st_size
tarinfo.mtime = file_stat.st_mtime
tarinfo.type = tarfile.REGTYPE
tarinfo.linkname = ''
tarinfo.linkname = ""
if user_name:
tarinfo.uname = user_name
@@ -89,7 +103,7 @@ def _regular_file_tar_generator(b_in_path, file_stat, out_file, user_id, group_i
size = tarinfo.size
total_size += size
with open(b_in_path, 'rb') as f:
with open(b_in_path, "rb") as f:
while size > 0:
to_read = min(size, 65536)
buf = f.read(to_read)
@@ -117,16 +131,20 @@ def _regular_file_tar_generator(b_in_path, file_stat, out_file, user_id, group_i
yield tarfile.NUL * (tarfile.RECORDSIZE - remainder)
def _regular_content_tar_generator(content, out_file, user_id, group_id, mode, user_name=None):
def _regular_content_tar_generator(
content, out_file, user_id, group_id, mode, user_name=None
):
tarinfo = tarfile.TarInfo()
tarinfo.name = os.path.splitdrive(to_text(out_file))[1].replace(os.sep, '/').lstrip('/')
tarinfo.name = (
os.path.splitdrive(to_text(out_file))[1].replace(os.sep, "/").lstrip("/")
)
tarinfo.mode = mode
tarinfo.uid = user_id
tarinfo.gid = group_id
tarinfo.size = len(content)
tarinfo.mtime = int(datetime.datetime.now().timestamp())
tarinfo.type = tarfile.REGTYPE
tarinfo.linkname = ''
tarinfo.linkname = ""
if user_name:
tarinfo.uname = user_name
@@ -152,13 +170,22 @@ def _regular_content_tar_generator(content, out_file, user_id, group_id, mode, u
yield tarfile.NUL * (tarfile.RECORDSIZE - remainder)
def put_file(client, container, in_path, out_path, user_id, group_id, mode=None, user_name=None, follow_links=False):
def put_file(
client,
container,
in_path,
out_path,
user_id,
group_id,
mode=None,
user_name=None,
follow_links=False,
):
"""Transfer a file from local to Docker container."""
if not os.path.exists(to_bytes(in_path, errors='surrogate_or_strict')):
raise DockerFileNotFound(
f"file or module does not exist: {to_native(in_path)}")
if not os.path.exists(to_bytes(in_path, errors="surrogate_or_strict")):
raise DockerFileNotFound(f"file or module does not exist: {to_native(in_path)}")
b_in_path = to_bytes(in_path, errors='surrogate_or_strict')
b_in_path = to_bytes(in_path, errors="surrogate_or_strict")
out_dir, out_file = os.path.split(out_path)
@@ -168,28 +195,53 @@ def put_file(client, container, in_path, out_path, user_id, group_id, mode=None,
file_stat = os.lstat(b_in_path)
if stat.S_ISREG(file_stat.st_mode):
stream = _regular_file_tar_generator(b_in_path, file_stat, out_file, user_id, group_id, mode=mode, user_name=user_name)
stream = _regular_file_tar_generator(
b_in_path,
file_stat,
out_file,
user_id,
group_id,
mode=mode,
user_name=user_name,
)
elif stat.S_ISLNK(file_stat.st_mode):
stream = _symlink_tar_generator(b_in_path, file_stat, out_file, user_id, group_id, mode=mode, user_name=user_name)
stream = _symlink_tar_generator(
b_in_path,
file_stat,
out_file,
user_id,
group_id,
mode=mode,
user_name=user_name,
)
else:
file_part = ' referenced by' if follow_links else ''
file_part = " referenced by" if follow_links else ""
raise DockerFileCopyError(
f'File{file_part} {in_path} is neither a regular file nor a symlink (stat mode {oct(file_stat.st_mode)}).')
f"File{file_part} {in_path} is neither a regular file nor a symlink (stat mode {oct(file_stat.st_mode)})."
)
ok = _put_archive(client, container, out_dir, stream)
if not ok:
raise DockerUnexpectedError(f'Unknown error while creating file "{out_path}" in container "{container}".')
raise DockerUnexpectedError(
f'Unknown error while creating file "{out_path}" in container "{container}".'
)
def put_file_content(client, container, content, out_path, user_id, group_id, mode, user_name=None):
def put_file_content(
client, container, content, out_path, user_id, group_id, mode, user_name=None
):
"""Transfer a file from local to Docker container."""
out_dir, out_file = os.path.split(out_path)
stream = _regular_content_tar_generator(content, out_file, user_id, group_id, mode, user_name=user_name)
stream = _regular_content_tar_generator(
content, out_file, user_id, group_id, mode, user_name=user_name
)
ok = _put_archive(client, container, out_dir, stream)
if not ok:
raise DockerUnexpectedError(f'Unknown error while creating file "{out_path}" in container "{container}".')
raise DockerUnexpectedError(
f'Unknown error while creating file "{out_path}" in container "{container}".'
)
def stat_file(client, container, in_path, follow_links=False, log=None):
@@ -208,30 +260,32 @@ def stat_file(client, container, in_path, follow_links=False, log=None):
while True:
if in_path in considered_in_paths:
raise DockerFileCopyError(f'Found infinite symbolic link loop when trying to stating "{in_path}"')
raise DockerFileCopyError(
f'Found infinite symbolic link loop when trying to stating "{in_path}"'
)
considered_in_paths.add(in_path)
if log:
log(f'FETCH: Stating "{in_path}"')
response = client._head(
client._url('/containers/{0}/archive', container),
params={'path': in_path},
client._url("/containers/{0}/archive", container),
params={"path": in_path},
)
if response.status_code == 404:
return in_path, None, None
client._raise_for_status(response)
header = response.headers.get('x-docker-container-path-stat')
header = response.headers.get("x-docker-container-path-stat")
try:
stat_data = json.loads(base64.b64decode(header))
except Exception as exc:
raise DockerUnexpectedError(
f'When retrieving information for {in_path} from {container}, obtained header {header!r} that cannot be loaded as JSON: {exc}'
f"When retrieving information for {in_path} from {container}, obtained header {header!r} that cannot be loaded as JSON: {exc}"
)
# https://pkg.go.dev/io/fs#FileMode: bit 32 - 5 means ModeSymlink
if stat_data['mode'] & (1 << (32 - 5)) != 0:
link_target = stat_data['linkTarget']
if stat_data["mode"] & (1 << (32 - 5)) != 0:
link_target = stat_data["linkTarget"]
if not follow_links:
return in_path, stat_data, link_target
in_path = os.path.join(os.path.split(in_path)[0], link_target)
@@ -243,7 +297,7 @@ def stat_file(client, container, in_path, follow_links=False, log=None):
class _RawGeneratorFileobj(io.RawIOBase):
def __init__(self, stream):
self._stream = stream
self._buf = b''
self._buf = b""
def readable(self):
return True
@@ -251,7 +305,7 @@ class _RawGeneratorFileobj(io.RawIOBase):
def _readinto_from_buf(self, b, index, length):
cpy = min(length - index, len(self._buf))
if cpy:
b[index:index + cpy] = self._buf[:cpy]
b[index : index + cpy] = self._buf[:cpy]
self._buf = self._buf[cpy:]
index += cpy
return index
@@ -273,38 +327,55 @@ class _RawGeneratorFileobj(io.RawIOBase):
def _stream_generator_to_fileobj(stream):
'''Given a generator that generates chunks of bytes, create a readable buffered stream.'''
"""Given a generator that generates chunks of bytes, create a readable buffered stream."""
raw = _RawGeneratorFileobj(stream)
return io.BufferedReader(raw)
def fetch_file_ex(client, container, in_path, process_none, process_regular, process_symlink, process_other, follow_links=False, log=None):
def fetch_file_ex(
client,
container,
in_path,
process_none,
process_regular,
process_symlink,
process_other,
follow_links=False,
log=None,
):
"""Fetch a file (as a tar file entry) from a Docker container to local."""
considered_in_paths = set()
while True:
if in_path in considered_in_paths:
raise DockerFileCopyError(f'Found infinite symbolic link loop when trying to fetch "{in_path}"')
raise DockerFileCopyError(
f'Found infinite symbolic link loop when trying to fetch "{in_path}"'
)
considered_in_paths.add(in_path)
if log:
log(f'FETCH: Fetching "{in_path}"')
try:
stream = client.get_raw_stream(
'/containers/{0}/archive', container,
params={'path': in_path},
headers={'Accept-Encoding': 'identity'},
"/containers/{0}/archive",
container,
params={"path": in_path},
headers={"Accept-Encoding": "identity"},
)
except NotFound:
return process_none(in_path)
with tarfile.open(fileobj=_stream_generator_to_fileobj(stream), mode='r|') as tar:
with tarfile.open(
fileobj=_stream_generator_to_fileobj(stream), mode="r|"
) as tar:
symlink_member = None
result = None
found = False
for member in tar:
if found:
raise DockerUnexpectedError('Received tarfile contains more than one file!')
raise DockerUnexpectedError(
"Received tarfile contains more than one file!"
)
found = True
if member.issym():
symlink_member = member
@@ -316,21 +387,23 @@ def fetch_file_ex(client, container, in_path, process_none, process_regular, pro
if symlink_member:
if not follow_links:
return process_symlink(in_path, symlink_member)
in_path = os.path.join(os.path.split(in_path)[0], symlink_member.linkname)
in_path = os.path.join(
os.path.split(in_path)[0], symlink_member.linkname
)
if log:
log(f'FETCH: Following symbolic link to "{in_path}"')
continue
if found:
return result
raise DockerUnexpectedError('Received tarfile is empty!')
raise DockerUnexpectedError("Received tarfile is empty!")
def fetch_file(client, container, in_path, out_path, follow_links=False, log=None):
b_out_path = to_bytes(out_path, errors='surrogate_or_strict')
b_out_path = to_bytes(out_path, errors="surrogate_or_strict")
def process_none(in_path):
raise DockerFileNotFound(
f'File {in_path} does not exist in container {container}'
f"File {in_path} does not exist in container {container}"
)
def process_regular(in_path, tar, member):
@@ -338,7 +411,7 @@ def fetch_file(client, container, in_path, out_path, follow_links=False, log=Non
os.unlink(b_out_path)
with tar.extractfile(member) as in_f:
with open(b_out_path, 'wb') as out_f:
with open(b_out_path, "wb") as out_f:
shutil.copyfileobj(in_f, out_f)
return in_path
@@ -350,56 +423,71 @@ def fetch_file(client, container, in_path, out_path, follow_links=False, log=Non
return in_path
def process_other(in_path, member):
raise DockerFileCopyError(f'Remote file "{in_path}" is not a regular file or a symbolic link')
raise DockerFileCopyError(
f'Remote file "{in_path}" is not a regular file or a symbolic link'
)
return fetch_file_ex(client, container, in_path, process_none, process_regular, process_symlink, process_other, follow_links=follow_links, log=log)
return fetch_file_ex(
client,
container,
in_path,
process_none,
process_regular,
process_symlink,
process_other,
follow_links=follow_links,
log=log,
)
def _execute_command(client, container, command, log=None, check_rc=False):
if log:
log(f'Executing {command} in {container}')
log(f"Executing {command} in {container}")
data = {
'Container': container,
'User': '',
'Privileged': False,
'Tty': False,
'AttachStdin': False,
'AttachStdout': True,
'AttachStderr': True,
'Cmd': command,
"Container": container,
"User": "",
"Privileged": False,
"Tty": False,
"AttachStdin": False,
"AttachStdout": True,
"AttachStderr": True,
"Cmd": command,
}
if 'detachKeys' in client._general_configs:
data['detachKeys'] = client._general_configs['detachKeys']
if "detachKeys" in client._general_configs:
data["detachKeys"] = client._general_configs["detachKeys"]
try:
exec_data = client.post_json_to_json('/containers/{0}/exec', container, data=data)
exec_data = client.post_json_to_json(
"/containers/{0}/exec", container, data=data
)
except NotFound as e:
raise DockerFileCopyError(f'Could not find container "{container}"') from e
except APIError as e:
if e.response is not None and e.response.status_code == 409:
raise DockerFileCopyError(f'Cannot execute command in paused container "{container}"') from e
raise DockerFileCopyError(
f'Cannot execute command in paused container "{container}"'
) from e
raise
exec_id = exec_data['Id']
exec_id = exec_data["Id"]
data = {
'Tty': False,
'Detach': False
}
stdout, stderr = client.post_json_to_stream('/exec/{0}/start', exec_id, stream=False, demux=True, tty=False)
data = {"Tty": False, "Detach": False}
stdout, stderr = client.post_json_to_stream(
"/exec/{0}/start", exec_id, stream=False, demux=True, tty=False
)
result = client.get_json('/exec/{0}/json', exec_id)
result = client.get_json("/exec/{0}/json", exec_id)
rc = result.get('ExitCode') or 0
stdout = stdout or b''
stderr = stderr or b''
rc = result.get("ExitCode") or 0
stdout = stdout or b""
stderr = stderr or b""
if log:
log(f'Exit code {rc}, stdout {stdout!r}, stderr {stderr!r}')
log(f"Exit code {rc}, stdout {stdout!r}, stderr {stderr!r}")
if check_rc and rc != 0:
command_str = ' '.join(command)
command_str = " ".join(command)
raise DockerUnexpectedError(
f'Obtained unexpected exit code {rc} when running "{command_str}" in {container}.\nSTDOUT: {stdout}\nSTDERR: {stderr}'
)
@@ -408,12 +496,14 @@ def _execute_command(client, container, command, log=None, check_rc=False):
def determine_user_group(client, container, log=None):
dummy, stdout, stderr = _execute_command(client, container, ['/bin/sh', '-c', 'id -u && id -g'], check_rc=True, log=log)
dummy, stdout, stderr = _execute_command(
client, container, ["/bin/sh", "-c", "id -u && id -g"], check_rc=True, log=log
)
stdout_lines = stdout.splitlines()
if len(stdout_lines) != 2:
raise DockerUnexpectedError(
f'Expected two-line output to obtain user and group ID for container {container}, but got {len(stdout_lines)} lines:\n{stdout}'
f"Expected two-line output to obtain user and group ID for container {container}, but got {len(stdout_lines)} lines:\n{stdout}"
)
user_id, group_id = stdout_lines
+25 -24
View File
@@ -10,18 +10,18 @@ import tarfile
class ImageArchiveManifestSummary(object):
'''
"""
Represents data extracted from a manifest.json found in the tar archive output of the
"docker image save some:tag > some.tar" command.
'''
"""
def __init__(self, image_id, repo_tags):
'''
"""
:param image_id: File name portion of Config entry, e.g. abcde12345 from abcde12345.json
:type image_id: str
:param repo_tags Docker image names, e.g. ["hello-world:latest"]
:type repo_tags: list[str]
'''
"""
self.image_id = image_id
self.repo_tags = repo_tags
@@ -32,7 +32,7 @@ class ImageArchiveInvalidException(Exception):
def api_image_id(archive_image_id):
'''
"""
Accepts an image hash in the format stored in manifest.json, and returns an equivalent identifier
that represents the same image hash, but in the format presented by the Docker Engine API.
@@ -41,13 +41,13 @@ def api_image_id(archive_image_id):
:returns: Prefixed hash used by REST api
:rtype: str
'''
"""
return f'sha256:{archive_image_id}'
return f"sha256:{archive_image_id}"
def load_archived_image_manifest(archive_path):
'''
"""
Attempts to get image IDs and image names from metadata stored in the image
archive tar file.
@@ -63,17 +63,17 @@ def load_archived_image_manifest(archive_path):
:return: None, if no file at archive_path, or a list of ImageArchiveManifestSummary objects.
:rtype: ImageArchiveManifestSummary
'''
"""
try:
# FileNotFoundError does not exist in Python 2
if not os.path.isfile(archive_path):
return None
with tarfile.open(archive_path, 'r') as tf:
with tarfile.open(archive_path, "r") as tf:
try:
try:
with tf.extractfile('manifest.json') as ef:
with tf.extractfile("manifest.json") as ef:
manifest = json.load(ef)
except Exception as exc:
raise ImageArchiveInvalidException(
@@ -88,7 +88,7 @@ def load_archived_image_manifest(archive_path):
result = []
for index, meta in enumerate(manifest):
try:
config_file = meta['Config']
config_file = meta["Config"]
except KeyError as exc:
raise ImageArchiveInvalidException(
f"Failed to get Config entry from {index + 1}th manifest in manifest.json: {exc}"
@@ -103,23 +103,22 @@ def load_archived_image_manifest(archive_path):
f"Failed to extract image id from config file name {config_file}: {exc}"
) from exc
for prefix in (
'blobs/sha256/', # Moby 25.0.0, Docker API 1.44
):
for prefix in ("blobs/sha256/",): # Moby 25.0.0, Docker API 1.44
if image_id.startswith(prefix):
image_id = image_id[len(prefix):]
image_id = image_id[len(prefix) :]
try:
repo_tags = meta['RepoTags']
repo_tags = meta["RepoTags"]
except KeyError as exc:
raise ImageArchiveInvalidException(
f"Failed to get RepoTags entry from {index + 1}th manifest in manifest.json: {exc}"
) from exc
result.append(ImageArchiveManifestSummary(
image_id=image_id,
repo_tags=repo_tags
))
result.append(
ImageArchiveManifestSummary(
image_id=image_id, repo_tags=repo_tags
)
)
return result
except ImageArchiveInvalidException:
@@ -132,11 +131,13 @@ def load_archived_image_manifest(archive_path):
except ImageArchiveInvalidException:
raise
except Exception as exc:
raise ImageArchiveInvalidException(f"Failed to open tar file {archive_path}: {exc}") from exc
raise ImageArchiveInvalidException(
f"Failed to open tar file {archive_path}: {exc}"
) from exc
def archived_image_manifest(archive_path):
'''
"""
Attempts to get Image.Id and image name from metadata stored in the image
archive tar file.
@@ -152,7 +153,7 @@ def archived_image_manifest(archive_path):
:return: None, if no file at archive_path, or the extracted image ID, which will not have a sha256: prefix.
:rtype: ImageArchiveManifestSummary
'''
"""
results = load_archived_image_manifest(archive_path)
if results is None:
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-1
View File
@@ -8,5 +8,4 @@
from __future__ import annotations
import selectors # noqa: F401, pylint: disable=unused-import
+31 -21
View File
@@ -4,14 +4,14 @@
from __future__ import annotations
import os
import os.path
import socket as pysocket
import struct
from ansible_collections.community.docker.plugins.module_utils._api.utils import socket as docker_socket
from ansible_collections.community.docker.plugins.module_utils._api.utils import (
socket as docker_socket,
)
from ansible_collections.community.docker.plugins.module_utils.socket_helper import (
make_unblocking,
shutdown_writing,
@@ -31,19 +31,21 @@ class DockerSocketHandlerBase(object):
self._log = log
else:
self._log = lambda msg: True
self._paramiko_read_workaround = hasattr(sock, 'send_ready') and 'paramiko' in str(type(sock))
self._paramiko_read_workaround = hasattr(
sock, "send_ready"
) and "paramiko" in str(type(sock))
self._sock = sock
self._block_done_callback = None
self._block_buffer = []
self._eof = False
self._read_buffer = b''
self._write_buffer = b''
self._read_buffer = b""
self._write_buffer = b""
self._end_of_writing = False
self._current_stream = None
self._current_missing = 0
self._current_buffer = b''
self._current_buffer = b""
self._selector = self._selectors.DefaultSelector()
self._selector.register(self._sock, self._selectors.EVENT_READ)
@@ -70,26 +72,26 @@ class DockerSocketHandlerBase(object):
def _read(self):
if self._eof:
return
if hasattr(self._sock, 'recv'):
if hasattr(self._sock, "recv"):
try:
data = self._sock.recv(262144)
except Exception as e:
# After calling self._sock.shutdown(), OpenSSL's/urllib3's
# WrappedSocket seems to eventually raise ZeroReturnError in
# case of EOF
if 'OpenSSL.SSL.ZeroReturnError' in str(type(e)):
if "OpenSSL.SSL.ZeroReturnError" in str(type(e)):
self._eof = True
return
else:
raise
elif isinstance(self._sock, getattr(pysocket, 'SocketIO')):
elif isinstance(self._sock, getattr(pysocket, "SocketIO")):
data = self._sock.read()
else:
data = os.read(self._sock.fileno())
if data is None:
# no data available
return
self._log(f'read {len(data)} bytes')
self._log(f"read {len(data)} bytes")
if len(data) == 0:
# Stream EOF
self._eof = True
@@ -103,10 +105,12 @@ class DockerSocketHandlerBase(object):
self._current_missing -= n
if self._current_missing == 0:
self._add_block(self._current_stream, self._current_buffer)
self._current_buffer = b''
self._current_buffer = b""
if len(self._read_buffer) < 8:
break
self._current_stream, self._current_missing = struct.unpack('>BxxxL', self._read_buffer[:8])
self._current_stream, self._current_missing = struct.unpack(
">BxxxL", self._read_buffer[:8]
)
self._read_buffer = self._read_buffer[8:]
if self._current_missing < 0:
# Stream EOF (as reported by docker daemon)
@@ -116,22 +120,28 @@ class DockerSocketHandlerBase(object):
def _handle_end_of_writing(self):
if self._end_of_writing and len(self._write_buffer) == 0:
self._end_of_writing = False
self._log('Shutting socket down for writing')
self._log("Shutting socket down for writing")
shutdown_writing(self._sock, self._log)
def _write(self):
if len(self._write_buffer) > 0:
written = write_to_socket(self._sock, self._write_buffer)
self._write_buffer = self._write_buffer[written:]
self._log(f'wrote {written} bytes, {len(self._write_buffer)} are left')
self._log(f"wrote {written} bytes, {len(self._write_buffer)} are left")
if len(self._write_buffer) > 0:
self._selector.modify(self._sock, self._selectors.EVENT_READ | self._selectors.EVENT_WRITE)
self._selector.modify(
self._sock, self._selectors.EVENT_READ | self._selectors.EVENT_WRITE
)
else:
self._selector.modify(self._sock, self._selectors.EVENT_READ)
self._handle_end_of_writing()
def select(self, timeout=None, _internal_recursion=False):
if not _internal_recursion and self._paramiko_read_workaround and len(self._write_buffer) > 0:
if (
not _internal_recursion
and self._paramiko_read_workaround
and len(self._write_buffer) > 0
):
# When the SSH transport is used, Docker SDK for Python internally uses Paramiko, whose
# Channel object supports select(), but only for reading
# (https://github.com/paramiko/paramiko/issues/695).
@@ -147,13 +157,13 @@ class DockerSocketHandlerBase(object):
return True
if timeout is not None:
timeout -= PARAMIKO_POLL_TIMEOUT
self._log(f'select... ({timeout})')
self._log(f"select... ({timeout})")
events = self._selector.select(timeout)
for key, event in events:
if key.fileobj == self._sock:
ev_read = event & self._selectors.EVENT_READ != 0
ev_write = event & self._selectors.EVENT_WRITE != 0
self._log(f'select event read:{ev_read} write:{ev_write}')
self._log(f"select event read:{ev_read} write:{ev_write}")
if event & self._selectors.EVENT_READ != 0:
self._read()
if event & self._selectors.EVENT_WRITE != 0:
@@ -182,14 +192,14 @@ class DockerSocketHandlerBase(object):
elif stream_id == docker_socket.STDERR:
stderr.append(data)
else:
raise ValueError(f'{stream_id} is not a valid stream ID')
raise ValueError(f"{stream_id} is not a valid stream ID")
self.end_of_writing()
self.set_block_done_callback(append_block)
while not self._eof:
self.select()
return b''.join(stdout), b''.join(stderr)
return b"".join(stdout), b"".join(stderr)
def write(self, str):
self._write_buffer += str
+19 -12
View File
@@ -4,7 +4,6 @@
from __future__ import annotations
import fcntl
import os
import os.path
@@ -12,17 +11,25 @@ import socket as pysocket
def make_file_unblocking(file):
fcntl.fcntl(file.fileno(), fcntl.F_SETFL, fcntl.fcntl(file.fileno(), fcntl.F_GETFL) | os.O_NONBLOCK)
fcntl.fcntl(
file.fileno(),
fcntl.F_SETFL,
fcntl.fcntl(file.fileno(), fcntl.F_GETFL) | os.O_NONBLOCK,
)
def make_file_blocking(file):
fcntl.fcntl(file.fileno(), fcntl.F_SETFL, fcntl.fcntl(file.fileno(), fcntl.F_GETFL) & ~os.O_NONBLOCK)
fcntl.fcntl(
file.fileno(),
fcntl.F_SETFL,
fcntl.fcntl(file.fileno(), fcntl.F_GETFL) & ~os.O_NONBLOCK,
)
def make_unblocking(sock):
if hasattr(sock, '_sock'):
if hasattr(sock, "_sock"):
sock._sock.setblocking(0)
elif hasattr(sock, 'setblocking'):
elif hasattr(sock, "setblocking"):
sock.setblocking(0)
else:
make_file_unblocking(sock)
@@ -37,27 +44,27 @@ def shutdown_writing(sock, log=_empty_writer):
# a close_notify TLS alert without completely shutting down the connection.
# Calling sock.shutdown(pysocket.SHUT_WR) simply turns of TLS encryption and from that
# point on the raw encrypted data is returned when sock.recv() is called. :-(
if hasattr(sock, 'shutdown_write'):
if hasattr(sock, "shutdown_write"):
sock.shutdown_write()
elif hasattr(sock, 'shutdown'):
elif hasattr(sock, "shutdown"):
try:
sock.shutdown(pysocket.SHUT_WR)
except TypeError as e:
# probably: "TypeError: shutdown() takes 1 positional argument but 2 were given"
log(f'Shutting down for writing not possible; trying shutdown instead: {e}')
log(f"Shutting down for writing not possible; trying shutdown instead: {e}")
sock.shutdown()
elif isinstance(sock, getattr(pysocket, 'SocketIO')):
elif isinstance(sock, getattr(pysocket, "SocketIO")):
sock._sock.shutdown(pysocket.SHUT_WR)
else:
log('No idea how to signal end of writing')
log("No idea how to signal end of writing")
def write_to_socket(sock, data):
if hasattr(sock, '_send_until_done'):
if hasattr(sock, "_send_until_done"):
# WrappedSocket (urllib3/contrib/pyopenssl) does not have `send`, but
# only `sendall`, which uses `_send_until_done` under the hood.
return sock._send_until_done(data)
elif hasattr(sock, 'send'):
elif hasattr(sock, "send"):
return sock.send(data)
else:
return os.write(sock.fileno(), data)
+57 -35
View File
@@ -5,19 +5,22 @@
from __future__ import annotations
import json
from time import sleep
try:
from docker.errors import APIError, NotFound
except ImportError:
# missing Docker SDK for Python handled in ansible.module_utils.docker.common
pass
from ansible_collections.community.docker.plugins.module_utils.version import LooseVersion
from ansible_collections.community.docker.plugins.module_utils.common import AnsibleDockerClient
from ansible_collections.community.docker.plugins.module_utils.common import (
AnsibleDockerClient,
)
from ansible_collections.community.docker.plugins.module_utils.version import (
LooseVersion,
)
class AnsibleDockerSwarmClient(AnsibleDockerClient):
@@ -41,8 +44,8 @@ class AnsibleDockerSwarmClient(AnsibleDockerClient):
if info:
json_str = json.dumps(info, ensure_ascii=False)
swarm_info = json.loads(json_str)
if swarm_info['Swarm']['NodeID']:
return swarm_info['Swarm']['NodeID']
if swarm_info["Swarm"]["NodeID"]:
return swarm_info["Swarm"]["NodeID"]
return None
def check_if_swarm_node(self, node_id=None):
@@ -66,9 +69,13 @@ class AnsibleDockerSwarmClient(AnsibleDockerClient):
if info:
json_str = json.dumps(info, ensure_ascii=False)
swarm_info = json.loads(json_str)
if swarm_info['Swarm']['NodeID']:
if swarm_info["Swarm"]["NodeID"]:
return True
if swarm_info['Swarm']['LocalNodeState'] in ('active', 'pending', 'locked'):
if swarm_info["Swarm"]["LocalNodeState"] in (
"active",
"pending",
"locked",
):
return True
return False
else:
@@ -77,7 +84,7 @@ class AnsibleDockerSwarmClient(AnsibleDockerClient):
except APIError:
return
if node_info['ID'] is not None:
if node_info["ID"] is not None:
return True
return False
@@ -100,7 +107,9 @@ class AnsibleDockerSwarmClient(AnsibleDockerClient):
If host is not a swarm manager then Ansible task on this host should end with 'failed' state
"""
if not self.check_if_swarm_manager():
self.fail("Error running docker swarm module: must run on swarm manager node")
self.fail(
"Error running docker swarm module: must run on swarm manager node"
)
def check_if_swarm_worker(self):
"""
@@ -136,7 +145,7 @@ class AnsibleDockerSwarmClient(AnsibleDockerClient):
if retry > 0:
sleep(5)
node_info = self.get_node_inspect(node_id=node_id)
if node_info['Status']['State'] == 'down':
if node_info["Status"]["State"] == "down":
return True
return False
@@ -160,7 +169,9 @@ class AnsibleDockerSwarmClient(AnsibleDockerClient):
node_info = self.inspect_node(node_id=node_id)
except APIError as exc:
if exc.status_code == 503:
self.fail("Cannot inspect node: To inspect node execute module on Swarm Manager")
self.fail(
"Cannot inspect node: To inspect node execute module on Swarm Manager"
)
if exc.status_code == 404:
if skip_missing:
return None
@@ -171,16 +182,19 @@ class AnsibleDockerSwarmClient(AnsibleDockerClient):
json_str = json.dumps(node_info, ensure_ascii=False)
node_info = json.loads(json_str)
if 'ManagerStatus' in node_info:
if node_info['ManagerStatus'].get('Leader'):
if "ManagerStatus" in node_info:
if node_info["ManagerStatus"].get("Leader"):
# This is workaround of bug in Docker when in some cases the Leader IP is 0.0.0.0
# Check moby/moby#35437 for details
count_colons = node_info['ManagerStatus']['Addr'].count(":")
count_colons = node_info["ManagerStatus"]["Addr"].count(":")
if count_colons == 1:
swarm_leader_ip = node_info['ManagerStatus']['Addr'].split(":", 1)[0] or node_info['Status']['Addr']
swarm_leader_ip = (
node_info["ManagerStatus"]["Addr"].split(":", 1)[0]
or node_info["Status"]["Addr"]
)
else:
swarm_leader_ip = node_info['Status']['Addr']
node_info['Status']['Addr'] = swarm_leader_ip
swarm_leader_ip = node_info["Status"]["Addr"]
node_info["Status"]["Addr"] = swarm_leader_ip
return node_info
def get_all_nodes_inspect(self):
@@ -194,7 +208,9 @@ class AnsibleDockerSwarmClient(AnsibleDockerClient):
node_info = self.nodes()
except APIError as exc:
if exc.status_code == 503:
self.fail("Cannot inspect node: To inspect node execute module on Swarm Manager")
self.fail(
"Cannot inspect node: To inspect node execute module on Swarm Manager"
)
self.fail(f"Error while reading from Swarm manager: {exc}")
except Exception as exc:
self.fail(f"Error inspecting swarm node: {exc}")
@@ -203,7 +219,7 @@ class AnsibleDockerSwarmClient(AnsibleDockerClient):
node_info = json.loads(json_str)
return node_info
def get_all_nodes_list(self, output='short'):
def get_all_nodes_list(self, output="short"):
"""
Returns list of nodes registered in Swarm
@@ -219,22 +235,26 @@ class AnsibleDockerSwarmClient(AnsibleDockerClient):
if nodes_inspect is None:
return None
if output == 'short':
if output == "short":
for node in nodes_inspect:
nodes_list.append(node['Description']['Hostname'])
elif output == 'long':
nodes_list.append(node["Description"]["Hostname"])
elif output == "long":
for node in nodes_inspect:
node_property = {}
node_property.update({'ID': node['ID']})
node_property.update({'Hostname': node['Description']['Hostname']})
node_property.update({'Status': node['Status']['State']})
node_property.update({'Availability': node['Spec']['Availability']})
if 'ManagerStatus' in node:
if node['ManagerStatus']['Leader'] is True:
node_property.update({'Leader': True})
node_property.update({'ManagerStatus': node['ManagerStatus']['Reachability']})
node_property.update({'EngineVersion': node['Description']['Engine']['EngineVersion']})
node_property.update({"ID": node["ID"]})
node_property.update({"Hostname": node["Description"]["Hostname"]})
node_property.update({"Status": node["Status"]["State"]})
node_property.update({"Availability": node["Spec"]["Availability"]})
if "ManagerStatus" in node:
if node["ManagerStatus"]["Leader"] is True:
node_property.update({"Leader": True})
node_property.update(
{"ManagerStatus": node["ManagerStatus"]["Reachability"]}
)
node_property.update(
{"EngineVersion": node["Description"]["Engine"]["EngineVersion"]}
)
nodes_list.append(node_property)
else:
@@ -243,10 +263,10 @@ class AnsibleDockerSwarmClient(AnsibleDockerClient):
return nodes_list
def get_node_name_by_id(self, nodeid):
return self.get_node_inspect(nodeid)['Description']['Hostname']
return self.get_node_inspect(nodeid)["Description"]["Hostname"]
def get_unlock_key(self):
if self.docker_py_version < LooseVersion('2.7.0'):
if self.docker_py_version < LooseVersion("2.7.0"):
return None
return super(AnsibleDockerSwarmClient, self).get_unlock_key()
@@ -268,7 +288,9 @@ class AnsibleDockerSwarmClient(AnsibleDockerClient):
return None
except APIError as exc:
if exc.status_code == 503:
self.fail("Cannot inspect service: To inspect service execute module on Swarm Manager")
self.fail(
"Cannot inspect service: To inspect service execute module on Swarm Manager"
)
self.fail(f"Error inspecting swarm service: {exc}")
except Exception as exc:
self.fail(f"Error inspecting swarm service: {exc}")
+134 -93
View File
@@ -4,7 +4,6 @@
from __future__ import annotations
import json
import re
from datetime import timedelta
@@ -15,45 +14,64 @@ from ansible.module_utils.common.collections import is_sequence
from ansible.module_utils.common.text.converters import to_text
DEFAULT_DOCKER_HOST = 'unix:///var/run/docker.sock'
DEFAULT_DOCKER_HOST = "unix:///var/run/docker.sock"
DEFAULT_TLS = False
DEFAULT_TLS_VERIFY = False
DEFAULT_TLS_HOSTNAME = 'localhost' # deprecated
DEFAULT_TLS_HOSTNAME = "localhost" # deprecated
DEFAULT_TIMEOUT_SECONDS = 60
DOCKER_COMMON_ARGS = dict(
docker_host=dict(type='str', default=DEFAULT_DOCKER_HOST, fallback=(env_fallback, ['DOCKER_HOST']), aliases=['docker_url']),
tls_hostname=dict(type='str', fallback=(env_fallback, ['DOCKER_TLS_HOSTNAME'])),
api_version=dict(type='str', default='auto', fallback=(env_fallback, ['DOCKER_API_VERSION']), aliases=['docker_api_version']),
timeout=dict(type='int', default=DEFAULT_TIMEOUT_SECONDS, fallback=(env_fallback, ['DOCKER_TIMEOUT'])),
ca_path=dict(type='path', aliases=['ca_cert', 'tls_ca_cert', 'cacert_path']),
client_cert=dict(type='path', aliases=['tls_client_cert', 'cert_path']),
client_key=dict(type='path', aliases=['tls_client_key', 'key_path']),
tls=dict(type='bool', default=DEFAULT_TLS, fallback=(env_fallback, ['DOCKER_TLS'])),
use_ssh_client=dict(type='bool', default=False),
validate_certs=dict(type='bool', default=DEFAULT_TLS_VERIFY, fallback=(env_fallback, ['DOCKER_TLS_VERIFY']), aliases=['tls_verify']),
debug=dict(type='bool', default=False)
docker_host=dict(
type="str",
default=DEFAULT_DOCKER_HOST,
fallback=(env_fallback, ["DOCKER_HOST"]),
aliases=["docker_url"],
),
tls_hostname=dict(type="str", fallback=(env_fallback, ["DOCKER_TLS_HOSTNAME"])),
api_version=dict(
type="str",
default="auto",
fallback=(env_fallback, ["DOCKER_API_VERSION"]),
aliases=["docker_api_version"],
),
timeout=dict(
type="int",
default=DEFAULT_TIMEOUT_SECONDS,
fallback=(env_fallback, ["DOCKER_TIMEOUT"]),
),
ca_path=dict(type="path", aliases=["ca_cert", "tls_ca_cert", "cacert_path"]),
client_cert=dict(type="path", aliases=["tls_client_cert", "cert_path"]),
client_key=dict(type="path", aliases=["tls_client_key", "key_path"]),
tls=dict(type="bool", default=DEFAULT_TLS, fallback=(env_fallback, ["DOCKER_TLS"])),
use_ssh_client=dict(type="bool", default=False),
validate_certs=dict(
type="bool",
default=DEFAULT_TLS_VERIFY,
fallback=(env_fallback, ["DOCKER_TLS_VERIFY"]),
aliases=["tls_verify"],
),
debug=dict(type="bool", default=False),
)
DOCKER_COMMON_ARGS_VARS = dict([
[option_name, f'ansible_docker_{option_name}']
for option_name in DOCKER_COMMON_ARGS
if option_name != 'debug'
])
DOCKER_COMMON_ARGS_VARS = dict(
[
[option_name, f"ansible_docker_{option_name}"]
for option_name in DOCKER_COMMON_ARGS
if option_name != "debug"
]
)
DOCKER_MUTUALLY_EXCLUSIVE = []
DOCKER_REQUIRED_TOGETHER = [
['client_cert', 'client_key']
]
DOCKER_REQUIRED_TOGETHER = [["client_cert", "client_key"]]
DEFAULT_DOCKER_REGISTRY = 'https://index.docker.io/v1/'
BYTE_SUFFIXES = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']
DEFAULT_DOCKER_REGISTRY = "https://index.docker.io/v1/"
BYTE_SUFFIXES = ["B", "KB", "MB", "GB", "TB", "PB"]
def is_image_name_id(name):
"""Check whether the given image name is in fact an image ID (hash)."""
if re.match('^sha256:[0-9a-fA-F]{64}$', name):
if re.match("^sha256:[0-9a-fA-F]{64}$", name):
return True
return False
@@ -64,7 +82,7 @@ def is_valid_tag(tag, allow_empty=False):
return allow_empty
# See here ("Extended description") for a definition what tags can be:
# https://docs.docker.com/engine/reference/commandline/tag/
return bool(re.match('^[a-zA-Z0-9_][a-zA-Z0-9_.-]{0,127}$', tag))
return bool(re.match("^[a-zA-Z0-9_][a-zA-Z0-9_.-]{0,127}$", tag))
def sanitize_result(data):
@@ -90,10 +108,12 @@ def log_debug(msg, pretty_print=False):
If ``pretty_print=True``, the message will be pretty-printed as JSON.
"""
with open('docker.log', 'a') as log_file:
with open("docker.log", "a") as log_file:
if pretty_print:
log_file.write(json.dumps(msg, sort_keys=True, indent=4, separators=(',', ': ')))
log_file.write('\n')
log_file.write(
json.dumps(msg, sort_keys=True, indent=4, separators=(",", ": "))
)
log_file.write("\n")
else:
log_file.write(f"{msg}\n")
@@ -108,17 +128,19 @@ class DockerBaseClass(object):
# log_debug(msg, pretty_print=pretty_print)
def update_tls_hostname(result, old_behavior=False, deprecate_function=None, uses_tls=True):
if result['tls_hostname'] is None:
def update_tls_hostname(
result, old_behavior=False, deprecate_function=None, uses_tls=True
):
if result["tls_hostname"] is None:
# get default machine name from the url
parsed_url = urlparse(result['docker_host'])
result['tls_hostname'] = parsed_url.netloc.rsplit(':', 1)[0]
parsed_url = urlparse(result["docker_host"])
result["tls_hostname"] = parsed_url.netloc.rsplit(":", 1)[0]
def compare_dict_allow_more_present(av, bv):
'''
"""
Compare two dictionaries for whether every entry of the first is in the second.
'''
"""
for key, value in av.items():
if key not in bv:
return False
@@ -128,7 +150,7 @@ def compare_dict_allow_more_present(av, bv):
def compare_generic(a, b, method, datatype):
'''
"""
Compare values a and b as described by method and datatype.
Returns ``True`` if the values compare equal, and ``False`` if not.
@@ -151,8 +173,8 @@ def compare_generic(a, b, method, datatype):
not matter and which contain ``dict``s; ``allow_more_present`` is used
for the ``dict``s, and these are assumed to be dictionaries of values;
- ``dict``: for dictionaries of values.
'''
if method == 'ignore':
"""
if method == "ignore":
return True
# If a or b is None:
if a is None or b is None:
@@ -161,18 +183,18 @@ def compare_generic(a, b, method, datatype):
return True
# Otherwise, not equal for values, and equal
# if the other is empty for set/list/dict
if datatype == 'value':
if datatype == "value":
return False
# For allow_more_present, allow a to be None
if method == 'allow_more_present' and a is None:
if method == "allow_more_present" and a is None:
return True
# Otherwise, the iterable object which is not None must have length 0
return len(b if a is None else a) == 0
# Do proper comparison (both objects not None)
if datatype == 'value':
if datatype == "value":
return a == b
elif datatype == 'list':
if method == 'strict':
elif datatype == "list":
if method == "strict":
return a == b
else:
i = 0
@@ -183,19 +205,19 @@ def compare_generic(a, b, method, datatype):
return False
i += 1
return True
elif datatype == 'dict':
if method == 'strict':
elif datatype == "dict":
if method == "strict":
return a == b
else:
return compare_dict_allow_more_present(a, b)
elif datatype == 'set':
elif datatype == "set":
set_a = set(a)
set_b = set(b)
if method == 'strict':
if method == "strict":
return set_a == set_b
else:
return set_b >= set_a
elif datatype == 'set(dict)':
elif datatype == "set(dict)":
for av in a:
found = False
for bv in b:
@@ -204,7 +226,7 @@ def compare_generic(a, b, method, datatype):
break
if not found:
return False
if method == 'strict':
if method == "strict":
# If we would know that both a and b do not contain duplicates,
# we could simply compare len(a) to len(b) to finish this test.
# We can assume that b has no duplicates (as it is returned by
@@ -225,11 +247,13 @@ class DifferenceTracker(object):
self._diff = []
def add(self, name, parameter=None, active=None):
self._diff.append(dict(
name=name,
parameter=parameter,
active=active,
))
self._diff.append(
dict(
name=name,
parameter=parameter,
active=active,
)
)
def merge(self, other_tracker):
self._diff.extend(other_tracker._diff)
@@ -239,41 +263,41 @@ class DifferenceTracker(object):
return len(self._diff) == 0
def get_before_after(self):
'''
"""
Return texts ``before`` and ``after``.
'''
"""
before = dict()
after = dict()
for item in self._diff:
before[item['name']] = item['active']
after[item['name']] = item['parameter']
before[item["name"]] = item["active"]
after[item["name"]] = item["parameter"]
return before, after
def has_difference_for(self, name):
'''
"""
Returns a boolean if a difference exists for name
'''
return any(diff for diff in self._diff if diff['name'] == name)
"""
return any(diff for diff in self._diff if diff["name"] == name)
def get_legacy_docker_container_diffs(self):
'''
"""
Return differences in the docker_container legacy format.
'''
"""
result = []
for entry in self._diff:
item = dict()
item[entry['name']] = dict(
parameter=entry['parameter'],
container=entry['active'],
item[entry["name"]] = dict(
parameter=entry["parameter"],
container=entry["active"],
)
result.append(item)
return result
def get_legacy_docker_diffs(self):
'''
"""
Return differences in the docker_container legacy format.
'''
result = [entry['name'] for entry in self._diff]
"""
result = [entry["name"] for entry in self._diff]
return result
@@ -291,31 +315,38 @@ def sanitize_labels(labels, labels_field, client=None, module=None):
if not isinstance(k, str):
fail(f"The key {k!r} of {labels_field} is not a string!")
if isinstance(v, (bool, float)):
fail(f"The value {v!r} for {k!r} of {labels_field} is not a string or something than can be safely converted to a string!")
fail(
f"The value {v!r} for {k!r} of {labels_field} is not a string or something than can be safely converted to a string!"
)
labels[k] = to_text(v)
def clean_dict_booleans_for_docker_api(data, allow_sequences=False):
'''
"""
Go does not like Python booleans 'True' or 'False', while Ansible is just
fine with them in YAML. As such, they need to be converted in cases where
we pass dictionaries to the Docker API (e.g. docker_network's
driver_options and docker_prune's filters). When `allow_sequences=True`
YAML sequences (lists, tuples) are converted to [str] instead of str([...])
which is the expected format of filters which accept lists such as labels.
'''
"""
def sanitize(value):
if value is True:
return 'true'
return "true"
elif value is False:
return 'false'
return "false"
else:
return str(value)
result = dict()
if data is not None:
for k, v in data.items():
result[str(k)] = [sanitize(e) for e in v] if allow_sequences and is_sequence(v) else sanitize(v)
result[str(k)] = (
[sanitize(e) for e in v]
if allow_sequences and is_sequence(v)
else sanitize(v)
)
return result
@@ -324,30 +355,30 @@ def convert_duration_to_nanosecond(time_str):
Return time duration in nanosecond.
"""
if not isinstance(time_str, str):
raise ValueError(f'Missing unit in duration - {time_str}')
raise ValueError(f"Missing unit in duration - {time_str}")
regex = re.compile(
r'^(((?P<hours>\d+)h)?'
r'((?P<minutes>\d+)m(?!s))?'
r'((?P<seconds>\d+)s)?'
r'((?P<milliseconds>\d+)ms)?'
r'((?P<microseconds>\d+)us)?)$'
r"^(((?P<hours>\d+)h)?"
r"((?P<minutes>\d+)m(?!s))?"
r"((?P<seconds>\d+)s)?"
r"((?P<milliseconds>\d+)ms)?"
r"((?P<microseconds>\d+)us)?)$"
)
parts = regex.match(time_str)
if not parts:
raise ValueError(f'Invalid time duration - {time_str}')
raise ValueError(f"Invalid time duration - {time_str}")
parts = parts.groupdict()
time_params = {}
for (name, value) in parts.items():
for name, value in parts.items():
if value:
time_params[name] = int(value)
delta = timedelta(**time_params)
time_in_nanoseconds = (
delta.microseconds + (delta.seconds + delta.days * 24 * 3600) * 10 ** 6
) * 10 ** 3
delta.microseconds + (delta.seconds + delta.days * 24 * 3600) * 10**6
) * 10**3
return time_in_nanoseconds
@@ -355,7 +386,7 @@ def convert_duration_to_nanosecond(time_str):
def normalize_healthcheck_test(test):
if isinstance(test, (tuple, list)):
return [str(e) for e in test]
return ['CMD-SHELL', str(test)]
return ["CMD-SHELL", str(test)]
def normalize_healthcheck(healthcheck, normalize_test=False):
@@ -365,9 +396,17 @@ def normalize_healthcheck(healthcheck, normalize_test=False):
result = dict()
# All supported healthcheck parameters
options = ('test', 'test_cli_compatible', 'interval', 'timeout', 'start_period', 'start_interval', 'retries')
options = (
"test",
"test_cli_compatible",
"interval",
"timeout",
"start_period",
"start_interval",
"retries",
)
duration_options = ('interval', 'timeout', 'start_period', 'start_interval')
duration_options = ("interval", "timeout", "start_period", "start_interval")
for key in options:
if key in healthcheck:
@@ -378,16 +417,18 @@ def normalize_healthcheck(healthcheck, normalize_test=False):
continue
if key in duration_options:
value = convert_duration_to_nanosecond(value)
if not value and not (healthcheck.get('test_cli_compatible') and key == 'test'):
if not value and not (
healthcheck.get("test_cli_compatible") and key == "test"
):
continue
if key == 'retries':
if key == "retries":
try:
value = int(value)
except ValueError:
raise ValueError(
f'Cannot parse number of retries for healthcheck. Expected an integer, got "{value}".'
)
if key == 'test' and value and normalize_test:
if key == "test" and value and normalize_test:
value = normalize_healthcheck_test(value)
result[key] = value
@@ -399,12 +440,12 @@ def parse_healthcheck(healthcheck):
Return dictionary of healthcheck parameters and boolean if
healthcheck defined in image was requested to be disabled.
"""
if (not healthcheck) or (not healthcheck.get('test')):
if (not healthcheck) or (not healthcheck.get("test")):
return None, None
result = normalize_healthcheck(healthcheck, normalize_test=True)
if result['test'] == ['NONE']:
if result["test"] == ["NONE"]:
# If the user explicitly disables the healthcheck, return None
# as the healthcheck object, and set disable_healthcheck to True
return None, True
+4 -2
View File
@@ -8,5 +8,7 @@
from __future__ import annotations
from ansible.module_utils.compat.version import LooseVersion, StrictVersion # noqa: F401, pylint: disable=unused-import
from ansible.module_utils.compat.version import ( # noqa: F401, pylint: disable=unused-import
LooseVersion,
StrictVersion,
)
+27 -25
View File
@@ -82,11 +82,11 @@ from ansible.module_utils.basic import AnsibleModule
def main():
module = AnsibleModule(dict(), supports_check_mode=True)
cpuset_path = '/proc/self/cpuset'
mountinfo_path = '/proc/self/mountinfo'
cpuset_path = "/proc/self/cpuset"
mountinfo_path = "/proc/self/mountinfo"
container_id = ''
container_type = ''
container_id = ""
container_type = ""
contents = None
if os.path.exists(cpuset_path):
@@ -98,49 +98,51 @@ def main():
# While this was true and worked well for a long time, this seems to be no longer accurate
# with newer Docker / Podman versions and/or with cgroupv2. That's why the /proc/self/mountinfo
# detection further down is done when this test is inconclusive.
with open(cpuset_path, 'rb') as f:
contents = f.read().decode('utf-8')
with open(cpuset_path, "rb") as f:
contents = f.read().decode("utf-8")
cgroup_path, cgroup_name = os.path.split(contents.strip())
if cgroup_path == '/docker':
if cgroup_path == "/docker":
container_id = cgroup_name
container_type = 'docker'
container_type = "docker"
if cgroup_path == '/azpl_job':
if cgroup_path == "/azpl_job":
container_id = cgroup_name
container_type = 'azure_pipelines'
container_type = "azure_pipelines"
if cgroup_path == '/actions_job':
if cgroup_path == "/actions_job":
container_id = cgroup_name
container_type = 'github_actions'
container_type = "github_actions"
if not container_id and os.path.exists(mountinfo_path):
with open(mountinfo_path, 'rb') as f:
contents = f.read().decode('utf-8')
with open(mountinfo_path, "rb") as f:
contents = f.read().decode("utf-8")
# As to why this works, see the explanations by Matt Clay in
# https://github.com/ansible/ansible/blob/80d2f8da02052f64396da6b8caaf820eedbf18e2/test/lib/ansible_test/_internal/docker_util.py#L571-L610
for line in contents.splitlines():
parts = line.split()
if len(parts) >= 5 and parts[4] == '/etc/hostname':
m = re.match('.*/([a-f0-9]{64})/hostname$', parts[3])
if len(parts) >= 5 and parts[4] == "/etc/hostname":
m = re.match(".*/([a-f0-9]{64})/hostname$", parts[3])
if m:
container_id = m.group(1)
container_type = 'docker'
container_type = "docker"
m = re.match('.*/([a-f0-9]{64})/userdata/hostname$', parts[3])
m = re.match(".*/([a-f0-9]{64})/userdata/hostname$", parts[3])
if m:
container_id = m.group(1)
container_type = 'podman'
container_type = "podman"
module.exit_json(ansible_facts=dict(
ansible_module_running_in_container=container_id != '',
ansible_module_container_id=container_id,
ansible_module_container_type=container_type,
))
module.exit_json(
ansible_facts=dict(
ansible_module_running_in_container=container_id != "",
ansible_module_container_id=container_id,
ansible_module_container_type=container_type,
)
)
if __name__ == '__main__':
if __name__ == "__main__":
main()
+130 -96
View File
@@ -439,19 +439,18 @@ actions:
import traceback
from ansible.module_utils.common.validation import check_type_int
from ansible_collections.community.docker.plugins.module_utils.common_cli import (
AnsibleModuleDockerClient,
DockerException,
)
from ansible_collections.community.docker.plugins.module_utils.compose_v2 import (
BaseComposeManager,
common_compose_argspec_ex,
is_failed,
)
from ansible_collections.community.docker.plugins.module_utils.version import LooseVersion
from ansible_collections.community.docker.plugins.module_utils.version import (
LooseVersion,
)
class ServicesManager(BaseComposeManager):
@@ -459,86 +458,90 @@ class ServicesManager(BaseComposeManager):
super(ServicesManager, self).__init__(client)
parameters = self.client.module.params
self.state = parameters['state']
self.dependencies = parameters['dependencies']
self.pull = parameters['pull']
self.build = parameters['build']
self.ignore_build_events = parameters['ignore_build_events']
self.recreate = parameters['recreate']
self.remove_images = parameters['remove_images']
self.remove_volumes = parameters['remove_volumes']
self.remove_orphans = parameters['remove_orphans']
self.renew_anon_volumes = parameters['renew_anon_volumes']
self.timeout = parameters['timeout']
self.services = parameters['services'] or []
self.scale = parameters['scale'] or {}
self.wait = parameters['wait']
self.wait_timeout = parameters['wait_timeout']
self.yes = parameters['assume_yes']
if self.compose_version < LooseVersion('2.32.0') and self.yes:
self.fail(f'assume_yes=true needs Docker Compose 2.32.0 or newer, not version {self.compose_version}')
self.state = parameters["state"]
self.dependencies = parameters["dependencies"]
self.pull = parameters["pull"]
self.build = parameters["build"]
self.ignore_build_events = parameters["ignore_build_events"]
self.recreate = parameters["recreate"]
self.remove_images = parameters["remove_images"]
self.remove_volumes = parameters["remove_volumes"]
self.remove_orphans = parameters["remove_orphans"]
self.renew_anon_volumes = parameters["renew_anon_volumes"]
self.timeout = parameters["timeout"]
self.services = parameters["services"] or []
self.scale = parameters["scale"] or {}
self.wait = parameters["wait"]
self.wait_timeout = parameters["wait_timeout"]
self.yes = parameters["assume_yes"]
if self.compose_version < LooseVersion("2.32.0") and self.yes:
self.fail(
f"assume_yes=true needs Docker Compose 2.32.0 or newer, not version {self.compose_version}"
)
for key, value in self.scale.items():
if not isinstance(key, str):
self.fail(f'The key {key!r} for `scale` is not a string')
self.fail(f"The key {key!r} for `scale` is not a string")
try:
value = check_type_int(value)
except TypeError as exc:
self.fail(f'The value {value!r} for `scale[{key!r}]` is not an integer')
self.fail(f"The value {value!r} for `scale[{key!r}]` is not an integer")
if value < 0:
self.fail(f'The value {value!r} for `scale[{key!r}]` is negative')
self.fail(f"The value {value!r} for `scale[{key!r}]` is negative")
self.scale[key] = value
def run(self):
if self.state == 'present':
if self.state == "present":
result = self.cmd_up()
elif self.state == 'stopped':
elif self.state == "stopped":
result = self.cmd_stop()
elif self.state == 'restarted':
elif self.state == "restarted":
result = self.cmd_restart()
elif self.state == 'absent':
elif self.state == "absent":
result = self.cmd_down()
result['containers'] = self.list_containers()
result['images'] = self.list_images()
result["containers"] = self.list_containers()
result["images"] = self.list_images()
self.cleanup_result(result)
return result
def get_up_cmd(self, dry_run, no_start=False):
args = self.get_base_args() + ['up', '--detach', '--no-color', '--quiet-pull']
if self.pull != 'policy':
args.extend(['--pull', self.pull])
args = self.get_base_args() + ["up", "--detach", "--no-color", "--quiet-pull"]
if self.pull != "policy":
args.extend(["--pull", self.pull])
if self.remove_orphans:
args.append('--remove-orphans')
if self.recreate == 'always':
args.append('--force-recreate')
if self.recreate == 'never':
args.append('--no-recreate')
args.append("--remove-orphans")
if self.recreate == "always":
args.append("--force-recreate")
if self.recreate == "never":
args.append("--no-recreate")
if self.renew_anon_volumes:
args.append('--renew-anon-volumes')
args.append("--renew-anon-volumes")
if not self.dependencies:
args.append('--no-deps')
args.append("--no-deps")
if self.timeout is not None:
args.extend(['--timeout', f'{self.timeout}'])
if self.build == 'always':
args.append('--build')
elif self.build == 'never':
args.append('--no-build')
args.extend(["--timeout", f"{self.timeout}"])
if self.build == "always":
args.append("--build")
elif self.build == "never":
args.append("--no-build")
for key, value in sorted(self.scale.items()):
args.extend(['--scale', f'{key}={value}'])
args.extend(["--scale", f"{key}={value}"])
if self.wait:
args.append('--wait')
args.append("--wait")
if self.wait_timeout is not None:
args.extend(['--wait-timeout', str(self.wait_timeout)])
args.extend(["--wait-timeout", str(self.wait_timeout)])
if no_start:
args.append('--no-start')
args.append("--no-start")
if dry_run:
args.append('--dry-run')
args.append("--dry-run")
if self.yes:
# Note that for Docker Compose 2.32.x and 2.33.x, the long form is '--y' and not '--yes'.
# This was fixed in Docker Compose 2.34.0 (https://github.com/docker/compose/releases/tag/v2.34.0).
args.append('-y' if self.compose_version < LooseVersion('2.34.0') else '--yes')
args.append('--')
args.append(
"-y" if self.compose_version < LooseVersion("2.34.0") else "--yes"
)
args.append("--")
for service in self.services:
args.append(service)
return args
@@ -549,24 +552,31 @@ class ServicesManager(BaseComposeManager):
rc, stdout, stderr = self.client.call_cli(*args, cwd=self.project_src)
events = self.parse_events(stderr, dry_run=self.check_mode, nonzero_rc=rc != 0)
self.emit_warnings(events)
self.update_result(result, events, stdout, stderr, ignore_service_pull_events=True, ignore_build_events=self.ignore_build_events)
self.update_result(
result,
events,
stdout,
stderr,
ignore_service_pull_events=True,
ignore_build_events=self.ignore_build_events,
)
self.update_failed(result, events, args, stdout, stderr, rc)
return result
def get_stop_cmd(self, dry_run):
args = self.get_base_args() + ['stop']
args = self.get_base_args() + ["stop"]
if self.timeout is not None:
args.extend(['--timeout', f'{self.timeout}'])
args.extend(["--timeout", f"{self.timeout}"])
if dry_run:
args.append('--dry-run')
args.append('--')
args.append("--dry-run")
args.append("--")
for service in self.services:
args.append(service)
return args
def _are_containers_stopped(self):
for container in self.list_containers_raw():
if container['State'] not in ('created', 'exited', 'stopped', 'killed'):
if container["State"] not in ("created", "exited", "stopped", "killed"):
return False
return True
@@ -578,20 +588,33 @@ class ServicesManager(BaseComposeManager):
# Make sure all containers are created
args_1 = self.get_up_cmd(self.check_mode, no_start=True)
rc_1, stdout_1, stderr_1 = self.client.call_cli(*args_1, cwd=self.project_src)
events_1 = self.parse_events(stderr_1, dry_run=self.check_mode, nonzero_rc=rc_1 != 0)
events_1 = self.parse_events(
stderr_1, dry_run=self.check_mode, nonzero_rc=rc_1 != 0
)
self.emit_warnings(events_1)
self.update_result(result, events_1, stdout_1, stderr_1, ignore_service_pull_events=True, ignore_build_events=self.ignore_build_events)
self.update_result(
result,
events_1,
stdout_1,
stderr_1,
ignore_service_pull_events=True,
ignore_build_events=self.ignore_build_events,
)
is_failed_1 = is_failed(events_1, rc_1)
if not is_failed_1 and not self._are_containers_stopped():
# Make sure all containers are stopped
args_2 = self.get_stop_cmd(self.check_mode)
rc_2, stdout_2, stderr_2 = self.client.call_cli(*args_2, cwd=self.project_src)
events_2 = self.parse_events(stderr_2, dry_run=self.check_mode, nonzero_rc=rc_2 != 0)
rc_2, stdout_2, stderr_2 = self.client.call_cli(
*args_2, cwd=self.project_src
)
events_2 = self.parse_events(
stderr_2, dry_run=self.check_mode, nonzero_rc=rc_2 != 0
)
self.emit_warnings(events_2)
self.update_result(result, events_2, stdout_2, stderr_2)
else:
args_2 = []
rc_2, stdout_2, stderr_2 = 0, b'', b''
rc_2, stdout_2, stderr_2 = 0, b"", b""
events_2 = []
# Compose result
self.update_failed(
@@ -605,14 +628,14 @@ class ServicesManager(BaseComposeManager):
return result
def get_restart_cmd(self, dry_run):
args = self.get_base_args() + ['restart']
args = self.get_base_args() + ["restart"]
if not self.dependencies:
args.append('--no-deps')
args.append("--no-deps")
if self.timeout is not None:
args.extend(['--timeout', f'{self.timeout}'])
args.extend(["--timeout", f"{self.timeout}"])
if dry_run:
args.append('--dry-run')
args.append('--')
args.append("--dry-run")
args.append("--")
for service in self.services:
args.append(service)
return args
@@ -628,18 +651,18 @@ class ServicesManager(BaseComposeManager):
return result
def get_down_cmd(self, dry_run):
args = self.get_base_args() + ['down']
args = self.get_base_args() + ["down"]
if self.remove_orphans:
args.append('--remove-orphans')
args.append("--remove-orphans")
if self.remove_images:
args.extend(['--rmi', self.remove_images])
args.extend(["--rmi", self.remove_images])
if self.remove_volumes:
args.append('--volumes')
args.append("--volumes")
if self.timeout is not None:
args.extend(['--timeout', f'{self.timeout}'])
args.extend(["--timeout", f"{self.timeout}"])
if dry_run:
args.append('--dry-run')
args.append('--')
args.append("--dry-run")
args.append("--")
for service in self.services:
args.append(service)
return args
@@ -657,31 +680,39 @@ class ServicesManager(BaseComposeManager):
def main():
argument_spec = dict(
state=dict(type='str', default='present', choices=['absent', 'present', 'stopped', 'restarted']),
dependencies=dict(type='bool', default=True),
pull=dict(type='str', choices=['always', 'missing', 'never', 'policy'], default='policy'),
build=dict(type='str', choices=['always', 'never', 'policy'], default='policy'),
recreate=dict(type='str', default='auto', choices=['always', 'never', 'auto']),
renew_anon_volumes=dict(type='bool', default=False),
remove_images=dict(type='str', choices=['all', 'local']),
remove_volumes=dict(type='bool', default=False),
remove_orphans=dict(type='bool', default=False),
timeout=dict(type='int'),
services=dict(type='list', elements='str'),
scale=dict(type='dict'),
wait=dict(type='bool', default=False),
wait_timeout=dict(type='int'),
ignore_build_events=dict(type='bool', default=True),
assume_yes=dict(type='bool', default=False),
state=dict(
type="str",
default="present",
choices=["absent", "present", "stopped", "restarted"],
),
dependencies=dict(type="bool", default=True),
pull=dict(
type="str",
choices=["always", "missing", "never", "policy"],
default="policy",
),
build=dict(type="str", choices=["always", "never", "policy"], default="policy"),
recreate=dict(type="str", default="auto", choices=["always", "never", "auto"]),
renew_anon_volumes=dict(type="bool", default=False),
remove_images=dict(type="str", choices=["all", "local"]),
remove_volumes=dict(type="bool", default=False),
remove_orphans=dict(type="bool", default=False),
timeout=dict(type="int"),
services=dict(type="list", elements="str"),
scale=dict(type="dict"),
wait=dict(type="bool", default=False),
wait_timeout=dict(type="int"),
ignore_build_events=dict(type="bool", default=True),
assume_yes=dict(type="bool", default=False),
)
argspec_ex = common_compose_argspec_ex()
argument_spec.update(argspec_ex.pop('argspec'))
argument_spec.update(argspec_ex.pop("argspec"))
client = AnsibleModuleDockerClient(
argument_spec=argument_spec,
supports_check_mode=True,
needs_api_version=False,
**argspec_ex
**argspec_ex,
)
try:
@@ -690,8 +721,11 @@ def main():
manager.cleanup()
client.module.exit_json(**result)
except DockerException as e:
client.fail(f'An unexpected Docker error occurred: {e}', exception=traceback.format_exc())
client.fail(
f"An unexpected Docker error occurred: {e}",
exception=traceback.format_exc(),
)
if __name__ == '__main__':
if __name__ == "__main__":
main()
+56 -55
View File
@@ -168,12 +168,10 @@ import shlex
import traceback
from ansible.module_utils.common.text.converters import to_text
from ansible_collections.community.docker.plugins.module_utils.common_cli import (
AnsibleModuleDockerClient,
DockerException,
)
from ansible_collections.community.docker.plugins.module_utils.compose_v2 import (
BaseComposeManager,
common_compose_argspec_ex,
@@ -185,26 +183,26 @@ class ExecManager(BaseComposeManager):
super(ExecManager, self).__init__(client)
parameters = self.client.module.params
self.service = parameters['service']
self.index = parameters['index']
self.chdir = parameters['chdir']
self.detach = parameters['detach']
self.user = parameters['user']
self.stdin = parameters['stdin']
self.strip_empty_ends = parameters['strip_empty_ends']
self.privileged = parameters['privileged']
self.tty = parameters['tty']
self.env = parameters['env']
self.service = parameters["service"]
self.index = parameters["index"]
self.chdir = parameters["chdir"]
self.detach = parameters["detach"]
self.user = parameters["user"]
self.stdin = parameters["stdin"]
self.strip_empty_ends = parameters["strip_empty_ends"]
self.privileged = parameters["privileged"]
self.tty = parameters["tty"]
self.env = parameters["env"]
self.argv = parameters['argv']
if parameters['command'] is not None:
self.argv = shlex.split(parameters['command'])
self.argv = parameters["argv"]
if parameters["command"] is not None:
self.argv = shlex.split(parameters["command"])
if self.detach and self.stdin is not None:
self.mail('If detach=true, stdin cannot be provided.')
self.mail("If detach=true, stdin cannot be provided.")
if self.stdin is not None and parameters['stdin_add_newline']:
self.stdin += '\n'
if self.stdin is not None and parameters["stdin_add_newline"]:
self.stdin += "\n"
if self.env is not None:
for name, value in list(self.env.items()):
@@ -213,27 +211,27 @@ class ExecManager(BaseComposeManager):
"Non-string value found for env option. Ambiguous env options must be "
f"wrapped in quotes to avoid them being interpreted. Key: {name}"
)
self.env[name] = to_text(value, errors='surrogate_or_strict')
self.env[name] = to_text(value, errors="surrogate_or_strict")
def get_exec_cmd(self, dry_run, no_start=False):
args = self.get_base_args(plain_progress=True) + ['exec']
args = self.get_base_args(plain_progress=True) + ["exec"]
if self.index is not None:
args.extend(['--index', str(self.index)])
args.extend(["--index", str(self.index)])
if self.chdir is not None:
args.extend(['--workdir', self.chdir])
args.extend(["--workdir", self.chdir])
if self.detach:
args.extend(['--detach'])
args.extend(["--detach"])
if self.user is not None:
args.extend(['--user', self.user])
args.extend(["--user", self.user])
if self.privileged:
args.append('--privileged')
args.append("--privileged")
if not self.tty:
args.append('--no-TTY')
args.append("--no-TTY")
if self.env:
for name, value in list(self.env.items()):
args.append('--env')
args.append(f'{name}={value}')
args.append('--')
args.append("--env")
args.append(f"{name}={value}")
args.append("--")
args.append(self.service)
args.extend(self.argv)
return args
@@ -241,52 +239,52 @@ class ExecManager(BaseComposeManager):
def run(self):
args = self.get_exec_cmd(self.check_mode)
kwargs = {
'cwd': self.project_src,
"cwd": self.project_src,
}
if self.stdin is not None:
kwargs['data'] = self.stdin.encode('utf-8')
kwargs["data"] = self.stdin.encode("utf-8")
if self.detach:
kwargs['check_rc'] = True
kwargs["check_rc"] = True
rc, stdout, stderr = self.client.call_cli(*args, **kwargs)
if self.detach:
return {}
stdout = to_text(stdout)
stderr = to_text(stderr)
if self.strip_empty_ends:
stdout = stdout.rstrip('\r\n')
stderr = stderr.rstrip('\r\n')
stdout = stdout.rstrip("\r\n")
stderr = stderr.rstrip("\r\n")
return {
'changed': True,
'rc': rc,
'stdout': stdout,
'stderr': stderr,
"changed": True,
"rc": rc,
"stdout": stdout,
"stderr": stderr,
}
def main():
argument_spec = dict(
service=dict(type='str', required=True),
index=dict(type='int'),
argv=dict(type='list', elements='str'),
command=dict(type='str'),
chdir=dict(type='str'),
detach=dict(type='bool', default=False),
user=dict(type='str'),
stdin=dict(type='str'),
stdin_add_newline=dict(type='bool', default=True),
strip_empty_ends=dict(type='bool', default=True),
privileged=dict(type='bool', default=False),
tty=dict(type='bool', default=True),
env=dict(type='dict'),
service=dict(type="str", required=True),
index=dict(type="int"),
argv=dict(type="list", elements="str"),
command=dict(type="str"),
chdir=dict(type="str"),
detach=dict(type="bool", default=False),
user=dict(type="str"),
stdin=dict(type="str"),
stdin_add_newline=dict(type="bool", default=True),
strip_empty_ends=dict(type="bool", default=True),
privileged=dict(type="bool", default=False),
tty=dict(type="bool", default=True),
env=dict(type="dict"),
)
argspec_ex = common_compose_argspec_ex()
argument_spec.update(argspec_ex.pop('argspec'))
argument_spec.update(argspec_ex.pop("argspec"))
client = AnsibleModuleDockerClient(
argument_spec=argument_spec,
supports_check_mode=False,
needs_api_version=False,
**argspec_ex
**argspec_ex,
)
try:
@@ -295,8 +293,11 @@ def main():
manager.cleanup()
client.module.exit_json(**result)
except DockerException as e:
client.fail(f'An unexpected Docker error occurred: {e}', exception=traceback.format_exc())
client.fail(
f"An unexpected Docker error occurred: {e}",
exception=traceback.format_exc(),
)
if __name__ == '__main__':
if __name__ == "__main__":
main()
+38 -27
View File
@@ -116,13 +116,13 @@ from ansible_collections.community.docker.plugins.module_utils.common_cli import
AnsibleModuleDockerClient,
DockerException,
)
from ansible_collections.community.docker.plugins.module_utils.compose_v2 import (
BaseComposeManager,
common_compose_argspec_ex,
)
from ansible_collections.community.docker.plugins.module_utils.version import LooseVersion
from ansible_collections.community.docker.plugins.module_utils.version import (
LooseVersion,
)
class PullManager(BaseComposeManager):
@@ -130,31 +130,33 @@ class PullManager(BaseComposeManager):
super(PullManager, self).__init__(client)
parameters = self.client.module.params
self.policy = parameters['policy']
self.ignore_buildable = parameters['ignore_buildable']
self.include_deps = parameters['include_deps']
self.services = parameters['services'] or []
self.policy = parameters["policy"]
self.ignore_buildable = parameters["ignore_buildable"]
self.include_deps = parameters["include_deps"]
self.services = parameters["services"] or []
if self.policy != 'always' and self.compose_version < LooseVersion('2.22.0'):
if self.policy != "always" and self.compose_version < LooseVersion("2.22.0"):
# https://github.com/docker/compose/pull/10981 - 2.22.0
self.fail(
f'A pull policy other than always is only supported since Docker Compose 2.22.0. {self.client.get_cli()} has version {self.compose_version}')
if self.ignore_buildable and self.compose_version < LooseVersion('2.15.0'):
f"A pull policy other than always is only supported since Docker Compose 2.22.0. {self.client.get_cli()} has version {self.compose_version}"
)
if self.ignore_buildable and self.compose_version < LooseVersion("2.15.0"):
# https://github.com/docker/compose/pull/10134 - 2.15.0
self.fail(
f'--ignore-buildable is only supported since Docker Compose 2.15.0. {self.client.get_cli()} has version {self.compose_version}')
f"--ignore-buildable is only supported since Docker Compose 2.15.0. {self.client.get_cli()} has version {self.compose_version}"
)
def get_pull_cmd(self, dry_run, no_start=False):
args = self.get_base_args() + ['pull']
if self.policy != 'always':
args.extend(['--policy', self.policy])
args = self.get_base_args() + ["pull"]
if self.policy != "always":
args.extend(["--policy", self.policy])
if self.ignore_buildable:
args.append('--ignore-buildable')
args.append("--ignore-buildable")
if self.include_deps:
args.append('--include-deps')
args.append("--include-deps")
if dry_run:
args.append('--dry-run')
args.append('--')
args.append("--dry-run")
args.append("--")
for service in self.services:
args.append(service)
return args
@@ -165,7 +167,13 @@ class PullManager(BaseComposeManager):
rc, stdout, stderr = self.client.call_cli(*args, cwd=self.project_src)
events = self.parse_events(stderr, dry_run=self.check_mode, nonzero_rc=rc != 0)
self.emit_warnings(events)
self.update_result(result, events, stdout, stderr, ignore_service_pull_events=self.policy != 'missing' and not self.check_mode)
self.update_result(
result,
events,
stdout,
stderr,
ignore_service_pull_events=self.policy != "missing" and not self.check_mode,
)
self.update_failed(result, events, args, stdout, stderr, rc)
self.cleanup_result(result)
return result
@@ -173,19 +181,19 @@ class PullManager(BaseComposeManager):
def main():
argument_spec = dict(
policy=dict(type='str', choices=['always', 'missing'], default='always'),
ignore_buildable=dict(type='bool', default=False),
include_deps=dict(type='bool', default=False),
services=dict(type='list', elements='str'),
policy=dict(type="str", choices=["always", "missing"], default="always"),
ignore_buildable=dict(type="bool", default=False),
include_deps=dict(type="bool", default=False),
services=dict(type="list", elements="str"),
)
argspec_ex = common_compose_argspec_ex()
argument_spec.update(argspec_ex.pop('argspec'))
argument_spec.update(argspec_ex.pop("argspec"))
client = AnsibleModuleDockerClient(
argument_spec=argument_spec,
supports_check_mode=True,
needs_api_version=False,
**argspec_ex
**argspec_ex,
)
try:
@@ -194,8 +202,11 @@ def main():
manager.cleanup()
client.module.exit_json(**result)
except DockerException as e:
client.fail(f'An unexpected docker error occurred: {e}', exception=traceback.format_exc())
client.fail(
f"An unexpected docker error occurred: {e}",
exception=traceback.format_exc(),
)
if __name__ == '__main__':
if __name__ == "__main__":
main()
+96 -95
View File
@@ -241,12 +241,10 @@ import shlex
import traceback
from ansible.module_utils.common.text.converters import to_text
from ansible_collections.community.docker.plugins.module_utils.common_cli import (
AnsibleModuleDockerClient,
DockerException,
)
from ansible_collections.community.docker.plugins.module_utils.compose_v2 import (
BaseComposeManager,
common_compose_argspec_ex,
@@ -258,39 +256,39 @@ class ExecManager(BaseComposeManager):
super(ExecManager, self).__init__(client)
parameters = self.client.module.params
self.service = parameters['service']
self.build = parameters['build']
self.cap_add = parameters['cap_add']
self.cap_drop = parameters['cap_drop']
self.entrypoint = parameters['entrypoint']
self.interactive = parameters['interactive']
self.labels = parameters['labels']
self.name = parameters['name']
self.no_deps = parameters['no_deps']
self.publish = parameters['publish']
self.quiet_pull = parameters['quiet_pull']
self.remove_orphans = parameters['remove_orphans']
self.do_cleanup = parameters['cleanup']
self.service_ports = parameters['service_ports']
self.use_aliases = parameters['use_aliases']
self.volumes = parameters['volumes']
self.chdir = parameters['chdir']
self.detach = parameters['detach']
self.user = parameters['user']
self.stdin = parameters['stdin']
self.strip_empty_ends = parameters['strip_empty_ends']
self.tty = parameters['tty']
self.env = parameters['env']
self.service = parameters["service"]
self.build = parameters["build"]
self.cap_add = parameters["cap_add"]
self.cap_drop = parameters["cap_drop"]
self.entrypoint = parameters["entrypoint"]
self.interactive = parameters["interactive"]
self.labels = parameters["labels"]
self.name = parameters["name"]
self.no_deps = parameters["no_deps"]
self.publish = parameters["publish"]
self.quiet_pull = parameters["quiet_pull"]
self.remove_orphans = parameters["remove_orphans"]
self.do_cleanup = parameters["cleanup"]
self.service_ports = parameters["service_ports"]
self.use_aliases = parameters["use_aliases"]
self.volumes = parameters["volumes"]
self.chdir = parameters["chdir"]
self.detach = parameters["detach"]
self.user = parameters["user"]
self.stdin = parameters["stdin"]
self.strip_empty_ends = parameters["strip_empty_ends"]
self.tty = parameters["tty"]
self.env = parameters["env"]
self.argv = parameters['argv']
if parameters['command'] is not None:
self.argv = shlex.split(parameters['command'])
self.argv = parameters["argv"]
if parameters["command"] is not None:
self.argv = shlex.split(parameters["command"])
if self.detach and self.stdin is not None:
self.mail('If detach=true, stdin cannot be provided.')
self.mail("If detach=true, stdin cannot be provided.")
if self.stdin is not None and parameters['stdin_add_newline']:
self.stdin += '\n'
if self.stdin is not None and parameters["stdin_add_newline"]:
self.stdin += "\n"
if self.env is not None:
for name, value in list(self.env.items()):
@@ -299,58 +297,58 @@ class ExecManager(BaseComposeManager):
"Non-string value found for env option. Ambiguous env options must be "
f"wrapped in quotes to avoid them being interpreted. Key: {name}"
)
self.env[name] = to_text(value, errors='surrogate_or_strict')
self.env[name] = to_text(value, errors="surrogate_or_strict")
def get_run_cmd(self, dry_run, no_start=False):
args = self.get_base_args(plain_progress=True) + ['run']
args = self.get_base_args(plain_progress=True) + ["run"]
if self.build:
args.append('--build')
args.append("--build")
if self.cap_add:
for cap in self.cap_add:
args.extend(['--cap-add', cap])
args.extend(["--cap-add", cap])
if self.cap_drop:
for cap in self.cap_drop:
args.extend(['--cap-drop', cap])
args.extend(["--cap-drop", cap])
if self.entrypoint is not None:
args.extend(['--entrypoint', self.entrypoint])
args.extend(["--entrypoint", self.entrypoint])
if not self.interactive:
args.append('--no-interactive')
args.append("--no-interactive")
if self.labels:
for label in self.labels:
args.extend(['--label', label])
args.extend(["--label", label])
if self.name is not None:
args.extend(['--name', self.name])
args.extend(["--name", self.name])
if self.no_deps:
args.append('--no-deps')
args.append("--no-deps")
if self.publish:
for publish in self.publish:
args.extend(['--publish', publish])
args.extend(["--publish", publish])
if self.quiet_pull:
args.append('--quiet-pull')
args.append("--quiet-pull")
if self.remove_orphans:
args.append('--remove-orphans')
args.append("--remove-orphans")
if self.do_cleanup:
args.append('--rm')
args.append("--rm")
if self.service_ports:
args.append('--service-ports')
args.append("--service-ports")
if self.use_aliases:
args.append('--use-aliases')
args.append("--use-aliases")
if self.volumes:
for volume in self.volumes:
args.extend(['--volume', volume])
args.extend(["--volume", volume])
if self.chdir is not None:
args.extend(['--workdir', self.chdir])
args.extend(["--workdir", self.chdir])
if self.detach:
args.extend(['--detach'])
args.extend(["--detach"])
if self.user is not None:
args.extend(['--user', self.user])
args.extend(["--user", self.user])
if not self.tty:
args.append('--no-TTY')
args.append("--no-TTY")
if self.env:
for name, value in list(self.env.items()):
args.append('--env')
args.append(f'{name}={value}')
args.append('--')
args.append("--env")
args.append(f"{name}={value}")
args.append("--")
args.append(self.service)
if self.argv:
args.extend(self.argv)
@@ -359,67 +357,67 @@ class ExecManager(BaseComposeManager):
def run(self):
args = self.get_run_cmd(self.check_mode)
kwargs = {
'cwd': self.project_src,
"cwd": self.project_src,
}
if self.stdin is not None:
kwargs['data'] = self.stdin.encode('utf-8')
kwargs["data"] = self.stdin.encode("utf-8")
if self.detach:
kwargs['check_rc'] = True
kwargs["check_rc"] = True
rc, stdout, stderr = self.client.call_cli(*args, **kwargs)
if self.detach:
return {
'container_id': stdout.strip(),
"container_id": stdout.strip(),
}
stdout = to_text(stdout)
stderr = to_text(stderr)
if self.strip_empty_ends:
stdout = stdout.rstrip('\r\n')
stderr = stderr.rstrip('\r\n')
stdout = stdout.rstrip("\r\n")
stderr = stderr.rstrip("\r\n")
return {
'changed': True,
'rc': rc,
'stdout': stdout,
'stderr': stderr,
"changed": True,
"rc": rc,
"stdout": stdout,
"stderr": stderr,
}
def main():
argument_spec = dict(
service=dict(type='str', required=True),
argv=dict(type='list', elements='str'),
command=dict(type='str'),
build=dict(type='bool', default=False),
cap_add=dict(type='list', elements='str'),
cap_drop=dict(type='list', elements='str'),
entrypoint=dict(type='str'),
interactive=dict(type='bool', default=True),
labels=dict(type='list', elements='str'),
name=dict(type='str'),
no_deps=dict(type='bool', default=False),
publish=dict(type='list', elements='str'),
quiet_pull=dict(type='bool', default=False),
remove_orphans=dict(type='bool', default=False),
cleanup=dict(type='bool', default=False),
service_ports=dict(type='bool', default=False),
use_aliases=dict(type='bool', default=False),
volumes=dict(type='list', elements='str'),
chdir=dict(type='str'),
detach=dict(type='bool', default=False),
user=dict(type='str'),
stdin=dict(type='str'),
stdin_add_newline=dict(type='bool', default=True),
strip_empty_ends=dict(type='bool', default=True),
tty=dict(type='bool', default=True),
env=dict(type='dict'),
service=dict(type="str", required=True),
argv=dict(type="list", elements="str"),
command=dict(type="str"),
build=dict(type="bool", default=False),
cap_add=dict(type="list", elements="str"),
cap_drop=dict(type="list", elements="str"),
entrypoint=dict(type="str"),
interactive=dict(type="bool", default=True),
labels=dict(type="list", elements="str"),
name=dict(type="str"),
no_deps=dict(type="bool", default=False),
publish=dict(type="list", elements="str"),
quiet_pull=dict(type="bool", default=False),
remove_orphans=dict(type="bool", default=False),
cleanup=dict(type="bool", default=False),
service_ports=dict(type="bool", default=False),
use_aliases=dict(type="bool", default=False),
volumes=dict(type="list", elements="str"),
chdir=dict(type="str"),
detach=dict(type="bool", default=False),
user=dict(type="str"),
stdin=dict(type="str"),
stdin_add_newline=dict(type="bool", default=True),
strip_empty_ends=dict(type="bool", default=True),
tty=dict(type="bool", default=True),
env=dict(type="dict"),
)
argspec_ex = common_compose_argspec_ex()
argument_spec.update(argspec_ex.pop('argspec'))
argument_spec.update(argspec_ex.pop("argspec"))
client = AnsibleModuleDockerClient(
argument_spec=argument_spec,
supports_check_mode=False,
needs_api_version=False,
**argspec_ex
**argspec_ex,
)
try:
@@ -428,8 +426,11 @@ def main():
manager.cleanup()
client.module.exit_json(**result)
except DockerException as e:
client.fail(f'An unexpected Docker error occurred: {e}', exception=traceback.format_exc())
client.fail(
f"An unexpected Docker error occurred: {e}",
exception=traceback.format_exc(),
)
if __name__ == '__main__':
if __name__ == "__main__":
main()
+81 -72
View File
@@ -199,12 +199,14 @@ import base64
import hashlib
import traceback
try:
from docker.errors import DockerException, APIError
from docker.errors import APIError, DockerException
except ImportError:
# missing Docker SDK for Python handled in ansible.module_utils.docker.common
pass
from ansible.module_utils.common.text.converters import to_bytes
from ansible_collections.community.docker.plugins.module_utils.common import (
AnsibleDockerClient,
RequestException,
@@ -214,7 +216,6 @@ from ansible_collections.community.docker.plugins.module_utils.util import (
compare_generic,
sanitize_labels,
)
from ansible.module_utils.common.text.converters import to_bytes
class ConfigManager(DockerBaseClass):
@@ -228,26 +229,26 @@ class ConfigManager(DockerBaseClass):
self.check_mode = self.client.check_mode
parameters = self.client.module.params
self.name = parameters.get('name')
self.state = parameters.get('state')
self.data = parameters.get('data')
self.name = parameters.get("name")
self.state = parameters.get("state")
self.data = parameters.get("data")
if self.data is not None:
if parameters.get('data_is_b64'):
if parameters.get("data_is_b64"):
self.data = base64.b64decode(self.data)
else:
self.data = to_bytes(self.data)
data_src = parameters.get('data_src')
data_src = parameters.get("data_src")
if data_src is not None:
try:
with open(data_src, 'rb') as f:
with open(data_src, "rb") as f:
self.data = f.read()
except Exception as exc:
self.client.fail(f'Error while reading {data_src}: {exc}')
self.labels = parameters.get('labels')
self.force = parameters.get('force')
self.rolling_versions = parameters.get('rolling_versions')
self.versions_to_keep = parameters.get('versions_to_keep')
self.template_driver = parameters.get('template_driver')
self.client.fail(f"Error while reading {data_src}: {exc}")
self.labels = parameters.get("labels")
self.force = parameters.get("force")
self.rolling_versions = parameters.get("rolling_versions")
self.versions_to_keep = parameters.get("versions_to_keep")
self.template_driver = parameters.get("template_driver")
if self.rolling_versions:
self.version = 0
@@ -256,16 +257,18 @@ class ConfigManager(DockerBaseClass):
def __call__(self):
self.get_config()
if self.state == 'present':
if self.state == "present":
self.data_key = hashlib.sha224(self.data).hexdigest()
self.present()
self.remove_old_versions()
elif self.state == 'absent':
elif self.state == "absent":
self.absent()
def get_version(self, config):
try:
return int(config.get('Spec', {}).get('Labels', {}).get('ansible_version', 0))
return int(
config.get("Spec", {}).get("Labels", {}).get("ansible_version", 0)
)
except ValueError:
return 0
@@ -277,9 +280,9 @@ class ConfigManager(DockerBaseClass):
self.remove_config(self.configs.pop(0))
def get_config(self):
''' Find an existing config. '''
"""Find an existing config."""
try:
configs = self.client.configs(filters={'name': self.name})
configs = self.client.configs(filters={"name": self.name})
except APIError as exc:
self.client.fail(f"Error accessing config {self.name}: {exc}")
@@ -287,25 +290,23 @@ class ConfigManager(DockerBaseClass):
self.configs = [
config
for config in configs
if config['Spec']['Name'].startswith(f'{self.name}_v')
if config["Spec"]["Name"].startswith(f"{self.name}_v")
]
self.configs.sort(key=self.get_version)
else:
self.configs = [
config for config in configs if config['Spec']['Name'] == self.name
config for config in configs if config["Spec"]["Name"] == self.name
]
def create_config(self):
''' Create a new config '''
"""Create a new config"""
config_id = None
# We ca not see the data after creation, so adding a label we can use for idempotency check
labels = {
'ansible_key': self.data_key
}
labels = {"ansible_key": self.data_key}
if self.rolling_versions:
self.version += 1
labels['ansible_version'] = str(self.version)
self.name = f'{self.name}_v{self.version}'
labels["ansible_version"] = str(self.version)
self.name = f"{self.name}_v{self.version}"
if self.labels:
labels.update(self.labels)
@@ -314,49 +315,53 @@ class ConfigManager(DockerBaseClass):
# only use templating argument when self.template_driver is defined
kwargs = {}
if self.template_driver:
kwargs['templating'] = {
'name': self.template_driver
}
config_id = self.client.create_config(self.name, self.data, labels=labels, **kwargs)
self.configs += self.client.configs(filters={'id': config_id})
kwargs["templating"] = {"name": self.template_driver}
config_id = self.client.create_config(
self.name, self.data, labels=labels, **kwargs
)
self.configs += self.client.configs(filters={"id": config_id})
except APIError as exc:
self.client.fail(f"Error creating config: {exc}")
if isinstance(config_id, dict):
config_id = config_id['ID']
config_id = config_id["ID"]
return config_id
def remove_config(self, config):
try:
if not self.check_mode:
self.client.remove_config(config['ID'])
self.client.remove_config(config["ID"])
except APIError as exc:
self.client.fail(f"Error removing config {config['Spec']['Name']}: {exc}")
def present(self):
''' Handles state == 'present', creating or updating the config '''
"""Handles state == 'present', creating or updating the config"""
if self.configs:
config = self.configs[-1]
self.results['config_id'] = config['ID']
self.results['config_name'] = config['Spec']['Name']
self.results["config_id"] = config["ID"]
self.results["config_name"] = config["Spec"]["Name"]
data_changed = False
template_driver_changed = False
attrs = config.get('Spec', {})
if attrs.get('Labels', {}).get('ansible_key'):
if attrs['Labels']['ansible_key'] != self.data_key:
attrs = config.get("Spec", {})
if attrs.get("Labels", {}).get("ansible_key"):
if attrs["Labels"]["ansible_key"] != self.data_key:
data_changed = True
else:
if not self.force:
self.client.module.warn("'ansible_key' label not found. Config will not be changed unless the force parameter is set to 'true'")
self.client.module.warn(
"'ansible_key' label not found. Config will not be changed unless the force parameter is set to 'true'"
)
# template_driver has changed if it was set in the previous config
# and now it differs, or if it was not set but now it is.
if attrs.get('Templating', {}).get('Name'):
if attrs['Templating']['Name'] != self.template_driver:
if attrs.get("Templating", {}).get("Name"):
if attrs["Templating"]["Name"] != self.template_driver:
template_driver_changed = True
elif self.template_driver:
template_driver_changed = True
labels_changed = not compare_generic(self.labels, attrs.get('Labels'), 'allow_more_present', 'dict')
labels_changed = not compare_generic(
self.labels, attrs.get("Labels"), "allow_more_present", "dict"
)
if self.rolling_versions:
self.version = self.get_version(config)
if data_changed or template_driver_changed or labels_changed or self.force:
@@ -364,46 +369,46 @@ class ConfigManager(DockerBaseClass):
if not self.rolling_versions:
self.absent()
config_id = self.create_config()
self.results['changed'] = True
self.results['config_id'] = config_id
self.results['config_name'] = self.name
self.results["changed"] = True
self.results["config_id"] = config_id
self.results["config_name"] = self.name
else:
self.results['changed'] = True
self.results['config_id'] = self.create_config()
self.results['config_name'] = self.name
self.results["changed"] = True
self.results["config_id"] = self.create_config()
self.results["config_name"] = self.name
def absent(self):
''' Handles state == 'absent', removing the config '''
"""Handles state == 'absent', removing the config"""
if self.configs:
for config in self.configs:
self.remove_config(config)
self.results['changed'] = True
self.results["changed"] = True
def main():
argument_spec = dict(
name=dict(type='str', required=True),
state=dict(type='str', default='present', choices=['absent', 'present']),
data=dict(type='str'),
data_is_b64=dict(type='bool', default=False),
data_src=dict(type='path'),
labels=dict(type='dict'),
force=dict(type='bool', default=False),
rolling_versions=dict(type='bool', default=False),
versions_to_keep=dict(type='int', default=5),
template_driver=dict(type='str', choices=['golang']),
name=dict(type="str", required=True),
state=dict(type="str", default="present", choices=["absent", "present"]),
data=dict(type="str"),
data_is_b64=dict(type="bool", default=False),
data_src=dict(type="path"),
labels=dict(type="dict"),
force=dict(type="bool", default=False),
rolling_versions=dict(type="bool", default=False),
versions_to_keep=dict(type="int", default=5),
template_driver=dict(type="str", choices=["golang"]),
)
required_if = [
('state', 'present', ['data', 'data_src'], True),
("state", "present", ["data", "data_src"], True),
]
mutually_exclusive = [
('data', 'data_src'),
("data", "data_src"),
]
option_minimal_versions = dict(
template_driver=dict(docker_py_version='5.0.3', docker_api_version='1.37'),
template_driver=dict(docker_py_version="5.0.3", docker_api_version="1.37"),
)
client = AnsibleDockerClient(
@@ -411,11 +416,11 @@ def main():
supports_check_mode=True,
required_if=required_if,
mutually_exclusive=mutually_exclusive,
min_docker_version='2.6.0',
min_docker_api_version='1.30',
min_docker_version="2.6.0",
min_docker_api_version="1.30",
option_minimal_versions=option_minimal_versions,
)
sanitize_labels(client.module.params['labels'], 'labels', client)
sanitize_labels(client.module.params["labels"], "labels", client)
try:
results = dict(
@@ -425,12 +430,16 @@ def main():
ConfigManager(client, results)()
client.module.exit_json(**results)
except DockerException as e:
client.fail(f'An unexpected Docker error occurred: {e}', exception=traceback.format_exc())
client.fail(
f"An unexpected Docker error occurred: {e}",
exception=traceback.format_exc(),
)
except RequestException as e:
client.fail(
f'An unexpected requests error occurred when Docker SDK for Python tried to talk to the docker daemon: {e}',
exception=traceback.format_exc())
f"An unexpected requests error occurred when Docker SDK for Python tried to talk to the docker daemon: {e}",
exception=traceback.format_exc(),
)
if __name__ == '__main__':
if __name__ == "__main__":
main()
+1 -2
View File
@@ -1334,7 +1334,6 @@ status:
from ansible_collections.community.docker.plugins.module_utils.module_container.docker_api import (
DockerAPIEngineDriver,
)
from ansible_collections.community.docker.plugins.module_utils.module_container.module import (
run_module,
)
@@ -1345,5 +1344,5 @@ def main():
run_module(engine_driver)
if __name__ == '__main__':
if __name__ == "__main__":
main()
+427 -190
View File
@@ -172,14 +172,19 @@ import traceback
from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
from ansible.module_utils.common.validation import check_type_int
from ansible_collections.community.docker.plugins.module_utils._api.errors import APIError, DockerException, NotFound
from ansible_collections.community.docker.plugins.module_utils._api.errors import (
APIError,
DockerException,
NotFound,
)
from ansible_collections.community.docker.plugins.module_utils._scramble import (
generate_insecure_key,
scramble,
)
from ansible_collections.community.docker.plugins.module_utils.common_api import (
AnsibleDockerClient,
RequestException,
)
from ansible_collections.community.docker.plugins.module_utils.copy import (
DockerFileCopyError,
DockerFileNotFound,
@@ -191,14 +196,12 @@ from ansible_collections.community.docker.plugins.module_utils.copy import (
stat_file,
)
from ansible_collections.community.docker.plugins.module_utils._scramble import generate_insecure_key, scramble
def are_fileobjs_equal(f1, f2):
'''Given two (buffered) file objects, compare their contents.'''
"""Given two (buffered) file objects, compare their contents."""
blocksize = 65536
b1buf = b''
b2buf = b''
b1buf = b""
b2buf = b""
while True:
if f1 and len(b1buf) < blocksize:
f1b = f1.read(blocksize)
@@ -226,14 +229,14 @@ def are_fileobjs_equal(f1, f2):
def are_fileobjs_equal_read_first(f1, f2):
'''Given two (buffered) file objects, compare their contents.
"""Given two (buffered) file objects, compare their contents.
Returns a tuple (is_equal, content_of_f1), where the first element indicates
whether the two file objects have the same content, and the second element is
the content of the first file object.'''
the content of the first file object."""
blocksize = 65536
b1buf = b''
b2buf = b''
b1buf = b""
b2buf = b""
is_equal = True
content = []
while True:
@@ -268,7 +271,7 @@ def are_fileobjs_equal_read_first(f1, f2):
if f1:
content.append(f1.read())
return is_equal, b''.join(content)
return is_equal, b"".join(content)
def is_container_file_not_regular_file(container_stat):
@@ -283,18 +286,18 @@ def is_container_file_not_regular_file(container_stat):
32 - 11, # ModeCharDevice
32 - 13, # ModeIrregular
):
if container_stat['mode'] & (1 << bit) != 0:
if container_stat["mode"] & (1 << bit) != 0:
return True
return False
def get_container_file_mode(container_stat):
mode = container_stat['mode'] & 0xFFF
if container_stat['mode'] & (1 << (32 - 9)) != 0: # ModeSetuid
mode = container_stat["mode"] & 0xFFF
if container_stat["mode"] & (1 << (32 - 9)) != 0: # ModeSetuid
mode |= stat.S_ISUID # set UID bit
if container_stat['mode'] & (1 << (32 - 10)) != 0: # ModeSetgid
if container_stat["mode"] & (1 << (32 - 10)) != 0: # ModeSetgid
mode |= stat.S_ISGID # set GID bit
if container_stat['mode'] & (1 << (32 - 12)) != 0: # ModeSticky
if container_stat["mode"] & (1 << (32 - 12)) != 0: # ModeSticky
mode |= stat.S_ISVTX # sticky bit
return mode
@@ -302,77 +305,88 @@ def get_container_file_mode(container_stat):
def add_other_diff(diff, in_path, member):
if diff is None:
return
diff['before_header'] = in_path
diff["before_header"] = in_path
if member.isdir():
diff['before'] = '(directory)'
diff["before"] = "(directory)"
elif member.issym() or member.islnk():
diff['before'] = member.linkname
diff["before"] = member.linkname
elif member.ischr():
diff['before'] = '(character device)'
diff["before"] = "(character device)"
elif member.isblk():
diff['before'] = '(block device)'
diff["before"] = "(block device)"
elif member.isfifo():
diff['before'] = '(fifo)'
diff["before"] = "(fifo)"
elif member.isdev():
diff['before'] = '(device)'
diff["before"] = "(device)"
elif member.isfile():
raise DockerUnexpectedError('should not be a regular file')
raise DockerUnexpectedError("should not be a regular file")
else:
diff['before'] = '(unknown filesystem object)'
diff["before"] = "(unknown filesystem object)"
def retrieve_diff(client, container, container_path, follow_links, diff, max_file_size_for_diff, regular_stat=None, link_target=None):
def retrieve_diff(
client,
container,
container_path,
follow_links,
diff,
max_file_size_for_diff,
regular_stat=None,
link_target=None,
):
if diff is None:
return
if regular_stat is not None:
# First handle all filesystem object types that are not regular files
if regular_stat['mode'] & (1 << (32 - 1)) != 0:
diff['before_header'] = container_path
diff['before'] = '(directory)'
if regular_stat["mode"] & (1 << (32 - 1)) != 0:
diff["before_header"] = container_path
diff["before"] = "(directory)"
return
elif regular_stat['mode'] & (1 << (32 - 4)) != 0:
diff['before_header'] = container_path
diff['before'] = '(temporary file)'
elif regular_stat["mode"] & (1 << (32 - 4)) != 0:
diff["before_header"] = container_path
diff["before"] = "(temporary file)"
return
elif regular_stat['mode'] & (1 << (32 - 5)) != 0:
diff['before_header'] = container_path
diff['before'] = link_target
elif regular_stat["mode"] & (1 << (32 - 5)) != 0:
diff["before_header"] = container_path
diff["before"] = link_target
return
elif regular_stat['mode'] & (1 << (32 - 6)) != 0:
diff['before_header'] = container_path
diff['before'] = '(device)'
elif regular_stat["mode"] & (1 << (32 - 6)) != 0:
diff["before_header"] = container_path
diff["before"] = "(device)"
return
elif regular_stat['mode'] & (1 << (32 - 7)) != 0:
diff['before_header'] = container_path
diff['before'] = '(named pipe)'
elif regular_stat["mode"] & (1 << (32 - 7)) != 0:
diff["before_header"] = container_path
diff["before"] = "(named pipe)"
return
elif regular_stat['mode'] & (1 << (32 - 8)) != 0:
diff['before_header'] = container_path
diff['before'] = '(socket)'
elif regular_stat["mode"] & (1 << (32 - 8)) != 0:
diff["before_header"] = container_path
diff["before"] = "(socket)"
return
elif regular_stat['mode'] & (1 << (32 - 11)) != 0:
diff['before_header'] = container_path
diff['before'] = '(character device)'
elif regular_stat["mode"] & (1 << (32 - 11)) != 0:
diff["before_header"] = container_path
diff["before"] = "(character device)"
return
elif regular_stat['mode'] & (1 << (32 - 13)) != 0:
diff['before_header'] = container_path
diff['before'] = '(unknown filesystem object)'
elif regular_stat["mode"] & (1 << (32 - 13)) != 0:
diff["before_header"] = container_path
diff["before"] = "(unknown filesystem object)"
return
# Check whether file is too large
if regular_stat['size'] > max_file_size_for_diff > 0:
diff['dst_larger'] = max_file_size_for_diff
if regular_stat["size"] > max_file_size_for_diff > 0:
diff["dst_larger"] = max_file_size_for_diff
return
# We need to get hold of the content
def process_none(in_path):
diff['before'] = ''
diff["before"] = ""
def process_regular(in_path, tar, member):
add_diff_dst_from_regular_member(diff, max_file_size_for_diff, in_path, tar, member)
add_diff_dst_from_regular_member(
diff, max_file_size_for_diff, in_path, tar, member
)
def process_symlink(in_path, member):
diff['before_header'] = in_path
diff['before'] = member.linkname
diff["before_header"] = in_path
diff["before"] = member.linkname
def process_other(in_path, member):
add_other_diff(diff, in_path, member)
@@ -390,53 +404,57 @@ def retrieve_diff(client, container, container_path, follow_links, diff, max_fil
def is_binary(content):
if b'\x00' in content:
if b"\x00" in content:
return True
# TODO: better detection
# (ansible-core also just checks for 0x00, and even just sticks to the first 8k, so this is not too bad...)
return False
def are_fileobjs_equal_with_diff_of_first(f1, f2, size, diff, max_file_size_for_diff, container_path):
def are_fileobjs_equal_with_diff_of_first(
f1, f2, size, diff, max_file_size_for_diff, container_path
):
if diff is None:
return are_fileobjs_equal(f1, f2)
if size > max_file_size_for_diff > 0:
diff['dst_larger'] = max_file_size_for_diff
diff["dst_larger"] = max_file_size_for_diff
return are_fileobjs_equal(f1, f2)
is_equal, content = are_fileobjs_equal_read_first(f1, f2)
if is_binary(content):
diff['dst_binary'] = 1
diff["dst_binary"] = 1
else:
diff['before_header'] = container_path
diff['before'] = to_text(content)
diff["before_header"] = container_path
diff["before"] = to_text(content)
return is_equal
def add_diff_dst_from_regular_member(diff, max_file_size_for_diff, container_path, tar, member):
def add_diff_dst_from_regular_member(
diff, max_file_size_for_diff, container_path, tar, member
):
if diff is None:
return
if member.size > max_file_size_for_diff > 0:
diff['dst_larger'] = max_file_size_for_diff
diff["dst_larger"] = max_file_size_for_diff
return
with tar.extractfile(member) as tar_f:
content = tar_f.read()
if is_binary(content):
diff['dst_binary'] = 1
diff["dst_binary"] = 1
else:
diff['before_header'] = container_path
diff['before'] = to_text(content)
diff["before_header"] = container_path
diff["before"] = to_text(content)
def copy_dst_to_src(diff):
if diff is None:
return
for f, t in [
('dst_size', 'src_size'),
('dst_binary', 'src_binary'),
('before_header', 'after_header'),
('before', 'after'),
("dst_size", "src_size"),
("dst_binary", "src_binary"),
("before_header", "after_header"),
("before", "after"),
]:
if f in diff:
diff[t] = diff[f]
@@ -444,38 +462,61 @@ def copy_dst_to_src(diff):
diff.pop(t)
def is_file_idempotent(client, container, managed_path, container_path, follow_links, local_follow_links, owner_id, group_id, mode,
force=False, diff=None, max_file_size_for_diff=1):
def is_file_idempotent(
client,
container,
managed_path,
container_path,
follow_links,
local_follow_links,
owner_id,
group_id,
mode,
force=False,
diff=None,
max_file_size_for_diff=1,
):
# Retrieve information of local file
try:
file_stat = os.stat(managed_path) if local_follow_links else os.lstat(managed_path)
file_stat = (
os.stat(managed_path) if local_follow_links else os.lstat(managed_path)
)
except OSError as exc:
if exc.errno == 2:
raise DockerFileNotFound(f'Cannot find local file {managed_path}')
raise DockerFileNotFound(f"Cannot find local file {managed_path}")
raise
if mode is None:
mode = stat.S_IMODE(file_stat.st_mode)
if not stat.S_ISLNK(file_stat.st_mode) and not stat.S_ISREG(file_stat.st_mode):
raise DockerFileCopyError('Local path {managed_path} is not a symbolic link or file')
raise DockerFileCopyError(
"Local path {managed_path} is not a symbolic link or file"
)
if diff is not None:
if file_stat.st_size > max_file_size_for_diff > 0:
diff['src_larger'] = max_file_size_for_diff
diff["src_larger"] = max_file_size_for_diff
elif stat.S_ISLNK(file_stat.st_mode):
diff['after_header'] = managed_path
diff['after'] = os.readlink(managed_path)
diff["after_header"] = managed_path
diff["after"] = os.readlink(managed_path)
else:
with open(managed_path, 'rb') as f:
with open(managed_path, "rb") as f:
content = f.read()
if is_binary(content):
diff['src_binary'] = 1
diff["src_binary"] = 1
else:
diff['after_header'] = managed_path
diff['after'] = to_text(content)
diff["after_header"] = managed_path
diff["after"] = to_text(content)
# When forcing and we are not following links in the container, go!
if force and not follow_links:
retrieve_diff(client, container, container_path, follow_links, diff, max_file_size_for_diff)
retrieve_diff(
client,
container,
container_path,
follow_links,
diff,
max_file_size_for_diff,
)
return container_path, mode, False
# Resolve symlinks in the container (if requested), and get information on container's file
@@ -493,40 +534,112 @@ def is_file_idempotent(client, container, managed_path, container_path, follow_l
# If the file was not found, continue
if regular_stat is None:
if diff is not None:
diff['before_header'] = container_path
diff['before'] = ''
diff["before_header"] = container_path
diff["before"] = ""
return container_path, mode, False
# When forcing, go!
if force:
retrieve_diff(client, container, container_path, follow_links, diff, max_file_size_for_diff, regular_stat, link_target)
retrieve_diff(
client,
container,
container_path,
follow_links,
diff,
max_file_size_for_diff,
regular_stat,
link_target,
)
return container_path, mode, False
# If force is set to False, and the destination exists, assume there's nothing to do
if force is False:
retrieve_diff(client, container, container_path, follow_links, diff, max_file_size_for_diff, regular_stat, link_target)
retrieve_diff(
client,
container,
container_path,
follow_links,
diff,
max_file_size_for_diff,
regular_stat,
link_target,
)
copy_dst_to_src(diff)
return container_path, mode, True
# Basic idempotency checks
if stat.S_ISLNK(file_stat.st_mode):
if link_target is None:
retrieve_diff(client, container, container_path, follow_links, diff, max_file_size_for_diff, regular_stat, link_target)
retrieve_diff(
client,
container,
container_path,
follow_links,
diff,
max_file_size_for_diff,
regular_stat,
link_target,
)
return container_path, mode, False
local_link_target = os.readlink(managed_path)
retrieve_diff(client, container, container_path, follow_links, diff, max_file_size_for_diff, regular_stat, link_target)
retrieve_diff(
client,
container,
container_path,
follow_links,
diff,
max_file_size_for_diff,
regular_stat,
link_target,
)
return container_path, mode, local_link_target == link_target
if link_target is not None:
retrieve_diff(client, container, container_path, follow_links, diff, max_file_size_for_diff, regular_stat, link_target)
retrieve_diff(
client,
container,
container_path,
follow_links,
diff,
max_file_size_for_diff,
regular_stat,
link_target,
)
return container_path, mode, False
if is_container_file_not_regular_file(regular_stat):
retrieve_diff(client, container, container_path, follow_links, diff, max_file_size_for_diff, regular_stat, link_target)
retrieve_diff(
client,
container,
container_path,
follow_links,
diff,
max_file_size_for_diff,
regular_stat,
link_target,
)
return container_path, mode, False
if file_stat.st_size != regular_stat['size']:
retrieve_diff(client, container, container_path, follow_links, diff, max_file_size_for_diff, regular_stat, link_target)
if file_stat.st_size != regular_stat["size"]:
retrieve_diff(
client,
container,
container_path,
follow_links,
diff,
max_file_size_for_diff,
regular_stat,
link_target,
)
return container_path, mode, False
if mode != get_container_file_mode(regular_stat):
retrieve_diff(client, container, container_path, follow_links, diff, max_file_size_for_diff, regular_stat, link_target)
retrieve_diff(
client,
container,
container_path,
follow_links,
diff,
max_file_size_for_diff,
regular_stat,
link_target,
)
return container_path, mode, False
# Fetch file from container
@@ -535,25 +648,31 @@ def is_file_idempotent(client, container, managed_path, container_path, follow_l
def process_regular(in_path, tar, member):
# Check things like user/group ID and mode
if any([
member.mode & 0xFFF != mode,
member.uid != owner_id,
member.gid != group_id,
not stat.S_ISREG(file_stat.st_mode),
member.size != file_stat.st_size,
]):
add_diff_dst_from_regular_member(diff, max_file_size_for_diff, in_path, tar, member)
if any(
[
member.mode & 0xFFF != mode,
member.uid != owner_id,
member.gid != group_id,
not stat.S_ISREG(file_stat.st_mode),
member.size != file_stat.st_size,
]
):
add_diff_dst_from_regular_member(
diff, max_file_size_for_diff, in_path, tar, member
)
return container_path, mode, False
with tar.extractfile(member) as tar_f:
with open(managed_path, 'rb') as local_f:
is_equal = are_fileobjs_equal_with_diff_of_first(tar_f, local_f, member.size, diff, max_file_size_for_diff, in_path)
with open(managed_path, "rb") as local_f:
is_equal = are_fileobjs_equal_with_diff_of_first(
tar_f, local_f, member.size, diff, max_file_size_for_diff, in_path
)
return container_path, mode, is_equal
def process_symlink(in_path, member):
if diff is not None:
diff['before_header'] = in_path
diff['before'] = member.linkname
diff["before_header"] = in_path
diff["before"] = member.linkname
# Check things like user/group ID and mode
if member.mode & 0xFFF != mode:
@@ -585,8 +704,20 @@ def is_file_idempotent(client, container, managed_path, container_path, follow_l
)
def copy_file_into_container(client, container, managed_path, container_path, follow_links, local_follow_links,
owner_id, group_id, mode, force=False, diff=False, max_file_size_for_diff=1):
def copy_file_into_container(
client,
container,
managed_path,
container_path,
follow_links,
local_follow_links,
owner_id,
group_id,
mode,
force=False,
diff=False,
max_file_size_for_diff=1,
):
if diff:
diff = {}
else:
@@ -625,24 +756,42 @@ def copy_file_into_container(client, container, managed_path, container_path, fo
changed=changed,
)
if diff:
result['diff'] = diff
result["diff"] = diff
client.module.exit_json(**result)
def is_content_idempotent(client, container, content, container_path, follow_links, owner_id, group_id, mode,
force=False, diff=None, max_file_size_for_diff=1):
def is_content_idempotent(
client,
container,
content,
container_path,
follow_links,
owner_id,
group_id,
mode,
force=False,
diff=None,
max_file_size_for_diff=1,
):
if diff is not None:
if len(content) > max_file_size_for_diff > 0:
diff['src_larger'] = max_file_size_for_diff
diff["src_larger"] = max_file_size_for_diff
elif is_binary(content):
diff['src_binary'] = 1
diff["src_binary"] = 1
else:
diff['after_header'] = 'dynamically generated'
diff['after'] = to_text(content)
diff["after_header"] = "dynamically generated"
diff["after"] = to_text(content)
# When forcing and we are not following links in the container, go!
if force and not follow_links:
retrieve_diff(client, container, container_path, follow_links, diff, max_file_size_for_diff)
retrieve_diff(
client,
container,
container_path,
follow_links,
diff,
max_file_size_for_diff,
)
return container_path, mode, False
# Resolve symlinks in the container (if requested), and get information on container's file
@@ -660,60 +809,125 @@ def is_content_idempotent(client, container, content, container_path, follow_lin
# If the file was not found, continue
if regular_stat is None:
if diff is not None:
diff['before_header'] = container_path
diff['before'] = ''
diff["before_header"] = container_path
diff["before"] = ""
return container_path, mode, False
# When forcing, go!
if force:
retrieve_diff(client, container, container_path, follow_links, diff, max_file_size_for_diff, regular_stat, link_target)
retrieve_diff(
client,
container,
container_path,
follow_links,
diff,
max_file_size_for_diff,
regular_stat,
link_target,
)
return container_path, mode, False
# If force is set to False, and the destination exists, assume there's nothing to do
if force is False:
retrieve_diff(client, container, container_path, follow_links, diff, max_file_size_for_diff, regular_stat, link_target)
retrieve_diff(
client,
container,
container_path,
follow_links,
diff,
max_file_size_for_diff,
regular_stat,
link_target,
)
copy_dst_to_src(diff)
return container_path, mode, True
# Basic idempotency checks
if link_target is not None:
retrieve_diff(client, container, container_path, follow_links, diff, max_file_size_for_diff, regular_stat, link_target)
retrieve_diff(
client,
container,
container_path,
follow_links,
diff,
max_file_size_for_diff,
regular_stat,
link_target,
)
return container_path, mode, False
if is_container_file_not_regular_file(regular_stat):
retrieve_diff(client, container, container_path, follow_links, diff, max_file_size_for_diff, regular_stat, link_target)
retrieve_diff(
client,
container,
container_path,
follow_links,
diff,
max_file_size_for_diff,
regular_stat,
link_target,
)
return container_path, mode, False
if len(content) != regular_stat['size']:
retrieve_diff(client, container, container_path, follow_links, diff, max_file_size_for_diff, regular_stat, link_target)
if len(content) != regular_stat["size"]:
retrieve_diff(
client,
container,
container_path,
follow_links,
diff,
max_file_size_for_diff,
regular_stat,
link_target,
)
return container_path, mode, False
if mode != get_container_file_mode(regular_stat):
retrieve_diff(client, container, container_path, follow_links, diff, max_file_size_for_diff, regular_stat, link_target)
retrieve_diff(
client,
container,
container_path,
follow_links,
diff,
max_file_size_for_diff,
regular_stat,
link_target,
)
return container_path, mode, False
# Fetch file from container
def process_none(in_path):
if diff is not None:
diff['before'] = ''
diff["before"] = ""
return container_path, mode, False
def process_regular(in_path, tar, member):
# Check things like user/group ID and mode
if any([
member.mode & 0xFFF != mode,
member.uid != owner_id,
member.gid != group_id,
member.size != len(content),
]):
add_diff_dst_from_regular_member(diff, max_file_size_for_diff, in_path, tar, member)
if any(
[
member.mode & 0xFFF != mode,
member.uid != owner_id,
member.gid != group_id,
member.size != len(content),
]
):
add_diff_dst_from_regular_member(
diff, max_file_size_for_diff, in_path, tar, member
)
return container_path, mode, False
with tar.extractfile(member) as tar_f:
is_equal = are_fileobjs_equal_with_diff_of_first(tar_f, io.BytesIO(content), member.size, diff, max_file_size_for_diff, in_path)
is_equal = are_fileobjs_equal_with_diff_of_first(
tar_f,
io.BytesIO(content),
member.size,
diff,
max_file_size_for_diff,
in_path,
)
return container_path, mode, is_equal
def process_symlink(in_path, member):
if diff is not None:
diff['before_header'] = in_path
diff['before'] = member.linkname
diff["before_header"] = in_path
diff["before"] = member.linkname
return container_path, mode, False
@@ -733,8 +947,19 @@ def is_content_idempotent(client, container, content, container_path, follow_lin
)
def copy_content_into_container(client, container, content, container_path, follow_links,
owner_id, group_id, mode, force=False, diff=False, max_file_size_for_diff=1):
def copy_content_into_container(
client,
container,
content,
container_path,
follow_links,
owner_id,
group_id,
mode,
force=False,
diff=False,
max_file_size_for_diff=1,
):
if diff:
diff = {}
else:
@@ -773,11 +998,11 @@ def copy_content_into_container(client, container, content, container_path, foll
if diff:
# Since the content is no_log, make sure that the before/after strings look sufficiently different
key = generate_insecure_key()
diff['scrambled_diff'] = base64.b64encode(key)
for k in ('before', 'after'):
diff["scrambled_diff"] = base64.b64encode(key)
for k in ("before", "after"):
if k in diff:
diff[k] = scramble(diff[k], key)
result['diff'] = diff
result["diff"] = diff
client.module.exit_json(**result)
@@ -786,65 +1011,68 @@ def parse_modern(mode):
return int(to_native(mode), 8)
if isinstance(mode, int):
return mode
raise TypeError(f'must be an octal string or an integer, got {mode!r}')
raise TypeError(f"must be an octal string or an integer, got {mode!r}")
def parse_octal_string_only(mode):
if isinstance(mode, str):
return int(to_native(mode), 8)
raise TypeError(f'must be an octal string, got {mode!r}')
raise TypeError(f"must be an octal string, got {mode!r}")
def main():
argument_spec = dict(
container=dict(type='str', required=True),
path=dict(type='path'),
container_path=dict(type='str', required=True),
follow=dict(type='bool', default=False),
local_follow=dict(type='bool', default=True),
owner_id=dict(type='int'),
group_id=dict(type='int'),
mode=dict(type='raw'),
mode_parse=dict(type='str', choices=['legacy', 'modern', 'octal_string_only'], default='legacy'),
force=dict(type='bool'),
content=dict(type='str', no_log=True),
content_is_b64=dict(type='bool', default=False),
container=dict(type="str", required=True),
path=dict(type="path"),
container_path=dict(type="str", required=True),
follow=dict(type="bool", default=False),
local_follow=dict(type="bool", default=True),
owner_id=dict(type="int"),
group_id=dict(type="int"),
mode=dict(type="raw"),
mode_parse=dict(
type="str",
choices=["legacy", "modern", "octal_string_only"],
default="legacy",
),
force=dict(type="bool"),
content=dict(type="str", no_log=True),
content_is_b64=dict(type="bool", default=False),
# Undocumented parameters for use by the action plugin
_max_file_size_for_diff=dict(type='int'),
_max_file_size_for_diff=dict(type="int"),
)
client = AnsibleDockerClient(
argument_spec=argument_spec,
min_docker_api_version='1.20',
min_docker_api_version="1.20",
supports_check_mode=True,
mutually_exclusive=[('path', 'content')],
required_together=[('owner_id', 'group_id')],
mutually_exclusive=[("path", "content")],
required_together=[("owner_id", "group_id")],
required_by={
'content': ['mode'],
"content": ["mode"],
},
)
container = client.module.params['container']
managed_path = client.module.params['path']
container_path = client.module.params['container_path']
follow = client.module.params['follow']
local_follow = client.module.params['local_follow']
owner_id = client.module.params['owner_id']
group_id = client.module.params['group_id']
mode = client.module.params['mode']
force = client.module.params['force']
content = client.module.params['content']
max_file_size_for_diff = client.module.params['_max_file_size_for_diff'] or 1
container = client.module.params["container"]
managed_path = client.module.params["path"]
container_path = client.module.params["container_path"]
follow = client.module.params["follow"]
local_follow = client.module.params["local_follow"]
owner_id = client.module.params["owner_id"]
group_id = client.module.params["group_id"]
mode = client.module.params["mode"]
force = client.module.params["force"]
content = client.module.params["content"]
max_file_size_for_diff = client.module.params["_max_file_size_for_diff"] or 1
if mode is not None:
mode_parse = client.module.params['mode_parse']
mode_parse = client.module.params["mode_parse"]
try:
if mode_parse == 'legacy':
if mode_parse == "legacy":
mode = check_type_int(mode)
elif mode_parse == 'modern':
elif mode_parse == "modern":
mode = parse_modern(mode)
elif mode_parse == 'octal_string_only':
elif mode_parse == "octal_string_only":
mode = parse_octal_string_only(mode)
except (TypeError, ValueError) as e:
client.fail(f"Error while parsing 'mode': {e}")
@@ -852,11 +1080,13 @@ def main():
client.fail(f"'mode' must not be negative; got {mode}")
if content is not None:
if client.module.params['content_is_b64']:
if client.module.params["content_is_b64"]:
try:
content = base64.b64decode(content)
except Exception as e: # depending on Python version and error, multiple different exceptions can be raised
client.fail(f'Cannot Base64 decode the content option: {e}')
except (
Exception
) as e: # depending on Python version and error, multiple different exceptions can be raised
client.fail(f"Cannot Base64 decode the content option: {e}")
else:
content = to_bytes(content)
@@ -899,24 +1129,31 @@ def main():
)
else:
# Can happen if a user explicitly passes `content: null` or `path: null`...
client.fail('One of path and content must be supplied')
client.fail("One of path and content must be supplied")
except NotFound as exc:
client.fail(f'Could not find container "{container}" or resource in it ({exc})')
except APIError as exc:
client.fail(f'An unexpected Docker error occurred for container "{container}": {exc}', exception=traceback.format_exc())
client.fail(
f'An unexpected Docker error occurred for container "{container}": {exc}',
exception=traceback.format_exc(),
)
except DockerException as exc:
client.fail(f'An unexpected Docker error occurred for container "{container}": {exc}', exception=traceback.format_exc())
client.fail(
f'An unexpected Docker error occurred for container "{container}": {exc}',
exception=traceback.format_exc(),
)
except RequestException as exc:
client.fail(
f'An unexpected requests error occurred for container "{container}" when trying to talk to the Docker daemon: {exc}',
exception=traceback.format_exc())
exception=traceback.format_exc(),
)
except DockerUnexpectedError as exc:
client.fail(f'Unexpected error: {exc}', exception=traceback.format_exc())
client.fail(f"Unexpected error: {exc}", exception=traceback.format_exc())
except DockerFileCopyError as exc:
client.fail(to_native(exc))
except OSError as exc:
client.fail(f'Unexpected error: {exc}', exception=traceback.format_exc())
client.fail(f"Unexpected error: {exc}", exception=traceback.format_exc())
if __name__ == '__main__':
if __name__ == "__main__":
main()
+90 -70
View File
@@ -167,110 +167,116 @@ import selectors
import shlex
import traceback
from ansible.module_utils.common.text.converters import to_text, to_bytes
from ansible_collections.community.docker.plugins.module_utils.common_api import (
AnsibleDockerClient,
RequestException,
)
from ansible_collections.community.docker.plugins.module_utils.socket_handler import (
DockerSocketHandlerModule,
)
from ansible.module_utils.common.text.converters import to_bytes, to_text
from ansible_collections.community.docker.plugins.module_utils._api.errors import (
APIError,
DockerException,
NotFound,
)
from ansible_collections.community.docker.plugins.module_utils._api.utils.utils import format_environment
from ansible_collections.community.docker.plugins.module_utils._api.utils.utils import (
format_environment,
)
from ansible_collections.community.docker.plugins.module_utils.common_api import (
AnsibleDockerClient,
RequestException,
)
from ansible_collections.community.docker.plugins.module_utils.socket_handler import (
DockerSocketHandlerModule,
)
def main():
argument_spec = dict(
container=dict(type='str', required=True),
argv=dict(type='list', elements='str'),
command=dict(type='str'),
chdir=dict(type='str'),
detach=dict(type='bool', default=False),
user=dict(type='str'),
stdin=dict(type='str'),
stdin_add_newline=dict(type='bool', default=True),
strip_empty_ends=dict(type='bool', default=True),
tty=dict(type='bool', default=False),
env=dict(type='dict'),
container=dict(type="str", required=True),
argv=dict(type="list", elements="str"),
command=dict(type="str"),
chdir=dict(type="str"),
detach=dict(type="bool", default=False),
user=dict(type="str"),
stdin=dict(type="str"),
stdin_add_newline=dict(type="bool", default=True),
strip_empty_ends=dict(type="bool", default=True),
tty=dict(type="bool", default=False),
env=dict(type="dict"),
)
option_minimal_versions = dict(
chdir=dict(docker_api_version='1.35'),
chdir=dict(docker_api_version="1.35"),
)
client = AnsibleDockerClient(
argument_spec=argument_spec,
option_minimal_versions=option_minimal_versions,
mutually_exclusive=[('argv', 'command')],
required_one_of=[('argv', 'command')],
mutually_exclusive=[("argv", "command")],
required_one_of=[("argv", "command")],
)
container = client.module.params['container']
argv = client.module.params['argv']
command = client.module.params['command']
chdir = client.module.params['chdir']
detach = client.module.params['detach']
user = client.module.params['user']
stdin = client.module.params['stdin']
strip_empty_ends = client.module.params['strip_empty_ends']
tty = client.module.params['tty']
env = client.module.params['env']
container = client.module.params["container"]
argv = client.module.params["argv"]
command = client.module.params["command"]
chdir = client.module.params["chdir"]
detach = client.module.params["detach"]
user = client.module.params["user"]
stdin = client.module.params["stdin"]
strip_empty_ends = client.module.params["strip_empty_ends"]
tty = client.module.params["tty"]
env = client.module.params["env"]
if env is not None:
for name, value in list(env.items()):
if not isinstance(value, str):
client.module.fail_json(
msg="Non-string value found for env option. Ambiguous env options must be "
f"wrapped in quotes to avoid them being interpreted. Key: {name}")
env[name] = to_text(value, errors='surrogate_or_strict')
f"wrapped in quotes to avoid them being interpreted. Key: {name}"
)
env[name] = to_text(value, errors="surrogate_or_strict")
if command is not None:
argv = shlex.split(command)
if detach and stdin is not None:
client.module.fail_json(msg='If detach=true, stdin cannot be provided.')
client.module.fail_json(msg="If detach=true, stdin cannot be provided.")
if stdin is not None and client.module.params['stdin_add_newline']:
stdin += '\n'
if stdin is not None and client.module.params["stdin_add_newline"]:
stdin += "\n"
try:
data = {
'Container': container,
'User': user or '',
'Privileged': False,
'Tty': False,
'AttachStdin': bool(stdin),
'AttachStdout': True,
'AttachStderr': True,
'Cmd': argv,
'Env': format_environment(env) if env is not None else None,
"Container": container,
"User": user or "",
"Privileged": False,
"Tty": False,
"AttachStdin": bool(stdin),
"AttachStdout": True,
"AttachStderr": True,
"Cmd": argv,
"Env": format_environment(env) if env is not None else None,
}
if chdir is not None:
data['WorkingDir'] = chdir
data["WorkingDir"] = chdir
exec_data = client.post_json_to_json('/containers/{0}/exec', container, data=data)
exec_id = exec_data['Id']
exec_data = client.post_json_to_json(
"/containers/{0}/exec", container, data=data
)
exec_id = exec_data["Id"]
data = {
'Tty': tty,
'Detach': detach,
"Tty": tty,
"Detach": detach,
}
if detach:
client.post_json_to_text('/exec/{0}/start', exec_id, data=data)
client.post_json_to_text("/exec/{0}/start", exec_id, data=data)
client.module.exit_json(changed=True, exec_id=exec_id)
else:
if stdin and not detach:
exec_socket = client.post_json_to_stream_socket('/exec/{0}/start', exec_id, data=data)
exec_socket = client.post_json_to_stream_socket(
"/exec/{0}/start", exec_id, data=data
)
try:
with DockerSocketHandlerModule(exec_socket, client.module, selectors) as exec_socket_handler:
with DockerSocketHandlerModule(
exec_socket, client.module, selectors
) as exec_socket_handler:
if stdin:
exec_socket_handler.write(to_bytes(stdin))
@@ -278,35 +284,49 @@ def main():
finally:
exec_socket.close()
else:
stdout, stderr = client.post_json_to_stream('/exec/{0}/start', exec_id, data=data, stream=False, tty=tty, demux=True)
stdout, stderr = client.post_json_to_stream(
"/exec/{0}/start",
exec_id,
data=data,
stream=False,
tty=tty,
demux=True,
)
result = client.get_json('/exec/{0}/json', exec_id)
result = client.get_json("/exec/{0}/json", exec_id)
stdout = to_text(stdout or b'')
stderr = to_text(stderr or b'')
stdout = to_text(stdout or b"")
stderr = to_text(stderr or b"")
if strip_empty_ends:
stdout = stdout.rstrip('\r\n')
stderr = stderr.rstrip('\r\n')
stdout = stdout.rstrip("\r\n")
stderr = stderr.rstrip("\r\n")
client.module.exit_json(
changed=True,
stdout=stdout,
stderr=stderr,
rc=result.get('ExitCode') or 0,
rc=result.get("ExitCode") or 0,
)
except NotFound:
client.fail(f'Could not find container "{container}"')
except APIError as e:
if e.response is not None and e.response.status_code == 409:
client.fail(f'The container "{container}" has been paused ({e})')
client.fail(f'An unexpected Docker error occurred: {e}', exception=traceback.format_exc())
client.fail(
f"An unexpected Docker error occurred: {e}",
exception=traceback.format_exc(),
)
except DockerException as e:
client.fail(f'An unexpected Docker error occurred: {e}', exception=traceback.format_exc())
client.fail(
f"An unexpected Docker error occurred: {e}",
exception=traceback.format_exc(),
)
except RequestException as e:
client.fail(
f'An unexpected requests error occurred when trying to talk to the Docker daemon: {e}',
exception=traceback.format_exc())
f"An unexpected requests error occurred when trying to talk to the Docker daemon: {e}",
exception=traceback.format_exc(),
)
if __name__ == '__main__':
if __name__ == "__main__":
main()
+13 -7
View File
@@ -77,16 +77,18 @@ container:
import traceback
from ansible_collections.community.docker.plugins.module_utils._api.errors import (
DockerException,
)
from ansible_collections.community.docker.plugins.module_utils.common_api import (
AnsibleDockerClient,
RequestException,
)
from ansible_collections.community.docker.plugins.module_utils._api.errors import DockerException
def main():
argument_spec = dict(
name=dict(type='str', required=True),
name=dict(type="str", required=True),
)
client = AnsibleDockerClient(
@@ -95,7 +97,7 @@ def main():
)
try:
container = client.get_container(client.module.params['name'])
container = client.get_container(client.module.params["name"])
client.module.exit_json(
changed=False,
@@ -103,12 +105,16 @@ def main():
container=container,
)
except DockerException as e:
client.fail(f'An unexpected Docker error occurred: {e}', exception=traceback.format_exc())
client.fail(
f"An unexpected Docker error occurred: {e}",
exception=traceback.format_exc(),
)
except RequestException as e:
client.fail(
f'An unexpected requests error occurred when trying to talk to the Docker daemon: {e}',
exception=traceback.format_exc())
f"An unexpected requests error occurred when trying to talk to the Docker daemon: {e}",
exception=traceback.format_exc(),
)
if __name__ == '__main__':
if __name__ == "__main__":
main()
+62 -47
View File
@@ -176,7 +176,6 @@ import traceback
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.common.text.converters import to_text
from ansible_collections.community.docker.plugins.module_utils._api.context.api import (
ContextAPI,
)
@@ -196,10 +195,10 @@ def tls_context_to_json(context):
if context is None:
return None
return {
'client_cert': context.cert[0] if context.cert else None,
'client_key': context.cert[1] if context.cert else None,
'ca_cert': context.ca_cert,
'verify': context.verify,
"client_cert": context.cert[0] if context.cert else None,
"client_key": context.cert[1] if context.cert else None,
"ca_cert": context.ca_cert,
"verify": context.verify,
# 'ssl_version': context.ssl_version, -- this isn't used anymore
}
@@ -210,52 +209,52 @@ def to_bool(value):
def context_to_json(context, current):
module_config = {}
if 'docker' in context.endpoints:
endpoint = context.endpoints['docker']
if isinstance(endpoint.get('Host'), str):
host_str = to_text(endpoint['Host'])
if "docker" in context.endpoints:
endpoint = context.endpoints["docker"]
if isinstance(endpoint.get("Host"), str):
host_str = to_text(endpoint["Host"])
# Adjust protocol name so that it works with the Docker CLI tool as well
proto = None
idx = host_str.find('://')
idx = host_str.find("://")
if idx >= 0:
proto = host_str[:idx]
host_str = host_str[idx + 3:]
if proto in ('http', 'https'):
proto = 'tcp'
if proto == 'http+unix':
proto = 'unix'
host_str = host_str[idx + 3 :]
if proto in ("http", "https"):
proto = "tcp"
if proto == "http+unix":
proto = "unix"
if proto:
host_str = f"{proto}://{host_str}"
# Create config for the modules
module_config['docker_host'] = host_str
if context.tls_cfg.get('docker'):
tls_cfg = context.tls_cfg['docker']
module_config["docker_host"] = host_str
if context.tls_cfg.get("docker"):
tls_cfg = context.tls_cfg["docker"]
if tls_cfg.ca_cert:
module_config['ca_path'] = tls_cfg.ca_cert
module_config["ca_path"] = tls_cfg.ca_cert
if tls_cfg.cert:
module_config['client_cert'] = tls_cfg.cert[0]
module_config['client_key'] = tls_cfg.cert[1]
module_config['validate_certs'] = tls_cfg.verify
module_config['tls'] = True
module_config["client_cert"] = tls_cfg.cert[0]
module_config["client_key"] = tls_cfg.cert[1]
module_config["validate_certs"] = tls_cfg.verify
module_config["tls"] = True
else:
module_config['tls'] = to_bool(endpoint.get('SkipTLSVerify'))
module_config["tls"] = to_bool(endpoint.get("SkipTLSVerify"))
return {
'current': current,
'name': context.name,
'description': context.description,
'meta_path': None if context.meta_path is IN_MEMORY else context.meta_path,
'tls_path': None if context.tls_path is IN_MEMORY else context.tls_path,
'config': module_config,
"current": current,
"name": context.name,
"description": context.description,
"meta_path": None if context.meta_path is IN_MEMORY else context.meta_path,
"tls_path": None if context.tls_path is IN_MEMORY else context.tls_path,
"config": module_config,
}
def main():
argument_spec = dict(
only_current=dict(type='bool', default=False),
name=dict(type='str'),
cli_context=dict(type='str'),
only_current=dict(type="bool", default=False),
name=dict(type="str"),
cli_context=dict(type="str"),
)
module = AnsibleModule(
@@ -267,15 +266,22 @@ def main():
)
try:
if module.params['cli_context']:
current_context_name, current_context_source = module.params['cli_context'], "cli_context module option"
if module.params["cli_context"]:
current_context_name, current_context_source = (
module.params["cli_context"],
"cli_context module option",
)
else:
current_context_name, current_context_source = get_current_context_name_with_source()
if module.params['name']:
contexts = [ContextAPI.get_context(module.params['name'])]
current_context_name, current_context_source = (
get_current_context_name_with_source()
)
if module.params["name"]:
contexts = [ContextAPI.get_context(module.params["name"])]
if not contexts[0]:
module.fail_json(msg=f"There is no context of name {module.params['name']!r}")
elif module.params['only_current']:
module.fail_json(
msg=f"There is no context of name {module.params['name']!r}"
)
elif module.params["only_current"]:
contexts = [ContextAPI.get_context(current_context_name)]
if not contexts[0]:
module.fail_json(
@@ -284,10 +290,13 @@ def main():
else:
contexts = ContextAPI.contexts()
json_contexts = sorted([
context_to_json(context, context.name == current_context_name)
for context in contexts
], key=lambda entry: entry['name'])
json_contexts = sorted(
[
context_to_json(context, context.name == current_context_name)
for context in contexts
],
key=lambda entry: entry["name"],
)
module.exit_json(
changed=False,
@@ -295,10 +304,16 @@ def main():
current_context_name=current_context_name,
)
except ContextException as e:
module.fail_json(msg=f'Error when handling Docker contexts: {e}', exception=traceback.format_exc())
module.fail_json(
msg=f"Error when handling Docker contexts: {e}",
exception=traceback.format_exc(),
)
except DockerException as e:
module.fail_json(msg=f'An unexpected Docker error occurred: {e}', exception=traceback.format_exc())
module.fail_json(
msg=f"An unexpected Docker error occurred: {e}",
exception=traceback.format_exc(),
)
if __name__ == '__main__':
if __name__ == "__main__":
main()
+81 -58
View File
@@ -213,17 +213,21 @@ disk_usage:
import traceback
from ansible_collections.community.docker.plugins.module_utils._api.errors import (
APIError,
DockerException,
)
from ansible_collections.community.docker.plugins.module_utils._api.utils.utils import (
convert_filters,
)
from ansible_collections.community.docker.plugins.module_utils.common_api import (
AnsibleDockerClient,
RequestException,
)
from ansible_collections.community.docker.plugins.module_utils.util import (
DockerBaseClass,
clean_dict_booleans_for_docker_api,
)
from ansible_collections.community.docker.plugins.module_utils._api.errors import DockerException, APIError
from ansible_collections.community.docker.plugins.module_utils._api.utils.utils import convert_filters
class DockerHostManager(DockerBaseClass):
@@ -234,24 +238,28 @@ class DockerHostManager(DockerBaseClass):
self.client = client
self.results = results
self.verbose_output = self.client.module.params['verbose_output']
self.verbose_output = self.client.module.params["verbose_output"]
listed_objects = ['volumes', 'networks', 'containers', 'images']
listed_objects = ["volumes", "networks", "containers", "images"]
self.results['host_info'] = self.get_docker_host_info()
self.results["host_info"] = self.get_docker_host_info()
# At this point we definitely know that we can talk to the Docker daemon
self.results['can_talk_to_docker'] = True
self.client.fail_results['can_talk_to_docker'] = True
self.results["can_talk_to_docker"] = True
self.client.fail_results["can_talk_to_docker"] = True
if self.client.module.params['disk_usage']:
self.results['disk_usage'] = self.get_docker_disk_usage_facts()
if self.client.module.params["disk_usage"]:
self.results["disk_usage"] = self.get_docker_disk_usage_facts()
for docker_object in listed_objects:
if self.client.module.params[docker_object]:
returned_name = docker_object
filter_name = docker_object + "_filters"
filters = clean_dict_booleans_for_docker_api(client.module.params.get(filter_name), True)
self.results[returned_name] = self.get_docker_items_list(docker_object, filters)
filters = clean_dict_booleans_for_docker_api(
client.module.params.get(filter_name), True
)
self.results[returned_name] = self.get_docker_items_list(
docker_object, filters
)
def get_docker_host_info(self):
try:
@@ -264,7 +272,7 @@ class DockerHostManager(DockerBaseClass):
if self.verbose_output:
return self.client.df()
else:
return dict(LayersSize=self.client.df()['LayersSize'])
return dict(LayersSize=self.client.df()["LayersSize"])
except APIError as exc:
self.client.fail(f"Error inspecting docker host: {exc}")
@@ -272,44 +280,52 @@ class DockerHostManager(DockerBaseClass):
items = None
items_list = []
header_containers = ['Id', 'Image', 'Command', 'Created', 'Status', 'Ports', 'Names']
header_volumes = ['Driver', 'Name']
header_images = ['Id', 'RepoTags', 'Created', 'Size']
header_networks = ['Id', 'Driver', 'Name', 'Scope']
header_containers = [
"Id",
"Image",
"Command",
"Created",
"Status",
"Ports",
"Names",
]
header_volumes = ["Driver", "Name"]
header_images = ["Id", "RepoTags", "Created", "Size"]
header_networks = ["Id", "Driver", "Name", "Scope"]
filter_arg = dict()
if filters:
filter_arg['filters'] = filters
filter_arg["filters"] = filters
try:
if docker_object == 'containers':
if docker_object == "containers":
params = {
'limit': -1,
'all': 1 if self.client.module.params['containers_all'] else 0,
'size': 0,
'trunc_cmd': 0,
'filters': convert_filters(filters) if filters else None,
"limit": -1,
"all": 1 if self.client.module.params["containers_all"] else 0,
"size": 0,
"trunc_cmd": 0,
"filters": convert_filters(filters) if filters else None,
}
items = self.client.get_json("/containers/json", params=params)
elif docker_object == 'networks':
params = {
'filters': convert_filters(filters or {})
}
elif docker_object == "networks":
params = {"filters": convert_filters(filters or {})}
items = self.client.get_json("/networks", params=params)
elif docker_object == 'images':
elif docker_object == "images":
params = {
'only_ids': 0,
'all': 0,
'filters': convert_filters(filters) if filters else None,
"only_ids": 0,
"all": 0,
"filters": convert_filters(filters) if filters else None,
}
items = self.client.get_json("/images/json", params=params)
elif docker_object == 'volumes':
elif docker_object == "volumes":
params = {
'filters': convert_filters(filters) if filters else None,
"filters": convert_filters(filters) if filters else None,
}
items = self.client.get_json('/volumes', params=params)
items = items['Volumes']
items = self.client.get_json("/volumes", params=params)
items = items["Volumes"]
except APIError as exc:
self.client.fail(f"Error inspecting docker host for object '{docker_object}': {exc}")
self.client.fail(
f"Error inspecting docker host for object '{docker_object}': {exc}"
)
if self.verbose_output:
return items
@@ -317,16 +333,16 @@ class DockerHostManager(DockerBaseClass):
for item in items:
item_record = dict()
if docker_object == 'containers':
if docker_object == "containers":
for key in header_containers:
item_record[key] = item.get(key)
elif docker_object == 'networks':
elif docker_object == "networks":
for key in header_networks:
item_record[key] = item.get(key)
elif docker_object == 'images':
elif docker_object == "images":
for key in header_images:
item_record[key] = item.get(key)
elif docker_object == 'volumes':
elif docker_object == "volumes":
for key in header_volumes:
item_record[key] = item.get(key)
items_list.append(item_record)
@@ -336,17 +352,17 @@ class DockerHostManager(DockerBaseClass):
def main():
argument_spec = dict(
containers=dict(type='bool', default=False),
containers_all=dict(type='bool', default=False),
containers_filters=dict(type='dict'),
images=dict(type='bool', default=False),
images_filters=dict(type='dict'),
networks=dict(type='bool', default=False),
networks_filters=dict(type='dict'),
volumes=dict(type='bool', default=False),
volumes_filters=dict(type='dict'),
disk_usage=dict(type='bool', default=False),
verbose_output=dict(type='bool', default=False),
containers=dict(type="bool", default=False),
containers_all=dict(type="bool", default=False),
containers_filters=dict(type="dict"),
images=dict(type="bool", default=False),
images_filters=dict(type="dict"),
networks=dict(type="bool", default=False),
networks_filters=dict(type="dict"),
volumes=dict(type="bool", default=False),
volumes_filters=dict(type="dict"),
disk_usage=dict(type="bool", default=False),
verbose_output=dict(type="bool", default=False),
)
client = AnsibleDockerClient(
@@ -356,9 +372,12 @@ def main():
can_talk_to_docker=False,
),
)
if client.module.params['api_version'] is None or client.module.params['api_version'].lower() == 'auto':
if (
client.module.params["api_version"] is None
or client.module.params["api_version"].lower() == "auto"
):
# At this point we know that we can talk to Docker, since we asked it for the API version
client.fail_results['can_talk_to_docker'] = True
client.fail_results["can_talk_to_docker"] = True
try:
results = dict(
@@ -368,12 +387,16 @@ def main():
DockerHostManager(client, results)
client.module.exit_json(**results)
except DockerException as e:
client.fail(f'An unexpected Docker error occurred: {e}', exception=traceback.format_exc())
client.fail(
f"An unexpected Docker error occurred: {e}",
exception=traceback.format_exc(),
)
except RequestException as e:
client.fail(
f'An unexpected requests error occurred when trying to talk to the Docker daemon: {e}',
exception=traceback.format_exc())
f"An unexpected requests error occurred when trying to talk to the Docker daemon: {e}",
exception=traceback.format_exc(),
)
if __name__ == '__main__':
if __name__ == "__main__":
main()
File diff suppressed because it is too large Load Diff
+186 -152
View File
@@ -285,23 +285,21 @@ import traceback
from ansible.module_utils.common.text.converters import to_native
from ansible.module_utils.common.text.formatters import human_to_bytes
from ansible_collections.community.docker.plugins.module_utils._api.utils.utils import (
parse_repository_tag,
)
from ansible_collections.community.docker.plugins.module_utils.common_cli import (
AnsibleModuleDockerClient,
DockerException,
)
from ansible_collections.community.docker.plugins.module_utils.util import (
DockerBaseClass,
clean_dict_booleans_for_docker_api,
is_image_name_id,
is_valid_tag,
)
from ansible_collections.community.docker.plugins.module_utils.version import LooseVersion
from ansible_collections.community.docker.plugins.module_utils._api.utils.utils import (
parse_repository_tag,
from ansible_collections.community.docker.plugins.module_utils.version import (
LooseVersion,
)
@@ -309,15 +307,15 @@ def convert_to_bytes(value, module, name, unlimited_value=None):
if value is None:
return value
try:
if unlimited_value is not None and value in ('unlimited', str(unlimited_value)):
if unlimited_value is not None and value in ("unlimited", str(unlimited_value)):
return unlimited_value
return human_to_bytes(value)
except ValueError as exc:
module.fail_json(msg=f'Failed to convert {name} to bytes: {exc}')
module.fail_json(msg=f"Failed to convert {name} to bytes: {exc}")
def dict_to_list(dictionary, concat='='):
return [f'{k}{concat}{v}' for k, v in sorted(dictionary.items())]
def dict_to_list(dictionary, concat="="):
return [f"{k}{concat}{v}" for k, v in sorted(dictionary.items())]
def _quote_csv(input):
@@ -334,47 +332,59 @@ class ImageBuilder(DockerBaseClass):
self.check_mode = self.client.check_mode
parameters = self.client.module.params
self.cache_from = parameters['cache_from']
self.pull = parameters['pull']
self.network = parameters['network']
self.nocache = parameters['nocache']
self.etc_hosts = clean_dict_booleans_for_docker_api(parameters['etc_hosts'])
self.args = clean_dict_booleans_for_docker_api(parameters['args'])
self.target = parameters['target']
self.platform = parameters['platform']
self.shm_size = convert_to_bytes(parameters['shm_size'], self.client.module, 'shm_size')
self.labels = clean_dict_booleans_for_docker_api(parameters['labels'])
self.rebuild = parameters['rebuild']
self.secrets = parameters['secrets']
self.outputs = parameters['outputs']
self.cache_from = parameters["cache_from"]
self.pull = parameters["pull"]
self.network = parameters["network"]
self.nocache = parameters["nocache"]
self.etc_hosts = clean_dict_booleans_for_docker_api(parameters["etc_hosts"])
self.args = clean_dict_booleans_for_docker_api(parameters["args"])
self.target = parameters["target"]
self.platform = parameters["platform"]
self.shm_size = convert_to_bytes(
parameters["shm_size"], self.client.module, "shm_size"
)
self.labels = clean_dict_booleans_for_docker_api(parameters["labels"])
self.rebuild = parameters["rebuild"]
self.secrets = parameters["secrets"]
self.outputs = parameters["outputs"]
buildx = self.client.get_client_plugin_info('buildx')
buildx = self.client.get_client_plugin_info("buildx")
if buildx is None:
self.fail(f'Docker CLI {self.client.get_cli()} does not have the buildx plugin installed')
buildx_version = buildx['Version'].lstrip('v')
self.fail(
f"Docker CLI {self.client.get_cli()} does not have the buildx plugin installed"
)
buildx_version = buildx["Version"].lstrip("v")
if self.secrets:
for secret in self.secrets:
if secret['type'] in ('env', 'value'):
if LooseVersion(buildx_version) < LooseVersion('0.6.0'):
self.fail(f'The Docker buildx plugin has version {buildx_version}, but 0.6.0 is needed for secrets of type=env and type=value')
if secret["type"] in ("env", "value"):
if LooseVersion(buildx_version) < LooseVersion("0.6.0"):
self.fail(
f"The Docker buildx plugin has version {buildx_version}, but 0.6.0 is needed for secrets of type=env and type=value"
)
if self.outputs and len(self.outputs) > 1:
if LooseVersion(buildx_version) < LooseVersion('0.13.0'):
self.fail(f'The Docker buildx plugin has version {buildx_version}, but 0.13.0 is needed to specify more than one output')
if LooseVersion(buildx_version) < LooseVersion("0.13.0"):
self.fail(
f"The Docker buildx plugin has version {buildx_version}, but 0.13.0 is needed to specify more than one output"
)
self.path = parameters['path']
self.path = parameters["path"]
if not os.path.isdir(self.path):
self.fail(f'"{self.path}" is not an existing directory')
self.dockerfile = parameters['dockerfile']
if self.dockerfile and not os.path.isfile(os.path.join(self.path, self.dockerfile)):
self.fail(f'"{os.path.join(self.path, self.dockerfile)}" is not an existing file')
self.dockerfile = parameters["dockerfile"]
if self.dockerfile and not os.path.isfile(
os.path.join(self.path, self.dockerfile)
):
self.fail(
f'"{os.path.join(self.path, self.dockerfile)}" is not an existing file'
)
self.name = parameters['name']
self.tag = parameters['tag']
self.name = parameters["name"]
self.tag = parameters["tag"]
if not is_valid_tag(self.tag, allow_empty=True):
self.fail(f'"{self.tag}" is not a valid docker tag')
if is_image_name_id(self.name):
self.fail('Image name must not be a digest')
self.fail("Image name must not be a digest")
# If name contains a tag, it takes precedence over tag parameter.
repo, repo_tag = parse_repository_tag(self.name)
@@ -383,25 +393,27 @@ class ImageBuilder(DockerBaseClass):
self.tag = repo_tag
if is_image_name_id(self.tag):
self.fail('Image name must not contain a digest, but have a tag')
self.fail("Image name must not contain a digest, but have a tag")
if self.outputs:
found = False
name_tag = f'{self.name}:{self.tag}'
name_tag = f"{self.name}:{self.tag}"
for output in self.outputs:
if output['type'] == 'image':
if not output['name']:
if output["type"] == "image":
if not output["name"]:
# Since we no longer pass --tag if --output is provided, we need to set this manually
output['name'] = [name_tag]
if output['name'] and name_tag in output['name']:
output["name"] = [name_tag]
if output["name"] and name_tag in output["name"]:
found = True
if not found:
self.outputs.append({
'type': 'image',
'name': [name_tag],
'push': False,
})
if LooseVersion(buildx_version) < LooseVersion('0.13.0'):
self.outputs.append(
{
"type": "image",
"name": [name_tag],
"push": False,
}
)
if LooseVersion(buildx_version) < LooseVersion("0.13.0"):
self.fail(
f"The output does not include an image with name {name_tag}, and the Docker"
f" buildx plugin has version {buildx_version} which only supports one output."
@@ -417,78 +429,86 @@ class ImageBuilder(DockerBaseClass):
def add_args(self, args):
environ_update = {}
if not self.outputs:
args.extend(['--tag', f'{self.name}:{self.tag}'])
args.extend(["--tag", f"{self.name}:{self.tag}"])
if self.dockerfile:
args.extend(['--file', os.path.join(self.path, self.dockerfile)])
args.extend(["--file", os.path.join(self.path, self.dockerfile)])
if self.cache_from:
self.add_list_arg(args, '--cache-from', self.cache_from)
self.add_list_arg(args, "--cache-from", self.cache_from)
if self.pull:
args.append('--pull')
args.append("--pull")
if self.network:
args.extend(['--network', self.network])
args.extend(["--network", self.network])
if self.nocache:
args.append('--no-cache')
args.append("--no-cache")
if self.etc_hosts:
self.add_list_arg(args, '--add-host', dict_to_list(self.etc_hosts, ':'))
self.add_list_arg(args, "--add-host", dict_to_list(self.etc_hosts, ":"))
if self.args:
self.add_list_arg(args, '--build-arg', dict_to_list(self.args))
self.add_list_arg(args, "--build-arg", dict_to_list(self.args))
if self.target:
args.extend(['--target', self.target])
args.extend(["--target", self.target])
if self.platform:
for platform in self.platform:
args.extend(['--platform', platform])
args.extend(["--platform", platform])
if self.shm_size:
args.extend(['--shm-size', str(self.shm_size)])
args.extend(["--shm-size", str(self.shm_size)])
if self.labels:
self.add_list_arg(args, '--label', dict_to_list(self.labels))
self.add_list_arg(args, "--label", dict_to_list(self.labels))
if self.secrets:
random_prefix = None
for index, secret in enumerate(self.secrets):
sid = secret['id']
if secret['type'] == 'file':
src = secret['src']
args.extend(['--secret', f'id={sid},type=file,src={src}'])
if secret['type'] == 'env':
env = secret['src']
args.extend(['--secret', f'id={sid},type=env,env={env}'])
if secret['type'] == 'value':
sid = secret["id"]
if secret["type"] == "file":
src = secret["src"]
args.extend(["--secret", f"id={sid},type=file,src={src}"])
if secret["type"] == "env":
env = secret["src"]
args.extend(["--secret", f"id={sid},type=env,env={env}"])
if secret["type"] == "value":
# We pass values on using environment variables. The user has been warned in the documentation
# that they should only use this mechanism when being comfortable with it.
if random_prefix is None:
# Use /dev/urandom to generate some entropy to make the environment variable's name unguessable
random_prefix = base64.b64encode(os.urandom(16)).decode('utf-8').replace('=', '')
env_name = f'ANSIBLE_DOCKER_COMPOSE_ENV_SECRET_{random_prefix}_{index}'
environ_update[env_name] = secret['value']
args.extend(['--secret', f'id={sid},type=env,env={env_name}'])
random_prefix = (
base64.b64encode(os.urandom(16))
.decode("utf-8")
.replace("=", "")
)
env_name = (
f"ANSIBLE_DOCKER_COMPOSE_ENV_SECRET_{random_prefix}_{index}"
)
environ_update[env_name] = secret["value"]
args.extend(["--secret", f"id={sid},type=env,env={env_name}"])
if self.outputs:
for output in self.outputs:
subargs = []
if output['type'] == 'local':
dest = output['dest']
subargs.extend(['type=local', f'dest={dest}'])
if output['type'] == 'tar':
dest = output['dest']
subargs.extend(['type=tar', f'dest={dest}'])
if output['type'] == 'oci':
dest = output['dest']
subargs.extend(['type=oci', f'dest={dest}'])
if output['type'] == 'docker':
subargs.append('type=docker')
dest = output['dest']
if output['dest'] is not None:
subargs.append(f'dest={dest}')
if output['context'] is not None:
context = output['context']
subargs.append(f'context={context}')
if output['type'] == 'image':
subargs.append('type=image')
if output['name'] is not None:
name = ','.join(output['name'])
subargs.append(f'name={name}')
if output['push']:
subargs.append('push=true')
if output["type"] == "local":
dest = output["dest"]
subargs.extend(["type=local", f"dest={dest}"])
if output["type"] == "tar":
dest = output["dest"]
subargs.extend(["type=tar", f"dest={dest}"])
if output["type"] == "oci":
dest = output["dest"]
subargs.extend(["type=oci", f"dest={dest}"])
if output["type"] == "docker":
subargs.append("type=docker")
dest = output["dest"]
if output["dest"] is not None:
subargs.append(f"dest={dest}")
if output["context"] is not None:
context = output["context"]
subargs.append(f"context={context}")
if output["type"] == "image":
subargs.append("type=image")
if output["name"] is not None:
name = ",".join(output["name"])
subargs.append(f"name={name}")
if output["push"]:
subargs.append("push=true")
if subargs:
args.extend(['--output', ','.join(_quote_csv(subarg) for subarg in subargs)])
args.extend(
["--output", ",".join(_quote_csv(subarg) for subarg in subargs)]
)
return environ_update
def build_image(self):
@@ -500,82 +520,93 @@ class ImageBuilder(DockerBaseClass):
)
if image:
if self.rebuild == 'never':
if self.rebuild == "never":
return results
results['changed'] = True
results["changed"] = True
if not self.check_mode:
args = ['buildx', 'build', '--progress', 'plain']
args = ["buildx", "build", "--progress", "plain"]
environ_update = self.add_args(args)
args.extend(['--', self.path])
rc, stdout, stderr = self.client.call_cli(*args, environ_update=environ_update)
args.extend(["--", self.path])
rc, stdout, stderr = self.client.call_cli(
*args, environ_update=environ_update
)
if rc != 0:
self.fail(f'Building {self.name}:{self.tag} failed', stdout=to_native(stdout), stderr=to_native(stderr), command=args)
results['stdout'] = to_native(stdout)
results['stderr'] = to_native(stderr)
results['image'] = self.client.find_image(self.name, self.tag) or {}
results['command'] = args
self.fail(
f"Building {self.name}:{self.tag} failed",
stdout=to_native(stdout),
stderr=to_native(stderr),
command=args,
)
results["stdout"] = to_native(stdout)
results["stderr"] = to_native(stderr)
results["image"] = self.client.find_image(self.name, self.tag) or {}
results["command"] = args
return results
def main():
argument_spec = dict(
name=dict(type='str', required=True),
tag=dict(type='str', default='latest'),
path=dict(type='path', required=True),
dockerfile=dict(type='str'),
cache_from=dict(type='list', elements='str'),
pull=dict(type='bool', default=False),
network=dict(type='str'),
nocache=dict(type='bool', default=False),
etc_hosts=dict(type='dict'),
args=dict(type='dict'),
target=dict(type='str'),
platform=dict(type='list', elements='str'),
shm_size=dict(type='str'),
labels=dict(type='dict'),
rebuild=dict(type='str', choices=['never', 'always'], default='never'),
name=dict(type="str", required=True),
tag=dict(type="str", default="latest"),
path=dict(type="path", required=True),
dockerfile=dict(type="str"),
cache_from=dict(type="list", elements="str"),
pull=dict(type="bool", default=False),
network=dict(type="str"),
nocache=dict(type="bool", default=False),
etc_hosts=dict(type="dict"),
args=dict(type="dict"),
target=dict(type="str"),
platform=dict(type="list", elements="str"),
shm_size=dict(type="str"),
labels=dict(type="dict"),
rebuild=dict(type="str", choices=["never", "always"], default="never"),
secrets=dict(
type='list',
elements='dict',
type="list",
elements="dict",
options=dict(
id=dict(type='str', required=True),
type=dict(type='str', choices=['file', 'env', 'value'], required=True),
src=dict(type='path'),
env=dict(type='str'),
value=dict(type='str', no_log=True),
id=dict(type="str", required=True),
type=dict(type="str", choices=["file", "env", "value"], required=True),
src=dict(type="path"),
env=dict(type="str"),
value=dict(type="str", no_log=True),
),
required_if=[
('type', 'file', ['src']),
('type', 'env', ['env']),
('type', 'value', ['value']),
("type", "file", ["src"]),
("type", "env", ["env"]),
("type", "value", ["value"]),
],
mutually_exclusive=[
('src', 'env', 'value'),
("src", "env", "value"),
],
no_log=False,
),
outputs=dict(
type='list',
elements='dict',
type="list",
elements="dict",
options=dict(
type=dict(type='str', choices=['local', 'tar', 'oci', 'docker', 'image'], required=True),
dest=dict(type='path'),
context=dict(type='str'),
name=dict(type='list', elements='str'),
push=dict(type='bool', default=False),
type=dict(
type="str",
choices=["local", "tar", "oci", "docker", "image"],
required=True,
),
dest=dict(type="path"),
context=dict(type="str"),
name=dict(type="list", elements="str"),
push=dict(type="bool", default=False),
),
required_if=[
('type', 'local', ['dest']),
('type', 'tar', ['dest']),
('type', 'oci', ['dest']),
("type", "local", ["dest"]),
("type", "tar", ["dest"]),
("type", "oci", ["dest"]),
],
mutually_exclusive=[
('dest', 'name'),
('dest', 'push'),
('context', 'name'),
('context', 'push'),
("dest", "name"),
("dest", "push"),
("context", "name"),
("context", "push"),
],
),
)
@@ -590,8 +621,11 @@ def main():
results = ImageBuilder(client).build_image()
client.module.exit_json(**results)
except DockerException as e:
client.fail(f'An unexpected Docker error occurred: {e}', exception=traceback.format_exc())
client.fail(
f"An unexpected Docker error occurred: {e}",
exception=traceback.format_exc(),
)
if __name__ == '__main__':
if __name__ == "__main__":
main()
+58 -45
View File
@@ -95,29 +95,29 @@ images:
import traceback
from ansible_collections.community.docker.plugins.module_utils._api.constants import (
DEFAULT_DATA_CHUNK_SIZE,
)
from ansible_collections.community.docker.plugins.module_utils._api.errors import (
DockerException,
)
from ansible_collections.community.docker.plugins.module_utils._api.utils.utils import (
parse_repository_tag,
)
from ansible_collections.community.docker.plugins.module_utils.common_api import (
AnsibleDockerClient,
RequestException,
)
from ansible_collections.community.docker.plugins.module_utils.image_archive import (
load_archived_image_manifest,
api_image_id,
ImageArchiveInvalidException,
api_image_id,
load_archived_image_manifest,
)
from ansible_collections.community.docker.plugins.module_utils.util import (
DockerBaseClass,
is_image_name_id,
is_valid_tag,
)
from ansible_collections.community.docker.plugins.module_utils._api.constants import (
DEFAULT_DATA_CHUNK_SIZE,
)
from ansible_collections.community.docker.plugins.module_utils._api.errors import DockerException
from ansible_collections.community.docker.plugins.module_utils._api.utils.utils import (
parse_repository_tag,
)
class ImageExportManager(DockerBaseClass):
@@ -128,47 +128,52 @@ class ImageExportManager(DockerBaseClass):
parameters = self.client.module.params
self.check_mode = self.client.check_mode
self.path = parameters['path']
self.force = parameters['force']
self.tag = parameters['tag']
self.path = parameters["path"]
self.force = parameters["force"]
self.tag = parameters["tag"]
if not is_valid_tag(self.tag, allow_empty=True):
self.fail(f'"{self.tag}" is not a valid docker tag')
# If name contains a tag, it takes precedence over tag parameter.
self.names = []
for name in parameters['names']:
for name in parameters["names"]:
if is_image_name_id(name):
self.names.append({'id': name, 'joined': name})
self.names.append({"id": name, "joined": name})
else:
repo, repo_tag = parse_repository_tag(name)
if not repo_tag:
repo_tag = self.tag
self.names.append({'name': repo, 'tag': repo_tag, 'joined': f'{repo}:{repo_tag}'})
self.names.append(
{"name": repo, "tag": repo_tag, "joined": f"{repo}:{repo_tag}"}
)
if not self.names:
self.fail('At least one image name must be specified')
self.fail("At least one image name must be specified")
def fail(self, msg):
self.client.fail(msg)
def get_export_reason(self):
if self.force:
return 'Exporting since force=true'
return "Exporting since force=true"
try:
archived_images = load_archived_image_manifest(self.path)
if archived_images is None:
return 'Overwriting since no image is present in archive'
return "Overwriting since no image is present in archive"
except ImageArchiveInvalidException as exc:
self.log(f'Unable to extract manifest summary from archive: {exc}')
return 'Overwriting an unreadable archive file'
self.log(f"Unable to extract manifest summary from archive: {exc}")
return "Overwriting an unreadable archive file"
left_names = list(self.names)
for archived_image in archived_images:
found = False
for i, name in enumerate(left_names):
if name['id'] == api_image_id(archived_image.image_id) and [name['joined']] == archived_image.repo_tags:
if (
name["id"] == api_image_id(archived_image.image_id)
and [name["joined"]] == archived_image.repo_tags
):
del left_names[i]
found = True
break
@@ -181,20 +186,22 @@ class ImageExportManager(DockerBaseClass):
def write_chunks(self, chunks):
try:
with open(self.path, 'wb') as fd:
with open(self.path, "wb") as fd:
for chunk in chunks:
fd.write(chunk)
except Exception as exc:
self.fail(f"Error writing image archive {self.path} - {exc}")
def export_images(self):
image_names = [name['joined'] for name in self.names]
image_names_str = ', '.join(image_names)
image_names = [name["joined"] for name in self.names]
image_names_str = ", ".join(image_names)
if len(image_names) == 1:
self.log(f"Getting archive of image {image_names[0]}")
try:
chunks = self.client._stream_raw_result(
self.client._get(self.client._url('/images/{0}/get', image_names[0]), stream=True),
self.client._get(
self.client._url("/images/{0}/get", image_names[0]), stream=True
),
DEFAULT_DATA_CHUNK_SIZE,
False,
)
@@ -205,9 +212,9 @@ class ImageExportManager(DockerBaseClass):
try:
chunks = self.client._stream_raw_result(
self.client._get(
self.client._url('/images/get'),
self.client._url("/images/get"),
stream=True,
params={'names': image_names},
params={"names": image_names},
),
DEFAULT_DATA_CHUNK_SIZE,
False,
@@ -224,26 +231,28 @@ class ImageExportManager(DockerBaseClass):
images = []
for name in self.names:
if 'id' in name:
image = self.client.find_image_by_id(name['id'], accept_missing_image=True)
if "id" in name:
image = self.client.find_image_by_id(
name["id"], accept_missing_image=True
)
else:
image = self.client.find_image(name=name['name'], tag=name['tag'])
image = self.client.find_image(name=name["name"], tag=name["tag"])
if not image:
self.fail(f"Image {name['joined']} not found")
images.append(image)
# Will have a 'sha256:' prefix
name['id'] = image['Id']
name["id"] = image["Id"]
results = {
'changed': False,
'images': images,
"changed": False,
"images": images,
}
reason = self.get_export_reason()
if reason is not None:
results['msg'] = reason
results['changed'] = True
results["msg"] = reason
results["changed"] = True
if not self.check_mode:
self.export_images()
@@ -253,10 +262,10 @@ class ImageExportManager(DockerBaseClass):
def main():
argument_spec = dict(
path=dict(type='path'),
force=dict(type='bool', default=False),
names=dict(type='list', elements='str', required=True, aliases=['name']),
tag=dict(type='str', default='latest'),
path=dict(type="path"),
force=dict(type="bool", default=False),
names=dict(type="list", elements="str", required=True, aliases=["name"]),
tag=dict(type="str", default="latest"),
)
client = AnsibleDockerClient(
@@ -268,12 +277,16 @@ def main():
results = ImageExportManager(client).run()
client.module.exit_json(**results)
except DockerException as e:
client.fail(f'An unexpected Docker error occurred: {e}', exception=traceback.format_exc())
client.fail(
f"An unexpected Docker error occurred: {e}",
exception=traceback.format_exc(),
)
except RequestException as e:
client.fail(
f'An unexpected requests error occurred when trying to talk to the Docker daemon: {e}',
exception=traceback.format_exc())
f"An unexpected requests error occurred when trying to talk to the Docker daemon: {e}",
exception=traceback.format_exc(),
)
if __name__ == '__main__':
if __name__ == "__main__":
main()
+28 -22
View File
@@ -137,6 +137,13 @@ images:
import traceback
from ansible_collections.community.docker.plugins.module_utils._api.errors import (
DockerException,
NotFound,
)
from ansible_collections.community.docker.plugins.module_utils._api.utils.utils import (
parse_repository_tag,
)
from ansible_collections.community.docker.plugins.module_utils.common_api import (
AnsibleDockerClient,
RequestException,
@@ -145,8 +152,6 @@ from ansible_collections.community.docker.plugins.module_utils.util import (
DockerBaseClass,
is_image_name_id,
)
from ansible_collections.community.docker.plugins.module_utils._api.errors import DockerException, NotFound
from ansible_collections.community.docker.plugins.module_utils._api.utils.utils import parse_repository_tag
class ImageManager(DockerBaseClass):
@@ -157,23 +162,23 @@ class ImageManager(DockerBaseClass):
self.client = client
self.results = results
self.name = self.client.module.params.get('name')
self.name = self.client.module.params.get("name")
self.log(f"Gathering facts for images: {self.name}")
if self.name:
self.results['images'] = self.get_facts()
self.results["images"] = self.get_facts()
else:
self.results['images'] = self.get_all_images()
self.results["images"] = self.get_all_images()
def fail(self, msg):
self.client.fail(msg)
def get_facts(self):
'''
"""
Lookup and inspect each image name found in the names parameter.
:returns array of image dictionaries
'''
"""
results = []
@@ -183,13 +188,13 @@ class ImageManager(DockerBaseClass):
for name in names:
if is_image_name_id(name):
self.log(f'Fetching image {name} (ID)')
self.log(f"Fetching image {name} (ID)")
image = self.client.find_image_by_id(name, accept_missing_image=True)
else:
repository, tag = parse_repository_tag(name)
if not tag:
tag = 'latest'
self.log(f'Fetching image {repository}:{tag}')
tag = "latest"
self.log(f"Fetching image {repository}:{tag}")
image = self.client.find_image(name=repository, tag=tag)
if image:
results.append(image)
@@ -198,13 +203,13 @@ class ImageManager(DockerBaseClass):
def get_all_images(self):
results = []
params = {
'only_ids': 0,
'all': 0,
"only_ids": 0,
"all": 0,
}
images = self.client.get_json("/images/json", params=params)
for image in images:
try:
inspection = self.client.get_json('/images/{0}/json', image['Id'])
inspection = self.client.get_json("/images/{0}/json", image["Id"])
except NotFound:
inspection = None
except Exception as exc:
@@ -215,7 +220,7 @@ class ImageManager(DockerBaseClass):
def main():
argument_spec = dict(
name=dict(type='list', elements='str'),
name=dict(type="list", elements="str"),
)
client = AnsibleDockerClient(
@@ -224,20 +229,21 @@ def main():
)
try:
results = dict(
changed=False,
images=[]
)
results = dict(changed=False, images=[])
ImageManager(client, results)
client.module.exit_json(**results)
except DockerException as e:
client.fail(f'An unexpected Docker error occurred: {e}', exception=traceback.format_exc())
client.fail(
f"An unexpected Docker error occurred: {e}",
exception=traceback.format_exc(),
)
except RequestException as e:
client.fail(
f'An unexpected requests error occurred when trying to talk to the Docker daemon: {e}',
exception=traceback.format_exc())
f"An unexpected requests error occurred when trying to talk to the Docker daemon: {e}",
exception=traceback.format_exc(),
)
if __name__ == '__main__':
if __name__ == "__main__":
main()
+48 -30
View File
@@ -82,6 +82,9 @@ images:
import errno
import traceback
from ansible_collections.community.docker.plugins.module_utils._api.errors import (
DockerException,
)
from ansible_collections.community.docker.plugins.module_utils.common_api import (
AnsibleDockerClient,
RequestException,
@@ -91,8 +94,6 @@ from ansible_collections.community.docker.plugins.module_utils.util import (
is_image_name_id,
)
from ansible_collections.community.docker.plugins.module_utils._api.errors import DockerException
class ImageManager(DockerBaseClass):
def __init__(self, client, results):
@@ -103,73 +104,86 @@ class ImageManager(DockerBaseClass):
parameters = self.client.module.params
self.check_mode = self.client.check_mode
self.path = parameters['path']
self.path = parameters["path"]
self.load_images()
@staticmethod
def _extract_output_line(line, output):
'''
"""
Extract text line from stream output and, if found, adds it to output.
'''
if 'stream' in line or 'status' in line:
"""
if "stream" in line or "status" in line:
# Make sure we have a string (assuming that line['stream'] and
# line['status'] are either not defined, falsish, or a string)
text_line = line.get('stream') or line.get('status') or ''
text_line = line.get("stream") or line.get("status") or ""
output.extend(text_line.splitlines())
def load_images(self):
'''
"""
Load images from a .tar archive
'''
"""
# Load image(s) from file
load_output = []
try:
self.log(f"Opening image {self.path}")
with open(self.path, 'rb') as image_tar:
with open(self.path, "rb") as image_tar:
self.log(f"Loading images from {self.path}")
res = self.client._post(self.client._url("/images/load"), data=image_tar, stream=True)
res = self.client._post(
self.client._url("/images/load"), data=image_tar, stream=True
)
for line in self.client._stream_helper(res, decode=True):
self.log(line, pretty_print=True)
self._extract_output_line(line, load_output)
except EnvironmentError as exc:
if exc.errno == errno.ENOENT:
self.client.fail(f"Error opening archive {self.path} - {exc}")
self.client.fail(f"Error loading archive {self.path} - {exc}", stdout='\n'.join(load_output))
self.client.fail(
f"Error loading archive {self.path} - {exc}",
stdout="\n".join(load_output),
)
except Exception as exc:
self.client.fail(f"Error loading archive {self.path} - {exc}", stdout='\n'.join(load_output))
self.client.fail(
f"Error loading archive {self.path} - {exc}",
stdout="\n".join(load_output),
)
# Collect loaded images
loaded_images = []
for line in load_output:
if line.startswith('Loaded image:'):
loaded_images.append(line[len('Loaded image:'):].strip())
if line.startswith('Loaded image ID:'):
loaded_images.append(line[len('Loaded image ID:'):].strip())
if line.startswith("Loaded image:"):
loaded_images.append(line[len("Loaded image:") :].strip())
if line.startswith("Loaded image ID:"):
loaded_images.append(line[len("Loaded image ID:") :].strip())
if not loaded_images:
self.client.fail("Detected no loaded images. Archive potentially corrupt?", stdout='\n'.join(load_output))
self.client.fail(
"Detected no loaded images. Archive potentially corrupt?",
stdout="\n".join(load_output),
)
images = []
for image_name in loaded_images:
if is_image_name_id(image_name):
images.append(self.client.find_image_by_id(image_name))
elif ':' in image_name:
image_name, tag = image_name.rsplit(':', 1)
elif ":" in image_name:
image_name, tag = image_name.rsplit(":", 1)
images.append(self.client.find_image(image_name, tag))
else:
self.client.module.warn(f'Image name "{image_name}" is neither ID nor has a tag')
self.client.module.warn(
f'Image name "{image_name}" is neither ID nor has a tag'
)
self.results['image_names'] = loaded_images
self.results['images'] = images
self.results['changed'] = True
self.results['stdout'] = '\n'.join(load_output)
self.results["image_names"] = loaded_images
self.results["images"] = images
self.results["changed"] = True
self.results["stdout"] = "\n".join(load_output)
def main():
client = AnsibleDockerClient(
argument_spec=dict(
path=dict(type='path', required=True),
path=dict(type="path", required=True),
),
supports_check_mode=False,
)
@@ -183,12 +197,16 @@ def main():
ImageManager(client, results)
client.module.exit_json(**results)
except DockerException as e:
client.fail(f'An unexpected Docker error occurred: {e}', exception=traceback.format_exc())
client.fail(
f"An unexpected Docker error occurred: {e}",
exception=traceback.format_exc(),
)
except RequestException as e:
client.fail(
f'An unexpected requests error occurred when trying to talk to the Docker daemon: {e}',
exception=traceback.format_exc())
f"An unexpected requests error occurred when trying to talk to the Docker daemon: {e}",
exception=traceback.format_exc(),
)
if __name__ == '__main__':
if __name__ == "__main__":
main()
+46 -41
View File
@@ -92,35 +92,34 @@ image:
import traceback
from ansible_collections.community.docker.plugins.module_utils._api.errors import (
DockerException,
)
from ansible_collections.community.docker.plugins.module_utils._api.utils.utils import (
parse_repository_tag,
)
from ansible_collections.community.docker.plugins.module_utils._platform import (
compare_platform_strings,
compose_platform_string,
normalize_platform_string,
)
from ansible_collections.community.docker.plugins.module_utils.common_api import (
AnsibleDockerClient,
RequestException,
)
from ansible_collections.community.docker.plugins.module_utils.util import (
DockerBaseClass,
is_image_name_id,
is_valid_tag,
)
from ansible_collections.community.docker.plugins.module_utils._api.errors import DockerException
from ansible_collections.community.docker.plugins.module_utils._api.utils.utils import (
parse_repository_tag,
)
from ansible_collections.community.docker.plugins.module_utils._platform import (
normalize_platform_string,
compare_platform_strings,
compose_platform_string,
)
def image_info(image):
result = {}
if image:
result['id'] = image['Id']
result["id"] = image["Id"]
else:
result['exists'] = False
result["exists"] = False
return result
@@ -132,10 +131,10 @@ class ImagePuller(DockerBaseClass):
self.check_mode = self.client.check_mode
parameters = self.client.module.params
self.name = parameters['name']
self.tag = parameters['tag']
self.platform = parameters['platform']
self.pull_mode = parameters['pull']
self.name = parameters["name"]
self.tag = parameters["tag"]
self.platform = parameters["platform"]
self.pull_mode = parameters["pull"]
if is_image_name_id(self.name):
self.client.fail("Cannot pull an image by ID")
@@ -157,47 +156,49 @@ class ImagePuller(DockerBaseClass):
diff=dict(before=image_info(image), after=image_info(image)),
)
if image and self.pull_mode == 'not_present':
if image and self.pull_mode == "not_present":
if self.platform is None:
return results
host_info = self.client.info()
wanted_platform = normalize_platform_string(
self.platform,
daemon_os=host_info.get('OSType'),
daemon_arch=host_info.get('Architecture'),
daemon_os=host_info.get("OSType"),
daemon_arch=host_info.get("Architecture"),
)
image_platform = compose_platform_string(
os=image.get('Os'),
arch=image.get('Architecture'),
variant=image.get('Variant'),
daemon_os=host_info.get('OSType'),
daemon_arch=host_info.get('Architecture'),
os=image.get("Os"),
arch=image.get("Architecture"),
variant=image.get("Variant"),
daemon_os=host_info.get("OSType"),
daemon_arch=host_info.get("Architecture"),
)
if compare_platform_strings(wanted_platform, image_platform):
return results
results['actions'].append(f'Pulled image {self.name}:{self.tag}')
results["actions"].append(f"Pulled image {self.name}:{self.tag}")
if self.check_mode:
results['changed'] = True
results['diff']['after'] = image_info(dict(Id='unknown'))
results["changed"] = True
results["diff"]["after"] = image_info(dict(Id="unknown"))
else:
results['image'], not_changed = self.client.pull_image(self.name, tag=self.tag, platform=self.platform)
results['changed'] = not not_changed
results['diff']['after'] = image_info(results['image'])
results["image"], not_changed = self.client.pull_image(
self.name, tag=self.tag, platform=self.platform
)
results["changed"] = not not_changed
results["diff"]["after"] = image_info(results["image"])
return results
def main():
argument_spec = dict(
name=dict(type='str', required=True),
tag=dict(type='str', default='latest'),
platform=dict(type='str'),
pull=dict(type='str', choices=['always', 'not_present'], default='always'),
name=dict(type="str", required=True),
tag=dict(type="str", default="latest"),
platform=dict(type="str"),
pull=dict(type="str", choices=["always", "not_present"], default="always"),
)
option_minimal_versions = dict(
platform=dict(docker_api_version='1.32'),
platform=dict(docker_api_version="1.32"),
)
client = AnsibleDockerClient(
@@ -210,12 +211,16 @@ def main():
results = ImagePuller(client).pull()
client.module.exit_json(**results)
except DockerException as e:
client.fail(f'An unexpected Docker error occurred: {e}', exception=traceback.format_exc())
client.fail(
f"An unexpected Docker error occurred: {e}",
exception=traceback.format_exc(),
)
except RequestException as e:
client.fail(
f'An unexpected requests error occurred when trying to talk to the Docker daemon: {e}',
exception=traceback.format_exc())
f"An unexpected requests error occurred when trying to talk to the Docker daemon: {e}",
exception=traceback.format_exc(),
)
if __name__ == '__main__':
if __name__ == "__main__":
main()
+39 -32
View File
@@ -74,27 +74,26 @@ image:
import base64
import traceback
from ansible_collections.community.docker.plugins.module_utils._api.auth import (
get_config_header,
resolve_repository_name,
)
from ansible_collections.community.docker.plugins.module_utils._api.errors import (
DockerException,
)
from ansible_collections.community.docker.plugins.module_utils._api.utils.utils import (
parse_repository_tag,
)
from ansible_collections.community.docker.plugins.module_utils.common_api import (
AnsibleDockerClient,
RequestException,
)
from ansible_collections.community.docker.plugins.module_utils.util import (
DockerBaseClass,
is_image_name_id,
is_valid_tag,
)
from ansible_collections.community.docker.plugins.module_utils._api.errors import DockerException
from ansible_collections.community.docker.plugins.module_utils._api.utils.utils import (
parse_repository_tag,
)
from ansible_collections.community.docker.plugins.module_utils._api.auth import (
get_config_header,
resolve_repository_name,
)
class ImagePusher(DockerBaseClass):
def __init__(self, client):
@@ -104,8 +103,8 @@ class ImagePusher(DockerBaseClass):
self.check_mode = self.client.check_mode
parameters = self.client.module.params
self.name = parameters['name']
self.tag = parameters['tag']
self.name = parameters["name"]
self.tag = parameters["tag"]
if is_image_name_id(self.name):
self.client.fail("Cannot push an image by ID")
@@ -126,7 +125,7 @@ class ImagePusher(DockerBaseClass):
def push(self):
image = self.client.find_image(name=self.name, tag=self.tag)
if not image:
self.client.fail(f'Cannot find image {self.name}:{self.tag}')
self.client.fail(f"Cannot find image {self.name}:{self.tag}")
results = dict(
changed=False,
@@ -136,7 +135,7 @@ class ImagePusher(DockerBaseClass):
push_registry, push_repo = resolve_repository_name(self.name)
try:
results['actions'].append(f'Pushed image {self.name}:{self.tag}')
results["actions"].append(f"Pushed image {self.name}:{self.tag}")
headers = {}
header = get_config_header(self.client, push_registry)
@@ -144,28 +143,32 @@ class ImagePusher(DockerBaseClass):
# For some reason, from Docker 28.3.3 on not specifying X-Registry-Auth seems to be invalid.
# See https://github.com/moby/moby/issues/50614.
header = base64.urlsafe_b64encode(b"{}")
headers['X-Registry-Auth'] = header
headers["X-Registry-Auth"] = header
response = self.client._post_json(
self.client._url("/images/{0}/push", self.name),
data=None,
headers=headers,
stream=True,
params={'tag': self.tag},
params={"tag": self.tag},
)
self.client._raise_for_status(response)
for line in self.client._stream_helper(response, decode=True):
self.log(line, pretty_print=True)
if line.get('errorDetail'):
raise Exception(line['errorDetail']['message'])
status = line.get('status')
if status == 'Pushing':
results['changed'] = True
if line.get("errorDetail"):
raise Exception(line["errorDetail"]["message"])
status = line.get("status")
if status == "Pushing":
results["changed"] = True
except Exception as exc:
if 'unauthorized' in str(exc):
if 'authentication required' in str(exc):
self.client.fail(f"Error pushing image {push_registry}/{push_repo}:{self.tag} - {exc}. Try logging into {push_registry} first.")
if "unauthorized" in str(exc):
if "authentication required" in str(exc):
self.client.fail(
f"Error pushing image {push_registry}/{push_repo}:{self.tag} - {exc}. Try logging into {push_registry} first."
)
else:
self.client.fail(f"Error pushing image {push_registry}/{push_repo}:{self.tag} - {exc}. Does the repository exist?")
self.client.fail(
f"Error pushing image {push_registry}/{push_repo}:{self.tag} - {exc}. Does the repository exist?"
)
self.client.fail(f"Error pushing image {self.name}:{self.tag}: {exc}")
return results
@@ -173,8 +176,8 @@ class ImagePusher(DockerBaseClass):
def main():
argument_spec = dict(
name=dict(type='str', required=True),
tag=dict(type='str', default='latest'),
name=dict(type="str", required=True),
tag=dict(type="str", default="latest"),
)
client = AnsibleDockerClient(
@@ -186,12 +189,16 @@ def main():
results = ImagePusher(client).push()
client.module.exit_json(**results)
except DockerException as e:
client.fail(f'An unexpected Docker error occurred: {e}', exception=traceback.format_exc())
client.fail(
f"An unexpected Docker error occurred: {e}",
exception=traceback.format_exc(),
)
except RequestException as e:
client.fail(
f'An unexpected requests error occurred when trying to talk to the Docker daemon: {e}',
exception=traceback.format_exc())
f"An unexpected requests error occurred when trying to talk to the Docker daemon: {e}",
exception=traceback.format_exc(),
)
if __name__ == '__main__':
if __name__ == "__main__":
main()
+71 -50
View File
@@ -99,22 +99,23 @@ untagged:
import traceback
from ansible_collections.community.docker.plugins.module_utils._api.errors import (
DockerException,
NotFound,
)
from ansible_collections.community.docker.plugins.module_utils._api.utils.utils import (
parse_repository_tag,
)
from ansible_collections.community.docker.plugins.module_utils.common_api import (
AnsibleDockerClient,
RequestException,
)
from ansible_collections.community.docker.plugins.module_utils.util import (
DockerBaseClass,
is_image_name_id,
is_valid_tag,
)
from ansible_collections.community.docker.plugins.module_utils._api.errors import DockerException, NotFound
from ansible_collections.community.docker.plugins.module_utils._api.utils.utils import (
parse_repository_tag,
)
class ImageRemover(DockerBaseClass):
@@ -126,10 +127,10 @@ class ImageRemover(DockerBaseClass):
self.diff = self.client.module._diff
parameters = self.client.module.params
self.name = parameters['name']
self.tag = parameters['tag']
self.force = parameters['force']
self.prune = parameters['prune']
self.name = parameters["name"]
self.tag = parameters["tag"]
self.force = parameters["force"]
self.prune = parameters["prune"]
if not is_valid_tag(self.tag, allow_empty=True):
self.fail(f'"{self.tag}" is not a valid docker tag')
@@ -149,9 +150,9 @@ class ImageRemover(DockerBaseClass):
return dict(exists=False)
return dict(
exists=True,
id=image['Id'],
tags=sorted(image.get('RepoTags') or []),
digests=sorted(image.get('RepoDigests') or []),
id=image["Id"],
tags=sorted(image.get("RepoTags") or []),
digests=sorted(image.get("RepoDigests") or []),
)
def absent(self):
@@ -172,20 +173,24 @@ class ImageRemover(DockerBaseClass):
name = f"{self.name}:{self.tag}"
if self.diff:
results['diff'] = dict(before=self.get_diff_state(image))
results["diff"] = dict(before=self.get_diff_state(image))
if not image:
if self.diff:
results['diff']['after'] = self.get_diff_state(image)
results["diff"]["after"] = self.get_diff_state(image)
return results
results['changed'] = True
results['actions'].append(f"Removed image {name}")
results['image'] = image
results["changed"] = True
results["actions"].append(f"Removed image {name}")
results["image"] = image
if not self.check_mode:
try:
res = self.client.delete_json('/images/{0}', name, params={'force': self.force, 'noprune': not self.prune})
res = self.client.delete_json(
"/images/{0}",
name,
params={"force": self.force, "noprune": not self.prune},
)
except NotFound:
# If the image vanished while we were trying to remove it, do not fail
res = []
@@ -193,45 +198,57 @@ class ImageRemover(DockerBaseClass):
self.fail(f"Error removing image {name} - {exc}")
for entry in res:
if entry.get('Untagged'):
results['untagged'].append(entry['Untagged'])
if entry.get('Deleted'):
results['deleted'].append(entry['Deleted'])
if entry.get("Untagged"):
results["untagged"].append(entry["Untagged"])
if entry.get("Deleted"):
results["deleted"].append(entry["Deleted"])
results['untagged'] = sorted(results['untagged'])
results['deleted'] = sorted(results['deleted'])
results["untagged"] = sorted(results["untagged"])
results["deleted"] = sorted(results["deleted"])
if self.diff:
image_after = self.client.find_image_by_id(image['Id'], accept_missing_image=True)
results['diff']['after'] = self.get_diff_state(image_after)
image_after = self.client.find_image_by_id(
image["Id"], accept_missing_image=True
)
results["diff"]["after"] = self.get_diff_state(image_after)
elif is_image_name_id(name):
results['deleted'].append(image['Id'])
results['untagged'] = sorted((image.get('RepoTags') or []) + (image.get('RepoDigests') or []))
if not self.force and results['untagged']:
self.fail('Cannot delete image by ID that is still in use - use force=true')
results["deleted"].append(image["Id"])
results["untagged"] = sorted(
(image.get("RepoTags") or []) + (image.get("RepoDigests") or [])
)
if not self.force and results["untagged"]:
self.fail(
"Cannot delete image by ID that is still in use - use force=true"
)
if self.diff:
results['diff']['after'] = self.get_diff_state({})
results["diff"]["after"] = self.get_diff_state({})
elif is_image_name_id(self.tag):
results['untagged'].append(name)
if len(image.get('RepoTags') or []) < 1 and len(image.get('RepoDigests') or []) < 2:
results['deleted'].append(image['Id'])
results["untagged"].append(name)
if (
len(image.get("RepoTags") or []) < 1
and len(image.get("RepoDigests") or []) < 2
):
results["deleted"].append(image["Id"])
if self.diff:
results['diff']['after'] = self.get_diff_state(image)
results["diff"]["after"] = self.get_diff_state(image)
try:
results['diff']['after']['digests'].remove(name)
results["diff"]["after"]["digests"].remove(name)
except ValueError:
pass
else:
results['untagged'].append(name)
if len(image.get('RepoTags') or []) < 2 and len(image.get('RepoDigests') or []) < 1:
results['deleted'].append(image['Id'])
results["untagged"].append(name)
if (
len(image.get("RepoTags") or []) < 2
and len(image.get("RepoDigests") or []) < 1
):
results["deleted"].append(image["Id"])
if self.diff:
results['diff']['after'] = self.get_diff_state(image)
results["diff"]["after"] = self.get_diff_state(image)
try:
results['diff']['after']['tags'].remove(name)
results["diff"]["after"]["tags"].remove(name)
except ValueError:
pass
@@ -240,10 +257,10 @@ class ImageRemover(DockerBaseClass):
def main():
argument_spec = dict(
name=dict(type='str', required=True),
tag=dict(type='str', default='latest'),
force=dict(type='bool', default=False),
prune=dict(type='bool', default=True),
name=dict(type="str", required=True),
tag=dict(type="str", default="latest"),
force=dict(type="bool", default=False),
prune=dict(type="bool", default=True),
)
client = AnsibleDockerClient(
@@ -255,12 +272,16 @@ def main():
results = ImageRemover(client).absent()
client.module.exit_json(**results)
except DockerException as e:
client.fail(f'An unexpected Docker error occurred: {e}', exception=traceback.format_exc())
client.fail(
f"An unexpected Docker error occurred: {e}",
exception=traceback.format_exc(),
)
except RequestException as e:
client.fail(
f'An unexpected requests error occurred when trying to talk to the Docker daemon: {e}',
exception=traceback.format_exc())
f"An unexpected requests error occurred when trying to talk to the Docker daemon: {e}",
exception=traceback.format_exc(),
)
if __name__ == '__main__':
if __name__ == "__main__":
main()
+50 -35
View File
@@ -103,41 +103,40 @@ tagged_images:
import traceback
from ansible.module_utils.common.text.formatters import human_to_bytes
from ansible_collections.community.docker.plugins.module_utils._api.errors import (
DockerException,
)
from ansible_collections.community.docker.plugins.module_utils._api.utils.utils import (
parse_repository_tag,
)
from ansible_collections.community.docker.plugins.module_utils.common_api import (
AnsibleDockerClient,
RequestException,
)
from ansible_collections.community.docker.plugins.module_utils.util import (
DockerBaseClass,
is_image_name_id,
is_valid_tag,
)
from ansible_collections.community.docker.plugins.module_utils._api.errors import DockerException
from ansible_collections.community.docker.plugins.module_utils._api.utils.utils import (
parse_repository_tag,
)
def convert_to_bytes(value, module, name, unlimited_value=None):
if value is None:
return value
try:
if unlimited_value is not None and value in ('unlimited', str(unlimited_value)):
if unlimited_value is not None and value in ("unlimited", str(unlimited_value)):
return unlimited_value
return human_to_bytes(value)
except ValueError as exc:
module.fail_json(msg=f'Failed to convert {name} to bytes: {exc}')
module.fail_json(msg=f"Failed to convert {name} to bytes: {exc}")
def image_info(name, tag, image):
result = dict(name=name, tag=tag)
if image:
result['id'] = image['Id']
result["id"] = image["Id"]
else:
result['exists'] = False
result["exists"] = False
return result
@@ -149,8 +148,8 @@ class ImageTagger(DockerBaseClass):
parameters = self.client.module.params
self.check_mode = self.client.check_mode
self.name = parameters['name']
self.tag = parameters['tag']
self.name = parameters["name"]
self.tag = parameters["tag"]
if not is_valid_tag(self.tag, allow_empty=True):
self.fail(f'"{self.tag}" is not a valid docker tag')
@@ -161,18 +160,22 @@ class ImageTagger(DockerBaseClass):
self.name = repo
self.tag = repo_tag
self.keep_existing_images = parameters['existing_images'] == 'keep'
self.keep_existing_images = parameters["existing_images"] == "keep"
# Make sure names in repository are valid images, and add tag if needed
self.repositories = []
for i, repository in enumerate(parameters['repository']):
for i, repository in enumerate(parameters["repository"]):
if is_image_name_id(repository):
self.fail(f"repository[{i + 1}] must not be an image ID; got: {repository}")
self.fail(
f"repository[{i + 1}] must not be an image ID; got: {repository}"
)
repo, repo_tag = parse_repository_tag(repository)
if not repo_tag:
repo_tag = parameters['tag']
repo_tag = parameters["tag"]
elif not is_valid_tag(repo_tag, allow_empty=False):
self.fail(f"repository[{i + 1}] must not have a digest; got: {repository}")
self.fail(
f"repository[{i + 1}] must not have a digest; got: {repository}"
)
self.repositories.append((repo, repo_tag))
def fail(self, msg):
@@ -182,7 +185,7 @@ class ImageTagger(DockerBaseClass):
tagged_image = self.client.find_image(name=name, tag=tag)
if tagged_image:
# Idempotency checks
if tagged_image['Id'] == image['Id']:
if tagged_image["Id"] == image["Id"]:
return (
False,
f"target image already exists ({tagged_image['Id']}) and is as expected",
@@ -201,11 +204,13 @@ class ImageTagger(DockerBaseClass):
if not self.check_mode:
try:
params = {
'tag': tag,
'repo': name,
'force': True,
"tag": tag,
"repo": name,
"force": True,
}
res = self.client._post(self.client._url('/images/{0}/tag', image['Id']), params=params)
res = self.client._post(
self.client._url("/images/{0}/tag", image["Id"]), params=params
)
self.client._raise_for_status(res)
if res.status_code != 201:
raise Exception("Tag operation failed.")
@@ -237,21 +242,27 @@ class ImageTagger(DockerBaseClass):
before.append(image_info(repository, tag, old_image))
after.append(image_info(repository, tag, image if tagged else old_image))
if tagged:
results['changed'] = True
results['actions'].append(f"Tagged image {image['Id']} as {repository}:{tag}: {msg}")
tagged_images.append(f'{repository}:{tag}')
results["changed"] = True
results["actions"].append(
f"Tagged image {image['Id']} as {repository}:{tag}: {msg}"
)
tagged_images.append(f"{repository}:{tag}")
else:
results['actions'].append(f"Not tagged image {image['Id']} as {repository}:{tag}: {msg}")
results["actions"].append(
f"Not tagged image {image['Id']} as {repository}:{tag}: {msg}"
)
return results
def main():
argument_spec = dict(
name=dict(type='str', required=True),
tag=dict(type='str', default='latest'),
repository=dict(type='list', elements='str', required=True),
existing_images=dict(type='str', choices=['keep', 'overwrite'], default='overwrite'),
name=dict(type="str", required=True),
tag=dict(type="str", default="latest"),
repository=dict(type="list", elements="str", required=True),
existing_images=dict(
type="str", choices=["keep", "overwrite"], default="overwrite"
),
)
client = AnsibleDockerClient(
@@ -263,12 +274,16 @@ def main():
results = ImageTagger(client).tag_images()
client.module.exit_json(**results)
except DockerException as e:
client.fail(f'An unexpected Docker error occurred: {e}', exception=traceback.format_exc())
client.fail(
f"An unexpected Docker error occurred: {e}",
exception=traceback.format_exc(),
)
except RequestException as e:
client.fail(
f'An unexpected requests error occurred when trying to talk to the Docker daemon: {e}',
exception=traceback.format_exc())
f"An unexpected requests error occurred when trying to talk to the Docker daemon: {e}",
exception=traceback.format_exc(),
)
if __name__ == '__main__':
if __name__ == "__main__":
main()
+111 -99
View File
@@ -122,7 +122,19 @@ import os
import traceback
from ansible.module_utils.common.text.converters import to_bytes, to_text
from ansible_collections.community.docker.plugins.module_utils._api import auth
from ansible_collections.community.docker.plugins.module_utils._api.auth import (
decode_auth,
)
from ansible_collections.community.docker.plugins.module_utils._api.credentials.errors import (
CredentialsNotFound,
)
from ansible_collections.community.docker.plugins.module_utils._api.credentials.store import (
Store,
)
from ansible_collections.community.docker.plugins.module_utils._api.errors import (
DockerException,
)
from ansible_collections.community.docker.plugins.module_utils.common_api import (
AnsibleDockerClient,
RequestException,
@@ -132,18 +144,12 @@ from ansible_collections.community.docker.plugins.module_utils.util import (
DockerBaseClass,
)
from ansible_collections.community.docker.plugins.module_utils._api import auth
from ansible_collections.community.docker.plugins.module_utils._api.auth import decode_auth
from ansible_collections.community.docker.plugins.module_utils._api.credentials.errors import CredentialsNotFound
from ansible_collections.community.docker.plugins.module_utils._api.credentials.store import Store
from ansible_collections.community.docker.plugins.module_utils._api.errors import DockerException
class DockerFileStore(object):
'''
"""
A custom credential store class that implements only the functionality we need to
update the docker config file when no credential helpers is provided.
'''
"""
program = "<legacy config>"
@@ -151,9 +157,7 @@ class DockerFileStore(object):
self._config_path = config_path
# Make sure we have a minimal config if none is available.
self._config = dict(
auths=dict()
)
self._config = dict(auths=dict())
try:
# Attempt to read the existing config.
@@ -168,39 +172,36 @@ class DockerFileStore(object):
@property
def config_path(self):
'''
"""
Return the config path configured in this DockerFileStore instance.
'''
"""
return self._config_path
def get(self, server):
'''
"""
Retrieve credentials for `server` if there are any in the config file.
Otherwise raise a `StoreError`
'''
"""
server_creds = self._config['auths'].get(server)
server_creds = self._config["auths"].get(server)
if not server_creds:
raise CredentialsNotFound('No matching credentials')
raise CredentialsNotFound("No matching credentials")
(username, password) = decode_auth(server_creds['auth'])
(username, password) = decode_auth(server_creds["auth"])
return dict(
Username=username,
Secret=password
)
return dict(Username=username, Secret=password)
def _write(self):
'''
"""
Write config back out to disk.
'''
"""
# Make sure directory exists
dir = os.path.dirname(self._config_path)
if not os.path.exists(dir):
os.makedirs(dir)
# Write config; make sure it has permissions 0x600
content = json.dumps(self._config, indent=4, sort_keys=True).encode('utf-8')
content = json.dumps(self._config, indent=4, sort_keys=True).encode("utf-8")
f = os.open(self._config_path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
try:
os.write(f, content)
@@ -208,32 +209,28 @@ class DockerFileStore(object):
os.close(f)
def store(self, server, username, password):
'''
"""
Add a credentials for `server` to the current configuration.
'''
"""
b64auth = base64.b64encode(
to_bytes(username) + b':' + to_bytes(password)
)
b64auth = base64.b64encode(to_bytes(username) + b":" + to_bytes(password))
auth = to_text(b64auth)
# build up the auth structure
if 'auths' not in self._config:
self._config['auths'] = dict()
if "auths" not in self._config:
self._config["auths"] = dict()
self._config['auths'][server] = dict(
auth=auth
)
self._config["auths"][server] = dict(auth=auth)
self._write()
def erase(self, server):
'''
"""
Remove credentials for the given server from the configuration.
'''
"""
if 'auths' in self._config and server in self._config['auths']:
self._config['auths'].pop(server)
if "auths" in self._config and server in self._config["auths"]:
self._config["auths"].pop(server)
self._write()
@@ -248,20 +245,20 @@ class LoginManager(DockerBaseClass):
parameters = self.client.module.params
self.check_mode = self.client.check_mode
self.registry_url = parameters.get('registry_url')
self.username = parameters.get('username')
self.password = parameters.get('password')
self.reauthorize = parameters.get('reauthorize')
self.config_path = parameters.get('config_path')
self.state = parameters.get('state')
self.registry_url = parameters.get("registry_url")
self.username = parameters.get("username")
self.password = parameters.get("password")
self.reauthorize = parameters.get("reauthorize")
self.config_path = parameters.get("config_path")
self.state = parameters.get("state")
def run(self):
'''
"""
Do the actual work of this task here. This allows instantiation for partial
testing.
'''
"""
if self.state == 'present':
if self.state == "present":
self.login()
else:
self.logout()
@@ -282,57 +279,63 @@ class LoginManager(DockerBaseClass):
authcfg = self.client._auth_configs.resolve_authconfig(self.registry_url)
# If we found an existing auth config for this registry and username
# combination, we can return it immediately unless reauth is requested.
if authcfg and authcfg.get('username') == self.username and not reauth:
if authcfg and authcfg.get("username") == self.username and not reauth:
return authcfg
req_data = {
'username': self.username,
'password': self.password,
'email': None,
'serveraddress': self.registry_url,
"username": self.username,
"password": self.password,
"email": None,
"serveraddress": self.registry_url,
}
response = self.client._post_json(self.client._url('/auth'), data=req_data)
response = self.client._post_json(self.client._url("/auth"), data=req_data)
if response.status_code == 200:
self.client._auth_configs.add_auth(self.registry_url or auth.INDEX_NAME, req_data)
self.client._auth_configs.add_auth(
self.registry_url or auth.INDEX_NAME, req_data
)
return self.client._result(response, json=True)
def login(self):
'''
"""
Log into the registry with provided username/password. On success update the config
file with the new authorization.
:return: None
'''
"""
self.results['actions'].append(f"Logged into {self.registry_url}")
self.results["actions"].append(f"Logged into {self.registry_url}")
self.log(f"Log into {self.registry_url} with username {self.username}")
try:
response = self._login(self.reauthorize)
except Exception as exc:
self.fail(f"Logging into {self.registry_url} for user {self.username} failed - {exc}")
self.fail(
f"Logging into {self.registry_url} for user {self.username} failed - {exc}"
)
# If user is already logged in, then response contains password for user
if 'password' in response:
if "password" in response:
# This returns correct password if user is logged in and wrong password is given.
# So if it returns another password as we passed, and the user did not request to
# reauthorize, still do it.
if not self.reauthorize and response['password'] != self.password:
if not self.reauthorize and response["password"] != self.password:
try:
response = self._login(True)
except Exception as exc:
self.fail(f"Logging into {self.registry_url} for user {self.username} failed - {exc}")
response.pop('password', None)
self.results['login_result'] = response
self.fail(
f"Logging into {self.registry_url} for user {self.username} failed - {exc}"
)
response.pop("password", None)
self.results["login_result"] = response
self.update_credentials()
def logout(self):
'''
"""
Log out of the registry. On success update the config file.
:return: None
'''
"""
# Get the configuration store.
store = self.get_credential_store_instance(self.registry_url, self.config_path)
@@ -342,20 +345,20 @@ class LoginManager(DockerBaseClass):
except CredentialsNotFound:
# get raises an exception on not found.
self.log(f"Credentials for {self.registry_url} not present, doing nothing.")
self.results['changed'] = False
self.results["changed"] = False
return
if not self.check_mode:
store.erase(self.registry_url)
self.results['changed'] = True
self.results["changed"] = True
def update_credentials(self):
'''
"""
If the authorization is not stored attempt to store authorization values via
the appropriate credential helper or to the config file.
:return: None
'''
"""
# Check to see if credentials already exist.
store = self.get_credential_store_instance(self.registry_url, self.config_path)
@@ -364,25 +367,30 @@ class LoginManager(DockerBaseClass):
current = store.get(self.registry_url)
except CredentialsNotFound:
# get raises an exception on not found.
current = dict(
Username='',
Secret=''
)
current = dict(Username="", Secret="")
if current['Username'] != self.username or current['Secret'] != self.password or self.reauthorize:
if (
current["Username"] != self.username
or current["Secret"] != self.password
or self.reauthorize
):
if not self.check_mode:
store.store(self.registry_url, self.username, self.password)
self.log(f"Writing credentials to configured helper {store.program} for {self.registry_url}")
self.results['actions'].append(f"Wrote credentials to configured helper {store.program} for {self.registry_url}")
self.results['changed'] = True
self.log(
f"Writing credentials to configured helper {store.program} for {self.registry_url}"
)
self.results["actions"].append(
f"Wrote credentials to configured helper {store.program} for {self.registry_url}"
)
self.results["changed"] = True
def get_credential_store_instance(self, registry, dockercfg_path):
'''
"""
Return an instance of docker.credentials.Store used by the given registry.
:return: A Store or None
:rtype: Union[docker.credentials.Store, NoneType]
'''
"""
credstore_env = self.client.credstore_env
@@ -402,16 +410,20 @@ class LoginManager(DockerBaseClass):
def main():
argument_spec = dict(
registry_url=dict(type='str', default=DEFAULT_DOCKER_REGISTRY, aliases=['registry', 'url']),
username=dict(type='str'),
password=dict(type='str', no_log=True),
reauthorize=dict(type='bool', default=False, aliases=['reauth']),
state=dict(type='str', default='present', choices=['present', 'absent']),
config_path=dict(type='path', default='~/.docker/config.json', aliases=['dockercfg_path']),
registry_url=dict(
type="str", default=DEFAULT_DOCKER_REGISTRY, aliases=["registry", "url"]
),
username=dict(type="str"),
password=dict(type="str", no_log=True),
reauthorize=dict(type="bool", default=False, aliases=["reauth"]),
state=dict(type="str", default="present", choices=["present", "absent"]),
config_path=dict(
type="path", default="~/.docker/config.json", aliases=["dockercfg_path"]
),
)
required_if = [
('state', 'present', ['username', 'password']),
("state", "present", ["username", "password"]),
]
client = AnsibleDockerClient(
@@ -421,25 +433,25 @@ def main():
)
try:
results = dict(
changed=False,
actions=[],
login_result={}
)
results = dict(changed=False, actions=[], login_result={})
manager = LoginManager(client, results)
manager.run()
if 'actions' in results:
del results['actions']
if "actions" in results:
del results["actions"]
client.module.exit_json(**results)
except DockerException as e:
client.fail(f'An unexpected Docker error occurred: {e}', exception=traceback.format_exc())
client.fail(
f"An unexpected Docker error occurred: {e}",
exception=traceback.format_exc(),
)
except RequestException as e:
client.fail(
f'An unexpected requests error occurred when trying to talk to the Docker daemon: {e}',
exception=traceback.format_exc())
f"An unexpected requests error occurred when trying to talk to the Docker daemon: {e}",
exception=traceback.format_exc(),
)
if __name__ == '__main__':
if __name__ == "__main__":
main()
+270 -184
View File
@@ -282,22 +282,23 @@ network:
"""
import re
import traceback
import time
import traceback
from ansible.module_utils.common.text.converters import to_native
from ansible_collections.community.docker.plugins.module_utils._api.errors import (
DockerException,
)
from ansible_collections.community.docker.plugins.module_utils.common_api import (
AnsibleDockerClient,
RequestException,
)
from ansible_collections.community.docker.plugins.module_utils.util import (
DockerBaseClass,
DifferenceTracker,
DockerBaseClass,
clean_dict_booleans_for_docker_api,
sanitize_labels,
)
from ansible_collections.community.docker.plugins.module_utils._api.errors import DockerException
class TaskParameters(DockerBaseClass):
@@ -331,15 +332,19 @@ class TaskParameters(DockerBaseClass):
# config_only sets driver to 'null' (and scope to 'local') so force that here. Otherwise we get
# diffs of 'null' --> 'bridge' given that the driver option defaults to 'bridge'.
if self.config_only:
self.driver = 'null'
self.driver = "null"
def container_names_in_network(network):
return [c['Name'] for c in network['Containers'].values()] if network['Containers'] else []
return (
[c["Name"] for c in network["Containers"].values()]
if network["Containers"]
else []
)
CIDR_IPV4 = re.compile(r'^([0-9]{1,3}\.){3}[0-9]{1,3}/([0-9]|[1-2][0-9]|3[0-2])$')
CIDR_IPV6 = re.compile(r'^[0-9a-fA-F:]+/([0-9]|[1-9][0-9]|1[0-2][0-9])$')
CIDR_IPV4 = re.compile(r"^([0-9]{1,3}\.){3}[0-9]{1,3}/([0-9]|[1-2][0-9]|3[0-2])$")
CIDR_IPV6 = re.compile(r"^[0-9a-fA-F:]+/([0-9]|[1-9][0-9]|1[0-2][0-9])$")
def validate_cidr(cidr):
@@ -352,9 +357,9 @@ def validate_cidr(cidr):
:raises ValueError: If ``cidr`` is not a valid CIDR
"""
if CIDR_IPV4.match(cidr):
return 'ipv4'
return "ipv4"
elif CIDR_IPV6.match(cidr):
return 'ipv6'
return "ipv6"
raise ValueError(f'"{cidr}" is not a valid CIDR')
@@ -366,9 +371,7 @@ def normalize_ipam_config_key(key):
:return Ansible module key
:rtype str
"""
special_cases = {
'AuxiliaryAddresses': 'aux_addresses'
}
special_cases = {"AuxiliaryAddresses": "aux_addresses"}
return special_cases.get(key, key.lower())
@@ -388,10 +391,7 @@ class DockerNetworkManager(object):
self.client = client
self.parameters = TaskParameters(client)
self.check_mode = self.client.check_mode
self.results = {
'changed': False,
'actions': []
}
self.results = {"changed": False, "actions": []}
self.diff = self.client.module._diff
self.diff_tracker = DifferenceTracker()
self.diff_result = dict()
@@ -399,87 +399,117 @@ class DockerNetworkManager(object):
self.existing_network = self.get_existing_network()
if not self.parameters.connected and self.existing_network:
self.parameters.connected = container_names_in_network(self.existing_network)
self.parameters.connected = container_names_in_network(
self.existing_network
)
if self.parameters.ipam_config:
try:
for ipam_config in self.parameters.ipam_config:
validate_cidr(ipam_config['subnet'])
validate_cidr(ipam_config["subnet"])
except ValueError as e:
self.client.fail(to_native(e))
if self.parameters.driver_options:
self.parameters.driver_options = clean_dict_booleans_for_docker_api(self.parameters.driver_options)
self.parameters.driver_options = clean_dict_booleans_for_docker_api(
self.parameters.driver_options
)
state = self.parameters.state
if state == 'present':
if state == "present":
self.present()
elif state == 'absent':
elif state == "absent":
self.absent()
if self.diff or self.check_mode or self.parameters.debug:
if self.diff:
self.diff_result['before'], self.diff_result['after'] = self.diff_tracker.get_before_after()
self.results['diff'] = self.diff_result
self.diff_result["before"], self.diff_result["after"] = (
self.diff_tracker.get_before_after()
)
self.results["diff"] = self.diff_result
def get_existing_network(self):
return self.client.get_network(name=self.parameters.name)
def has_different_config(self, net):
'''
"""
Evaluates an existing network and returns a tuple containing a boolean
indicating if the configuration is different and a list of differences.
:param net: the inspection output for an existing network
:return: (bool, list)
'''
"""
differences = DifferenceTracker()
if self.parameters.config_only is not None and self.parameters.config_only != net.get('ConfigOnly', False):
differences.add('config_only',
parameter=self.parameters.config_only,
active=net.get('ConfigOnly', False))
if self.parameters.config_from is not None and self.parameters.config_from != net.get('ConfigFrom', {}).get('Network', ''):
differences.add('config_from',
parameter=self.parameters.config_from,
active=net.get('ConfigFrom', {}).get('Network', ''))
if self.parameters.driver and self.parameters.driver != net['Driver']:
differences.add('driver',
parameter=self.parameters.driver,
active=net['Driver'])
if (
self.parameters.config_only is not None
and self.parameters.config_only != net.get("ConfigOnly", False)
):
differences.add(
"config_only",
parameter=self.parameters.config_only,
active=net.get("ConfigOnly", False),
)
if (
self.parameters.config_from is not None
and self.parameters.config_from
!= net.get("ConfigFrom", {}).get("Network", "")
):
differences.add(
"config_from",
parameter=self.parameters.config_from,
active=net.get("ConfigFrom", {}).get("Network", ""),
)
if self.parameters.driver and self.parameters.driver != net["Driver"]:
differences.add(
"driver", parameter=self.parameters.driver, active=net["Driver"]
)
if self.parameters.driver_options:
if not net.get('Options'):
differences.add('driver_options',
parameter=self.parameters.driver_options,
active=net.get('Options'))
if not net.get("Options"):
differences.add(
"driver_options",
parameter=self.parameters.driver_options,
active=net.get("Options"),
)
else:
for key, value in self.parameters.driver_options.items():
if not (key in net['Options']) or value != net['Options'][key]:
differences.add(f'driver_options.{key}',
parameter=value,
active=net['Options'].get(key))
if not (key in net["Options"]) or value != net["Options"][key]:
differences.add(
f"driver_options.{key}",
parameter=value,
active=net["Options"].get(key),
)
if self.parameters.ipam_driver:
if not net.get('IPAM') or net['IPAM']['Driver'] != self.parameters.ipam_driver:
differences.add('ipam_driver',
parameter=self.parameters.ipam_driver,
active=net.get('IPAM'))
if (
not net.get("IPAM")
or net["IPAM"]["Driver"] != self.parameters.ipam_driver
):
differences.add(
"ipam_driver",
parameter=self.parameters.ipam_driver,
active=net.get("IPAM"),
)
if self.parameters.ipam_driver_options is not None:
ipam_driver_options = net['IPAM'].get('Options') or {}
ipam_driver_options = net["IPAM"].get("Options") or {}
if ipam_driver_options != self.parameters.ipam_driver_options:
differences.add('ipam_driver_options',
parameter=self.parameters.ipam_driver_options,
active=ipam_driver_options)
differences.add(
"ipam_driver_options",
parameter=self.parameters.ipam_driver_options,
active=ipam_driver_options,
)
if self.parameters.ipam_config is not None and self.parameters.ipam_config:
if not net.get('IPAM') or not net['IPAM']['Config']:
differences.add('ipam_config',
parameter=self.parameters.ipam_config,
active=net.get('IPAM', {}).get('Config'))
if not net.get("IPAM") or not net["IPAM"]["Config"]:
differences.add(
"ipam_config",
parameter=self.parameters.ipam_config,
active=net.get("IPAM", {}).get("Config"),
)
else:
# Put network's IPAM config into the same format as module's IPAM config
net_ipam_configs = []
for net_ipam_config in net['IPAM']['Config']:
for net_ipam_config in net["IPAM"]["Config"]:
config = dict()
for k, v in net_ipam_config.items():
config[normalize_ipam_config_key(k)] = v
@@ -497,118 +527,154 @@ class DockerNetworkManager(object):
# (but have default value None if not specified)
continue
if value != net_config.get(key):
differences.add(f'ipam_config[{idx}].{key}',
parameter=value,
active=net_config.get(key))
differences.add(
f"ipam_config[{idx}].{key}",
parameter=value,
active=net_config.get(key),
)
if self.parameters.enable_ipv4 is not None and self.parameters.enable_ipv4 != net.get('EnableIPv4', False):
differences.add('enable_ipv4',
parameter=self.parameters.enable_ipv4,
active=net.get('EnableIPv4', False))
if self.parameters.enable_ipv6 is not None and self.parameters.enable_ipv6 != net.get('EnableIPv6', False):
differences.add('enable_ipv6',
parameter=self.parameters.enable_ipv6,
active=net.get('EnableIPv6', False))
if (
self.parameters.enable_ipv4 is not None
and self.parameters.enable_ipv4 != net.get("EnableIPv4", False)
):
differences.add(
"enable_ipv4",
parameter=self.parameters.enable_ipv4,
active=net.get("EnableIPv4", False),
)
if (
self.parameters.enable_ipv6 is not None
and self.parameters.enable_ipv6 != net.get("EnableIPv6", False)
):
differences.add(
"enable_ipv6",
parameter=self.parameters.enable_ipv6,
active=net.get("EnableIPv6", False),
)
if self.parameters.internal is not None and self.parameters.internal != net.get('Internal', False):
differences.add('internal',
parameter=self.parameters.internal,
active=net.get('Internal'))
if (
self.parameters.internal is not None
and self.parameters.internal != net.get("Internal", False)
):
differences.add(
"internal",
parameter=self.parameters.internal,
active=net.get("Internal"),
)
if self.parameters.scope is not None and self.parameters.scope != net.get('Scope'):
differences.add('scope',
parameter=self.parameters.scope,
active=net.get('Scope'))
if self.parameters.scope is not None and self.parameters.scope != net.get(
"Scope"
):
differences.add(
"scope", parameter=self.parameters.scope, active=net.get("Scope")
)
if self.parameters.attachable is not None and self.parameters.attachable != net.get('Attachable', False):
differences.add('attachable',
parameter=self.parameters.attachable,
active=net.get('Attachable'))
if self.parameters.ingress is not None and self.parameters.ingress != net.get('Ingress', False):
differences.add('ingress',
parameter=self.parameters.ingress,
active=net.get('Ingress'))
if (
self.parameters.attachable is not None
and self.parameters.attachable != net.get("Attachable", False)
):
differences.add(
"attachable",
parameter=self.parameters.attachable,
active=net.get("Attachable"),
)
if self.parameters.ingress is not None and self.parameters.ingress != net.get(
"Ingress", False
):
differences.add(
"ingress", parameter=self.parameters.ingress, active=net.get("Ingress")
)
if self.parameters.labels:
if not net.get('Labels'):
differences.add('labels',
parameter=self.parameters.labels,
active=net.get('Labels'))
if not net.get("Labels"):
differences.add(
"labels", parameter=self.parameters.labels, active=net.get("Labels")
)
else:
for key, value in self.parameters.labels.items():
if not (key in net['Labels']) or value != net['Labels'][key]:
differences.add(f'labels.{key}',
parameter=value,
active=net['Labels'].get(key))
if not (key in net["Labels"]) or value != net["Labels"][key]:
differences.add(
f"labels.{key}",
parameter=value,
active=net["Labels"].get(key),
)
return not differences.empty, differences
def create_network(self):
if not self.existing_network:
data = {
'Name': self.parameters.name,
'Driver': self.parameters.driver,
'Options': self.parameters.driver_options,
'IPAM': None,
'CheckDuplicate': None,
"Name": self.parameters.name,
"Driver": self.parameters.driver,
"Options": self.parameters.driver_options,
"IPAM": None,
"CheckDuplicate": None,
}
if self.parameters.config_only is not None:
data['ConfigOnly'] = self.parameters.config_only
data["ConfigOnly"] = self.parameters.config_only
if self.parameters.config_from:
data['ConfigFrom'] = {'Network': self.parameters.config_from}
data["ConfigFrom"] = {"Network": self.parameters.config_from}
if self.parameters.enable_ipv6 is not None:
data['EnableIPv6'] = self.parameters.enable_ipv6
data["EnableIPv6"] = self.parameters.enable_ipv6
if self.parameters.enable_ipv4 is not None:
data['EnableIPv4'] = self.parameters.enable_ipv4
data["EnableIPv4"] = self.parameters.enable_ipv4
if self.parameters.internal:
data['Internal'] = True
data["Internal"] = True
if self.parameters.scope is not None:
data['Scope'] = self.parameters.scope
data["Scope"] = self.parameters.scope
if self.parameters.attachable is not None:
data['Attachable'] = self.parameters.attachable
data["Attachable"] = self.parameters.attachable
if self.parameters.ingress is not None:
data['Ingress'] = self.parameters.ingress
data["Ingress"] = self.parameters.ingress
if self.parameters.labels is not None:
data["Labels"] = self.parameters.labels
ipam_pools = []
if self.parameters.ipam_config:
for ipam_pool in self.parameters.ipam_config:
ipam_pools.append({
'Subnet': ipam_pool['subnet'],
'IPRange': ipam_pool['iprange'],
'Gateway': ipam_pool['gateway'],
'AuxiliaryAddresses': ipam_pool['aux_addresses'],
})
ipam_pools.append(
{
"Subnet": ipam_pool["subnet"],
"IPRange": ipam_pool["iprange"],
"Gateway": ipam_pool["gateway"],
"AuxiliaryAddresses": ipam_pool["aux_addresses"],
}
)
if self.parameters.ipam_driver or self.parameters.ipam_driver_options or ipam_pools:
if (
self.parameters.ipam_driver
or self.parameters.ipam_driver_options
or ipam_pools
):
# Only add IPAM if a driver was specified or if IPAM parameters were
# specified. Leaving this parameter out can significantly speed up
# creation; on my machine creation with this option needs ~15 seconds,
# and without just a few seconds.
data['IPAM'] = {
'Driver': self.parameters.ipam_driver,
'Config': ipam_pools or [],
'Options': self.parameters.ipam_driver_options,
data["IPAM"] = {
"Driver": self.parameters.ipam_driver,
"Config": ipam_pools or [],
"Options": self.parameters.ipam_driver_options,
}
if not self.check_mode:
resp = self.client.post_json_to_json('/networks/create', data=data)
self.client.report_warnings(resp, ['Warning'])
self.existing_network = self.client.get_network(network_id=resp['Id'])
self.results['actions'].append(f"Created network {self.parameters.name} with driver {self.parameters.driver}")
self.results['changed'] = True
resp = self.client.post_json_to_json("/networks/create", data=data)
self.client.report_warnings(resp, ["Warning"])
self.existing_network = self.client.get_network(network_id=resp["Id"])
self.results["actions"].append(
f"Created network {self.parameters.name} with driver {self.parameters.driver}"
)
self.results["changed"] = True
def remove_network(self):
if self.existing_network:
self.disconnect_all_containers()
if not self.check_mode:
self.client.delete_call('/networks/{0}', self.parameters.name)
if self.existing_network.get('Scope', 'local') == 'swarm':
self.client.delete_call("/networks/{0}", self.parameters.name)
if self.existing_network.get("Scope", "local") == "swarm":
while self.get_existing_network():
time.sleep(0.1)
self.results['actions'].append(f"Removed network {self.parameters.name}")
self.results['changed'] = True
self.results["actions"].append(f"Removed network {self.parameters.name}")
self.results["changed"] = True
def is_container_connected(self, container_name):
if not self.existing_network:
@@ -621,11 +687,15 @@ class DockerNetworkManager(object):
return bool(container)
except DockerException as e:
self.client.fail(f'An unexpected Docker error occurred: {e}', exception=traceback.format_exc())
self.client.fail(
f"An unexpected Docker error occurred: {e}",
exception=traceback.format_exc(),
)
except RequestException as e:
self.client.fail(
f'An unexpected requests error occurred when trying to talk to the Docker daemon: {e}',
exception=traceback.format_exc())
f"An unexpected requests error occurred when trying to talk to the Docker daemon: {e}",
exception=traceback.format_exc(),
)
def connect_containers(self):
for name in self.parameters.connected:
@@ -635,38 +705,42 @@ class DockerNetworkManager(object):
"Container": name,
"EndpointConfig": None,
}
self.client.post_json('/networks/{0}/connect', self.parameters.name, data=data)
self.results['actions'].append(f"Connected container {name}")
self.results['changed'] = True
self.diff_tracker.add(f'connected.{name}', parameter=True, active=False)
self.client.post_json(
"/networks/{0}/connect", self.parameters.name, data=data
)
self.results["actions"].append(f"Connected container {name}")
self.results["changed"] = True
self.diff_tracker.add(f"connected.{name}", parameter=True, active=False)
def disconnect_missing(self):
if not self.existing_network:
return
containers = self.existing_network['Containers']
containers = self.existing_network["Containers"]
if not containers:
return
for c in containers.values():
name = c['Name']
name = c["Name"]
if name not in self.parameters.connected:
self.disconnect_container(name)
def disconnect_all_containers(self):
containers = self.client.get_network(name=self.parameters.name)['Containers']
containers = self.client.get_network(name=self.parameters.name)["Containers"]
if not containers:
return
for cont in containers.values():
self.disconnect_container(cont['Name'])
self.disconnect_container(cont["Name"])
def disconnect_container(self, container_name):
if not self.check_mode:
data = {"Container": container_name, "Force": True}
self.client.post_json('/networks/{0}/disconnect', self.parameters.name, data=data)
self.results['actions'].append(f"Disconnected container {container_name}")
self.results['changed'] = True
self.diff_tracker.add(f'connected.{container_name}',
parameter=False,
active=True)
self.client.post_json(
"/networks/{0}/disconnect", self.parameters.name, data=data
)
self.results["actions"].append(f"Disconnected container {container_name}")
self.results["changed"] = True
self.diff_tracker.add(
f"connected.{container_name}", parameter=False, active=True
)
def present(self):
different = False
@@ -674,7 +748,9 @@ class DockerNetworkManager(object):
if self.existing_network:
different, differences = self.has_different_config(self.existing_network)
self.diff_tracker.add('exists', parameter=True, active=self.existing_network is not None)
self.diff_tracker.add(
"exists", parameter=True, active=self.existing_network is not None
)
if self.parameters.force or different:
self.remove_network()
self.existing_network = None
@@ -685,55 +761,61 @@ class DockerNetworkManager(object):
self.disconnect_missing()
if self.diff or self.check_mode or self.parameters.debug:
self.diff_result['differences'] = differences.get_legacy_docker_diffs()
self.diff_result["differences"] = differences.get_legacy_docker_diffs()
self.diff_tracker.merge(differences)
if not self.check_mode and not self.parameters.debug:
self.results.pop('actions')
self.results.pop("actions")
network_facts = self.get_existing_network()
self.results['network'] = network_facts
self.results["network"] = network_facts
def absent(self):
self.diff_tracker.add('exists', parameter=False, active=self.existing_network is not None)
self.diff_tracker.add(
"exists", parameter=False, active=self.existing_network is not None
)
self.remove_network()
def main():
argument_spec = dict(
name=dict(type='str', required=True, aliases=['network_name']),
config_from=dict(type='str'),
config_only=dict(type='bool'),
connected=dict(type='list', default=[], elements='str', aliases=['containers']),
state=dict(type='str', default='present', choices=['present', 'absent']),
driver=dict(type='str', default='bridge'),
driver_options=dict(type='dict', default={}),
force=dict(type='bool', default=False),
appends=dict(type='bool', default=False, aliases=['incremental']),
ipam_driver=dict(type='str'),
ipam_driver_options=dict(type='dict'),
ipam_config=dict(type='list', elements='dict', options=dict(
subnet=dict(type='str'),
iprange=dict(type='str'),
gateway=dict(type='str'),
aux_addresses=dict(type='dict'),
)),
enable_ipv4=dict(type='bool'),
enable_ipv6=dict(type='bool'),
internal=dict(type='bool'),
labels=dict(type='dict', default={}),
debug=dict(type='bool', default=False),
scope=dict(type='str', choices=['local', 'global', 'swarm']),
attachable=dict(type='bool'),
ingress=dict(type='bool'),
name=dict(type="str", required=True, aliases=["network_name"]),
config_from=dict(type="str"),
config_only=dict(type="bool"),
connected=dict(type="list", default=[], elements="str", aliases=["containers"]),
state=dict(type="str", default="present", choices=["present", "absent"]),
driver=dict(type="str", default="bridge"),
driver_options=dict(type="dict", default={}),
force=dict(type="bool", default=False),
appends=dict(type="bool", default=False, aliases=["incremental"]),
ipam_driver=dict(type="str"),
ipam_driver_options=dict(type="dict"),
ipam_config=dict(
type="list",
elements="dict",
options=dict(
subnet=dict(type="str"),
iprange=dict(type="str"),
gateway=dict(type="str"),
aux_addresses=dict(type="dict"),
),
),
enable_ipv4=dict(type="bool"),
enable_ipv6=dict(type="bool"),
internal=dict(type="bool"),
labels=dict(type="dict", default={}),
debug=dict(type="bool", default=False),
scope=dict(type="str", choices=["local", "global", "swarm"]),
attachable=dict(type="bool"),
ingress=dict(type="bool"),
)
option_minimal_versions = dict(
config_from=dict(docker_api_version='1.30'),
config_only=dict(docker_api_version='1.30'),
scope=dict(docker_api_version='1.30'),
attachable=dict(docker_api_version='1.26'),
enable_ipv4=dict(docker_api_version='1.47'),
config_from=dict(docker_api_version="1.30"),
config_only=dict(docker_api_version="1.30"),
scope=dict(docker_api_version="1.30"),
attachable=dict(docker_api_version="1.26"),
enable_ipv4=dict(docker_api_version="1.47"),
)
client = AnsibleDockerClient(
@@ -742,17 +824,21 @@ def main():
# "The docker server >= 1.10.0"
option_minimal_versions=option_minimal_versions,
)
sanitize_labels(client.module.params['labels'], 'labels', client)
sanitize_labels(client.module.params["labels"], "labels", client)
try:
cm = DockerNetworkManager(client)
client.module.exit_json(**cm.results)
except DockerException as e:
client.fail(f'An unexpected Docker error occurred: {e}', exception=traceback.format_exc())
client.fail(
f"An unexpected Docker error occurred: {e}",
exception=traceback.format_exc(),
)
except RequestException as e:
client.fail(
f'An unexpected requests error occurred when trying to talk to the Docker daemon: {e}',
exception=traceback.format_exc())
f"An unexpected requests error occurred when trying to talk to the Docker daemon: {e}",
exception=traceback.format_exc(),
)
if __name__ == '__main__':
if __name__ == "__main__":
main()
+13 -7
View File
@@ -98,16 +98,18 @@ network:
import traceback
from ansible_collections.community.docker.plugins.module_utils._api.errors import (
DockerException,
)
from ansible_collections.community.docker.plugins.module_utils.common_api import (
AnsibleDockerClient,
RequestException,
)
from ansible_collections.community.docker.plugins.module_utils._api.errors import DockerException
def main():
argument_spec = dict(
name=dict(type='str', required=True),
name=dict(type="str", required=True),
)
client = AnsibleDockerClient(
@@ -116,7 +118,7 @@ def main():
)
try:
network = client.get_network(client.module.params['name'])
network = client.get_network(client.module.params["name"])
client.module.exit_json(
changed=False,
@@ -124,12 +126,16 @@ def main():
network=network,
)
except DockerException as e:
client.fail(f'An unexpected Docker error occurred: {e}', exception=traceback.format_exc())
client.fail(
f"An unexpected Docker error occurred: {e}",
exception=traceback.format_exc(),
)
except RequestException as e:
client.fail(
f'An unexpected requests error occurred when trying to talk to the Docker daemon: {e}',
exception=traceback.format_exc())
f"An unexpected requests error occurred when trying to talk to the Docker daemon: {e}",
exception=traceback.format_exc(),
)
if __name__ == '__main__':
if __name__ == "__main__":
main()
+55 -43
View File
@@ -6,6 +6,7 @@
from __future__ import annotations
DOCUMENTATION = r"""
module: docker_node
short_description: Manage Docker Swarm node
@@ -134,21 +135,24 @@ node:
import traceback
try:
from docker.errors import DockerException, APIError
from docker.errors import APIError, DockerException
except ImportError:
# missing Docker SDK for Python handled in ansible.module_utils.docker.common
pass
from ansible.module_utils.common.text.converters import to_native
from ansible_collections.community.docker.plugins.module_utils.common import (
DockerBaseClass,
RequestException,
)
from ansible.module_utils.common.text.converters import to_native
from ansible_collections.community.docker.plugins.module_utils.swarm import AnsibleDockerSwarmClient
from ansible_collections.community.docker.plugins.module_utils.util import sanitize_labels
from ansible_collections.community.docker.plugins.module_utils.swarm import (
AnsibleDockerSwarmClient,
)
from ansible_collections.community.docker.plugins.module_utils.util import (
sanitize_labels,
)
class TaskParameters(DockerBaseClass):
@@ -208,80 +212,84 @@ class SwarmNodeManager(DockerBaseClass):
)
if self.parameters.role is None:
node_spec['Role'] = node_info['Spec']['Role']
node_spec["Role"] = node_info["Spec"]["Role"]
else:
if not node_info['Spec']['Role'] == self.parameters.role:
node_spec['Role'] = self.parameters.role
if not node_info["Spec"]["Role"] == self.parameters.role:
node_spec["Role"] = self.parameters.role
changed = True
if self.parameters.availability is None:
node_spec['Availability'] = node_info['Spec']['Availability']
node_spec["Availability"] = node_info["Spec"]["Availability"]
else:
if not node_info['Spec']['Availability'] == self.parameters.availability:
node_info['Spec']['Availability'] = self.parameters.availability
if not node_info["Spec"]["Availability"] == self.parameters.availability:
node_info["Spec"]["Availability"] = self.parameters.availability
changed = True
if self.parameters.labels_state == 'replace':
if self.parameters.labels_state == "replace":
if self.parameters.labels is None:
node_spec['Labels'] = {}
if node_info['Spec']['Labels']:
node_spec["Labels"] = {}
if node_info["Spec"]["Labels"]:
changed = True
else:
if (node_info['Spec']['Labels'] or {}) != self.parameters.labels:
node_spec['Labels'] = self.parameters.labels
if (node_info["Spec"]["Labels"] or {}) != self.parameters.labels:
node_spec["Labels"] = self.parameters.labels
changed = True
elif self.parameters.labels_state == 'merge':
node_spec['Labels'] = dict(node_info['Spec']['Labels'] or {})
elif self.parameters.labels_state == "merge":
node_spec["Labels"] = dict(node_info["Spec"]["Labels"] or {})
if self.parameters.labels is not None:
for key, value in self.parameters.labels.items():
if node_spec['Labels'].get(key) != value:
node_spec['Labels'][key] = value
if node_spec["Labels"].get(key) != value:
node_spec["Labels"][key] = value
changed = True
if self.parameters.labels_to_remove is not None:
for key in self.parameters.labels_to_remove:
if self.parameters.labels is not None:
if not self.parameters.labels.get(key):
if node_spec['Labels'].get(key):
node_spec['Labels'].pop(key)
if node_spec["Labels"].get(key):
node_spec["Labels"].pop(key)
changed = True
else:
self.client.module.warn(
f"Label '{to_native(key)}' listed both in 'labels' and 'labels_to_remove'. "
"Keeping the assigned label value.")
"Keeping the assigned label value."
)
else:
if node_spec['Labels'].get(key):
node_spec['Labels'].pop(key)
if node_spec["Labels"].get(key):
node_spec["Labels"].pop(key)
changed = True
if changed is True:
if not self.check_mode:
try:
self.client.update_node(node_id=node_info['ID'], version=node_info['Version']['Index'],
node_spec=node_spec)
self.client.update_node(
node_id=node_info["ID"],
version=node_info["Version"]["Index"],
node_spec=node_spec,
)
except APIError as exc:
self.client.fail(f"Failed to update node : {exc}")
self.results['node'] = self.client.get_node_inspect(node_id=node_info['ID'])
self.results['changed'] = changed
self.results["node"] = self.client.get_node_inspect(node_id=node_info["ID"])
self.results["changed"] = changed
else:
self.results['node'] = node_info
self.results['changed'] = changed
self.results["node"] = node_info
self.results["changed"] = changed
def main():
argument_spec = dict(
hostname=dict(type='str', required=True),
labels=dict(type='dict'),
labels_state=dict(type='str', default='merge', choices=['merge', 'replace']),
labels_to_remove=dict(type='list', elements='str'),
availability=dict(type='str', choices=['active', 'pause', 'drain']),
role=dict(type='str', choices=['worker', 'manager']),
hostname=dict(type="str", required=True),
labels=dict(type="dict"),
labels_state=dict(type="str", default="merge", choices=["merge", "replace"]),
labels_to_remove=dict(type="list", elements="str"),
availability=dict(type="str", choices=["active", "pause", "drain"]),
role=dict(type="str", choices=["worker", "manager"]),
)
client = AnsibleDockerSwarmClient(
argument_spec=argument_spec,
supports_check_mode=True,
min_docker_version='2.4.0',
min_docker_version="2.4.0",
)
try:
@@ -292,12 +300,16 @@ def main():
SwarmNodeManager(client, results)
client.module.exit_json(**results)
except DockerException as e:
client.fail(f'An unexpected Docker error occurred: {e}', exception=traceback.format_exc())
client.fail(
f"An unexpected Docker error occurred: {e}",
exception=traceback.format_exc(),
)
except RequestException as e:
client.fail(
f'An unexpected requests error occurred when Docker SDK for Python tried to talk to the docker daemon: {e}',
exception=traceback.format_exc())
f"An unexpected requests error occurred when Docker SDK for Python tried to talk to the docker daemon: {e}",
exception=traceback.format_exc(),
)
if __name__ == '__main__':
if __name__ == "__main__":
main()
+21 -12
View File
@@ -91,7 +91,10 @@ import traceback
from ansible_collections.community.docker.plugins.module_utils.common import (
RequestException,
)
from ansible_collections.community.docker.plugins.module_utils.swarm import AnsibleDockerSwarmClient
from ansible_collections.community.docker.plugins.module_utils.swarm import (
AnsibleDockerSwarmClient,
)
try:
from docker.errors import DockerException
@@ -104,22 +107,24 @@ def get_node_facts(client):
results = []
if client.module.params['self'] is True:
if client.module.params["self"] is True:
self_node_id = client.get_swarm_node_id()
node_info = client.get_node_inspect(node_id=self_node_id)
results.append(node_info)
return results
if client.module.params['name'] is None:
if client.module.params["name"] is None:
node_info = client.get_all_nodes_inspect()
return node_info
nodes = client.module.params['name']
nodes = client.module.params["name"]
if not isinstance(nodes, list):
nodes = [nodes]
for next_node_name in nodes:
next_node_info = client.get_node_inspect(node_id=next_node_name, skip_missing=True)
next_node_info = client.get_node_inspect(
node_id=next_node_name, skip_missing=True
)
if next_node_info:
results.append(next_node_info)
return results
@@ -127,14 +132,14 @@ def get_node_facts(client):
def main():
argument_spec = dict(
name=dict(type='list', elements='str'),
self=dict(type='bool', default=False),
name=dict(type="list", elements="str"),
self=dict(type="bool", default=False),
)
client = AnsibleDockerSwarmClient(
argument_spec=argument_spec,
supports_check_mode=True,
min_docker_version='2.4.0',
min_docker_version="2.4.0",
)
client.fail_task_if_not_swarm_manager()
@@ -147,12 +152,16 @@ def main():
nodes=nodes,
)
except DockerException as e:
client.fail(f'An unexpected docker error occurred: {e}', exception=traceback.format_exc())
client.fail(
f"An unexpected docker error occurred: {e}",
exception=traceback.format_exc(),
)
except RequestException as e:
client.fail(
f'An unexpected requests error occurred when Docker SDK for Python tried to talk to the docker daemon: {e}',
exception=traceback.format_exc())
f"An unexpected requests error occurred when Docker SDK for Python tried to talk to the docker daemon: {e}",
exception=traceback.format_exc(),
)
if __name__ == '__main__':
if __name__ == "__main__":
main()
+114 -57
View File
@@ -131,20 +131,21 @@ actions:
import traceback
from ansible.module_utils.common.text.converters import to_native
from ansible_collections.community.docker.plugins.module_utils._api import auth
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 (
AnsibleDockerClient,
RequestException
RequestException,
)
from ansible_collections.community.docker.plugins.module_utils.util import (
DockerBaseClass,
DifferenceTracker,
DockerBaseClass,
)
from ansible_collections.community.docker.plugins.module_utils._api import auth
from ansible_collections.community.docker.plugins.module_utils._api.errors import APIError, DockerException, NotFound
class TaskParameters(DockerBaseClass):
def __init__(self, client):
@@ -162,11 +163,15 @@ class TaskParameters(DockerBaseClass):
def prepare_options(options):
return [f'{k}={v if v is not None else ""}' for k, v in options.items()] if options else []
return (
[f'{k}={v if v is not None else ""}' for k, v in options.items()]
if options
else []
)
def parse_options(options_list):
return dict(x.split('=', 1) for x in options_list) if options_list else {}
return dict(x.split("=", 1) for x in options_list) if options_list else {}
class DockerPluginManager(object):
@@ -187,23 +192,25 @@ class DockerPluginManager(object):
self.existing_plugin = self.get_existing_plugin()
state = self.parameters.state
if state == 'present':
if state == "present":
self.present()
elif state == 'absent':
elif state == "absent":
self.absent()
elif state == 'enable':
elif state == "enable":
self.enable()
elif state == 'disable':
elif state == "disable":
self.disable()
if self.diff or self.check_mode or self.parameters.debug:
if self.diff:
self.diff_result['before'], self.diff_result['after'] = self.diff_tracker.get_before_after()
self.diff_result["before"], self.diff_result["after"] = (
self.diff_tracker.get_before_after()
)
self.diff = self.diff_result
def get_existing_plugin(self):
try:
return self.client.get_json('/plugins/{0}/json', self.preferred_name)
return self.client.get_json("/plugins/{0}/json", self.preferred_name)
except NotFound:
return None
except APIError as e:
@@ -217,19 +224,27 @@ class DockerPluginManager(object):
"""
differences = DifferenceTracker()
if self.parameters.plugin_options:
settings = self.existing_plugin.get('Settings')
settings = self.existing_plugin.get("Settings")
if not settings:
differences.add('plugin_options', parameters=self.parameters.plugin_options, active=settings)
differences.add(
"plugin_options",
parameters=self.parameters.plugin_options,
active=settings,
)
else:
existing_options = parse_options(settings.get('Env'))
existing_options = parse_options(settings.get("Env"))
for key, value in self.parameters.plugin_options.items():
if ((not existing_options.get(key) and value) or
not value or
value != existing_options[key]):
differences.add(f'plugin_options.{key}',
parameter=value,
active=existing_options.get(key))
if (
(not existing_options.get(key) and value)
or not value
or value != existing_options[key]
):
differences.add(
f"plugin_options.{key}",
parameter=value,
active=existing_options.get(key),
)
return differences
@@ -239,26 +254,42 @@ class DockerPluginManager(object):
try:
# Get privileges
headers = {}
registry, repo_name = auth.resolve_repository_name(self.parameters.plugin_name)
registry, repo_name = auth.resolve_repository_name(
self.parameters.plugin_name
)
header = auth.get_config_header(self.client, registry)
if header:
headers['X-Registry-Auth'] = header
privileges = self.client.get_json('/plugins/privileges', params={'remote': self.parameters.plugin_name}, headers=headers)
headers["X-Registry-Auth"] = header
privileges = self.client.get_json(
"/plugins/privileges",
params={"remote": self.parameters.plugin_name},
headers=headers,
)
# Pull plugin
params = {
'remote': self.parameters.plugin_name,
"remote": self.parameters.plugin_name,
}
if self.parameters.alias:
params['name'] = self.parameters.alias
response = self.client._post_json(self.client._url('/plugins/pull'), params=params, headers=headers, data=privileges, stream=True)
params["name"] = self.parameters.alias
response = self.client._post_json(
self.client._url("/plugins/pull"),
params=params,
headers=headers,
data=privileges,
stream=True,
)
self.client._raise_for_status(response)
for data in self.client._stream_helper(response, decode=True):
pass
# Inspect and configure plugin
self.existing_plugin = self.client.get_json('/plugins/{0}/json', self.preferred_name)
self.existing_plugin = self.client.get_json(
"/plugins/{0}/json", self.preferred_name
)
if self.parameters.plugin_options:
data = prepare_options(self.parameters.plugin_options)
self.client.post_json('/plugins/{0}/set', self.preferred_name, data=data)
self.client.post_json(
"/plugins/{0}/set", self.preferred_name, data=data
)
except APIError as e:
self.client.fail(to_native(e))
@@ -270,7 +301,9 @@ class DockerPluginManager(object):
if self.existing_plugin:
if not self.check_mode:
try:
self.client.delete_call('/plugins/{0}', self.preferred_name, params={'force': force})
self.client.delete_call(
"/plugins/{0}", self.preferred_name, params={"force": force}
)
except APIError as e:
self.client.fail(to_native(e))
@@ -284,7 +317,9 @@ class DockerPluginManager(object):
if not self.check_mode:
try:
data = prepare_options(self.parameters.plugin_options)
self.client.post_json('/plugins/{0}/set', self.preferred_name, data=data)
self.client.post_json(
"/plugins/{0}/set", self.preferred_name, data=data
)
except APIError as e:
self.client.fail(to_native(e))
self.actions.append(f"Updated plugin {self.preferred_name} settings")
@@ -297,7 +332,9 @@ class DockerPluginManager(object):
if self.existing_plugin:
differences = self.has_different_config()
self.diff_tracker.add('exists', parameter=True, active=self.existing_plugin is not None)
self.diff_tracker.add(
"exists", parameter=True, active=self.existing_plugin is not None
)
if self.existing_plugin:
self.update_plugin()
@@ -316,10 +353,14 @@ class DockerPluginManager(object):
def enable(self):
timeout = self.parameters.enable_timeout
if self.existing_plugin:
if not self.existing_plugin.get('Enabled'):
if not self.existing_plugin.get("Enabled"):
if not self.check_mode:
try:
self.client.post_json('/plugins/{0}/enable', self.preferred_name, params={'timeout': timeout})
self.client.post_json(
"/plugins/{0}/enable",
self.preferred_name,
params={"timeout": timeout},
)
except APIError as e:
self.client.fail(to_native(e))
self.actions.append(f"Enabled plugin {self.preferred_name}")
@@ -328,7 +369,11 @@ class DockerPluginManager(object):
self.install_plugin()
if not self.check_mode:
try:
self.client.post_json('/plugins/{0}/enable', self.preferred_name, params={'timeout': timeout})
self.client.post_json(
"/plugins/{0}/enable",
self.preferred_name,
params={"timeout": timeout},
)
except APIError as e:
self.client.fail(to_native(e))
self.actions.append(f"Enabled plugin {self.preferred_name}")
@@ -336,10 +381,12 @@ class DockerPluginManager(object):
def disable(self):
if self.existing_plugin:
if self.existing_plugin.get('Enabled'):
if self.existing_plugin.get("Enabled"):
if not self.check_mode:
try:
self.client.post_json('/plugins/{0}/disable', self.preferred_name)
self.client.post_json(
"/plugins/{0}/disable", self.preferred_name
)
except APIError as e:
self.client.fail(to_native(e))
self.actions.append(f"Disable plugin {self.preferred_name}")
@@ -350,30 +397,36 @@ class DockerPluginManager(object):
@property
def result(self):
plugin_data = {}
if self.parameters.state != 'absent':
if self.parameters.state != "absent":
try:
plugin_data = self.client.get_json('/plugins/{0}/json', self.preferred_name)
plugin_data = self.client.get_json(
"/plugins/{0}/json", self.preferred_name
)
except NotFound:
# This can happen in check mode
pass
result = {
'actions': self.actions,
'changed': self.changed,
'diff': self.diff,
'plugin': plugin_data,
"actions": self.actions,
"changed": self.changed,
"diff": self.diff,
"plugin": plugin_data,
}
return dict((k, v) for k, v in result.items() if v is not None)
def main():
argument_spec = dict(
alias=dict(type='str'),
plugin_name=dict(type='str', required=True),
state=dict(type='str', default='present', choices=['present', 'absent', 'enable', 'disable']),
plugin_options=dict(type='dict', default={}),
debug=dict(type='bool', default=False),
force_remove=dict(type='bool', default=False),
enable_timeout=dict(type='int', default=0),
alias=dict(type="str"),
plugin_name=dict(type="str", required=True),
state=dict(
type="str",
default="present",
choices=["present", "absent", "enable", "disable"],
),
plugin_options=dict(type="dict", default={}),
debug=dict(type="bool", default=False),
force_remove=dict(type="bool", default=False),
enable_timeout=dict(type="int", default=0),
)
client = AnsibleDockerClient(
argument_spec=argument_spec,
@@ -384,12 +437,16 @@ def main():
cm = DockerPluginManager(client)
client.module.exit_json(**cm.result)
except DockerException as e:
client.fail(f'An unexpected docker error occurred: {e}', exception=traceback.format_exc())
client.fail(
f"An unexpected docker error occurred: {e}",
exception=traceback.format_exc(),
)
except RequestException as e:
client.fail(
f'An unexpected requests error occurred when trying to talk to the Docker daemon: {e}',
exception=traceback.format_exc())
f"An unexpected requests error occurred when trying to talk to the Docker daemon: {e}",
exception=traceback.format_exc(),
)
if __name__ == '__main__':
if __name__ == "__main__":
main()
+90 -69
View File
@@ -231,117 +231,138 @@ builder_cache_caches_deleted:
import traceback
from ansible.module_utils.common.text.formatters import human_to_bytes
from ansible_collections.community.docker.plugins.module_utils._api.errors import (
DockerException,
)
from ansible_collections.community.docker.plugins.module_utils._api.utils.utils import (
convert_filters,
)
from ansible_collections.community.docker.plugins.module_utils.common_api import (
AnsibleDockerClient,
RequestException,
)
from ansible_collections.community.docker.plugins.module_utils.util import clean_dict_booleans_for_docker_api
from ansible_collections.community.docker.plugins.module_utils._api.errors import DockerException
from ansible_collections.community.docker.plugins.module_utils._api.utils.utils import convert_filters
from ansible_collections.community.docker.plugins.module_utils.util import (
clean_dict_booleans_for_docker_api,
)
def main():
argument_spec = dict(
containers=dict(type='bool', default=False),
containers_filters=dict(type='dict'),
images=dict(type='bool', default=False),
images_filters=dict(type='dict'),
networks=dict(type='bool', default=False),
networks_filters=dict(type='dict'),
volumes=dict(type='bool', default=False),
volumes_filters=dict(type='dict'),
builder_cache=dict(type='bool', default=False),
builder_cache_all=dict(type='bool', default=False),
builder_cache_filters=dict(type='dict'),
builder_cache_keep_storage=dict(type='str'), # convert to bytes
containers=dict(type="bool", default=False),
containers_filters=dict(type="dict"),
images=dict(type="bool", default=False),
images_filters=dict(type="dict"),
networks=dict(type="bool", default=False),
networks_filters=dict(type="dict"),
volumes=dict(type="bool", default=False),
volumes_filters=dict(type="dict"),
builder_cache=dict(type="bool", default=False),
builder_cache_all=dict(type="bool", default=False),
builder_cache_filters=dict(type="dict"),
builder_cache_keep_storage=dict(type="str"), # convert to bytes
)
client = AnsibleDockerClient(
argument_spec=argument_spec,
option_minimal_versions=dict(
builder_cache=dict(docker_py_version='1.31'),
builder_cache_all=dict(docker_py_version='1.39'),
builder_cache_filters=dict(docker_py_version='1.31'),
builder_cache_keep_storage=dict(docker_py_version='1.39'),
builder_cache=dict(docker_py_version="1.31"),
builder_cache_all=dict(docker_py_version="1.39"),
builder_cache_filters=dict(docker_py_version="1.31"),
builder_cache_keep_storage=dict(docker_py_version="1.39"),
),
# supports_check_mode=True,
)
builder_cache_keep_storage = None
if client.module.params.get('builder_cache_keep_storage') is not None:
if client.module.params.get("builder_cache_keep_storage") is not None:
try:
builder_cache_keep_storage = human_to_bytes(client.module.params.get('builder_cache_keep_storage'))
builder_cache_keep_storage = human_to_bytes(
client.module.params.get("builder_cache_keep_storage")
)
except ValueError as exc:
client.module.fail_json(msg=f'Error while parsing value of builder_cache_keep_storage: {exc}')
client.module.fail_json(
msg=f"Error while parsing value of builder_cache_keep_storage: {exc}"
)
try:
result = dict()
changed = False
if client.module.params['containers']:
filters = clean_dict_booleans_for_docker_api(client.module.params.get('containers_filters'), allow_sequences=True)
params = {'filters': convert_filters(filters)}
res = client.post_to_json('/containers/prune', params=params)
result['containers'] = res.get('ContainersDeleted') or []
result['containers_space_reclaimed'] = res['SpaceReclaimed']
if result['containers'] or result['containers_space_reclaimed']:
if client.module.params["containers"]:
filters = clean_dict_booleans_for_docker_api(
client.module.params.get("containers_filters"), allow_sequences=True
)
params = {"filters": convert_filters(filters)}
res = client.post_to_json("/containers/prune", params=params)
result["containers"] = res.get("ContainersDeleted") or []
result["containers_space_reclaimed"] = res["SpaceReclaimed"]
if result["containers"] or result["containers_space_reclaimed"]:
changed = True
if client.module.params['images']:
filters = clean_dict_booleans_for_docker_api(client.module.params.get('images_filters'), allow_sequences=True)
params = {'filters': convert_filters(filters)}
res = client.post_to_json('/images/prune', params=params)
result['images'] = res.get('ImagesDeleted') or []
result['images_space_reclaimed'] = res['SpaceReclaimed']
if result['images'] or result['images_space_reclaimed']:
if client.module.params["images"]:
filters = clean_dict_booleans_for_docker_api(
client.module.params.get("images_filters"), allow_sequences=True
)
params = {"filters": convert_filters(filters)}
res = client.post_to_json("/images/prune", params=params)
result["images"] = res.get("ImagesDeleted") or []
result["images_space_reclaimed"] = res["SpaceReclaimed"]
if result["images"] or result["images_space_reclaimed"]:
changed = True
if client.module.params['networks']:
filters = clean_dict_booleans_for_docker_api(client.module.params.get('networks_filters'), allow_sequences=True)
params = {'filters': convert_filters(filters)}
res = client.post_to_json('/networks/prune', params=params)
result['networks'] = res.get('NetworksDeleted') or []
if result['networks']:
if client.module.params["networks"]:
filters = clean_dict_booleans_for_docker_api(
client.module.params.get("networks_filters"), allow_sequences=True
)
params = {"filters": convert_filters(filters)}
res = client.post_to_json("/networks/prune", params=params)
result["networks"] = res.get("NetworksDeleted") or []
if result["networks"]:
changed = True
if client.module.params['volumes']:
filters = clean_dict_booleans_for_docker_api(client.module.params.get('volumes_filters'), allow_sequences=True)
params = {'filters': convert_filters(filters)}
res = client.post_to_json('/volumes/prune', params=params)
result['volumes'] = res.get('VolumesDeleted') or []
result['volumes_space_reclaimed'] = res['SpaceReclaimed']
if result['volumes'] or result['volumes_space_reclaimed']:
if client.module.params["volumes"]:
filters = clean_dict_booleans_for_docker_api(
client.module.params.get("volumes_filters"), allow_sequences=True
)
params = {"filters": convert_filters(filters)}
res = client.post_to_json("/volumes/prune", params=params)
result["volumes"] = res.get("VolumesDeleted") or []
result["volumes_space_reclaimed"] = res["SpaceReclaimed"]
if result["volumes"] or result["volumes_space_reclaimed"]:
changed = True
if client.module.params['builder_cache']:
filters = clean_dict_booleans_for_docker_api(client.module.params.get('builder_cache_filters'), allow_sequences=True)
params = {'filters': convert_filters(filters)}
if client.module.params.get('builder_cache_all'):
params['all'] = 'true'
if client.module.params["builder_cache"]:
filters = clean_dict_booleans_for_docker_api(
client.module.params.get("builder_cache_filters"), allow_sequences=True
)
params = {"filters": convert_filters(filters)}
if client.module.params.get("builder_cache_all"):
params["all"] = "true"
if builder_cache_keep_storage is not None:
params['keep-storage'] = builder_cache_keep_storage
res = client.post_to_json('/build/prune', params=params)
result['builder_cache_space_reclaimed'] = res['SpaceReclaimed']
if result['builder_cache_space_reclaimed']:
params["keep-storage"] = builder_cache_keep_storage
res = client.post_to_json("/build/prune", params=params)
result["builder_cache_space_reclaimed"] = res["SpaceReclaimed"]
if result["builder_cache_space_reclaimed"]:
changed = True
if 'CachesDeleted' in res:
if "CachesDeleted" in res:
# API version 1.39+: return value CachesDeleted (list of str)
result['builder_cache_caches_deleted'] = res['CachesDeleted']
if result['builder_cache_caches_deleted']:
result["builder_cache_caches_deleted"] = res["CachesDeleted"]
if result["builder_cache_caches_deleted"]:
changed = True
result['changed'] = changed
result["changed"] = changed
client.module.exit_json(**result)
except DockerException as e:
client.fail(f'An unexpected Docker error occurred: {e}', exception=traceback.format_exc())
client.fail(
f"An unexpected Docker error occurred: {e}",
exception=traceback.format_exc(),
)
except RequestException as e:
client.fail(
f'An unexpected requests error occurred when trying to talk to the Docker daemon: {e}',
exception=traceback.format_exc())
f"An unexpected requests error occurred when trying to talk to the Docker daemon: {e}",
exception=traceback.format_exc(),
)
if __name__ == '__main__':
if __name__ == "__main__":
main()
+75 -68
View File
@@ -191,12 +191,14 @@ import base64
import hashlib
import traceback
try:
from docker.errors import DockerException, APIError
from docker.errors import APIError, DockerException
except ImportError:
# missing Docker SDK for Python handled in ansible.module_utils.docker.common
pass
from ansible.module_utils.common.text.converters import to_bytes
from ansible_collections.community.docker.plugins.module_utils.common import (
AnsibleDockerClient,
RequestException,
@@ -206,7 +208,6 @@ from ansible_collections.community.docker.plugins.module_utils.util import (
compare_generic,
sanitize_labels,
)
from ansible.module_utils.common.text.converters import to_bytes
class SecretManager(DockerBaseClass):
@@ -220,25 +221,25 @@ class SecretManager(DockerBaseClass):
self.check_mode = self.client.check_mode
parameters = self.client.module.params
self.name = parameters.get('name')
self.state = parameters.get('state')
self.data = parameters.get('data')
self.name = parameters.get("name")
self.state = parameters.get("state")
self.data = parameters.get("data")
if self.data is not None:
if parameters.get('data_is_b64'):
if parameters.get("data_is_b64"):
self.data = base64.b64decode(self.data)
else:
self.data = to_bytes(self.data)
data_src = parameters.get('data_src')
data_src = parameters.get("data_src")
if data_src is not None:
try:
with open(data_src, 'rb') as f:
with open(data_src, "rb") as f:
self.data = f.read()
except Exception as exc:
self.client.fail(f'Error while reading {data_src}: {exc}')
self.labels = parameters.get('labels')
self.force = parameters.get('force')
self.rolling_versions = parameters.get('rolling_versions')
self.versions_to_keep = parameters.get('versions_to_keep')
self.client.fail(f"Error while reading {data_src}: {exc}")
self.labels = parameters.get("labels")
self.force = parameters.get("force")
self.rolling_versions = parameters.get("rolling_versions")
self.versions_to_keep = parameters.get("versions_to_keep")
if self.rolling_versions:
self.version = 0
@@ -247,16 +248,18 @@ class SecretManager(DockerBaseClass):
def __call__(self):
self.get_secret()
if self.state == 'present':
if self.state == "present":
self.data_key = hashlib.sha224(self.data).hexdigest()
self.present()
self.remove_old_versions()
elif self.state == 'absent':
elif self.state == "absent":
self.absent()
def get_version(self, secret):
try:
return int(secret.get('Spec', {}).get('Labels', {}).get('ansible_version', 0))
return int(
secret.get("Spec", {}).get("Labels", {}).get("ansible_version", 0)
)
except ValueError:
return 0
@@ -268,9 +271,9 @@ class SecretManager(DockerBaseClass):
self.remove_secret(self.secrets.pop(0))
def get_secret(self):
''' Find an existing secret. '''
"""Find an existing secret."""
try:
secrets = self.client.secrets(filters={'name': self.name})
secrets = self.client.secrets(filters={"name": self.name})
except APIError as exc:
self.client.fail(f"Error accessing secret {self.name}: {exc}")
@@ -278,62 +281,66 @@ class SecretManager(DockerBaseClass):
self.secrets = [
secret
for secret in secrets
if secret['Spec']['Name'].startswith(f'{self.name}_v')
if secret["Spec"]["Name"].startswith(f"{self.name}_v")
]
self.secrets.sort(key=self.get_version)
else:
self.secrets = [
secret for secret in secrets if secret['Spec']['Name'] == self.name
secret for secret in secrets if secret["Spec"]["Name"] == self.name
]
def create_secret(self):
''' Create a new secret '''
"""Create a new secret"""
secret_id = None
# We cannot see the data after creation, so adding a label we can use for idempotency check
labels = {
'ansible_key': self.data_key
}
labels = {"ansible_key": self.data_key}
if self.rolling_versions:
self.version += 1
labels['ansible_version'] = str(self.version)
self.name = f'{self.name}_v{self.version}'
labels["ansible_version"] = str(self.version)
self.name = f"{self.name}_v{self.version}"
if self.labels:
labels.update(self.labels)
try:
if not self.check_mode:
secret_id = self.client.create_secret(self.name, self.data, labels=labels)
self.secrets += self.client.secrets(filters={'id': secret_id})
secret_id = self.client.create_secret(
self.name, self.data, labels=labels
)
self.secrets += self.client.secrets(filters={"id": secret_id})
except APIError as exc:
self.client.fail(f"Error creating secret: {exc}")
if isinstance(secret_id, dict):
secret_id = secret_id['ID']
secret_id = secret_id["ID"]
return secret_id
def remove_secret(self, secret):
try:
if not self.check_mode:
self.client.remove_secret(secret['ID'])
self.client.remove_secret(secret["ID"])
except APIError as exc:
self.client.fail(f"Error removing secret {secret['Spec']['Name']}: {exc}")
def present(self):
''' Handles state == 'present', creating or updating the secret '''
"""Handles state == 'present', creating or updating the secret"""
if self.secrets:
secret = self.secrets[-1]
self.results['secret_id'] = secret['ID']
self.results['secret_name'] = secret['Spec']['Name']
self.results["secret_id"] = secret["ID"]
self.results["secret_name"] = secret["Spec"]["Name"]
data_changed = False
attrs = secret.get('Spec', {})
if attrs.get('Labels', {}).get('ansible_key'):
if attrs['Labels']['ansible_key'] != self.data_key:
attrs = secret.get("Spec", {})
if attrs.get("Labels", {}).get("ansible_key"):
if attrs["Labels"]["ansible_key"] != self.data_key:
data_changed = True
else:
if not self.force:
self.client.module.warn("'ansible_key' label not found. Secret will not be changed unless the force parameter is set to 'true'")
labels_changed = not compare_generic(self.labels, attrs.get('Labels'), 'allow_more_present', 'dict')
self.client.module.warn(
"'ansible_key' label not found. Secret will not be changed unless the force parameter is set to 'true'"
)
labels_changed = not compare_generic(
self.labels, attrs.get("Labels"), "allow_more_present", "dict"
)
if self.rolling_versions:
self.version = self.get_version(secret)
if data_changed or labels_changed or self.force:
@@ -341,41 +348,41 @@ class SecretManager(DockerBaseClass):
if not self.rolling_versions:
self.absent()
secret_id = self.create_secret()
self.results['changed'] = True
self.results['secret_id'] = secret_id
self.results['secret_name'] = self.name
self.results["changed"] = True
self.results["secret_id"] = secret_id
self.results["secret_name"] = self.name
else:
self.results['changed'] = True
self.results['secret_id'] = self.create_secret()
self.results['secret_name'] = self.name
self.results["changed"] = True
self.results["secret_id"] = self.create_secret()
self.results["secret_name"] = self.name
def absent(self):
''' Handles state == 'absent', removing the secret '''
"""Handles state == 'absent', removing the secret"""
if self.secrets:
for secret in self.secrets:
self.remove_secret(secret)
self.results['changed'] = True
self.results["changed"] = True
def main():
argument_spec = dict(
name=dict(type='str', required=True),
state=dict(type='str', default='present', choices=['absent', 'present']),
data=dict(type='str', no_log=True),
data_is_b64=dict(type='bool', default=False),
data_src=dict(type='path'),
labels=dict(type='dict'),
force=dict(type='bool', default=False),
rolling_versions=dict(type='bool', default=False),
versions_to_keep=dict(type='int', default=5),
name=dict(type="str", required=True),
state=dict(type="str", default="present", choices=["absent", "present"]),
data=dict(type="str", no_log=True),
data_is_b64=dict(type="bool", default=False),
data_src=dict(type="path"),
labels=dict(type="dict"),
force=dict(type="bool", default=False),
rolling_versions=dict(type="bool", default=False),
versions_to_keep=dict(type="int", default=5),
)
required_if = [
('state', 'present', ['data', 'data_src'], True),
("state", "present", ["data", "data_src"], True),
]
mutually_exclusive = [
('data', 'data_src'),
("data", "data_src"),
]
client = AnsibleDockerClient(
@@ -383,26 +390,26 @@ def main():
supports_check_mode=True,
required_if=required_if,
mutually_exclusive=mutually_exclusive,
min_docker_version='2.1.0',
min_docker_version="2.1.0",
)
sanitize_labels(client.module.params['labels'], 'labels', client)
sanitize_labels(client.module.params["labels"], "labels", client)
try:
results = dict(
changed=False,
secret_id='',
secret_name=''
)
results = dict(changed=False, secret_id="", secret_name="")
SecretManager(client, results)()
client.module.exit_json(**results)
except DockerException as e:
client.fail(f'An unexpected Docker error occurred: {e}', exception=traceback.format_exc())
client.fail(
f"An unexpected Docker error occurred: {e}",
exception=traceback.format_exc(),
)
except RequestException as e:
client.fail(
f'An unexpected requests error occurred when Docker SDK for Python tried to talk to the docker daemon: {e}',
exception=traceback.format_exc())
f"An unexpected requests error occurred when Docker SDK for Python tried to talk to the docker daemon: {e}",
exception=traceback.format_exc(),
)
if __name__ == '__main__':
if __name__ == "__main__":
main()
+50 -33
View File
@@ -159,34 +159,37 @@ import json
import os
import tempfile
import traceback
from time import sleep
from ansible.module_utils.common.text.converters import to_native
from ansible_collections.community.docker.plugins.module_utils.common_cli import (
AnsibleModuleDockerClient,
DockerException,
)
try:
from jsondiff import diff as json_diff
HAS_JSONDIFF = True
except ImportError:
HAS_JSONDIFF = False
try:
from yaml import dump as yaml_dump
HAS_YAML = True
except ImportError:
HAS_YAML = False
def docker_stack_services(client, stack_name):
rc, out, err = client.call_cli("stack", "services", stack_name, "--format", "{{.Name}}")
rc, out, err = client.call_cli(
"stack", "services", stack_name, "--format", "{{.Name}}"
)
if to_native(err) == f"Nothing found in stack: {stack_name}\n":
return []
return to_native(out).strip().split('\n')
return to_native(out).strip().split("\n")
def docker_service_inspect(client, service_name):
@@ -194,7 +197,7 @@ def docker_service_inspect(client, service_name):
if rc != 0:
return None
else:
ret = json.loads(out)[0]['Spec']
ret = json.loads(out)[0]["Spec"]
return ret
@@ -207,11 +210,9 @@ def docker_stack_deploy(client, stack_name, compose_files):
if client.module.params["with_registry_auth"]:
command += ["--with-registry-auth"]
if client.module.params["resolve_image"]:
command += ["--resolve-image",
client.module.params["resolve_image"]]
command += ["--resolve-image", client.module.params["resolve_image"]]
for compose_file in compose_files:
command += ["--compose-file",
compose_file]
command += ["--compose-file", compose_file]
command += [stack_name]
rc, out, err = client.call_cli(*command)
return rc, to_native(out), to_native(err)
@@ -240,15 +241,15 @@ def docker_stack_rm(client, stack_name, retries, interval):
def main():
client = AnsibleModuleDockerClient(
argument_spec={
'name': dict(type='str', required=True),
'compose': dict(type='list', elements='raw', default=[]),
'prune': dict(type='bool', default=False),
'detach': dict(type='bool', default=True),
'with_registry_auth': dict(type='bool', default=False),
'resolve_image': dict(type='str', choices=['always', 'changed', 'never']),
'state': dict(type='str', default='present', choices=['present', 'absent']),
'absent_retries': dict(type='int', default=0),
'absent_retries_interval': dict(type='int', default=1)
"name": dict(type="str", required=True),
"compose": dict(type="list", elements="raw", default=[]),
"prune": dict(type="bool", default=False),
"detach": dict(type="bool", default=True),
"with_registry_auth": dict(type="bool", default=False),
"resolve_image": dict(type="str", choices=["always", "changed", "never"]),
"state": dict(type="str", default="present", choices=["present", "absent"]),
"absent_retries": dict(type="int", default=0),
"absent_retries_interval": dict(type="int", default=1),
},
supports_check_mode=False,
)
@@ -260,28 +261,32 @@ def main():
return client.fail("yaml is not installed, try 'pip install pyyaml'")
try:
state = client.module.params['state']
compose = client.module.params['compose']
name = client.module.params['name']
absent_retries = client.module.params['absent_retries']
absent_retries_interval = client.module.params['absent_retries_interval']
state = client.module.params["state"]
compose = client.module.params["compose"]
name = client.module.params["name"]
absent_retries = client.module.params["absent_retries"]
absent_retries_interval = client.module.params["absent_retries_interval"]
if state == 'present':
if state == "present":
if not compose:
client.fail("compose parameter must be a list containing at least one element")
client.fail(
"compose parameter must be a list containing at least one element"
)
compose_files = []
for i, compose_def in enumerate(compose):
if isinstance(compose_def, dict):
compose_file_fd, compose_file = tempfile.mkstemp()
client.module.add_cleanup_file(compose_file)
with os.fdopen(compose_file_fd, 'w') as stack_file:
with os.fdopen(compose_file_fd, "w") as stack_file:
compose_files.append(compose_file)
stack_file.write(yaml_dump(compose_def))
elif isinstance(compose_def, str):
compose_files.append(compose_def)
else:
client.fail(f"compose element '{compose_def}' must be a string or a dictionary")
client.fail(
f"compose element '{compose_def}' must be a string or a dictionary"
)
before_stack_services = docker_stack_inspect(client, name)
@@ -290,13 +295,20 @@ def main():
after_stack_services = docker_stack_inspect(client, name)
if rc != 0:
client.fail("docker stack up deploy command failed", rc=rc, stdout=out, stderr=err)
client.fail(
"docker stack up deploy command failed",
rc=rc,
stdout=out,
stderr=err,
)
before_after_differences = json_diff(before_stack_services, after_stack_services)
before_after_differences = json_diff(
before_stack_services, after_stack_services
)
for k in before_after_differences.keys():
if isinstance(before_after_differences[k], dict):
before_after_differences[k].pop('UpdatedAt', None)
before_after_differences[k].pop('Version', None)
before_after_differences[k].pop("UpdatedAt", None)
before_after_differences[k].pop("Version", None)
if not list(before_after_differences[k].keys()):
before_after_differences.pop(k)
@@ -322,7 +334,9 @@ def main():
else:
if docker_stack_services(client, name):
rc, out, err = docker_stack_rm(client, name, absent_retries, absent_retries_interval)
rc, out, err = docker_stack_rm(
client, name, absent_retries, absent_retries_interval
)
if rc != 0:
client.module.fail_json(
msg="'docker stack down' command failed",
@@ -340,7 +354,10 @@ def main():
)
client.module.exit_json(changed=False)
except DockerException as e:
client.fail(f'An unexpected Docker error occurred: {e}', exception=traceback.format_exc())
client.fail(
f"An unexpected Docker error occurred: {e}",
exception=traceback.format_exc(),
)
if __name__ == "__main__":
+13 -8
View File
@@ -7,6 +7,7 @@
from __future__ import annotations
DOCUMENTATION = r"""
module: docker_stack_info
author: "Jose Angel Munoz (@imjoseangel)"
@@ -78,7 +79,6 @@ import json
import traceback
from ansible.module_utils.common.text.converters import to_native
from ansible_collections.community.docker.plugins.module_utils.common_cli import (
AnsibleModuleDockerClient,
DockerException,
@@ -86,31 +86,36 @@ from ansible_collections.community.docker.plugins.module_utils.common_cli import
def docker_stack_list(module):
docker_bin = module.get_bin_path('docker', required=True)
docker_bin = module.get_bin_path("docker", required=True)
rc, out, err = module.run_command(
[docker_bin, "stack", "ls", "--format={{json .}}"])
[docker_bin, "stack", "ls", "--format={{json .}}"]
)
return rc, out.strip(), err.strip()
def main():
client = AnsibleModuleDockerClient(
argument_spec={
},
argument_spec={},
supports_check_mode=True,
)
try:
rc, ret, stderr = client.call_cli_json_stream('stack', 'ls', '--format={{json .}}', check_rc=True)
rc, ret, stderr = client.call_cli_json_stream(
"stack", "ls", "--format={{json .}}", check_rc=True
)
client.module.exit_json(
changed=False,
rc=rc,
stdout='\n'.join([json.dumps(entry) for entry in ret]),
stdout="\n".join([json.dumps(entry) for entry in ret]),
stderr=to_native(stderr).strip(),
results=ret,
)
except DockerException as e:
client.fail(f'An unexpected Docker error occurred: {e}', exception=traceback.format_exc())
client.fail(
f"An unexpected Docker error occurred: {e}",
exception=traceback.format_exc(),
)
if __name__ == "__main__":
+14 -10
View File
@@ -7,6 +7,7 @@
from __future__ import annotations
DOCUMENTATION = r"""
module: docker_stack_task_info
author: "Jose Angel Munoz (@imjoseangel)"
@@ -86,7 +87,6 @@ import json
import traceback
from ansible.module_utils.common.text.converters import to_native
from ansible_collections.community.docker.plugins.module_utils.common_cli import (
AnsibleModuleDockerClient,
DockerException,
@@ -94,33 +94,37 @@ from ansible_collections.community.docker.plugins.module_utils.common_cli import
def docker_stack_task(module, stack_name):
docker_bin = module.get_bin_path('docker', required=True)
docker_bin = module.get_bin_path("docker", required=True)
rc, out, err = module.run_command(
[docker_bin, "stack", "ps", stack_name, "--format={{json .}}"])
[docker_bin, "stack", "ps", stack_name, "--format={{json .}}"]
)
return rc, out.strip(), err.strip()
def main():
client = AnsibleModuleDockerClient(
argument_spec={
'name': dict(type='str', required=True)
},
argument_spec={"name": dict(type="str", required=True)},
supports_check_mode=True,
)
try:
name = client.module.params['name']
rc, ret, stderr = client.call_cli_json_stream('stack', 'ps', name, '--format={{json .}}', check_rc=True)
name = client.module.params["name"]
rc, ret, stderr = client.call_cli_json_stream(
"stack", "ps", name, "--format={{json .}}", check_rc=True
)
client.module.exit_json(
changed=False,
rc=rc,
stdout='\n'.join([json.dumps(entry) for entry in ret]),
stdout="\n".join([json.dumps(entry) for entry in ret]),
stderr=to_native(stderr).strip(),
results=ret,
)
except DockerException as e:
client.fail(f'An unexpected Docker error occurred: {e}', exception=traceback.format_exc())
client.fail(
f"An unexpected Docker error occurred: {e}",
exception=traceback.format_exc(),
)
if __name__ == "__main__":
+179 -149
View File
@@ -6,6 +6,7 @@
from __future__ import annotations
DOCUMENTATION = r"""
module: docker_swarm
short_description: Manage Swarm cluster
@@ -292,8 +293,9 @@ actions:
import json
import traceback
try:
from docker.errors import DockerException, APIError
from docker.errors import APIError, DockerException
except ImportError:
# missing Docker SDK for Python handled in ansible.module_utils.docker.common
pass
@@ -302,13 +304,14 @@ from ansible_collections.community.docker.plugins.module_utils.common import (
DockerBaseClass,
RequestException,
)
from ansible_collections.community.docker.plugins.module_utils.swarm import (
AnsibleDockerSwarmClient,
)
from ansible_collections.community.docker.plugins.module_utils.util import (
DifferenceTracker,
sanitize_labels,
)
from ansible_collections.community.docker.plugins.module_utils.swarm import AnsibleDockerSwarmClient
class TaskParameters(DockerBaseClass):
def __init__(self):
@@ -353,68 +356,70 @@ class TaskParameters(DockerBaseClass):
return result
def update_from_swarm_info(self, swarm_info):
spec = swarm_info['Spec']
spec = swarm_info["Spec"]
ca_config = spec.get('CAConfig') or dict()
ca_config = spec.get("CAConfig") or dict()
if self.node_cert_expiry is None:
self.node_cert_expiry = ca_config.get('NodeCertExpiry')
self.node_cert_expiry = ca_config.get("NodeCertExpiry")
if self.ca_force_rotate is None:
self.ca_force_rotate = ca_config.get('ForceRotate')
self.ca_force_rotate = ca_config.get("ForceRotate")
dispatcher = spec.get('Dispatcher') or dict()
dispatcher = spec.get("Dispatcher") or dict()
if self.dispatcher_heartbeat_period is None:
self.dispatcher_heartbeat_period = dispatcher.get('HeartbeatPeriod')
self.dispatcher_heartbeat_period = dispatcher.get("HeartbeatPeriod")
raft = spec.get('Raft') or dict()
raft = spec.get("Raft") or dict()
if self.snapshot_interval is None:
self.snapshot_interval = raft.get('SnapshotInterval')
self.snapshot_interval = raft.get("SnapshotInterval")
if self.keep_old_snapshots is None:
self.keep_old_snapshots = raft.get('KeepOldSnapshots')
self.keep_old_snapshots = raft.get("KeepOldSnapshots")
if self.heartbeat_tick is None:
self.heartbeat_tick = raft.get('HeartbeatTick')
self.heartbeat_tick = raft.get("HeartbeatTick")
if self.log_entries_for_slow_followers is None:
self.log_entries_for_slow_followers = raft.get('LogEntriesForSlowFollowers')
self.log_entries_for_slow_followers = raft.get("LogEntriesForSlowFollowers")
if self.election_tick is None:
self.election_tick = raft.get('ElectionTick')
self.election_tick = raft.get("ElectionTick")
orchestration = spec.get('Orchestration') or dict()
orchestration = spec.get("Orchestration") or dict()
if self.task_history_retention_limit is None:
self.task_history_retention_limit = orchestration.get('TaskHistoryRetentionLimit')
self.task_history_retention_limit = orchestration.get(
"TaskHistoryRetentionLimit"
)
encryption_config = spec.get('EncryptionConfig') or dict()
encryption_config = spec.get("EncryptionConfig") or dict()
if self.autolock_managers is None:
self.autolock_managers = encryption_config.get('AutoLockManagers')
self.autolock_managers = encryption_config.get("AutoLockManagers")
if self.name is None:
self.name = spec['Name']
self.name = spec["Name"]
if self.labels is None:
self.labels = spec.get('Labels') or {}
self.labels = spec.get("Labels") or {}
if 'LogDriver' in spec['TaskDefaults']:
self.log_driver = spec['TaskDefaults']['LogDriver']
if "LogDriver" in spec["TaskDefaults"]:
self.log_driver = spec["TaskDefaults"]["LogDriver"]
def update_parameters(self, client):
assign = dict(
snapshot_interval='snapshot_interval',
task_history_retention_limit='task_history_retention_limit',
keep_old_snapshots='keep_old_snapshots',
log_entries_for_slow_followers='log_entries_for_slow_followers',
heartbeat_tick='heartbeat_tick',
election_tick='election_tick',
dispatcher_heartbeat_period='dispatcher_heartbeat_period',
node_cert_expiry='node_cert_expiry',
name='name',
labels='labels',
signing_ca_cert='signing_ca_cert',
signing_ca_key='signing_ca_key',
ca_force_rotate='ca_force_rotate',
autolock_managers='autolock_managers',
log_driver='log_driver',
snapshot_interval="snapshot_interval",
task_history_retention_limit="task_history_retention_limit",
keep_old_snapshots="keep_old_snapshots",
log_entries_for_slow_followers="log_entries_for_slow_followers",
heartbeat_tick="heartbeat_tick",
election_tick="election_tick",
dispatcher_heartbeat_period="dispatcher_heartbeat_period",
node_cert_expiry="node_cert_expiry",
name="name",
labels="labels",
signing_ca_cert="signing_ca_cert",
signing_ca_key="signing_ca_key",
ca_force_rotate="ca_force_rotate",
autolock_managers="autolock_managers",
log_driver="log_driver",
)
params = dict()
for dest, source in assign.items():
if not client.option_minimal_versions[source]['supported']:
if not client.option_minimal_versions[source]["supported"]:
continue
value = getattr(self, source)
if value is not None:
@@ -423,12 +428,21 @@ class TaskParameters(DockerBaseClass):
def compare_to_active(self, other, client, differences):
for k in self.__dict__:
if k in ('advertise_addr', 'listen_addr', 'remote_addrs', 'join_token',
'rotate_worker_token', 'rotate_manager_token', 'spec',
'default_addr_pool', 'subnet_size', 'data_path_addr',
'data_path_port'):
if k in (
"advertise_addr",
"listen_addr",
"remote_addrs",
"join_token",
"rotate_worker_token",
"rotate_manager_token",
"spec",
"default_addr_pool",
"subnet_size",
"data_path_addr",
"data_path_port",
):
continue
if not client.option_minimal_versions[k]['supported']:
if not client.option_minimal_versions[k]["supported"]:
continue
value = getattr(self, k)
if value is None:
@@ -437,9 +451,9 @@ class TaskParameters(DockerBaseClass):
if value != other_value:
differences.add(k, parameter=value, active=other_value)
if self.rotate_worker_token:
differences.add('rotate_worker_token', parameter=True, active=False)
differences.add("rotate_worker_token", parameter=True, active=False)
if self.rotate_manager_token:
differences.add('rotate_manager_token', parameter=True, active=False)
differences.add("rotate_manager_token", parameter=True, active=False)
return differences
@@ -454,9 +468,9 @@ class SwarmManager(DockerBaseClass):
self.check_mode = self.client.check_mode
self.swarm_info = {}
self.state = client.module.params['state']
self.force = client.module.params['force']
self.node_id = client.module.params['node_id']
self.state = client.module.params["state"]
self.force = client.module.params["force"]
self.node_id = client.module.params["node_id"]
self.differences = DifferenceTracker()
self.parameters = TaskParameters.from_ansible_params(client)
@@ -475,8 +489,8 @@ class SwarmManager(DockerBaseClass):
if self.client.module._diff or self.parameters.debug:
diff = dict()
diff['before'], diff['after'] = self.differences.get_before_after()
self.results['diff'] = diff
diff["before"], diff["after"] = self.differences.get_before_after()
self.results["diff"] = diff
def inspect_swarm(self):
try:
@@ -484,8 +498,8 @@ class SwarmManager(DockerBaseClass):
json_str = json.dumps(data, ensure_ascii=False)
self.swarm_info = json.loads(json_str)
self.results['changed'] = False
self.results['swarm_facts'] = self.swarm_info
self.results["changed"] = False
self.results["swarm_facts"] = self.swarm_info
unlock_key = self.get_unlock_key()
self.swarm_info.update(unlock_key)
@@ -493,7 +507,7 @@ class SwarmManager(DockerBaseClass):
return
def get_unlock_key(self):
default = {'UnlockKey': None}
default = {"UnlockKey": None}
if not self.has_swarm_lock_changed():
return default
try:
@@ -503,7 +517,7 @@ class SwarmManager(DockerBaseClass):
def has_swarm_lock_changed(self):
return self.parameters.autolock_managers and (
self.created or self.differences.has_difference_for('autolock_managers')
self.created or self.differences.has_difference_for("autolock_managers")
)
def init_swarm(self):
@@ -513,19 +527,19 @@ class SwarmManager(DockerBaseClass):
if not self.check_mode:
init_arguments = {
'advertise_addr': self.parameters.advertise_addr,
'listen_addr': self.parameters.listen_addr,
'force_new_cluster': self.force,
'swarm_spec': self.parameters.spec,
"advertise_addr": self.parameters.advertise_addr,
"listen_addr": self.parameters.listen_addr,
"force_new_cluster": self.force,
"swarm_spec": self.parameters.spec,
}
if self.parameters.default_addr_pool is not None:
init_arguments['default_addr_pool'] = self.parameters.default_addr_pool
init_arguments["default_addr_pool"] = self.parameters.default_addr_pool
if self.parameters.subnet_size is not None:
init_arguments['subnet_size'] = self.parameters.subnet_size
init_arguments["subnet_size"] = self.parameters.subnet_size
if self.parameters.data_path_addr is not None:
init_arguments['data_path_addr'] = self.parameters.data_path_addr
init_arguments["data_path_addr"] = self.parameters.data_path_addr
if self.parameters.data_path_port is not None:
init_arguments['data_path_port'] = self.parameters.data_path_port
init_arguments["data_path_port"] = self.parameters.data_path_port
try:
self.client.init_swarm(**init_arguments)
except APIError as exc:
@@ -537,180 +551,196 @@ class SwarmManager(DockerBaseClass):
self.created = True
self.inspect_swarm()
self.results['actions'].append(f"New Swarm cluster created: {self.swarm_info.get('ID')}")
self.differences.add('state', parameter='present', active='absent')
self.results['changed'] = True
self.results['swarm_facts'] = {
'JoinTokens': self.swarm_info.get('JoinTokens'),
'UnlockKey': self.swarm_info.get('UnlockKey')
self.results["actions"].append(
f"New Swarm cluster created: {self.swarm_info.get('ID')}"
)
self.differences.add("state", parameter="present", active="absent")
self.results["changed"] = True
self.results["swarm_facts"] = {
"JoinTokens": self.swarm_info.get("JoinTokens"),
"UnlockKey": self.swarm_info.get("UnlockKey"),
}
def __update_swarm(self):
try:
self.inspect_swarm()
version = self.swarm_info['Version']['Index']
version = self.swarm_info["Version"]["Index"]
self.parameters.update_from_swarm_info(self.swarm_info)
old_parameters = TaskParameters()
old_parameters.update_from_swarm_info(self.swarm_info)
self.parameters.compare_to_active(old_parameters, self.client, self.differences)
self.parameters.compare_to_active(
old_parameters, self.client, self.differences
)
if self.differences.empty:
self.results['actions'].append("No modification")
self.results['changed'] = False
self.results["actions"].append("No modification")
self.results["changed"] = False
return
update_parameters = TaskParameters.from_ansible_params(self.client)
update_parameters.update_parameters(self.client)
if not self.check_mode:
self.client.update_swarm(
version=version, swarm_spec=update_parameters.spec,
version=version,
swarm_spec=update_parameters.spec,
rotate_worker_token=self.parameters.rotate_worker_token,
rotate_manager_token=self.parameters.rotate_manager_token)
rotate_manager_token=self.parameters.rotate_manager_token,
)
except APIError as exc:
self.client.fail(f"Can not update a Swarm Cluster: {exc}")
return
self.inspect_swarm()
self.results['actions'].append("Swarm cluster updated")
self.results['changed'] = True
self.results["actions"].append("Swarm cluster updated")
self.results["changed"] = True
def join(self):
if self.client.check_if_swarm_node():
self.results['actions'].append("This node is already part of a swarm.")
self.results["actions"].append("This node is already part of a swarm.")
return
if not self.check_mode:
join_arguments = {
'remote_addrs': self.parameters.remote_addrs,
'join_token': self.parameters.join_token,
'listen_addr': self.parameters.listen_addr,
'advertise_addr': self.parameters.advertise_addr,
"remote_addrs": self.parameters.remote_addrs,
"join_token": self.parameters.join_token,
"listen_addr": self.parameters.listen_addr,
"advertise_addr": self.parameters.advertise_addr,
}
if self.parameters.data_path_addr is not None:
join_arguments['data_path_addr'] = self.parameters.data_path_addr
join_arguments["data_path_addr"] = self.parameters.data_path_addr
try:
self.client.join_swarm(**join_arguments)
except APIError as exc:
self.client.fail(f"Can not join the Swarm Cluster: {exc}")
self.results['actions'].append("New node is added to swarm cluster")
self.differences.add('joined', parameter=True, active=False)
self.results['changed'] = True
self.results["actions"].append("New node is added to swarm cluster")
self.differences.add("joined", parameter=True, active=False)
self.results["changed"] = True
def leave(self):
if not self.client.check_if_swarm_node():
self.results['actions'].append("This node is not part of a swarm.")
self.results["actions"].append("This node is not part of a swarm.")
return
if not self.check_mode:
try:
self.client.leave_swarm(force=self.force)
except APIError as exc:
self.client.fail(f"This node can not leave the Swarm Cluster: {exc}")
self.results['actions'].append("Node has left the swarm cluster")
self.differences.add('joined', parameter='absent', active='present')
self.results['changed'] = True
self.results["actions"].append("Node has left the swarm cluster")
self.differences.add("joined", parameter="absent", active="present")
self.results["changed"] = True
def remove(self):
if not self.client.check_if_swarm_manager():
self.client.fail("This node is not a manager.")
try:
status_down = self.client.check_if_swarm_node_is_down(node_id=self.node_id, repeat_check=5)
status_down = self.client.check_if_swarm_node_is_down(
node_id=self.node_id, repeat_check=5
)
except APIError:
return
if not status_down:
self.client.fail("Can not remove the node. The status node is ready and not down.")
self.client.fail(
"Can not remove the node. The status node is ready and not down."
)
if not self.check_mode:
try:
self.client.remove_node(node_id=self.node_id, force=self.force)
except APIError as exc:
self.client.fail(f"Can not remove the node from the Swarm Cluster: {exc}")
self.results['actions'].append("Node is removed from swarm cluster.")
self.differences.add('joined', parameter=False, active=True)
self.results['changed'] = True
self.client.fail(
f"Can not remove the node from the Swarm Cluster: {exc}"
)
self.results["actions"].append("Node is removed from swarm cluster.")
self.differences.add("joined", parameter=False, active=True)
self.results["changed"] = True
def _detect_remove_operation(client):
return client.module.params['state'] == 'remove'
return client.module.params["state"] == "remove"
def main():
argument_spec = dict(
advertise_addr=dict(type='str'),
data_path_addr=dict(type='str'),
data_path_port=dict(type='int'),
state=dict(type='str', default='present', choices=['present', 'join', 'absent', 'remove']),
force=dict(type='bool', default=False),
listen_addr=dict(type='str', default='0.0.0.0:2377'),
remote_addrs=dict(type='list', elements='str'),
join_token=dict(type='str', no_log=True),
snapshot_interval=dict(type='int'),
task_history_retention_limit=dict(type='int'),
keep_old_snapshots=dict(type='int'),
log_entries_for_slow_followers=dict(type='int'),
heartbeat_tick=dict(type='int'),
election_tick=dict(type='int'),
dispatcher_heartbeat_period=dict(type='int'),
node_cert_expiry=dict(type='int'),
name=dict(type='str'),
labels=dict(type='dict'),
signing_ca_cert=dict(type='str'),
signing_ca_key=dict(type='str', no_log=True),
ca_force_rotate=dict(type='int'),
autolock_managers=dict(type='bool'),
node_id=dict(type='str'),
rotate_worker_token=dict(type='bool', default=False),
rotate_manager_token=dict(type='bool', default=False),
default_addr_pool=dict(type='list', elements='str'),
subnet_size=dict(type='int'),
advertise_addr=dict(type="str"),
data_path_addr=dict(type="str"),
data_path_port=dict(type="int"),
state=dict(
type="str",
default="present",
choices=["present", "join", "absent", "remove"],
),
force=dict(type="bool", default=False),
listen_addr=dict(type="str", default="0.0.0.0:2377"),
remote_addrs=dict(type="list", elements="str"),
join_token=dict(type="str", no_log=True),
snapshot_interval=dict(type="int"),
task_history_retention_limit=dict(type="int"),
keep_old_snapshots=dict(type="int"),
log_entries_for_slow_followers=dict(type="int"),
heartbeat_tick=dict(type="int"),
election_tick=dict(type="int"),
dispatcher_heartbeat_period=dict(type="int"),
node_cert_expiry=dict(type="int"),
name=dict(type="str"),
labels=dict(type="dict"),
signing_ca_cert=dict(type="str"),
signing_ca_key=dict(type="str", no_log=True),
ca_force_rotate=dict(type="int"),
autolock_managers=dict(type="bool"),
node_id=dict(type="str"),
rotate_worker_token=dict(type="bool", default=False),
rotate_manager_token=dict(type="bool", default=False),
default_addr_pool=dict(type="list", elements="str"),
subnet_size=dict(type="int"),
)
required_if = [
('state', 'join', ['remote_addrs', 'join_token']),
('state', 'remove', ['node_id'])
("state", "join", ["remote_addrs", "join_token"]),
("state", "remove", ["node_id"]),
]
option_minimal_versions = dict(
labels=dict(docker_py_version='2.6.0', docker_api_version='1.32'),
signing_ca_cert=dict(docker_py_version='2.6.0', docker_api_version='1.30'),
signing_ca_key=dict(docker_py_version='2.6.0', docker_api_version='1.30'),
ca_force_rotate=dict(docker_py_version='2.6.0', docker_api_version='1.30'),
autolock_managers=dict(docker_py_version='2.6.0'),
log_driver=dict(docker_py_version='2.6.0'),
labels=dict(docker_py_version="2.6.0", docker_api_version="1.32"),
signing_ca_cert=dict(docker_py_version="2.6.0", docker_api_version="1.30"),
signing_ca_key=dict(docker_py_version="2.6.0", docker_api_version="1.30"),
ca_force_rotate=dict(docker_py_version="2.6.0", docker_api_version="1.30"),
autolock_managers=dict(docker_py_version="2.6.0"),
log_driver=dict(docker_py_version="2.6.0"),
remove_operation=dict(
docker_py_version='2.4.0',
docker_py_version="2.4.0",
detect_usage=_detect_remove_operation,
usage_msg='remove swarm nodes'
usage_msg="remove swarm nodes",
),
default_addr_pool=dict(docker_py_version='4.0.0', docker_api_version='1.39'),
subnet_size=dict(docker_py_version='4.0.0', docker_api_version='1.39'),
data_path_addr=dict(docker_py_version='4.0.0', docker_api_version='1.30'),
data_path_port=dict(docker_py_version='6.0.0', docker_api_version='1.40'),
default_addr_pool=dict(docker_py_version="4.0.0", docker_api_version="1.39"),
subnet_size=dict(docker_py_version="4.0.0", docker_api_version="1.39"),
data_path_addr=dict(docker_py_version="4.0.0", docker_api_version="1.30"),
data_path_port=dict(docker_py_version="6.0.0", docker_api_version="1.40"),
)
client = AnsibleDockerSwarmClient(
argument_spec=argument_spec,
supports_check_mode=True,
required_if=required_if,
min_docker_version='1.10.0',
min_docker_version="1.10.0",
option_minimal_versions=option_minimal_versions,
)
sanitize_labels(client.module.params['labels'], 'labels', client)
sanitize_labels(client.module.params["labels"], "labels", client)
try:
results = dict(
changed=False,
result='',
actions=[]
)
results = dict(changed=False, result="", actions=[])
SwarmManager(client, results)()
client.module.exit_json(**results)
except DockerException as e:
client.fail(f'An unexpected Docker error occurred: {e}', exception=traceback.format_exc())
client.fail(
f"An unexpected Docker error occurred: {e}",
exception=traceback.format_exc(),
)
except RequestException as e:
client.fail(
f'An unexpected requests error occurred when Docker SDK for Python tried to talk to the docker daemon: {e}',
exception=traceback.format_exc())
f"An unexpected requests error occurred when Docker SDK for Python tried to talk to the docker daemon: {e}",
exception=traceback.format_exc(),
)
if __name__ == '__main__':
if __name__ == "__main__":
main()
+95 -68
View File
@@ -6,6 +6,7 @@
from __future__ import annotations
DOCUMENTATION = r"""
module: docker_swarm_info
@@ -186,14 +187,19 @@ tasks:
import traceback
try:
from docker.errors import DockerException, APIError
from docker.errors import APIError, DockerException
except ImportError:
# missing Docker SDK for Python handled in ansible.module_utils.docker_common
pass
from ansible_collections.community.docker.plugins.module_utils.swarm import AnsibleDockerSwarmClient
from ansible_collections.community.docker.plugins.module_utils.common import RequestException
from ansible_collections.community.docker.plugins.module_utils.common import (
RequestException,
)
from ansible_collections.community.docker.plugins.module_utils.swarm import (
AnsibleDockerSwarmClient,
)
from ansible_collections.community.docker.plugins.module_utils.util import (
DockerBaseClass,
clean_dict_booleans_for_docker_api,
@@ -208,22 +214,26 @@ class DockerSwarmManager(DockerBaseClass):
self.client = client
self.results = results
self.verbose_output = self.client.module.params['verbose_output']
self.verbose_output = self.client.module.params["verbose_output"]
listed_objects = ['tasks', 'services', 'nodes']
listed_objects = ["tasks", "services", "nodes"]
self.client.fail_task_if_not_swarm_manager()
self.results['swarm_facts'] = self.get_docker_swarm_facts()
self.results["swarm_facts"] = self.get_docker_swarm_facts()
for docker_object in listed_objects:
if self.client.module.params[docker_object]:
returned_name = docker_object
filter_name = docker_object + "_filters"
filters = clean_dict_booleans_for_docker_api(client.module.params.get(filter_name))
self.results[returned_name] = self.get_docker_items_list(docker_object, filters)
if self.client.module.params['unlock_key']:
self.results['swarm_unlock_key'] = self.get_docker_swarm_unlock_key()
filters = clean_dict_booleans_for_docker_api(
client.module.params.get(filter_name)
)
self.results[returned_name] = self.get_docker_items_list(
docker_object, filters
)
if self.client.module.params["unlock_key"]:
self.results["swarm_unlock_key"] = self.get_docker_swarm_unlock_key()
def get_docker_swarm_facts(self):
try:
@@ -236,14 +246,16 @@ class DockerSwarmManager(DockerBaseClass):
items_list = []
try:
if docker_object == 'nodes':
if docker_object == "nodes":
items = self.client.nodes(filters=filters)
elif docker_object == 'tasks':
elif docker_object == "tasks":
items = self.client.tasks(filters=filters)
elif docker_object == 'services':
elif docker_object == "services":
items = self.client.services(filters=filters)
except APIError as exc:
self.client.fail(f"Error inspecting docker swarm for object '{docker_object}': {exc}")
self.client.fail(
f"Error inspecting docker swarm for object '{docker_object}': {exc}"
)
if self.verbose_output:
return items
@@ -251,14 +263,14 @@ class DockerSwarmManager(DockerBaseClass):
for item in items:
item_record = dict()
if docker_object == 'nodes':
if docker_object == "nodes":
item_record = self.get_essential_facts_nodes(item)
elif docker_object == 'tasks':
elif docker_object == "tasks":
item_record = self.get_essential_facts_tasks(item)
elif docker_object == 'services':
elif docker_object == "services":
item_record = self.get_essential_facts_services(item)
if item_record.get('Mode') == 'Global':
item_record['Replicas'] = len(items)
if item_record.get("Mode") == "Global":
item_record["Replicas"] = len(items)
items_list.append(item_record)
return items_list
@@ -267,35 +279,42 @@ class DockerSwarmManager(DockerBaseClass):
def get_essential_facts_nodes(item):
object_essentials = dict()
object_essentials['ID'] = item.get('ID')
object_essentials['Hostname'] = item['Description']['Hostname']
object_essentials['Status'] = item['Status']['State']
object_essentials['Availability'] = item['Spec']['Availability']
if 'ManagerStatus' in item:
object_essentials['ManagerStatus'] = item['ManagerStatus']['Reachability']
if 'Leader' in item['ManagerStatus'] and item['ManagerStatus']['Leader'] is True:
object_essentials['ManagerStatus'] = "Leader"
object_essentials["ID"] = item.get("ID")
object_essentials["Hostname"] = item["Description"]["Hostname"]
object_essentials["Status"] = item["Status"]["State"]
object_essentials["Availability"] = item["Spec"]["Availability"]
if "ManagerStatus" in item:
object_essentials["ManagerStatus"] = item["ManagerStatus"]["Reachability"]
if (
"Leader" in item["ManagerStatus"]
and item["ManagerStatus"]["Leader"] is True
):
object_essentials["ManagerStatus"] = "Leader"
else:
object_essentials['ManagerStatus'] = None
object_essentials['EngineVersion'] = item['Description']['Engine']['EngineVersion']
object_essentials["ManagerStatus"] = None
object_essentials["EngineVersion"] = item["Description"]["Engine"][
"EngineVersion"
]
return object_essentials
def get_essential_facts_tasks(self, item):
object_essentials = dict()
object_essentials['ID'] = item['ID']
object_essentials["ID"] = item["ID"]
# Returning container ID to not trigger another connection to host
# Container ID is sufficient to get extended info in other tasks
object_essentials['ContainerID'] = item['Status']['ContainerStatus']['ContainerID']
object_essentials['Image'] = item['Spec']['ContainerSpec']['Image']
object_essentials['Node'] = self.client.get_node_name_by_id(item['NodeID'])
object_essentials['DesiredState'] = item['DesiredState']
object_essentials['CurrentState'] = item['Status']['State']
if 'Err' in item['Status']:
object_essentials['Error'] = item['Status']['Err']
object_essentials["ContainerID"] = item["Status"]["ContainerStatus"][
"ContainerID"
]
object_essentials["Image"] = item["Spec"]["ContainerSpec"]["Image"]
object_essentials["Node"] = self.client.get_node_name_by_id(item["NodeID"])
object_essentials["DesiredState"] = item["DesiredState"]
object_essentials["CurrentState"] = item["Status"]["State"]
if "Err" in item["Status"]:
object_essentials["Error"] = item["Status"]["Err"]
else:
object_essentials['Error'] = None
object_essentials["Error"] = None
return object_essentials
@@ -303,47 +322,51 @@ class DockerSwarmManager(DockerBaseClass):
def get_essential_facts_services(item):
object_essentials = dict()
object_essentials['ID'] = item['ID']
object_essentials['Name'] = item['Spec']['Name']
if 'Replicated' in item['Spec']['Mode']:
object_essentials['Mode'] = "Replicated"
object_essentials['Replicas'] = item['Spec']['Mode']['Replicated']['Replicas']
elif 'Global' in item['Spec']['Mode']:
object_essentials['Mode'] = "Global"
object_essentials["ID"] = item["ID"]
object_essentials["Name"] = item["Spec"]["Name"]
if "Replicated" in item["Spec"]["Mode"]:
object_essentials["Mode"] = "Replicated"
object_essentials["Replicas"] = item["Spec"]["Mode"]["Replicated"][
"Replicas"
]
elif "Global" in item["Spec"]["Mode"]:
object_essentials["Mode"] = "Global"
# Number of replicas have to be updated in calling method or may be left as None
object_essentials['Replicas'] = None
object_essentials['Image'] = item['Spec']['TaskTemplate']['ContainerSpec']['Image']
if item['Spec'].get('EndpointSpec') and 'Ports' in item['Spec']['EndpointSpec']:
object_essentials['Ports'] = item['Spec']['EndpointSpec']['Ports']
object_essentials["Replicas"] = None
object_essentials["Image"] = item["Spec"]["TaskTemplate"]["ContainerSpec"][
"Image"
]
if item["Spec"].get("EndpointSpec") and "Ports" in item["Spec"]["EndpointSpec"]:
object_essentials["Ports"] = item["Spec"]["EndpointSpec"]["Ports"]
else:
object_essentials['Ports'] = []
object_essentials["Ports"] = []
return object_essentials
def get_docker_swarm_unlock_key(self):
unlock_key = self.client.get_unlock_key() or {}
return unlock_key.get('UnlockKey') or None
return unlock_key.get("UnlockKey") or None
def main():
argument_spec = dict(
nodes=dict(type='bool', default=False),
nodes_filters=dict(type='dict'),
tasks=dict(type='bool', default=False),
tasks_filters=dict(type='dict'),
services=dict(type='bool', default=False),
services_filters=dict(type='dict'),
unlock_key=dict(type='bool', default=False),
verbose_output=dict(type='bool', default=False),
nodes=dict(type="bool", default=False),
nodes_filters=dict(type="dict"),
tasks=dict(type="bool", default=False),
tasks_filters=dict(type="dict"),
services=dict(type="bool", default=False),
services_filters=dict(type="dict"),
unlock_key=dict(type="bool", default=False),
verbose_output=dict(type="bool", default=False),
)
option_minimal_versions = dict(
unlock_key=dict(docker_py_version='2.7.0'),
unlock_key=dict(docker_py_version="2.7.0"),
)
client = AnsibleDockerSwarmClient(
argument_spec=argument_spec,
supports_check_mode=True,
min_docker_version='1.10.0',
min_docker_version="1.10.0",
option_minimal_versions=option_minimal_versions,
fail_results=dict(
can_talk_to_docker=False,
@@ -351,9 +374,9 @@ def main():
docker_swarm_manager=False,
),
)
client.fail_results['can_talk_to_docker'] = True
client.fail_results['docker_swarm_active'] = client.check_if_swarm_node()
client.fail_results['docker_swarm_manager'] = client.check_if_swarm_manager()
client.fail_results["can_talk_to_docker"] = True
client.fail_results["docker_swarm_active"] = client.check_if_swarm_node()
client.fail_results["docker_swarm_manager"] = client.check_if_swarm_manager()
try:
results = dict(
@@ -364,12 +387,16 @@ def main():
results.update(client.fail_results)
client.module.exit_json(**results)
except DockerException as e:
client.fail(f'An unexpected Docker error occurred: {e}', exception=traceback.format_exc())
client.fail(
f"An unexpected Docker error occurred: {e}",
exception=traceback.format_exc(),
)
except RequestException as e:
client.fail(
f'An unexpected requests error occurred when Docker SDK for Python tried to talk to the docker daemon: {e}',
exception=traceback.format_exc())
f"An unexpected requests error occurred when Docker SDK for Python tried to talk to the docker daemon: {e}",
exception=traceback.format_exc(),
)
if __name__ == '__main__':
if __name__ == "__main__":
main()
File diff suppressed because it is too large Load Diff
+17 -18
View File
@@ -64,6 +64,7 @@ service:
import traceback
try:
from docker.errors import DockerException
except ImportError:
@@ -73,27 +74,25 @@ except ImportError:
from ansible_collections.community.docker.plugins.module_utils.common import (
RequestException,
)
from ansible_collections.community.docker.plugins.module_utils.swarm import AnsibleDockerSwarmClient
from ansible_collections.community.docker.plugins.module_utils.swarm import (
AnsibleDockerSwarmClient,
)
def get_service_info(client):
service = client.module.params['name']
return client.get_service_inspect(
service_id=service,
skip_missing=True
)
service = client.module.params["name"]
return client.get_service_inspect(service_id=service, skip_missing=True)
def main():
argument_spec = dict(
name=dict(type='str', required=True),
name=dict(type="str", required=True),
)
client = AnsibleDockerSwarmClient(
argument_spec=argument_spec,
supports_check_mode=True,
min_docker_version='2.0.0',
min_docker_version="2.0.0",
)
client.fail_task_if_not_swarm_manager()
@@ -101,18 +100,18 @@ def main():
try:
service = get_service_info(client)
client.module.exit_json(
changed=False,
service=service,
exists=bool(service)
)
client.module.exit_json(changed=False, service=service, exists=bool(service))
except DockerException as e:
client.fail(f'An unexpected Docker error occurred: {e}', exception=traceback.format_exc())
client.fail(
f"An unexpected Docker error occurred: {e}",
exception=traceback.format_exc(),
)
except RequestException as e:
client.fail(
f'An unexpected requests error occurred when Docker SDK for Python tried to talk to the docker daemon: {e}',
exception=traceback.format_exc())
f"An unexpected requests error occurred when Docker SDK for Python tried to talk to the docker daemon: {e}",
exception=traceback.format_exc(),
)
if __name__ == '__main__':
if __name__ == "__main__":
main()
+93 -62
View File
@@ -120,20 +120,19 @@ volume:
import traceback
from ansible.module_utils.common.text.converters import to_native
from ansible_collections.community.docker.plugins.module_utils._api.errors import (
APIError,
DockerException,
)
from ansible_collections.community.docker.plugins.module_utils.common_api import (
AnsibleDockerClient,
RequestException,
)
from ansible_collections.community.docker.plugins.module_utils.util import (
DockerBaseClass,
DifferenceTracker,
DockerBaseClass,
sanitize_labels,
)
from ansible_collections.community.docker.plugins.module_utils._api.errors import (
APIError,
DockerException,
)
class TaskParameters(DockerBaseClass):
@@ -158,10 +157,7 @@ class DockerVolumeManager(object):
self.client = client
self.parameters = TaskParameters(client)
self.check_mode = self.client.check_mode
self.results = {
'changed': False,
'actions': []
}
self.results = {"changed": False, "actions": []}
self.diff = self.client.module._diff
self.diff_tracker = DifferenceTracker()
self.diff_result = dict()
@@ -169,27 +165,29 @@ class DockerVolumeManager(object):
self.existing_volume = self.get_existing_volume()
state = self.parameters.state
if state == 'present':
if state == "present":
self.present()
elif state == 'absent':
elif state == "absent":
self.absent()
if self.diff or self.check_mode or self.parameters.debug:
if self.diff:
self.diff_result['before'], self.diff_result['after'] = self.diff_tracker.get_before_after()
self.results['diff'] = self.diff_result
self.diff_result["before"], self.diff_result["after"] = (
self.diff_tracker.get_before_after()
)
self.results["diff"] = self.diff_result
def get_existing_volume(self):
try:
volumes = self.client.get_json('/volumes')
volumes = self.client.get_json("/volumes")
except APIError as e:
self.client.fail(to_native(e))
if volumes['Volumes'] is None:
if volumes["Volumes"] is None:
return None
for volume in volumes['Volumes']:
if volume['Name'] == self.parameters.volume_name:
for volume in volumes["Volumes"]:
if volume["Name"] == self.parameters.volume_name:
return volume
return None
@@ -201,27 +199,42 @@ class DockerVolumeManager(object):
:return: list of options that differ
"""
differences = DifferenceTracker()
if self.parameters.driver and self.parameters.driver != self.existing_volume['Driver']:
differences.add('driver', parameter=self.parameters.driver, active=self.existing_volume['Driver'])
if (
self.parameters.driver
and self.parameters.driver != self.existing_volume["Driver"]
):
differences.add(
"driver",
parameter=self.parameters.driver,
active=self.existing_volume["Driver"],
)
if self.parameters.driver_options:
if not self.existing_volume.get('Options'):
differences.add('driver_options',
parameter=self.parameters.driver_options,
active=self.existing_volume.get('Options'))
if not self.existing_volume.get("Options"):
differences.add(
"driver_options",
parameter=self.parameters.driver_options,
active=self.existing_volume.get("Options"),
)
else:
for key, value in self.parameters.driver_options.items():
if (not self.existing_volume['Options'].get(key) or
value != self.existing_volume['Options'][key]):
differences.add(f'driver_options.{key}',
parameter=value,
active=self.existing_volume['Options'].get(key))
if (
not self.existing_volume["Options"].get(key)
or value != self.existing_volume["Options"][key]
):
differences.add(
f"driver_options.{key}",
parameter=value,
active=self.existing_volume["Options"].get(key),
)
if self.parameters.labels:
existing_labels = self.existing_volume.get('Labels') or {}
existing_labels = self.existing_volume.get("Labels") or {}
for label in self.parameters.labels:
if existing_labels.get(label) != self.parameters.labels.get(label):
differences.add(f'labels.{label}',
parameter=self.parameters.labels.get(label),
active=existing_labels.get(label))
differences.add(
f"labels.{label}",
parameter=self.parameters.labels.get(label),
active=existing_labels.get(label),
)
return differences
@@ -230,67 +243,81 @@ class DockerVolumeManager(object):
if not self.check_mode:
try:
data = {
'Name': self.parameters.volume_name,
'Driver': self.parameters.driver,
'DriverOpts': self.parameters.driver_options,
"Name": self.parameters.volume_name,
"Driver": self.parameters.driver,
"DriverOpts": self.parameters.driver_options,
}
if self.parameters.labels is not None:
data['Labels'] = self.parameters.labels
resp = self.client.post_json_to_json('/volumes/create', data=data)
self.existing_volume = self.client.get_json('/volumes/{0}', resp['Name'])
data["Labels"] = self.parameters.labels
resp = self.client.post_json_to_json("/volumes/create", data=data)
self.existing_volume = self.client.get_json(
"/volumes/{0}", resp["Name"]
)
except APIError as e:
self.client.fail(to_native(e))
self.results['actions'].append(f"Created volume {self.parameters.volume_name} with driver {self.parameters.driver}")
self.results['changed'] = True
self.results["actions"].append(
f"Created volume {self.parameters.volume_name} with driver {self.parameters.driver}"
)
self.results["changed"] = True
def remove_volume(self):
if self.existing_volume:
if not self.check_mode:
try:
self.client.delete_call('/volumes/{0}', self.parameters.volume_name)
self.client.delete_call("/volumes/{0}", self.parameters.volume_name)
except APIError as e:
self.client.fail(to_native(e))
self.results['actions'].append(f"Removed volume {self.parameters.volume_name}")
self.results['changed'] = True
self.results["actions"].append(
f"Removed volume {self.parameters.volume_name}"
)
self.results["changed"] = True
def present(self):
differences = DifferenceTracker()
if self.existing_volume:
differences = self.has_different_config()
self.diff_tracker.add('exists', parameter=True, active=self.existing_volume is not None)
if (not differences.empty and self.parameters.recreate == 'options-changed') or self.parameters.recreate == 'always':
self.diff_tracker.add(
"exists", parameter=True, active=self.existing_volume is not None
)
if (
not differences.empty and self.parameters.recreate == "options-changed"
) or self.parameters.recreate == "always":
self.remove_volume()
self.existing_volume = None
self.create_volume()
if self.diff or self.check_mode or self.parameters.debug:
self.diff_result['differences'] = differences.get_legacy_docker_diffs()
self.diff_result["differences"] = differences.get_legacy_docker_diffs()
self.diff_tracker.merge(differences)
if not self.check_mode and not self.parameters.debug:
self.results.pop('actions')
self.results.pop("actions")
volume_facts = self.get_existing_volume()
self.results['volume'] = volume_facts
self.results["volume"] = volume_facts
def absent(self):
self.diff_tracker.add('exists', parameter=False, active=self.existing_volume is not None)
self.diff_tracker.add(
"exists", parameter=False, active=self.existing_volume is not None
)
self.remove_volume()
def main():
argument_spec = dict(
volume_name=dict(type='str', required=True, aliases=['name']),
state=dict(type='str', default='present', choices=['present', 'absent']),
driver=dict(type='str', default='local'),
driver_options=dict(type='dict', default={}),
labels=dict(type='dict'),
recreate=dict(type='str', default='never', choices=['always', 'never', 'options-changed']),
debug=dict(type='bool', default=False)
volume_name=dict(type="str", required=True, aliases=["name"]),
state=dict(type="str", default="present", choices=["present", "absent"]),
driver=dict(type="str", default="local"),
driver_options=dict(type="dict", default={}),
labels=dict(type="dict"),
recreate=dict(
type="str", default="never", choices=["always", "never", "options-changed"]
),
debug=dict(type="bool", default=False),
)
client = AnsibleDockerClient(
@@ -298,18 +325,22 @@ def main():
supports_check_mode=True,
# "The docker server >= 1.9.0"
)
sanitize_labels(client.module.params['labels'], 'labels', client)
sanitize_labels(client.module.params["labels"], "labels", client)
try:
cm = DockerVolumeManager(client)
client.module.exit_json(**cm.results)
except DockerException as e:
client.fail(f'An unexpected Docker error occurred: {e}', exception=traceback.format_exc())
client.fail(
f"An unexpected Docker error occurred: {e}",
exception=traceback.format_exc(),
)
except RequestException as e:
client.fail(
f'An unexpected requests error occurred when trying to talk to the Docker daemon: {e}',
exception=traceback.format_exc())
f"An unexpected requests error occurred when trying to talk to the Docker daemon: {e}",
exception=traceback.format_exc(),
)
if __name__ == '__main__':
if __name__ == "__main__":
main()
+15 -8
View File
@@ -72,16 +72,19 @@ volume:
import traceback
from ansible_collections.community.docker.plugins.module_utils._api.errors import (
DockerException,
NotFound,
)
from ansible_collections.community.docker.plugins.module_utils.common_api import (
AnsibleDockerClient,
RequestException,
)
from ansible_collections.community.docker.plugins.module_utils._api.errors import DockerException, NotFound
def get_existing_volume(client, volume_name):
try:
return client.get_json('/volumes/{0}', volume_name)
return client.get_json("/volumes/{0}", volume_name)
except NotFound as dummy:
return None
except Exception as exc:
@@ -90,7 +93,7 @@ def get_existing_volume(client, volume_name):
def main():
argument_spec = dict(
name=dict(type='str', required=True, aliases=['volume_name']),
name=dict(type="str", required=True, aliases=["volume_name"]),
)
client = AnsibleDockerClient(
@@ -99,7 +102,7 @@ def main():
)
try:
volume = get_existing_volume(client, client.module.params['name'])
volume = get_existing_volume(client, client.module.params["name"])
client.module.exit_json(
changed=False,
@@ -107,12 +110,16 @@ def main():
volume=volume,
)
except DockerException as e:
client.fail(f'An unexpected Docker error occurred: {e}', exception=traceback.format_exc())
client.fail(
f"An unexpected Docker error occurred: {e}",
exception=traceback.format_exc(),
)
except RequestException as e:
client.fail(
f'An unexpected requests error occurred when trying to talk to the Docker daemon: {e}',
exception=traceback.format_exc())
f"An unexpected requests error occurred when trying to talk to the Docker daemon: {e}",
exception=traceback.format_exc(),
)
if __name__ == '__main__':
if __name__ == "__main__":
main()
+14 -10
View File
@@ -2,17 +2,17 @@
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import (absolute_import, division, print_function)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
from ansible.errors import AnsibleConnectionFailure
from ansible.utils.display import Display
from ansible_collections.community.docker.plugins.module_utils.common import (
AnsibleDockerClientBase,
)
from ansible_collections.community.docker.plugins.module_utils.util import (
DOCKER_COMMON_ARGS,
)
@@ -24,18 +24,22 @@ class AnsibleDockerClient(AnsibleDockerClientBase):
self.display = Display()
super(AnsibleDockerClient, self).__init__(
min_docker_version=min_docker_version,
min_docker_api_version=min_docker_api_version)
min_docker_api_version=min_docker_api_version,
)
def fail(self, msg, **kwargs):
if kwargs:
msg += '\nContext:\n' + '\n'.join(f' {k} = {v!r}' for (k, v) in kwargs.items())
msg += "\nContext:\n" + "\n".join(
f" {k} = {v!r}" for (k, v) in kwargs.items()
)
raise AnsibleConnectionFailure(msg)
def deprecate(self, msg, version=None, date=None, collection_name=None):
self.display.deprecated(msg, version=version, date=date, collection_name=collection_name)
self.display.deprecated(
msg, version=version, date=date, collection_name=collection_name
)
def _get_params(self):
return dict([
(option, self.plugin.get_option(option))
for option in DOCKER_COMMON_ARGS
])
return dict(
[(option, self.plugin.get_option(option)) for option in DOCKER_COMMON_ARGS]
)
+11 -10
View File
@@ -4,14 +4,11 @@
from __future__ import annotations
from ansible.errors import AnsibleConnectionFailure
from ansible.utils.display import Display
from ansible_collections.community.docker.plugins.module_utils.common_api import (
AnsibleDockerClientBase,
)
from ansible_collections.community.docker.plugins.module_utils.util import (
DOCKER_COMMON_ARGS,
)
@@ -22,18 +19,22 @@ class AnsibleDockerClient(AnsibleDockerClientBase):
self.plugin = plugin
self.display = Display()
super(AnsibleDockerClient, self).__init__(
min_docker_api_version=min_docker_api_version)
min_docker_api_version=min_docker_api_version
)
def fail(self, msg, **kwargs):
if kwargs:
msg += '\nContext:\n' + '\n'.join(f' {k} = {v!r}' for (k, v) in kwargs.items())
msg += "\nContext:\n" + "\n".join(
f" {k} = {v!r}" for (k, v) in kwargs.items()
)
raise AnsibleConnectionFailure(msg)
def deprecate(self, msg, version=None, date=None, collection_name=None):
self.display.deprecated(msg, version=version, date=date, collection_name=collection_name)
self.display.deprecated(
msg, version=version, date=date, collection_name=collection_name
)
def _get_params(self):
return dict([
(option, self.plugin.get_option(option))
for option in DOCKER_COMMON_ARGS
])
return dict(
[(option, self.plugin.get_option(option)) for option in DOCKER_COMMON_ARGS]
)
+6 -2
View File
@@ -2,7 +2,9 @@
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import (absolute_import, division, print_function)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
@@ -15,4 +17,6 @@ from ansible_collections.community.docker.plugins.module_utils.socket_handler im
class DockerSocketHandler(DockerSocketHandlerBase):
def __init__(self, display, sock, log=None, container=None):
super(DockerSocketHandler, self).__init__(sock, selectors, log=lambda msg: display.vvvv(msg, host=container))
super(DockerSocketHandler, self).__init__(
sock, selectors, log=lambda msg: display.vvvv(msg, host=container)
)
+5 -4
View File
@@ -5,16 +5,17 @@
from __future__ import annotations
import re
from collections.abc import Mapping, Set
from ansible.module_utils.common.collections import is_sequence
from ansible.utils.unsafe_proxy import (
AnsibleUnsafe,
wrap_var as _make_unsafe,
)
from ansible.utils.unsafe_proxy import wrap_var as _make_unsafe
_RE_TEMPLATE_CHARS = re.compile('[{}]')
_RE_TEMPLATE_CHARS_BYTES = re.compile(b'[{}]')
_RE_TEMPLATE_CHARS = re.compile("[{}]")
_RE_TEMPLATE_CHARS_BYTES = re.compile(b"[{}]")
def make_unsafe(value):

Some files were not shown because too many files have changed in this diff Show More