mirror of
https://github.com/ansible-collections/community.general.git
synced 2025-04-26 12:21:26 -07:00
* 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
1206 lines
46 KiB
Python
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()
|