File: //proc/self/root/usr/lib/python2.7/site-packages/salt/utils/docker/translate/container.py
# -*- coding: utf-8 -*-
'''
Functions to translate input for container creation
'''
# Import Python libs
from __future__ import absolute_import, print_function, unicode_literals
import os
# Import Salt libs
from salt.exceptions import SaltInvocationError
# Import 3rd-party libs
from salt.ext import six
from salt.ext.six.moves import range # pylint: disable=import-error,redefined-builtin
# Import helpers
from . import helpers
ALIASES = {
'cmd': 'command',
'cpuset': 'cpuset_cpus',
'dns_option': 'dns_opt',
'env': 'environment',
'expose': 'ports',
'interactive': 'stdin_open',
'ipc': 'ipc_mode',
'label': 'labels',
'memory': 'mem_limit',
'memory_swap': 'memswap_limit',
'publish': 'port_bindings',
'publish_all': 'publish_all_ports',
'restart': 'restart_policy',
'rm': 'auto_remove',
'sysctl': 'sysctls',
'security_opts': 'security_opt',
'ulimit': 'ulimits',
'user_ns_mode': 'userns_mode',
'volume': 'volumes',
'workdir': 'working_dir',
}
ALIASES_REVMAP = dict([(y, x) for x, y in six.iteritems(ALIASES)])
def _merge_keys(kwargs):
'''
The log_config is a mixture of the CLI options --log-driver and --log-opt
(which we support in Salt as log_driver and log_opt, respectively), but it
must be submitted to the host config in the format {'Type': log_driver,
'Config': log_opt}. So, we need to construct this argument to be passed to
the API from those two arguments.
'''
log_driver = kwargs.pop('log_driver', helpers.NOTSET)
log_opt = kwargs.pop('log_opt', helpers.NOTSET)
if 'log_config' not in kwargs:
if log_driver is not helpers.NOTSET \
or log_opt is not helpers.NOTSET:
kwargs['log_config'] = {
'Type': log_driver
if log_driver is not helpers.NOTSET
else 'none',
'Config': log_opt
if log_opt is not helpers.NOTSET
else {}
}
def _post_processing(kwargs, skip_translate, invalid):
'''
Additional container-specific post-translation processing
'''
# Don't allow conflicting options to be set
if kwargs.get('port_bindings') is not None \
and kwargs.get('publish_all_ports'):
kwargs.pop('port_bindings')
invalid['port_bindings'] = 'Cannot be used when publish_all_ports=True'
if kwargs.get('hostname') is not None \
and kwargs.get('network_mode') == 'host':
kwargs.pop('hostname')
invalid['hostname'] = 'Cannot be used when network_mode=True'
# Make sure volumes and ports are defined to match the binds and port_bindings
if kwargs.get('binds') is not None \
and (skip_translate is True or
all(x not in skip_translate
for x in ('binds', 'volume', 'volumes'))):
# Make sure that all volumes defined in "binds" are included in the
# "volumes" param.
auto_volumes = []
if isinstance(kwargs['binds'], dict):
for val in six.itervalues(kwargs['binds']):
try:
if 'bind' in val:
auto_volumes.append(val['bind'])
except TypeError:
continue
else:
if isinstance(kwargs['binds'], list):
auto_volume_defs = kwargs['binds']
else:
try:
auto_volume_defs = helpers.split(kwargs['binds'])
except AttributeError:
auto_volume_defs = []
for val in auto_volume_defs:
try:
auto_volumes.append(helpers.split(val, ':')[1])
except IndexError:
continue
if auto_volumes:
actual_volumes = kwargs.setdefault('volumes', [])
actual_volumes.extend([x for x in auto_volumes
if x not in actual_volumes])
# Sort list to make unit tests more reliable
actual_volumes.sort()
if kwargs.get('port_bindings') is not None \
and all(x not in skip_translate
for x in ('port_bindings', 'expose', 'ports')):
# Make sure that all ports defined in "port_bindings" are included in
# the "ports" param.
ports_to_bind = list(kwargs['port_bindings'])
if ports_to_bind:
ports_to_open = set(kwargs.get('ports', []))
ports_to_open.update([helpers.get_port_def(x) for x in ports_to_bind])
kwargs['ports'] = list(ports_to_open)
if 'ports' in kwargs \
and all(x not in skip_translate for x in ('expose', 'ports')):
# TCP ports should only be passed as the port number. Normalize the
# input so a port definition of 80/tcp becomes just 80 instead of
# (80, 'tcp').
for index, _ in enumerate(kwargs['ports']):
try:
if kwargs['ports'][index][1] == 'tcp':
kwargs['ports'][index] = ports_to_open[index][0]
except TypeError:
continue
# Functions below must match names of docker-py arguments
def auto_remove(val, **kwargs): # pylint: disable=unused-argument
return helpers.translate_bool(val)
def binds(val, **kwargs): # pylint: disable=unused-argument
'''
On the CLI, these are passed as multiple instances of a given CLI option.
In Salt, we accept these as a comma-delimited list but the API expects a
Python list.
'''
if not isinstance(val, dict):
if not isinstance(val, list):
try:
val = helpers.split(val)
except AttributeError:
raise SaltInvocationError(
'\'{0}\' is not a dictionary or list of bind '
'definitions'.format(val)
)
return val
def blkio_weight(val, **kwargs): # pylint: disable=unused-argument
return helpers.translate_int(val)
def blkio_weight_device(val, **kwargs): # pylint: disable=unused-argument
'''
CLI input is a list of PATH:WEIGHT pairs, but the API expects a list of
dictionaries in the format [{'Path': path, 'Weight': weight}]
'''
val = helpers.map_vals(val, 'Path', 'Weight')
for idx in range(len(val)):
try:
val[idx]['Weight'] = int(val[idx]['Weight'])
except (TypeError, ValueError):
raise SaltInvocationError(
'Weight \'{Weight}\' for path \'{Path}\' is not an '
'integer'.format(**val[idx])
)
return val
def cap_add(val, **kwargs): # pylint: disable=unused-argument
return helpers.translate_stringlist(val)
def cap_drop(val, **kwargs): # pylint: disable=unused-argument
return helpers.translate_stringlist(val)
def command(val, **kwargs): # pylint: disable=unused-argument
return helpers.translate_command(val)
def cpuset_cpus(val, **kwargs): # pylint: disable=unused-argument
return helpers.translate_str(val)
def cpuset_mems(val, **kwargs): # pylint: disable=unused-argument
return helpers.translate_str(val)
def cpu_group(val, **kwargs): # pylint: disable=unused-argument
return helpers.translate_int(val)
def cpu_period(val, **kwargs): # pylint: disable=unused-argument
return helpers.translate_int(val)
def cpu_shares(val, **kwargs): # pylint: disable=unused-argument
return helpers.translate_int(val)
def detach(val, **kwargs): # pylint: disable=unused-argument
return helpers.translate_bool(val)
def device_read_bps(val, **kwargs): # pylint: disable=unused-argument
return helpers.translate_device_rates(val, numeric_rate=False)
def device_read_iops(val, **kwargs): # pylint: disable=unused-argument
return helpers.translate_device_rates(val, numeric_rate=True)
def device_write_bps(val, **kwargs): # pylint: disable=unused-argument
return helpers.translate_device_rates(val, numeric_rate=False)
def device_write_iops(val, **kwargs): # pylint: disable=unused-argument
return helpers.translate_device_rates(val, numeric_rate=True)
def devices(val, **kwargs): # pylint: disable=unused-argument
return helpers.translate_stringlist(val)
def dns_opt(val, **kwargs): # pylint: disable=unused-argument
return helpers.translate_stringlist(val)
def dns_search(val, **kwargs): # pylint: disable=unused-argument
return helpers.translate_stringlist(val)
def dns(val, **kwargs):
val = helpers.translate_stringlist(val)
if kwargs.get('validate_ip_addrs', True):
for item in val:
helpers.validate_ip(item)
return val
def domainname(val, **kwargs): # pylint: disable=unused-argument
return helpers.translate_str(val)
def entrypoint(val, **kwargs): # pylint: disable=unused-argument
return helpers.translate_command(val)
def environment(val, **kwargs): # pylint: disable=unused-argument
return helpers.translate_key_val(val, delimiter='=')
def extra_hosts(val, **kwargs):
val = helpers.translate_key_val(val, delimiter=':')
if kwargs.get('validate_ip_addrs', True):
for key in val:
helpers.validate_ip(val[key])
return val
def group_add(val, **kwargs): # pylint: disable=unused-argument
return helpers.translate_stringlist(val)
def host_config(val, **kwargs): # pylint: disable=unused-argument
return helpers.translate_dict(val)
def hostname(val, **kwargs): # pylint: disable=unused-argument
return helpers.translate_str(val)
def ipc_mode(val, **kwargs): # pylint: disable=unused-argument
return helpers.translate_str(val)
def isolation(val, **kwargs): # pylint: disable=unused-argument
return helpers.translate_str(val)
def labels(val, **kwargs): # pylint: disable=unused-argument
return helpers.translate_labels(val)
def links(val, **kwargs): # pylint: disable=unused-argument
return helpers.translate_key_val(val, delimiter=':')
def log_driver(val, **kwargs): # pylint: disable=unused-argument
return helpers.translate_str(val)
def log_opt(val, **kwargs): # pylint: disable=unused-argument
return helpers.translate_key_val(val, delimiter='=')
def lxc_conf(val, **kwargs): # pylint: disable=unused-argument
return helpers.translate_key_val(val, delimiter='=')
def mac_address(val, **kwargs): # pylint: disable=unused-argument
return helpers.translate_str(val)
def mem_limit(val, **kwargs): # pylint: disable=unused-argument
return helpers.translate_bytes(val)
def mem_swappiness(val, **kwargs): # pylint: disable=unused-argument
return helpers.translate_int(val)
def memswap_limit(val, **kwargs): # pylint: disable=unused-argument
return helpers.translate_bytes(val)
def name(val, **kwargs): # pylint: disable=unused-argument
return helpers.translate_str(val)
def network_disabled(val, **kwargs): # pylint: disable=unused-argument
return helpers.translate_bool(val)
def network_mode(val, **kwargs): # pylint: disable=unused-argument
return helpers.translate_str(val)
def oom_kill_disable(val, **kwargs): # pylint: disable=unused-argument
return helpers.translate_bool(val)
def oom_score_adj(val, **kwargs): # pylint: disable=unused-argument
return helpers.translate_int(val)
def pid_mode(val, **kwargs): # pylint: disable=unused-argument
return helpers.translate_str(val)
def pids_limit(val, **kwargs): # pylint: disable=unused-argument
return helpers.translate_int(val)
def port_bindings(val, **kwargs):
'''
On the CLI, these are passed as multiple instances of a given CLI option.
In Salt, we accept these as a comma-delimited list but the API expects a
Python dictionary mapping ports to their bindings. The format the API
expects is complicated depending on whether or not the external port maps
to a different internal port, or if the port binding is for UDP instead of
TCP (the default). For reference, see the "Port bindings" section in the
docker-py documentation at the following URL:
http://docker-py.readthedocs.io/en/stable/api.html
'''
validate_ip_addrs = kwargs.get('validate_ip_addrs', True)
if not isinstance(val, dict):
if not isinstance(val, list):
try:
val = helpers.split(val)
except AttributeError:
val = helpers.split(six.text_type(val))
for idx in range(len(val)):
if not isinstance(val[idx], six.string_types):
val[idx] = six.text_type(val[idx])
def _format_port(port_num, proto):
return six.text_type(port_num) + '/udp' if proto.lower() == 'udp' else port_num
bindings = {}
for binding in val:
bind_parts = helpers.split(binding, ':')
num_bind_parts = len(bind_parts)
if num_bind_parts == 1:
# Single port or port range being passed through (no
# special mapping)
container_port = six.text_type(bind_parts[0])
if container_port == '':
raise SaltInvocationError(
'Empty port binding definition found'
)
container_port, _, proto = container_port.partition('/')
try:
start, end = helpers.get_port_range(container_port)
except ValueError as exc:
# Using __str__() to avoid deprecation warning for using
# the message attribute of the ValueError.
raise SaltInvocationError(exc.__str__())
bind_vals = [
(_format_port(port_num, proto), None)
for port_num in range(start, end + 1)
]
elif num_bind_parts == 2:
if bind_parts[0] == '':
raise SaltInvocationError(
'Empty host port in port binding definition '
'\'{0}\''.format(binding)
)
if bind_parts[1] == '':
raise SaltInvocationError(
'Empty container port in port binding definition '
'\'{0}\''.format(binding)
)
container_port, _, proto = bind_parts[1].partition('/')
try:
cport_start, cport_end = \
helpers.get_port_range(container_port)
hport_start, hport_end = \
helpers.get_port_range(bind_parts[0])
except ValueError as exc:
# Using __str__() to avoid deprecation warning for
# using the message attribute of the ValueError.
raise SaltInvocationError(exc.__str__())
if (hport_end - hport_start) != (cport_end - cport_start):
# Port range is mismatched
raise SaltInvocationError(
'Host port range ({0}) does not have the same '
'number of ports as the container port range '
'({1})'.format(bind_parts[0], container_port)
)
cport_list = list(range(cport_start, cport_end + 1))
hport_list = list(range(hport_start, hport_end + 1))
bind_vals = [
(_format_port(cport_list[x], proto), hport_list[x])
for x in range(len(cport_list))
]
elif num_bind_parts == 3:
host_ip, host_port = bind_parts[0:2]
if validate_ip_addrs:
helpers.validate_ip(host_ip)
container_port, _, proto = bind_parts[2].partition('/')
try:
cport_start, cport_end = \
helpers.get_port_range(container_port)
except ValueError as exc:
# Using __str__() to avoid deprecation warning for
# using the message attribute of the ValueError.
raise SaltInvocationError(exc.__str__())
cport_list = list(range(cport_start, cport_end + 1))
if host_port == '':
hport_list = [None] * len(cport_list)
else:
try:
hport_start, hport_end = \
helpers.get_port_range(host_port)
except ValueError as exc:
# Using __str__() to avoid deprecation warning for
# using the message attribute of the ValueError.
raise SaltInvocationError(exc.__str__())
hport_list = list(range(hport_start, hport_end + 1))
if (hport_end - hport_start) != (cport_end - cport_start):
# Port range is mismatched
raise SaltInvocationError(
'Host port range ({0}) does not have the same '
'number of ports as the container port range '
'({1})'.format(host_port, container_port)
)
bind_vals = [(
_format_port(val, proto),
(host_ip,) if hport_list[idx] is None
else (host_ip, hport_list[idx])
) for idx, val in enumerate(cport_list)]
else:
raise SaltInvocationError(
'\'{0}\' is an invalid port binding definition (at most '
'3 components are allowed, found {1})'.format(
binding, num_bind_parts
)
)
for cport, bind_def in bind_vals:
if cport not in bindings:
bindings[cport] = bind_def
else:
if isinstance(bindings[cport], list):
# Append to existing list of bindings for this
# container port.
bindings[cport].append(bind_def)
else:
bindings[cport] = [bindings[cport], bind_def]
for idx in range(len(bindings[cport])):
if bindings[cport][idx] is None:
# Now that we are adding multiple
# bindings
try:
# Convert 1234/udp to 1234
bindings[cport][idx] = int(cport.split('/')[0])
except AttributeError:
# Port was tcp, the AttributeError
# signifies that the split failed
# because the port number was
# already defined as an integer.
# Just use the cport.
bindings[cport][idx] = cport
val = bindings
return val
def ports(val, **kwargs): # pylint: disable=unused-argument
'''
Like cap_add, cap_drop, etc., this option can be specified multiple times,
and each time can be a port number or port range. Ultimately, the API
expects a list, but elements in the list are ints when the port is TCP, and
a tuple (port_num, 'udp') when the port is UDP.
'''
if not isinstance(val, list):
try:
val = helpers.split(val)
except AttributeError:
if isinstance(val, six.integer_types):
val = [val]
else:
raise SaltInvocationError(
'\'{0}\' is not a valid port definition'.format(val)
)
new_ports = set()
for item in val:
if isinstance(item, six.integer_types):
new_ports.add(item)
continue
try:
item, _, proto = item.partition('/')
except AttributeError:
raise SaltInvocationError(
'\'{0}\' is not a valid port definition'.format(item)
)
try:
range_start, range_end = \
helpers.get_port_range(item)
except ValueError as exc:
# Using __str__() to avoid deprecation warning for using
# the "message" attribute of the ValueError.
raise SaltInvocationError(exc.__str__())
new_ports.update([helpers.get_port_def(x, proto)
for x in range(range_start, range_end + 1)])
return list(new_ports)
def privileged(val, **kwargs): # pylint: disable=unused-argument
return helpers.translate_bool(val)
def publish_all_ports(val, **kwargs): # pylint: disable=unused-argument
return helpers.translate_bool(val)
def read_only(val, **kwargs): # pylint: disable=unused-argument
return helpers.translate_bool(val)
def restart_policy(val, **kwargs): # pylint: disable=unused-argument
'''
CLI input is in the format NAME[:RETRY_COUNT] but the API expects {'Name':
name, 'MaximumRetryCount': retry_count}. We will use the 'fill' kwarg here
to make sure the mapped result uses '0' for the count if this optional
value was omitted.
'''
val = helpers.map_vals(
val,
'Name',
'MaximumRetryCount',
fill='0')
# map_vals() converts the input into a list of dicts, but the API
# wants just a dict, so extract the value from the single-element
# list. If there was more than one element in the list, then
# invalid input was passed (i.e. a comma-separated list, when what
# we wanted was a single value).
if len(val) != 1:
raise SaltInvocationError('Only one policy is permitted')
val = val[0]
try:
# The count needs to be an integer
val['MaximumRetryCount'] = int(val['MaximumRetryCount'])
except (TypeError, ValueError):
# Non-numeric retry count passed
raise SaltInvocationError(
'Retry count \'{0}\' is non-numeric'.format(val['MaximumRetryCount'])
)
return val
def security_opt(val, **kwargs): # pylint: disable=unused-argument
return helpers.translate_stringlist(val)
def shm_size(val, **kwargs): # pylint: disable=unused-argument
return helpers.translate_bytes(val)
def stdin_open(val, **kwargs): # pylint: disable=unused-argument
return helpers.translate_bool(val)
def stop_signal(val, **kwargs): # pylint: disable=unused-argument
return helpers.translate_str(val)
def stop_timeout(val, **kwargs): # pylint: disable=unused-argument
return helpers.translate_int(val)
def storage_opt(val, **kwargs): # pylint: disable=unused-argument
return helpers.translate_key_val(val, delimiter='=')
def sysctls(val, **kwargs): # pylint: disable=unused-argument
return helpers.translate_key_val(val, delimiter='=')
def tmpfs(val, **kwargs): # pylint: disable=unused-argument
return helpers.translate_dict(val)
def tty(val, **kwargs): # pylint: disable=unused-argument
return helpers.translate_bool(val)
def ulimits(val, **kwargs): # pylint: disable=unused-argument
val = helpers.translate_stringlist(val)
for idx in range(len(val)):
if not isinstance(val[idx], dict):
try:
ulimit_name, limits = \
helpers.split(val[idx], '=', 1)
comps = helpers.split(limits, ':', 1)
except (AttributeError, ValueError):
raise SaltInvocationError(
'Ulimit definition \'{0}\' is not in the format '
'type=soft_limit[:hard_limit]'.format(val[idx])
)
if len(comps) == 1:
comps *= 2
soft_limit, hard_limit = comps
try:
val[idx] = {'Name': ulimit_name,
'Soft': int(soft_limit),
'Hard': int(hard_limit)}
except (TypeError, ValueError):
raise SaltInvocationError(
'Limit \'{0}\' contains non-numeric value(s)'.format(
val[idx]
)
)
return val
def user(val, **kwargs): # pylint: disable=unused-argument
'''
This can be either a string or a numeric uid
'''
if not isinstance(val, six.integer_types):
# Try to convert to integer. This will fail if the value is a
# username. This is OK, as we check below to make sure that the
# value is either a string or integer. Trying to convert to an
# integer first though will allow us to catch the edge case in
# which a quoted uid is passed (e.g. '1000').
try:
val = int(val)
except (TypeError, ValueError):
pass
if not isinstance(val, (six.integer_types, six.string_types)):
raise SaltInvocationError('Value must be a username or uid')
elif isinstance(val, six.integer_types) and val < 0:
raise SaltInvocationError('\'{0}\' is an invalid uid'.format(val))
return val
def userns_mode(val, **kwargs): # pylint: disable=unused-argument
return helpers.translate_str(val)
def volume_driver(val, **kwargs): # pylint: disable=unused-argument
return helpers.translate_str(val)
def volumes(val, **kwargs): # pylint: disable=unused-argument
'''
Should be a list of absolute paths
'''
val = helpers.translate_stringlist(val)
for item in val:
if not os.path.isabs(item):
raise SaltInvocationError(
'\'{0}\' is not an absolute path'.format(item)
)
return val
def volumes_from(val, **kwargs): # pylint: disable=unused-argument
return helpers.translate_stringlist(val)
def working_dir(val, **kwargs): # pylint: disable=unused-argument
'''
Must be an absolute path
'''
try:
is_abs = os.path.isabs(val)
except AttributeError:
is_abs = False
if not is_abs:
raise SaltInvocationError('\'{0}\' is not an absolute path'.format(val))
return val