community.general/lib/ansible/modules/cloud/docker/docker_swarm_service.py
Rich Wareham fd32760d7a docker_swarm_service: use exact name match when finding services (#50665)
* docker_swarm_service: use exact name match when finding services

The Docker API's filtering support allows filtering for substring
matches which means that when we filter the list of running services we
may accidentally match a service called "foobar" when looking for a
service named "foo".

Fix this by filtering the list of services returned from the Docker API
so that name matches are exact. It is still worth passing the filter
parameter to the Docker API because it reduces the number of results
passed back which may be important for remote Docker connections.

Closes 50654.

* add changelog fragment for #50654
2019-01-14 12:00:34 +01:00

1206 lines
46 KiB
Python

#!/usr/bin/python
#
# (c) 2017, Dario Zanzico (git@dariozanzico.com)
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
ANSIBLE_METADATA = {'status': ['preview'],
'supported_by': 'community',
'metadata_version': '1.1'}
DOCUMENTATION = '''
---
module: docker_swarm_service
author: "Dario Zanzico (@dariko), Jason Witkowski (@jwitko)"
short_description: docker swarm service
description: |
Manage docker services. Allows live altering of already defined services
version_added: "2.7"
options:
name:
required: true
description:
- Service name
image:
required: true
description:
- Service image path and tag.
Maps docker service IMAGE parameter.
state:
required: true
default: present
description:
- Service state.
choices:
- present
- absent
args:
required: false
default: []
description:
- List comprised of the command and the arguments to be run inside
- the container
constraints:
required: false
default: []
description:
- List of the service constraints.
- Maps docker service --constraint option.
hostname:
required: false
default: ""
description:
- Container hostname
- Maps docker service --hostname option.
- Requires api_version >= 1.25
tty:
required: false
type: bool
default: False
description:
- Allocate a pseudo-TTY
- Maps docker service --tty option.
- Requires api_version >= 1.25
dns:
required: false
default: []
description:
- List of custom DNS servers.
- Maps docker service --dns option.
- Requires api_version >= 1.25
dns_search:
required: false
default: []
description:
- List of custom DNS search domains.
- Maps docker service --dns-search option.
- Requires api_version >= 1.25
dns_options:
required: false
default: []
description:
- List of custom DNS options.
- Maps docker service --dns-option option.
- Requires api_version >= 1.25
force_update:
required: false
type: bool
default: False
description:
- Force update even if no changes require it.
- Maps to docker service update --force option.
- Requires api_version >= 1.25
labels:
required: false
description:
- List of the service labels.
- Maps docker service --label option.
container_labels:
required: false
description:
- List of the service containers labels.
- Maps docker service --container-label option.
default: []
endpoint_mode:
required: false
description:
- Service endpoint mode.
- Maps docker service --endpoint-mode option.
default: vip
choices:
- vip
- dnsrr
env:
required: false
default: []
description:
- List of the service environment variables.
- Maps docker service --env option.
log_driver:
required: false
default: json-file
description:
- Configure the logging driver for a service
log_driver_options:
required: false
default: []
description:
- Options for service logging driver
limit_cpu:
required: false
default: 0.000
description:
- Service CPU limit. 0 equals no limit.
- Maps docker service --limit-cpu option.
reserve_cpu:
required: false
default: 0.000
description:
- Service CPU reservation. 0 equals no reservation.
- Maps docker service --reserve-cpu option.
limit_memory:
required: false
default: 0
description:
- Service memory limit in MB. 0 equals no limit.
- Maps docker service --limit-memory option.
reserve_memory:
required: false
default: 0
description:
- Service memory reservation in MB. 0 equals no reservation.
- Maps docker service --reserve-memory option.
mode:
required: false
default: replicated
description:
- Service replication mode.
- Maps docker service --mode option.
mounts:
required: false
description:
- List of dictionaries describing the service mounts.
- Every item must be a dictionary exposing the keys source, target, type (defaults to 'bind'), readonly (defaults to false)
- Maps docker service --mount option.
default: []
secrets:
required: false
description:
- List of dictionaries describing the service secrets.
- Every item must be a dictionary exposing the keys secret_id, secret_name, filename, uid (defaults to 0), gid (defaults to 0), mode (defaults to 0o444)
- Maps docker service --secret option.
default: []
configs:
required: false
description:
- List of dictionaries describing the service configs.
- Every item must be a dictionary exposing the keys config_id, config_name, filename, uid (defaults to 0), gid (defaults to 0), mode (defaults to 0o444)
- Maps docker service --config option.
default: null
networks:
required: false
default: []
description:
- List of the service networks names.
- Maps docker service --network option.
publish:
default: []
required: false
description:
- List of dictionaries describing the service published ports.
- Every item must be a dictionary exposing the keys published_port, target_port, protocol (defaults to 'tcp')
- Only used with api_version >= 1.25
- If api_version >= 1.32 and docker python library >= 3.0.0 attribute 'mode' can be set to 'ingress' or 'host' (default 'ingress').
replicas:
required: false
default: -1
description:
- Number of containers instantiated in the service. Valid only if ``mode=='replicated'``.
- If set to -1, and service is not present, service replicas will be set to 1.
- If set to -1, and service is present, service replicas will be unchanged.
- Maps docker service --replicas option.
restart_policy:
required: false
default: none
description:
- Restart condition of the service.
- Maps docker service --restart-condition option.
choices:
- none
- on-failure
- any
restart_policy_attempts:
required: false
default: 0
description:
- Maximum number of service restarts.
- Maps docker service --restart-max-attempts option.
restart_policy_delay:
required: false
default: 0
description:
- Delay between restarts.
- Maps docker service --restart-delay option.
restart_policy_window:
required: false
default: 0
description:
- Restart policy evaluation window.
- Maps docker service --restart-window option.
update_delay:
required: false
default: 10
description:
- Rolling update delay
- Maps docker service --update-delay option
update_parallelism:
required: false
default: 1
description:
- Rolling update parallelism
- Maps docker service --update-parallelism option
update_failure_action:
required: false
default: continue
description:
- Action to take in case of container failure
- Maps to docker service --update-failure-action option
choices:
- continue
- pause
update_monitor:
required: false
default: 5000000000
description:
- Time to monitor updated tasks for failures, in nanoseconds.
- Maps to docker service --update-monitor option
update_max_failure_ratio:
required: false
default: 0.00
description:
- Fraction of tasks that may fail during an update before the failure action is invoked
- Maps to docker service --update-max-failure-ratio
update_order:
required: false
default: null
description:
- Specifies the order of operations when rolling out an updated task.
- Maps to docker service --update-order
- Requires docker api version >= 1.29
user:
required: false
default: root
description:
- username or UID.
- "If set to C(null) the image provided value (or the one already
set for the service) will be used"
extends_documentation_fragment:
- docker
requirements:
- "docker-py >= 2.0"
- "Please note that the L(docker-py,https://pypi.org/project/docker-py/) 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).
Version 2.1.0 or newer is only available with the C(docker) module."
'''
RETURN = '''
ansible_swarm_service:
returned: always
type: dict
description:
- Dictionary of variables representing the current state of the service.
Matches the module parameters format.
- Note that facts are not part of registered vars but accessible directly.
sample: '{
"args": [
"sleep",
"3600"
],
"constraints": [],
"container_labels": {},
"endpoint_mode": "vip",
"env": [
"ENVVAR1=envvar1"
],
"force_update": False,
"image": "alpine",
"labels": {},
"limit_cpu": 0.0,
"limit_memory": 0,
"log_driver": "json-file",
"log_driver_options": {},
"mode": "replicated",
"mounts": [
{
"source": "/tmp/",
"target": "/remote_tmp/",
"type": "bind"
}
],
"secrets": [],
"configs": [],
"networks": [],
"publish": [],
"replicas": 1,
"reserve_cpu": 0.0,
"reserve_memory": 0,
"restart_policy": "any",
"restart_policy_attempts": 5,
"restart_policy_delay": 0,
"restart_policy_window": 30,
"update_delay": 10,
"update_parallelism": 1,
"update_failure_action": "continue",
"update_monitor": 5000000000
"update_max_failure_ratio": 0,
"update_order": "stop-first"
}'
changes:
returned: always
description:
- List of changed service attributes if a service has been altered,
[] otherwhise
type: list
sample: ['container_labels', 'replicas']
rebuilt:
returned: always
description:
- True if the service has been recreated (removed and created)
type: bool
sample: True
'''
EXAMPLES = '''
- name: define myservice
docker_swarm_service:
name: myservice
image: "alpine"
args:
- "sleep"
- "3600"
mounts:
- source: /tmp/
target: /remote_tmp/
type: bind
env:
- "ENVVAR1=envvar1"
log_driver: fluentd
log_driver_options:
fluentd-address: "127.0.0.1:24224"
fluentd-async-connect: true
tag: "{{.Name}}/{{.ID}}"
restart_policy: any
restart_policy_attempts: 5
restart_policy_window: 30
register: dss_out1
- name: change myservice.env
docker_swarm_service:
name: myservice
image: "alpine"
args:
- "sleep"
- "7200"
mounts:
- source: /tmp/
target: /remote_tmp/
type: bind
env:
- "ENVVAR1=envvar1"
restart_policy: any
restart_policy_attempts: 5
restart_policy_window: 30
register: dss_out2
- name: test for changed myservice facts
fail:
msg: unchanged service
when: "{{ dss_out1 == dss_out2 }}"
- name: change myservice.image
docker_swarm_service:
name: myservice
image: "alpine:edge"
args:
- "sleep"
- "7200"
mounts:
- source: /tmp/
target: /remote_tmp/
type: bind
env:
- "ENVVAR1=envvar1"
restart_policy: any
restart_policy_attempts: 5
restart_policy_window: 30
register: dss_out3
- name: test for changed myservice facts
fail:
msg: unchanged service
when: "{{ dss_out2 == dss_out3 }}"
- name: remove mount
docker_swarm_service:
name: myservice
image: "alpine:edge"
args:
- "sleep"
- "7200"
env:
- "ENVVAR1=envvar1"
restart_policy: any
restart_policy_attempts: 5
restart_policy_window: 30
register: dss_out4
- name: test for changed myservice facts
fail:
msg: unchanged service
when: "{{ dss_out3 == dss_out4 }}"
- name: keep service as it is
docker_swarm_service:
name: myservice
image: "alpine:edge"
args:
- "sleep"
- "7200"
env:
- "ENVVAR1=envvar1"
restart_policy: any
restart_policy_attempts: 5
restart_policy_window: 30
register: dss_out5
- name: test for changed service facts
fail:
msg: changed service
when: "{{ dss_out5 != dss_out5 }}"
- name: remove myservice
docker_swarm_service:
name: myservice
state: absent
'''
import time
from ansible.module_utils.docker_common import (
DockerBaseClass,
AnsibleDockerClient,
docker_version,
DifferenceTracker,
)
from ansible.module_utils.basic import human_to_bytes
from ansible.module_utils._text import to_text
try:
from distutils.version import LooseVersion
from docker import types
except Exception:
# missing docker-py handled in ansible.module_utils.docker
pass
class DockerService(DockerBaseClass):
def __init__(self):
super(DockerService, self).__init__()
self.constraints = []
self.image = ""
self.args = []
self.endpoint_mode = "vip"
self.dns = []
self.hostname = ""
self.tty = False
self.dns_search = []
self.dns_options = []
self.env = []
self.force_update = None
self.log_driver = "json-file"
self.log_driver_options = {}
self.labels = {}
self.container_labels = {}
self.limit_cpu = 0.000
self.limit_memory = 0
self.reserve_cpu = 0.000
self.reserve_memory = 0
self.mode = "replicated"
self.user = "root"
self.mounts = []
self.configs = None
self.secrets = []
self.constraints = []
self.networks = []
self.publish = []
self.replicas = -1
self.service_id = False
self.service_version = False
self.restart_policy = None
self.restart_policy_attempts = None
self.restart_policy_delay = None
self.restart_policy_window = None
self.update_delay = None
self.update_parallelism = 1
self.update_failure_action = "continue"
self.update_monitor = 5000000000
self.update_max_failure_ratio = 0.00
self.update_order = None
def get_facts(self):
return {
'image': self.image,
'mounts': self.mounts,
'configs': self.configs,
'networks': self.networks,
'args': self.args,
'tty': self.tty,
'dns': self.dns,
'dns_search': self.dns_search,
'dns_options': self.dns_options,
'hostname': self.hostname,
'env': self.env,
'force_update': self.force_update,
'log_driver': self.log_driver,
'log_driver_options': self.log_driver_options,
'publish': self.publish,
'constraints': self.constraints,
'labels': self.labels,
'container_labels': self.container_labels,
'mode': self.mode,
'replicas': self.replicas,
'endpoint_mode': self.endpoint_mode,
'restart_policy': self.restart_policy,
'limit_cpu': self.limit_cpu,
'limit_memory': self.limit_memory,
'reserve_cpu': self.reserve_cpu,
'reserve_memory': self.reserve_memory,
'restart_policy_delay': self.restart_policy_delay,
'restart_policy_attempts': self.restart_policy_attempts,
'restart_policy_window': self.restart_policy_window,
'update_delay': self.update_delay,
'update_parallelism': self.update_parallelism,
'update_failure_action': self.update_failure_action,
'update_monitor': self.update_monitor,
'update_max_failure_ratio': self.update_max_failure_ratio,
'update_order': self.update_order}
@staticmethod
def from_ansible_params(ap, old_service):
s = DockerService()
s.constraints = ap['constraints']
s.image = ap['image']
s.args = ap['args']
s.endpoint_mode = ap['endpoint_mode']
s.dns = ap['dns']
s.dns_search = ap['dns_search']
s.dns_options = ap['dns_options']
s.hostname = ap['hostname']
s.tty = ap['tty']
s.env = ap['env']
s.log_driver = ap['log_driver']
s.log_driver_options = ap['log_driver_options']
s.labels = ap['labels']
s.container_labels = ap['container_labels']
s.limit_cpu = ap['limit_cpu']
s.reserve_cpu = ap['reserve_cpu']
s.mode = ap['mode']
s.networks = ap['networks']
s.restart_policy = ap['restart_policy']
s.restart_policy_attempts = ap['restart_policy_attempts']
s.restart_policy_delay = ap['restart_policy_delay']
s.restart_policy_window = ap['restart_policy_window']
s.update_delay = ap['update_delay']
s.update_parallelism = ap['update_parallelism']
s.update_failure_action = ap['update_failure_action']
s.update_monitor = ap['update_monitor']
s.update_max_failure_ratio = ap['update_max_failure_ratio']
s.update_order = ap['update_order']
s.user = ap['user']
if ap['force_update']:
s.force_update = int(str(time.time()).replace('.', ''))
if ap['replicas'] == -1:
if old_service:
s.replicas = old_service.replicas
else:
s.replicas = 1
else:
s.replicas = ap['replicas']
for param_name in ['reserve_memory', 'limit_memory']:
if ap.get(param_name):
try:
setattr(s, param_name, human_to_bytes(ap[param_name]))
except ValueError as exc:
raise Exception("Failed to convert %s to bytes: %s" % (param_name, exc))
s.publish = []
for param_p in ap['publish']:
service_p = {}
service_p['protocol'] = param_p.get('protocol', 'tcp')
service_p['mode'] = param_p.get('mode', None)
service_p['published_port'] = int(param_p['published_port'])
service_p['target_port'] = int(param_p['target_port'])
if service_p['protocol'] not in ['tcp', 'udp']:
raise ValueError("got publish.protocol '%s', valid values:'tcp', 'udp'" %
service_p['protocol'])
if service_p['mode'] not in [None, 'ingress', 'host']:
raise ValueError("got publish.mode '%s', valid values:'ingress', 'host'" %
service_p['mode'])
s.publish.append(service_p)
s.mounts = []
for param_m in ap['mounts']:
service_m = {}
service_m['readonly'] = bool(param_m.get('readonly', False))
service_m['type'] = param_m.get('type', 'bind')
service_m['source'] = param_m['source']
service_m['target'] = param_m['target']
s.mounts.append(service_m)
s.configs = None
if ap['configs']:
s.configs = []
for param_m in ap['configs']:
service_c = {}
service_c['config_id'] = param_m['config_id']
service_c['config_name'] = str(param_m['config_name'])
service_c['filename'] = param_m.get('filename', service_c['config_name'])
service_c['uid'] = int(param_m.get('uid', "0"))
service_c['gid'] = int(param_m.get('gid', "0"))
service_c['mode'] = param_m.get('mode', 0o444)
s.configs.append(service_c)
s.secrets = []
for param_m in ap['secrets']:
service_s = {}
service_s['secret_id'] = param_m['secret_id']
service_s['secret_name'] = str(param_m['secret_name'])
service_s['filename'] = param_m.get('filename', service_s['secret_name'])
service_s['uid'] = int(param_m.get('uid', "0"))
service_s['gid'] = int(param_m.get('gid', "0"))
service_s['mode'] = param_m.get('mode', 0o444)
s.secrets.append(service_s)
return s
def compare(self, os):
differences = DifferenceTracker()
needs_rebuild = False
force_update = False
if self.endpoint_mode != os.endpoint_mode:
differences.add('endpoint_mode', parameter=self.endpoint_mode, active=os.endpoint_mode)
if self.env != os.env:
differences.add('env', parameter=self.env, active=os.env)
if self.log_driver != os.log_driver:
differences.add('log_driver', parameter=self.log_driver, active=os.log_driver)
if self.log_driver_options != os.log_driver_options:
differences.add('log_opt', parameter=self.log_driver_options, active=os.log_driver_options)
if self.mode != os.mode:
needs_rebuild = True
differences.add('mode', parameter=self.mode, active=os.mode)
if self.mounts != os.mounts:
differences.add('mounts', parameter=self.mounts, active=os.mounts)
if self.configs != os.configs:
differences.add('configs', parameter=self.configs, active=os.configs)
if self.secrets != os.secrets:
differences.add('secrets', parameter=self.secrets, active=os.secrets)
if self.networks != os.networks:
differences.add('networks', parameter=self.networks, active=os.networks)
needs_rebuild = True
if self.replicas != os.replicas:
differences.add('replicas', parameter=self.replicas, active=os.replicas)
if self.args != os.args:
differences.add('args', parameter=self.args, active=os.args)
if self.constraints != os.constraints:
differences.add('constraints', parameter=self.constraints, active=os.constraints)
if self.labels != os.labels:
differences.add('labels', parameter=self.labels, active=os.labels)
if self.limit_cpu != os.limit_cpu:
differences.add('limit_cpu', parameter=self.limit_cpu, active=os.limit_cpu)
if self.limit_memory != os.limit_memory:
differences.add('limit_memory', parameter=self.limit_memory, active=os.limit_memory)
if self.reserve_cpu != os.reserve_cpu:
differences.add('reserve_cpu', parameter=self.reserve_cpu, active=os.reserve_cpu)
if self.reserve_memory != os.reserve_memory:
differences.add('reserve_memory', parameter=self.reserve_memory, active=os.reserve_memory)
if self.container_labels != os.container_labels:
differences.add('container_labels', parameter=self.container_labels, active=os.container_labels)
if self.publish != os.publish:
differences.add('publish', parameter=self.publish, active=os.publish)
if self.restart_policy != os.restart_policy:
differences.add('restart_policy', parameter=self.restart_policy, active=os.restart_policy)
if self.restart_policy_attempts != os.restart_policy_attempts:
differences.add('restart_policy_attempts', parameter=self.restart_policy_attempts, active=os.restart_policy_attempts)
if self.restart_policy_delay != os.restart_policy_delay:
differences.add('restart_policy_delay', parameter=self.restart_policy_delay, active=os.restart_policy_delay)
if self.restart_policy_window != os.restart_policy_window:
differences.add('restart_policy_window', parameter=self.restart_policy_window, active=os.restart_policy_window)
if self.update_delay != os.update_delay:
differences.add('update_delay', parameter=self.update_delay, active=os.update_delay)
if self.update_parallelism != os.update_parallelism:
differences.add('update_parallelism', parameter=self.update_parallelism, active=os.update_parallelism)
if self.update_failure_action != os.update_failure_action:
differences.add('update_failure_action', parameter=self.update_failure_action, active=os.update_failure_action)
if self.update_monitor != os.update_monitor:
differences.add('update_monitor', parameter=self.update_monitor, active=os.update_monitor)
if self.update_max_failure_ratio != os.update_max_failure_ratio:
differences.add('update_max_failure_ratio', parameter=self.update_max_failure_ratio, active=os.update_max_failure_ratio)
if self.update_order is not None and self.update_order != os.update_order:
differences.add('update_order', parameter=self.update_order, active=os.update_order)
if self.image != os.image.split('@')[0]:
differences.add('image', parameter=self.image, active=os.image.split('@')[0])
if self.user and self.user != os.user:
differences.add('user', parameter=self.user, active=os.user)
if self.dns != os.dns:
differences.add('dns', parameter=self.dns, active=os.dns)
if self.dns_search != os.dns_search:
differences.add('dns_search', parameter=self.dns_search, active=os.dns_search)
if self.dns_options != os.dns_options:
differences.add('dns_options', parameter=self.dns_options, active=os.dns_options)
if self.hostname != os.hostname:
differences.add('hostname', parameter=self.hostname, active=os.hostname)
if self.tty != os.tty:
differences.add('tty', parameter=self.tty, active=os.tty)
if self.force_update:
force_update = True
return not differences.empty or force_update, differences, needs_rebuild, force_update
def __str__(self):
return str({
'mode': self.mode,
'env': self.env,
'endpoint_mode': self.endpoint_mode,
'mounts': self.mounts,
'configs': self.configs,
'secrets': self.secrets,
'networks': self.networks,
'replicas': self.replicas})
def generate_docker_py_service_description(self, name, docker_networks):
mounts = []
for mount_config in self.mounts:
mounts.append(
types.Mount(target=mount_config['target'],
source=mount_config['source'],
type=mount_config['type'],
read_only=mount_config['readonly'])
)
configs = None
if self.configs:
configs = []
for config_config in self.configs:
configs.append(
types.ConfigReference(
config_id=config_config['config_id'],
config_name=config_config['config_name'],
filename=config_config.get('filename'),
uid=config_config.get('uid'),
gid=config_config.get('gid'),
mode=config_config.get('mode')
)
)
secrets = []
for secret_config in self.secrets:
secrets.append(
types.SecretReference(
secret_id=secret_config['secret_id'],
secret_name=secret_config['secret_name'],
filename=secret_config.get('filename'),
uid=secret_config.get('uid'),
gid=secret_config.get('gid'),
mode=secret_config.get('mode')
)
)
cspec = types.ContainerSpec(
image=self.image,
user=self.user,
dns_config=types.DNSConfig(nameservers=self.dns, search=self.dns_search, options=self.dns_options),
args=self.args,
env=self.env,
tty=self.tty,
hostname=self.hostname,
labels=self.container_labels,
mounts=mounts,
secrets=secrets,
configs=configs
)
log_driver = types.DriverConfig(name=self.log_driver, options=self.log_driver_options)
placement = types.Placement(constraints=self.constraints)
restart_policy = types.RestartPolicy(
condition=self.restart_policy,
delay=self.restart_policy_delay,
max_attempts=self.restart_policy_attempts,
window=self.restart_policy_window)
resources = types.Resources(
cpu_limit=int(self.limit_cpu * 1000000000.0),
mem_limit=self.limit_memory,
cpu_reservation=int(self.reserve_cpu * 1000000000.0),
mem_reservation=self.reserve_memory
)
update_policy = types.UpdateConfig(
parallelism=self.update_parallelism,
delay=self.update_delay,
failure_action=self.update_failure_action,
monitor=self.update_monitor,
max_failure_ratio=self.update_max_failure_ratio,
order=self.update_order
)
task_template = types.TaskTemplate(
container_spec=cspec,
log_driver=log_driver,
restart_policy=restart_policy,
placement=placement,
resources=resources,
force_update=self.force_update)
if self.mode == 'global':
self.replicas = None
mode = types.ServiceMode(self.mode, replicas=self.replicas)
networks = []
for network_name in self.networks:
network_id = None
try:
network_id = list(filter(lambda n: n['name'] == network_name, docker_networks))[0]['id']
except Exception:
pass
if network_id:
networks.append({'Target': network_id})
else:
raise Exception("no docker networks named: %s" % network_name)
ports = {}
for port in self.publish:
if port['mode']:
ports[int(port['published_port'])] = (int(port['target_port']), port['protocol'], port['mode'])
else:
ports[int(port['published_port'])] = (int(port['target_port']), port['protocol'])
endpoint_spec = types.EndpointSpec(mode=self.endpoint_mode, ports=ports)
return update_policy, task_template, networks, endpoint_spec, mode, self.labels
# def fail(self, msg):
# self.parameters.client.module.fail_json(msg=msg)
#
# @property
# def exists(self):
# return True if self.service else False
class DockerServiceManager():
def get_networks_names_ids(self):
return [{'name': n['Name'], 'id': n['Id']} for n in self.client.networks()]
def get_service(self, name):
# The Docker API allows filtering services by name but the filter looks
# for a substring match, not an exact match. (Filtering for "foo" would
# return information for services "foobar" and "foobuzz" even if the
# service "foo" doesn't exist.) Avoid incorrectly determining that a
# service is present by filtering the list of services returned from the
# Docker API so that the name must be an exact match.
raw_data = [
service for service in self.client.services(filters={'name': name})
if service['Spec']['Name'] == name
]
if len(raw_data) == 0:
return None
raw_data = raw_data[0]
networks_names_ids = self.get_networks_names_ids()
ds = DockerService()
task_template_data = raw_data['Spec']['TaskTemplate']
update_config_data = raw_data['Spec']['UpdateConfig']
ds.image = task_template_data['ContainerSpec']['Image']
ds.user = task_template_data['ContainerSpec'].get('User', 'root')
ds.env = task_template_data['ContainerSpec'].get('Env', [])
ds.args = task_template_data['ContainerSpec'].get('Args', [])
ds.update_delay = update_config_data['Delay']
ds.update_parallelism = update_config_data['Parallelism']
ds.update_failure_action = update_config_data['FailureAction']
ds.update_monitor = update_config_data['Monitor']
ds.update_max_failure_ratio = update_config_data['MaxFailureRatio']
if 'Order' in update_config_data:
ds.update_order = update_config_data['Order']
dns_config = task_template_data['ContainerSpec'].get('DNSConfig', None)
if dns_config:
if 'Nameservers' in dns_config.keys():
ds.dns = dns_config['Nameservers']
if 'Search' in dns_config.keys():
ds.dns_search = dns_config['Search']
if 'Options' in dns_config.keys():
ds.dns_options = dns_config['Options']
ds.hostname = task_template_data['ContainerSpec'].get('Hostname', '')
ds.tty = task_template_data['ContainerSpec'].get('TTY', False)
if 'Placement' in task_template_data.keys():
ds.constraints = task_template_data['Placement'].get('Constraints', [])
restart_policy_data = task_template_data.get('RestartPolicy', None)
if restart_policy_data:
ds.restart_policy = restart_policy_data.get('Condition')
ds.restart_policy_delay = restart_policy_data.get('Delay')
ds.restart_policy_attempts = restart_policy_data.get('MaxAttempts')
ds.restart_policy_window = restart_policy_data.get('Window')
raw_data_endpoint = raw_data.get('Endpoint', None)
if raw_data_endpoint:
raw_data_endpoint_spec = raw_data_endpoint.get('Spec', None)
if raw_data_endpoint_spec:
ds.endpoint_mode = raw_data_endpoint_spec.get('Mode', 'vip')
for port in raw_data_endpoint_spec.get('Ports', []):
ds.publish.append({
'protocol': port['Protocol'],
'mode': port.get('PublishMode', None),
'published_port': int(port['PublishedPort']),
'target_port': int(port['TargetPort'])})
if 'Resources' in task_template_data.keys():
if 'Limits' in task_template_data['Resources'].keys():
if 'NanoCPUs' in task_template_data['Resources']['Limits'].keys():
ds.limit_cpu = float(task_template_data['Resources']['Limits']['NanoCPUs']) / 1000000000
if 'MemoryBytes' in task_template_data['Resources']['Limits'].keys():
ds.limit_memory = int(task_template_data['Resources']['Limits']['MemoryBytes'])
if 'Reservations' in task_template_data['Resources'].keys():
if 'NanoCPUs' in task_template_data['Resources']['Reservations'].keys():
ds.reserve_cpu = float(task_template_data['Resources']['Reservations']['NanoCPUs']) / 1000000000
if 'MemoryBytes' in task_template_data['Resources']['Reservations'].keys():
ds.reserve_memory = int(
task_template_data['Resources']['Reservations']['MemoryBytes'])
ds.labels = raw_data['Spec'].get('Labels', {})
if 'LogDriver' in task_template_data.keys():
ds.log_driver = task_template_data['LogDriver'].get('Name', 'json-file')
ds.log_driver_options = task_template_data['LogDriver'].get('Options', {})
ds.container_labels = task_template_data['ContainerSpec'].get('Labels', {})
mode = raw_data['Spec']['Mode']
if 'Replicated' in mode.keys():
ds.mode = to_text('replicated', encoding='utf-8')
ds.replicas = mode['Replicated']['Replicas']
elif 'Global' in mode.keys():
ds.mode = 'global'
else:
raise Exception("Unknown service mode: %s" % mode)
for mount_data in raw_data['Spec']['TaskTemplate']['ContainerSpec'].get('Mounts', []):
ds.mounts.append({
'source': mount_data['Source'],
'type': mount_data['Type'],
'target': mount_data['Target'],
'readonly': mount_data.get('ReadOnly', False)})
for config_data in raw_data['Spec']['TaskTemplate']['ContainerSpec'].get('Configs', []):
ds.configs.append({
'config_id': config_data['ConfigID'],
'config_name': config_data['ConfigName'],
'filename': config_data['File'].get('Name'),
'uid': int(config_data['File'].get('UID')),
'gid': int(config_data['File'].get('GID')),
'mode': config_data['File'].get('Mode')
})
for secret_data in raw_data['Spec']['TaskTemplate']['ContainerSpec'].get('Secrets', []):
ds.secrets.append({
'secret_id': secret_data['SecretID'],
'secret_name': secret_data['SecretName'],
'filename': secret_data['File'].get('Name'),
'uid': int(secret_data['File'].get('UID')),
'gid': int(secret_data['File'].get('GID')),
'mode': secret_data['File'].get('Mode')
})
networks_names_ids = self.get_networks_names_ids()
for raw_network_data in raw_data['Spec']['TaskTemplate'].get('Networks', raw_data['Spec'].get('Networks', [])):
network_name = [network_name_id['name'] for network_name_id in networks_names_ids if
network_name_id['id'] == raw_network_data['Target']]
if len(network_name) == 0:
ds.networks.append(raw_network_data['Target'])
else:
ds.networks.append(network_name[0])
ds.service_version = raw_data['Version']['Index']
ds.service_id = raw_data['ID']
return ds
def update_service(self, name, old_service, new_service):
update_policy, task_template, networks, endpoint_spec, mode, labels = new_service.generate_docker_py_service_description(
name, self.get_networks_names_ids())
self.client.update_service(
old_service.service_id,
old_service.service_version,
name=name,
endpoint_spec=endpoint_spec,
networks=networks,
mode=mode,
update_config=update_policy,
task_template=task_template,
labels=labels)
def create_service(self, name, service):
update_policy, task_template, networks, endpoint_spec, mode, labels = service.generate_docker_py_service_description(
name, self.get_networks_names_ids())
self.client.create_service(
name=name,
endpoint_spec=endpoint_spec,
mode=mode,
networks=networks,
update_config=update_policy,
task_template=task_template,
labels=labels)
def remove_service(self, name):
self.client.remove_service(name)
def __init__(self, client):
self.client = client
self.diff_tracker = DifferenceTracker()
def test_parameter_versions(self):
parameters_versions = [
{'param': 'dns', 'attribute': 'dns', 'min_version': '1.25'},
{'param': 'dns_options', 'attribute': 'dns_options', 'min_version': '1.25'},
{'param': 'dns_search', 'attribute': 'dns_search', 'min_version': '1.25'},
{'param': 'hostname', 'attribute': 'hostname', 'min_version': '1.25'},
{'param': 'tty', 'attribute': 'tty', 'min_version': '1.25'},
{'param': 'secrets', 'attribute': 'secrets', 'min_version': '1.25'},
{'param': 'configs', 'attribute': 'configs', 'min_version': '1.30'},
{'param': 'update_order', 'attribute': 'update_order', 'min_version': '1.29'}]
params = self.client.module.params
empty_service = DockerService()
for pv in parameters_versions:
if (params[pv['param']] != getattr(empty_service, pv['attribute']) and
(LooseVersion(self.client.version()['ApiVersion']) <
LooseVersion(pv['min_version']))):
self.client.module.fail_json(
msg=('%s parameter supported only with api_version>=%s'
% (pv['param'], pv['min_version'])))
for publish_def in self.client.module.params.get('publish', []):
if 'mode' in publish_def.keys():
if LooseVersion(self.client.version()['ApiVersion']) < LooseVersion('1.25'):
self.client.module.fail_json(msg='publish.mode parameter supported only with api_version>=1.25')
if LooseVersion(docker_version) < LooseVersion('3.0.0'):
self.client.module.fail_json(msg='publish.mode parameter requires docker python library>=3.0.0')
def run(self):
self.test_parameter_versions()
module = self.client.module
try:
current_service = self.get_service(module.params['name'])
except Exception as e:
return module.fail_json(
msg="Error looking for service named %s: %s" %
(module.params['name'], e))
try:
new_service = DockerService.from_ansible_params(module.params, current_service)
except Exception as e:
return module.fail_json(
msg="Error parsing module parameters: %s" % e)
changed = False
msg = 'noop'
rebuilt = False
differences = DifferenceTracker()
facts = {}
if current_service:
if module.params['state'] == 'absent':
if not module.check_mode:
self.remove_service(module.params['name'])
msg = 'Service removed'
changed = True
else:
changed, differences, need_rebuild, force_update = new_service.compare(current_service)
if changed:
self.diff_tracker.merge(differences)
if need_rebuild:
if not module.check_mode:
self.remove_service(module.params['name'])
self.create_service(module.params['name'],
new_service)
msg = 'Service rebuilt'
rebuilt = True
else:
if not module.check_mode:
self.update_service(module.params['name'],
current_service,
new_service)
msg = 'Service updated'
rebuilt = False
else:
if force_update:
if not module.check_mode:
self.update_service(module.params['name'],
current_service,
new_service)
msg = 'Service forcefully updated'
rebuilt = False
changed = True
else:
msg = 'Service unchanged'
facts = new_service.get_facts()
else:
if module.params['state'] == 'absent':
msg = 'Service absent'
else:
if not module.check_mode:
service_id = self.create_service(module.params['name'],
new_service)
msg = 'Service created'
changed = True
facts = new_service.get_facts()
return msg, changed, rebuilt, differences.get_legacy_docker_diffs(), facts
def main():
argument_spec = dict(
name=dict(required=True),
image=dict(type='str'),
state=dict(default="present", choices=['present', 'absent']),
mounts=dict(default=[], type='list'),
configs=dict(default=None, type='list'),
secrets=dict(default=[], type='list'),
networks=dict(default=[], type='list'),
args=dict(default=[], type='list'),
env=dict(default=[], type='list'),
force_update=dict(default=False, type='bool'),
log_driver=dict(default="json-file", type='str'),
log_driver_options=dict(default={}, type='dict'),
publish=dict(default=[], type='list'),
constraints=dict(default=[], type='list'),
tty=dict(default=False, type='bool'),
dns=dict(default=[], type='list'),
dns_search=dict(default=[], type='list'),
dns_options=dict(default=[], type='list'),
hostname=dict(default="", type='str'),
labels=dict(default={}, type='dict'),
container_labels=dict(default={}, type='dict'),
mode=dict(default="replicated"),
replicas=dict(default=-1, type='int'),
endpoint_mode=dict(default='vip', choices=['vip', 'dnsrr']),
restart_policy=dict(default='none', choices=['none', 'on-failure', 'any']),
limit_cpu=dict(default=0, type='float'),
limit_memory=dict(default=0, type='str'),
reserve_cpu=dict(default=0, type='float'),
reserve_memory=dict(default=0, type='str'),
restart_policy_delay=dict(default=0, type='int'),
restart_policy_attempts=dict(default=0, type='int'),
restart_policy_window=dict(default=0, type='int'),
update_delay=dict(default=10, type='int'),
update_parallelism=dict(default=1, type='int'),
update_failure_action=dict(default='continue', choices=['continue', 'pause']),
update_monitor=dict(default=5000000000, type='int'),
update_max_failure_ratio=dict(default=0, type='float'),
update_order=dict(default=None, type='str'),
user=dict(default='root'))
required_if = [
('state', 'present', ['image'])
]
client = AnsibleDockerClient(
argument_spec=argument_spec,
required_if=required_if,
supports_check_mode=True,
min_docker_version='2.0.0',
)
dsm = DockerServiceManager(client)
msg, changed, rebuilt, changes, facts = dsm.run()
results = dict(
msg=msg,
changed=changed,
rebuilt=rebuilt,
changes=changes,
ansible_docker_service=facts,
)
if client.module._diff:
before, after = dsm.diff_tracker.get_before_after()
results['diff'] = dict(before=before, after=after)
client.module.exit_json(**results)
if __name__ == '__main__':
main()