File: //opt/saltstack/salt/lib/python3.10/site-packages/salt/utils/dockermod/__init__.py
"""
Common logic used by the docker state and execution module
This module contains logic to accommodate docker/salt CLI usage, as well as
input as formatted by states.
"""
import copy
import logging
import salt.utils.args
import salt.utils.data
import salt.utils.dockermod.translate
from salt.exceptions import CommandExecutionError, SaltInvocationError
from salt.utils.args import get_function_argspec as _argspec
from salt.utils.dockermod.translate.helpers import split as _split
try:
import docker
except ImportError:
docker = None
# These next two imports are only necessary to have access to the needed
# functions so that we can get argspecs for the container config, host config,
# and networking config (see the get_client_args() function).
try:
import docker.types # pylint: disable=no-name-in-module
except ImportError:
pass
try:
import docker.utils # pylint: disable=no-name-in-module
except ImportError:
pass
NOTSET = object()
__virtualname__ = "docker"
# Default timeout as of docker-py 1.0.0
CLIENT_TIMEOUT = 60
# Timeout for stopping the container, before a kill is invoked
SHUTDOWN_TIMEOUT = 10
log = logging.getLogger(__name__)
def __virtual__():
if docker is None:
return False
return __virtualname__
def get_client_args(limit=None):
if docker is None:
raise CommandExecutionError("docker Python module not imported")
limit = salt.utils.args.split_input(limit or [])
ret = {}
if not limit or any(
x in limit
for x in ("create_container", "host_config", "connect_container_to_network")
):
try:
ret["create_container"] = _argspec(docker.APIClient.create_container).args
except AttributeError:
try:
ret["create_container"] = _argspec(docker.Client.create_container).args
except AttributeError:
raise CommandExecutionError("Coult not get create_container argspec")
try:
ret["host_config"] = _argspec(docker.types.HostConfig.__init__).args
except AttributeError:
try:
ret["host_config"] = _argspec(docker.utils.create_host_config).args
except AttributeError:
raise CommandExecutionError("Could not get create_host_config argspec")
try:
ret["connect_container_to_network"] = _argspec(
docker.types.EndpointConfig.__init__
).args
except AttributeError:
try:
ret["connect_container_to_network"] = _argspec(
docker.utils.utils.create_endpoint_config
).args
except AttributeError:
try:
ret["connect_container_to_network"] = _argspec(
docker.utils.create_endpoint_config
).args
except AttributeError:
raise CommandExecutionError(
"Could not get connect_container_to_network argspec"
)
for key, wrapped_func in (
("logs", docker.api.container.ContainerApiMixin.logs),
("create_network", docker.api.network.NetworkApiMixin.create_network),
):
if not limit or key in limit:
try:
func_ref = wrapped_func
try:
# functools.wraps makes things a little easier in Python 3
ret[key] = _argspec(func_ref.__wrapped__).args
except AttributeError:
# functools.wraps changed (unlikely), bail out
ret[key] = []
except AttributeError:
# Function moved, bail out
ret[key] = []
if not limit or "ipam_config" in limit:
try:
ret["ipam_config"] = _argspec(docker.types.IPAMPool.__init__).args
except AttributeError:
try:
ret["ipam_config"] = _argspec(docker.utils.create_ipam_pool).args
except AttributeError:
raise CommandExecutionError("Could not get ipam args")
for item in ret:
# The API version is passed automagically by the API code that imports
# these classes/functions and is not an arg that we will be passing, so
# remove it if present. Similarly, don't include "self" if it shows up
# in the arglist.
for argname in ("version", "self"):
try:
ret[item].remove(argname)
except ValueError:
pass
# Remove any args in host or endpoint config from the create_container
# arglist. This keeps us from accidentally allowing args that docker-py has
# moved from the create_container function to the either the host or
# endpoint config.
for item in ("host_config", "connect_container_to_network"):
for val in ret.get(item, []):
try:
ret["create_container"].remove(val)
except ValueError:
# Arg is not in create_container arglist
pass
for item in ("create_container", "host_config", "connect_container_to_network"):
if limit and item not in limit:
ret.pop(item, None)
try:
ret["logs"].remove("container")
except (KeyError, ValueError, TypeError):
pass
return ret
def translate_input(
translator,
skip_translate=None,
ignore_collisions=False,
validate_ip_addrs=True,
**kwargs
):
"""
Translate CLI/SLS input into the format the API expects. The ``translator``
argument must be a module containing translation functions, within
salt.utils.dockermod.translate. A ``skip_translate`` kwarg can be passed to
control which arguments are translated. It can be either a comma-separated
list or an iterable containing strings (e.g. a list or tuple), and members
of that tuple will have their translation skipped. Optionally,
skip_translate can be set to True to skip *all* translation.
"""
kwargs = copy.deepcopy(salt.utils.args.clean_kwargs(**kwargs))
invalid = {}
collisions = []
if skip_translate is True:
# Skip all translation
return kwargs
else:
if not skip_translate:
skip_translate = ()
else:
try:
skip_translate = _split(skip_translate)
except AttributeError:
pass
if not hasattr(skip_translate, "__iter__"):
log.error("skip_translate is not an iterable, ignoring")
skip_translate = ()
try:
# Using list(kwargs) here because if there are any invalid arguments we
# will be popping them from the kwargs.
for key in list(kwargs):
real_key = translator.ALIASES.get(key, key)
if real_key in skip_translate:
continue
# ipam_pools is designed to be passed as a list of actual
# dictionaries, but if each of the dictionaries passed has a single
# element, it will be incorrectly repacked.
if key != "ipam_pools" and salt.utils.data.is_dictlist(kwargs[key]):
kwargs[key] = salt.utils.data.repack_dictlist(kwargs[key])
try:
kwargs[key] = getattr(translator, real_key)(
kwargs[key],
validate_ip_addrs=validate_ip_addrs,
skip_translate=skip_translate,
)
except AttributeError:
log.debug("No translation function for argument '%s'", key)
continue
except SaltInvocationError as exc:
kwargs.pop(key)
invalid[key] = exc.strerror
try:
translator._merge_keys(kwargs)
except AttributeError:
pass
# Convert CLI versions of commands to their docker-py counterparts
for key in translator.ALIASES:
if key in kwargs:
new_key = translator.ALIASES[key]
value = kwargs.pop(key)
if new_key in kwargs:
collisions.append(new_key)
else:
kwargs[new_key] = value
try:
translator._post_processing(kwargs, skip_translate, invalid)
except AttributeError:
pass
except Exception as exc: # pylint: disable=broad-except
error_message = str(exc)
log.error("Error translating input: '%s'", error_message, exc_info=True)
else:
error_message = None
error_data = {}
if error_message is not None:
error_data["error_message"] = error_message
if invalid:
error_data["invalid"] = invalid
if collisions and not ignore_collisions:
for item in collisions:
error_data.setdefault("collisions", []).append(
"'{}' is an alias for '{}', they cannot both be used".format(
translator.ALIASES_REVMAP[item], item
)
)
if error_data:
raise CommandExecutionError("Failed to translate input", info=error_data)
return kwargs
def create_ipam_config(*pools, **kwargs):
"""
Builds an IP address management (IPAM) config dictionary
"""
kwargs = salt.utils.args.clean_kwargs(**kwargs)
try:
# docker-py 2.0 and newer
pool_args = salt.utils.args.get_function_argspec(
docker.types.IPAMPool.__init__
).args
create_pool = docker.types.IPAMPool
create_config = docker.types.IPAMConfig
except AttributeError:
# docker-py < 2.0
pool_args = salt.utils.args.get_function_argspec(
docker.utils.create_ipam_pool
).args
create_pool = docker.utils.create_ipam_pool
create_config = docker.utils.create_ipam_config
for primary_key, alias_key in (("driver", "ipam_driver"), ("options", "ipam_opts")):
if alias_key in kwargs:
alias_val = kwargs.pop(alias_key)
if primary_key in kwargs:
log.warning(
"docker.create_ipam_config: Both '%s' and '%s' "
"passed. Ignoring '%s'",
alias_key,
primary_key,
alias_key,
)
else:
kwargs[primary_key] = alias_val
if salt.utils.data.is_dictlist(kwargs.get("options")):
kwargs["options"] = salt.utils.data.repack_dictlist(kwargs["options"])
# Get all of the IPAM pool args that were passed as individual kwargs
# instead of in the *pools tuple
pool_kwargs = {}
for key in list(kwargs):
if key in pool_args:
pool_kwargs[key] = kwargs.pop(key)
pool_configs = []
if pool_kwargs:
pool_configs.append(create_pool(**pool_kwargs))
pool_configs.extend([create_pool(**pool) for pool in pools])
if pool_configs:
# Sanity check the IPAM pools. docker-py's type/function for creating
# an IPAM pool will allow you to create a pool with a gateway, IP
# range, or map of aux addresses, even when no subnet is passed.
# However, attempting to use this IPAM pool when creating the network
# will cause the Docker Engine to throw an error.
if any("Subnet" not in pool for pool in pool_configs):
raise SaltInvocationError("A subnet is required in each IPAM pool")
else:
kwargs["pool_configs"] = pool_configs
ret = create_config(**kwargs)
pool_dicts = ret.get("Config")
if pool_dicts:
# When you inspect a network with custom IPAM configuration, only
# arguments which were explictly passed are reflected. By contrast,
# docker-py will include keys for arguments which were not passed in
# but set the value to None. Thus, for ease of comparison, the below
# loop will remove all keys with a value of None from the generated
# pool configs.
for idx, _ in enumerate(pool_dicts):
for key in list(pool_dicts[idx]):
if pool_dicts[idx][key] is None:
del pool_dicts[idx][key]
return ret