File: //proc/self/root/usr/lib/python2.7/site-packages/salt/utils/docker/__init__.py
# -*- coding: utf-8 -*-
'''
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 Python libs
from __future__ import absolute_import, print_function, unicode_literals
import copy
import logging
# Import Salt libs
import salt.utils.args
import salt.utils.data
import salt.utils.docker.translate
from salt.utils.docker.translate.helpers import split as _split
from salt.exceptions import CommandExecutionError, SaltInvocationError
from salt.utils.args import get_function_argspec as _argspec
# Import 3rd-party libs
from salt.ext import six
try:
import docker
HAS_DOCKER_PY = True
except ImportError:
HAS_DOCKER_PY = False
# 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
except ImportError:
pass
try:
import docker.utils
except ImportError:
pass
NOTSET = object()
# 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 get_client_args(limit=None):
if not HAS_DOCKER_PY:
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
if six.PY2:
try:
# create_network is decorated, so we have to dig into the
# closure created by functools.wraps
ret[key] = \
_argspec(func_ref.__func__.__closure__[0].cell_contents).args
except (AttributeError, IndexError):
# functools.wraps changed (unlikely), bail out
ret[key] = []
else:
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.docker.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 = exc.__str__()
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(
'\'{0}\' is an alias for \'{1}\', 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