mirror of
https://github.com/ansible-collections/community.general.git
synced 2024-09-14 20:13:21 +02:00
1963 lines
72 KiB
Python
1963 lines
72 KiB
Python
#!/usr/bin/python
|
|
#
|
|
# Copyright 2016 Red Hat | Ansible
|
|
#
|
|
# This file is part of Ansible
|
|
#
|
|
# Ansible is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# Ansible is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
DOCUMENTATION = '''
|
|
---
|
|
module: docker_container
|
|
|
|
short_description: manage docker containers
|
|
|
|
description:
|
|
- Manage the life cycle of docker containers.
|
|
- Supports check mode. Run with --check and --diff to view config difference and list of actions to be taken.
|
|
|
|
version_added: "2.1.0"
|
|
|
|
options:
|
|
blkio_weight:
|
|
description:
|
|
- Block IO (relative weight), between 10 and 1000.
|
|
default: null
|
|
required: false
|
|
capabilities:
|
|
description:
|
|
- List of capabilities to add to the container.
|
|
default: null
|
|
required: false
|
|
cleanup:
|
|
description:
|
|
- Use with I(detach) to remove the container after successful execution.
|
|
default: false
|
|
required: false
|
|
version_added: "2.2"
|
|
command:
|
|
description:
|
|
- Command to execute when the container starts.
|
|
default: null
|
|
required: false
|
|
cpu_period:
|
|
description:
|
|
- Limit CPU CFS (Completely Fair Scheduler) period
|
|
default: 0
|
|
required: false
|
|
cpu_quota:
|
|
description:
|
|
- Limit CPU CFS (Completely Fair Scheduler) quota
|
|
default: 0
|
|
required: false
|
|
cpuset_cpus:
|
|
description:
|
|
- CPUs in which to allow execution C(1,3) or C(1-3).
|
|
default: null
|
|
required: false
|
|
cpuset_mems:
|
|
description:
|
|
- Memory nodes (MEMs) in which to allow execution C(0-3) or C(0,1)
|
|
default: null
|
|
required: false
|
|
cpu_shares:
|
|
description:
|
|
- CPU shares (relative weight).
|
|
default: null
|
|
required: false
|
|
detach:
|
|
description:
|
|
- Enable detached mode to leave the container running in background.
|
|
If disabled, the task will reflect the status of the container run (failed if the command failed).
|
|
default: true
|
|
required: false
|
|
devices:
|
|
description:
|
|
- "List of host device bindings to add to the container. Each binding is a mapping expressed
|
|
in the format: <path_on_host>:<path_in_container>:<cgroup_permissions>"
|
|
default: null
|
|
required: false
|
|
dns_servers:
|
|
description:
|
|
- List of custom DNS servers.
|
|
default: null
|
|
required: false
|
|
dns_search_domains:
|
|
description:
|
|
- List of custom DNS search domains.
|
|
default: null
|
|
required: false
|
|
env:
|
|
description:
|
|
- Dictionary of key,value pairs.
|
|
default: null
|
|
required: false
|
|
env_file:
|
|
version_added: "2.2"
|
|
description:
|
|
- Path to a file containing environment variables I(FOO=BAR).
|
|
- If variable also present in C(env), then C(env) value will override.
|
|
- Requires docker-py >= 1.4.0.
|
|
default: null
|
|
required: false
|
|
entrypoint:
|
|
description:
|
|
- Command that overwrites the default ENTRYPOINT of the image.
|
|
default: null
|
|
required: false
|
|
etc_hosts:
|
|
description:
|
|
- Dict of host-to-IP mappings, where each host name is a key in the dictionary.
|
|
Each host name will be added to the container's /etc/hosts file.
|
|
default: null
|
|
required: false
|
|
exposed_ports:
|
|
description:
|
|
- List of additional container ports to expose for port mappings or links.
|
|
If the port is already exposed using EXPOSE in a Dockerfile, it does not
|
|
need to be exposed again.
|
|
default: null
|
|
required: false
|
|
aliases:
|
|
- exposed
|
|
force_kill:
|
|
description:
|
|
- Use the kill command when stopping a running container.
|
|
default: false
|
|
required: false
|
|
groups:
|
|
description:
|
|
- List of additional group names and/or IDs that the container process will run as.
|
|
default: null
|
|
required: false
|
|
hostname:
|
|
description:
|
|
- Container hostname.
|
|
default: null
|
|
required: false
|
|
ignore_image:
|
|
description:
|
|
- When C(state) is I(present) or I(started) the module compares the configuration of an existing
|
|
container to requested configuration. The evaluation includes the image version. If
|
|
the image version in the registry does not match the container, the container will be
|
|
recreated. Stop this behavior by setting C(ignore_image) to I(True).
|
|
default: false
|
|
required: false
|
|
version_added: "2.2"
|
|
image:
|
|
description:
|
|
- Repository path and tag used to create the container. If an image is not found or pull is true, the image
|
|
will be pulled from the registry. If no tag is included, 'latest' will be used.
|
|
default: null
|
|
required: false
|
|
interactive:
|
|
description:
|
|
- Keep stdin open after a container is launched, even if not attached.
|
|
default: false
|
|
required: false
|
|
ipc_mode:
|
|
description:
|
|
- Set the IPC mode for the container. Can be one of 'container:<name|id>' to reuse another
|
|
container's IPC namespace or 'host' to use the host's IPC namespace within the container.
|
|
default: null
|
|
required: false
|
|
keep_volumes:
|
|
description:
|
|
- Retain volumes associated with a removed container.
|
|
default: true
|
|
required: false
|
|
kill_signal:
|
|
description:
|
|
- Override default signal used to kill a running container.
|
|
default null:
|
|
required: false
|
|
kernel_memory:
|
|
description:
|
|
- "Kernel memory limit (format: <number>[<unit>]). Number is a positive integer.
|
|
Unit can be one of b, k, m, or g. Minimum is 4M."
|
|
default: 0
|
|
required: false
|
|
labels:
|
|
description:
|
|
- Dictionary of key value pairs.
|
|
default: null
|
|
required: false
|
|
links:
|
|
description:
|
|
- List of name aliases for linked containers in the format C(container_name:alias)
|
|
default: null
|
|
required: false
|
|
log_driver:
|
|
description:
|
|
- Specify the logging driver.
|
|
choices:
|
|
- json-file
|
|
- syslog
|
|
- journald
|
|
- gelf
|
|
- fluentd
|
|
- awslogs
|
|
- splunk
|
|
default: json-file
|
|
required: false
|
|
log_options:
|
|
description:
|
|
- Dictionary of options specific to the chosen log_driver. See https://docs.docker.com/engine/admin/logging/overview/
|
|
for details.
|
|
required: false
|
|
default: null
|
|
mac_address:
|
|
description:
|
|
- Container MAC address (e.g. 92:d0:c6:0a:29:33)
|
|
default: null
|
|
required: false
|
|
memory:
|
|
description:
|
|
- "Memory limit (format: <number>[<unit>]). Number is a positive integer.
|
|
Unit can be one of b, k, m, or g"
|
|
default: 0
|
|
required: false
|
|
memory_reservation:
|
|
description:
|
|
- "Memory soft limit (format: <number>[<unit>]). Number is a positive integer.
|
|
Unit can be one of b, k, m, or g"
|
|
default: 0
|
|
required: false
|
|
memory_swap:
|
|
description:
|
|
- Total memory limit (memory + swap, format:<number>[<unit>]).
|
|
Number is a positive integer. Unit can be one of b, k, m, or g.
|
|
default: 0
|
|
required: false
|
|
memory_swappiness:
|
|
description:
|
|
- Tune a container's memory swappiness behavior. Accepts an integer between 0 and 100.
|
|
default: 0
|
|
required: false
|
|
name:
|
|
description:
|
|
- Assign a name to a new container or match an existing container.
|
|
- When identifying an existing container name may be a name or a long or short container ID.
|
|
required: true
|
|
network_mode:
|
|
description:
|
|
- Connect the container to a network.
|
|
choices:
|
|
- bridge
|
|
- container:<name|id>
|
|
- host
|
|
- none
|
|
default: null
|
|
required: false
|
|
networks:
|
|
description:
|
|
- List of networks the container belongs to.
|
|
- Each network is a dict with keys C(name), C(ipv4_address), C(ipv6_address), C(links), C(aliases).
|
|
- For each network C(name) is required, all other keys are optional.
|
|
- If included, C(links) or C(aliases) are lists.
|
|
- For examples of the data structure and usage see EXAMPLES below.
|
|
- To remove a container from one or more networks, use the C(purge_networks) option.
|
|
default: null
|
|
required: false
|
|
version_added: "2.2"
|
|
oom_killer:
|
|
description:
|
|
- Whether or not to disable OOM Killer for the container.
|
|
default: false
|
|
required: false
|
|
paused:
|
|
description:
|
|
- Use with the started state to pause running processes inside the container.
|
|
default: false
|
|
required: false
|
|
pid_mode:
|
|
description:
|
|
- Set the PID namespace mode for the container. Currenly only supports 'host'.
|
|
default: null
|
|
required: false
|
|
privileged:
|
|
description:
|
|
- Give extended privileges to the container.
|
|
default: false
|
|
required: false
|
|
published_ports:
|
|
description:
|
|
- List of ports to publish from the container to the host.
|
|
- "Use docker CLI syntax: C(8000), C(9000:8000), or C(0.0.0.0:9000:8000), where 8000 is a
|
|
container port, 9000 is a host port, and 0.0.0.0 is a host interface."
|
|
- Container ports must be exposed either in the Dockerfile or via the C(expose) option.
|
|
- A value of ALL will publish all exposed container ports to random host ports, ignoring
|
|
any other mappings.
|
|
- If C(networks) parameter is provided, will inspect each network to see if there exists
|
|
a bridge network with optional parameter com.docker.network.bridge.host_binding_ipv4.
|
|
If such a network is found, then published ports where no host IP address is specified
|
|
will be bound to the host IP pointed to by com.docker.network.bridge.host_binding_ipv4.
|
|
Note that the first bridge network with a com.docker.network.bridge.host_binding_ipv4
|
|
value encountered in the list of C(networks) is the one that will be used.
|
|
aliases:
|
|
- ports
|
|
required: false
|
|
default: null
|
|
pull:
|
|
description:
|
|
- If true, always pull the latest version of an image. Otherwise, will only pull an image when missing.
|
|
default: false
|
|
required: false
|
|
purge_networks:
|
|
description:
|
|
- Remove the container from ALL networks not included in C(networks) parameter.
|
|
- Any default networks such as I(bridge), if not found in C(networks), will be removed as well.
|
|
default: false
|
|
required: false
|
|
version_added: "2.2"
|
|
read_only:
|
|
description:
|
|
- Mount the container's root file system as read-only.
|
|
default: false
|
|
required: false
|
|
recreate:
|
|
description:
|
|
- Use with present and started states to force the re-creation of an existing container.
|
|
default: false
|
|
required: false
|
|
restart:
|
|
description:
|
|
- Use with started state to force a matching container to be stopped and restarted.
|
|
default: false
|
|
required: false
|
|
restart_policy:
|
|
description:
|
|
- Container restart policy. Place quotes around I(no) option.
|
|
choices:
|
|
- always
|
|
- no
|
|
- on-failure
|
|
- unless-stopped
|
|
default: on-failure
|
|
required: false
|
|
restart_retries:
|
|
description:
|
|
- Use with restart policy to control maximum number of restart attempts.
|
|
default: 0
|
|
required: false
|
|
shm_size:
|
|
description:
|
|
- Size of `/dev/shm`. The format is `<number><unit>`. `number` must be greater than `0`.
|
|
Unit is optional and can be `b` (bytes), `k` (kilobytes), `m` (megabytes), or `g` (gigabytes).
|
|
- Ommitting the unit defaults to bytes. If you omit the size entirely, the system uses `64m`.
|
|
default: null
|
|
required: false
|
|
security_opts:
|
|
description:
|
|
- List of security options in the form of C("label:user:User")
|
|
default: null
|
|
required: false
|
|
state:
|
|
description:
|
|
- 'I(absent) - A container matching the specified name will be stopped and removed. Use force_kill to kill the container
|
|
rather than stopping it. Use keep_volumes to retain volumes associated with the removed container.'
|
|
- 'I(present) - Asserts the existence of a container matching the name and any provided configuration parameters. If no
|
|
container matches the name, a container will be created. If a container matches the name but the provided configuration
|
|
does not match, the container will be updated, if it can be. If it cannot be updated, it will be removed and re-created
|
|
with the requested config. Image version will be taken into account when comparing configuration. To ignore image
|
|
version use the ignore_image option. Use the recreate option to force the re-creation of the matching container. Use
|
|
force_kill to kill the container rather than stopping it. Use keep_volumes to retain volumes associated with a removed
|
|
container.'
|
|
- 'I(started) - Asserts there is a running container matching the name and any provided configuration. If no container
|
|
matches the name, a container will be created and started. If a container matching the name is found but the
|
|
configuration does not match, the container will be updated, if it can be. If it cannot be updated, it will be removed
|
|
and a new container will be created with the requested configuration and started. Image version will be taken into
|
|
account when comparing configuration. To ignore image version use the ignore_image option. Use recreate to always
|
|
re-create a matching container, even if it is running. Use restart to force a matching container to be stopped and
|
|
restarted. Use force_kill to kill a container rather than stopping it. Use keep_volumes to retain volumes associated
|
|
with a removed container.'
|
|
- 'I(stopped) - Asserts that the container is first I(present), and then if the container is running moves it to a stopped
|
|
state. Use force_kill to kill a container rather than stopping it.'
|
|
required: false
|
|
default: started
|
|
choices:
|
|
- absent
|
|
- present
|
|
- stopped
|
|
- started
|
|
stop_signal:
|
|
description:
|
|
- Override default signal used to stop the container.
|
|
default: null
|
|
required: false
|
|
stop_timeout:
|
|
description:
|
|
- Number of seconds to wait for the container to stop before sending SIGKILL.
|
|
required: false
|
|
default: null
|
|
trust_image_content:
|
|
description:
|
|
- If true, skip image verification.
|
|
default: false
|
|
requried: false
|
|
tty:
|
|
description:
|
|
- Allocate a psuedo-TTY.
|
|
default: false
|
|
required: false
|
|
ulimits:
|
|
description:
|
|
- "List of ulimit options. A ulimit is specified as C(nofile:262144:262144)"
|
|
default: null
|
|
required: false
|
|
user:
|
|
description:
|
|
- Sets the username or UID used and optionally the groupname or GID for the specified command.
|
|
- "Can be [ user | user:group | uid | uid:gid | user:gid | uid:group ]"
|
|
default: null
|
|
required: false
|
|
uts:
|
|
description:
|
|
- Set the UTS namespace mode for the container.
|
|
default: null
|
|
required: false
|
|
volumes:
|
|
description:
|
|
- List of volumes to mount within the container.
|
|
- "Use docker CLI-style syntax: C(/host:/container[:mode])"
|
|
- You can specify a read mode for the mount with either C(ro) or C(rw).
|
|
- SELinux hosts can additionally use C(z) or C(Z) to use a shared or
|
|
private label for the volume.
|
|
default: null
|
|
required: false
|
|
volume_driver:
|
|
description:
|
|
- The container volume driver.
|
|
default: none
|
|
required: false
|
|
volumes_from:
|
|
description:
|
|
- List of container names or Ids to get volumes from.
|
|
default: null
|
|
required: false
|
|
extends_documentation_fragment:
|
|
- docker
|
|
|
|
author:
|
|
- "Cove Schneider (@cove)"
|
|
- "Joshua Conner (@joshuaconner)"
|
|
- "Pavel Antonov (@softzilla)"
|
|
- "Thomas Steinbach (@ThomasSteinbach)"
|
|
- "Philippe Jandot (@zfil)"
|
|
- "Daan Oosterveld (@dusdanig)"
|
|
- "James Tanner (@jctanner)"
|
|
- "Chris Houseknecht (@chouseknecht)"
|
|
|
|
requirements:
|
|
- "python >= 2.6"
|
|
- "docker-py >= 1.7.0"
|
|
- "Docker API >= 1.20"
|
|
'''
|
|
|
|
EXAMPLES = '''
|
|
- name: Create a data container
|
|
docker_container:
|
|
name: mydata
|
|
image: busybox
|
|
volumes:
|
|
- /data
|
|
|
|
- name: Re-create a redis container
|
|
docker_container:
|
|
name: myredis
|
|
image: redis
|
|
command: redis-server --appendonly yes
|
|
state: present
|
|
recreate: yes
|
|
exposed_ports:
|
|
- 6379
|
|
volumes_from:
|
|
- mydata
|
|
|
|
- name: Restart a container
|
|
docker_container:
|
|
name: myapplication
|
|
image: someuser/appimage
|
|
state: started
|
|
restart: yes
|
|
links:
|
|
- "myredis:aliasedredis"
|
|
devices:
|
|
- "/dev/sda:/dev/xvda:rwm"
|
|
ports:
|
|
- "8080:9000"
|
|
- "127.0.0.1:8081:9001/udp"
|
|
env:
|
|
SECRET_KEY: ssssh
|
|
|
|
- name: Container present
|
|
docker_container:
|
|
name: mycontainer
|
|
state: present
|
|
image: ubuntu:14.04
|
|
command: sleep infinity
|
|
|
|
- name: Stop a contianer
|
|
docker_container:
|
|
name: mycontainer
|
|
state: stopped
|
|
|
|
- name: Start 4 load-balanced containers
|
|
docker_container:
|
|
name: "container{{ item }}"
|
|
recreate: yes
|
|
image: someuser/anotherappimage
|
|
command: sleep 1d
|
|
with_sequence: count=4
|
|
|
|
- name: remove container
|
|
docker_container:
|
|
name: ohno
|
|
state: absent
|
|
|
|
- name: Syslogging output
|
|
docker_container:
|
|
name: myservice
|
|
image: busybox
|
|
log_driver: syslog
|
|
log_options:
|
|
syslog-address: tcp://my-syslog-server:514
|
|
syslog-facility: daemon
|
|
syslog-tag: myservice
|
|
|
|
- name: Create db container and connect to network
|
|
docker_container:
|
|
name: db_test
|
|
image: "postgres:latest"
|
|
networks:
|
|
- name: "{{ docker_network_name }}"
|
|
|
|
- name: Start container, connect to network and link
|
|
docker_container:
|
|
name: sleeper
|
|
image: ubuntu:14.04
|
|
networks:
|
|
- name: TestingNet
|
|
ipv4_address: "172.1.1.100"
|
|
aliases:
|
|
- sleepyzz
|
|
links:
|
|
- db_test:db
|
|
- name: TestingNet2
|
|
|
|
- name: Start a container with a command
|
|
docker_container:
|
|
name: sleepy
|
|
image: ubuntu:14.04
|
|
command: sleep infinity
|
|
|
|
- name: Add container to networks
|
|
docker_container:
|
|
docker_container:
|
|
name: sleepy
|
|
networks:
|
|
- name: TestingNet
|
|
ipv4_address: 172.1.1.18
|
|
links:
|
|
- sleeper
|
|
- name: TestingNet2
|
|
ipv4_address: 172.1.10.20
|
|
|
|
- name: Update network with aliases
|
|
docker_container:
|
|
name: sleepy
|
|
networks:
|
|
- name: TestingNet
|
|
aliases:
|
|
- sleepyz
|
|
- zzzz
|
|
|
|
- name: Remove container from one network
|
|
docker_container:
|
|
name: sleepy
|
|
networks:
|
|
- name: TestingNet2
|
|
purge_networks: yes
|
|
|
|
- name: Remove container from all networks
|
|
docker_container:
|
|
name: sleepy
|
|
purge_networks: yes
|
|
|
|
'''
|
|
|
|
RETURN = '''
|
|
ansible_docker_container:
|
|
description:
|
|
- Facts representing the current state of the container. Matches the docker inspection output.
|
|
- Note that facts are not part of registered vars but accessible directly.
|
|
- Empty if C(state) is I(absent)
|
|
- If detached is I(False), will include Output attribute containing any output from container run.
|
|
returned: always
|
|
type: dict
|
|
sample: '{
|
|
"AppArmorProfile": "",
|
|
"Args": [],
|
|
"Config": {
|
|
"AttachStderr": false,
|
|
"AttachStdin": false,
|
|
"AttachStdout": false,
|
|
"Cmd": [
|
|
"/usr/bin/supervisord"
|
|
],
|
|
"Domainname": "",
|
|
"Entrypoint": null,
|
|
"Env": [
|
|
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
|
],
|
|
"ExposedPorts": {
|
|
"443/tcp": {},
|
|
"80/tcp": {}
|
|
},
|
|
"Hostname": "8e47bf643eb9",
|
|
"Image": "lnmp_nginx:v1",
|
|
"Labels": {},
|
|
"OnBuild": null,
|
|
"OpenStdin": false,
|
|
"StdinOnce": false,
|
|
"Tty": false,
|
|
"User": "",
|
|
"Volumes": {
|
|
"/tmp/lnmp/nginx-sites/logs/": {}
|
|
},
|
|
...
|
|
}'
|
|
'''
|
|
|
|
import re
|
|
|
|
from ansible.module_utils.docker_common import *
|
|
|
|
try:
|
|
from docker import utils
|
|
from docker.utils.types import Ulimit
|
|
except:
|
|
# missing docker-py handled in ansible.module_utils.docker
|
|
pass
|
|
|
|
|
|
REQUIRES_CONVERSION_TO_BYTES = [
|
|
'memory',
|
|
'memory_reservation',
|
|
'memory_swap',
|
|
'shm_size'
|
|
]
|
|
|
|
VOLUME_PERMISSIONS = ('rw', 'ro', 'z', 'Z')
|
|
|
|
class TaskParameters(DockerBaseClass):
|
|
'''
|
|
Access and parse module parameters
|
|
'''
|
|
|
|
def __init__(self, client):
|
|
super(TaskParameters, self).__init__()
|
|
self.client = client
|
|
|
|
self.blkio_weight = None
|
|
self.capabilities = None
|
|
self.cleanup = None
|
|
self.command = None
|
|
self.cpu_period = None
|
|
self.cpu_quota = None
|
|
self.cpuset_cpus = None
|
|
self.cpuset_mems = None
|
|
self.cpu_shares = None
|
|
self.detach = None
|
|
self.debug = None
|
|
self.devices = None
|
|
self.dns_servers = None
|
|
self.dns_opts = None
|
|
self.dns_search_domains = None
|
|
self.env = None
|
|
self.env_file = None
|
|
self.entrypoint = None
|
|
self.etc_hosts = None
|
|
self.exposed_ports = None
|
|
self.force_kill = None
|
|
self.groups = None
|
|
self.hostname = None
|
|
self.ignore_image = None
|
|
self.image = None
|
|
self.interactive = None
|
|
self.ipc_mode = None
|
|
self.keep_volumes = None
|
|
self.kernel_memory = None
|
|
self.kill_signal = None
|
|
self.labels = None
|
|
self.links = None
|
|
self.log_driver = None
|
|
self.log_options = None
|
|
self.mac_address = None
|
|
self.memory = None
|
|
self.memory_reservation = None
|
|
self.memory_swap = None
|
|
self.memory_swappiness = None
|
|
self.name = None
|
|
self.network_mode = None
|
|
self.networks = None
|
|
self.oom_killer = None
|
|
self.paused = None
|
|
self.pid_mode = None
|
|
self.privileged = None
|
|
self.purge_networks = None
|
|
self.pull = None
|
|
self.read_only = None
|
|
self.recreate = None
|
|
self.restart = None
|
|
self.restart_retries = None
|
|
self.restart_policy = None
|
|
self.shm_size = None
|
|
self.security_opts = None
|
|
self.state = None
|
|
self.stop_signal = None
|
|
self.stop_timeout = None
|
|
self.trust_image_content = None
|
|
self.tty = None
|
|
self.user = None
|
|
self.uts = None
|
|
self.volumes = None
|
|
self.volume_binds = dict()
|
|
self.volumes_from = None
|
|
self.volume_driver = None
|
|
|
|
for key, value in client.module.params.items():
|
|
setattr(self, key, value)
|
|
|
|
for param_name in REQUIRES_CONVERSION_TO_BYTES:
|
|
if client.module.params.get(param_name):
|
|
try:
|
|
setattr(self, param_name, human_to_bytes(client.module.params.get(param_name)))
|
|
except ValueError as exc:
|
|
self.fail("Failed to convert %s to bytes: %s" % (param_name, exc))
|
|
|
|
self.publish_all_ports = False
|
|
self.published_ports = self._parse_publish_ports()
|
|
if self.published_ports == 'all':
|
|
self.publish_all_ports = True
|
|
self.published_ports = None
|
|
|
|
self.ports = self._parse_exposed_ports(self.published_ports)
|
|
self.log("expose ports:")
|
|
self.log(self.ports, pretty_print=True)
|
|
|
|
self.links = self._parse_links(self.links)
|
|
|
|
if self.volumes:
|
|
self.volumes = self._expand_host_paths()
|
|
|
|
self.env = self._get_environment()
|
|
self.ulimits = self._parse_ulimits()
|
|
self.log_config = self._parse_log_config()
|
|
self.exp_links = None
|
|
self.volume_binds = self._get_volume_binds(self.volumes)
|
|
|
|
self.log("volumes:")
|
|
self.log(self.volumes, pretty_print=True)
|
|
self.log("volume binds:")
|
|
self.log(self.volume_binds, pretty_print=True)
|
|
|
|
if self.networks:
|
|
for network in self.networks:
|
|
if not network.get('name'):
|
|
self.fail("Parameter error: network must have a name attribute.")
|
|
network['id'] = self._get_network_id(network['name'])
|
|
if not network['id']:
|
|
self.fail("Parameter error: network named %s could not be found. Does it exist?" % network['name'])
|
|
if network.get('links'):
|
|
network['links'] = self._parse_links(network['links'])
|
|
|
|
def fail(self, msg):
|
|
self.client.module.fail_json(msg=msg)
|
|
|
|
@property
|
|
def update_parameters(self):
|
|
'''
|
|
Returns parameters used to update a container
|
|
'''
|
|
|
|
update_parameters = dict(
|
|
blkio_weight='blkio_weight',
|
|
cpu_period='cpu_period',
|
|
cpu_quota='cpu_quota',
|
|
cpu_shares='cpu_shares',
|
|
cpuset_cpus='cpuset_cpus',
|
|
mem_limit='memory',
|
|
mem_reservation='mem_reservation',
|
|
memswap_limit='memory_swap',
|
|
kernel_memory='kernel_memory'
|
|
)
|
|
result = dict()
|
|
for key, value in update_parameters.iteritems():
|
|
if getattr(self, value, None) is not None:
|
|
result[key] = getattr(self, value)
|
|
return result
|
|
|
|
@property
|
|
def create_parameters(self):
|
|
'''
|
|
Returns parameters used to create a container
|
|
'''
|
|
create_params = dict(
|
|
command='command',
|
|
hostname='hostname',
|
|
user='user',
|
|
detach='detach',
|
|
stdin_open='interactive',
|
|
tty='tty',
|
|
ports='ports',
|
|
environment='env',
|
|
name='name',
|
|
entrypoint='entrypoint',
|
|
cpu_shares='cpu_shares',
|
|
mac_address='mac_address',
|
|
labels='labels',
|
|
stop_signal='stop_signal',
|
|
volume_driver='volume_driver',
|
|
)
|
|
|
|
result = dict(
|
|
host_config=self._host_config(),
|
|
volumes=self._get_mounts(),
|
|
)
|
|
|
|
for key, value in create_params.items():
|
|
if getattr(self, value, None) is not None:
|
|
result[key] = getattr(self, value)
|
|
return result
|
|
|
|
def _expand_host_paths(self):
|
|
new_vols = []
|
|
for vol in self.volumes:
|
|
if ':' in vol:
|
|
if len(vol.split(':')) == 3:
|
|
host, container, mode = vol.split(':')
|
|
if re.match(r'[\.~]', host):
|
|
host = os.path.abspath(host)
|
|
new_vols.append("%s:%s:%s" % (host, container, mode))
|
|
continue
|
|
elif len(vol.split(':')) == 2:
|
|
parts = vol.split(':')
|
|
if parts[1] not in VOLUME_PERMISSIONS and re.match(r'[\.~]', parts[0]):
|
|
host = os.path.abspath(parts[0])
|
|
new_vols.append("%s:%s:rw" % (host, parts[1]))
|
|
continue
|
|
new_vols.append(vol)
|
|
return new_vols
|
|
|
|
def _get_mounts(self):
|
|
'''
|
|
Return a list of container mounts.
|
|
:return:
|
|
'''
|
|
result = []
|
|
if self.volumes:
|
|
for vol in self.volumes:
|
|
if ':' in vol:
|
|
if len(vol.split(':')) == 3:
|
|
host, container, _ = vol.split(':')
|
|
result.append(container)
|
|
continue
|
|
if len(vol.split(':')) == 2:
|
|
parts = vol.split(':')
|
|
if parts[1] not in VOLUME_PERMISSIONS:
|
|
result.append(parts[1])
|
|
continue
|
|
result.append(vol)
|
|
self.log("mounts:")
|
|
self.log(result, pretty_print=True)
|
|
return result
|
|
|
|
def _host_config(self):
|
|
'''
|
|
Returns parameters used to create a HostConfig object
|
|
'''
|
|
|
|
host_config_params=dict(
|
|
port_bindings='published_ports',
|
|
publish_all_ports='publish_all_ports',
|
|
links='links',
|
|
privileged='privileged',
|
|
dns='dns_servers',
|
|
dns_search='dns_search_domains',
|
|
binds='volume_binds',
|
|
volumes_from='volumes_from',
|
|
network_mode='network_mode',
|
|
cap_add='capabilities',
|
|
extra_hosts='etc_hosts',
|
|
read_only='read_only',
|
|
ipc_mode='ipc_mode',
|
|
security_opt='security_opts',
|
|
ulimits='ulimits',
|
|
log_config='log_config',
|
|
mem_limit='memory',
|
|
memswap_limit='memory_swap',
|
|
mem_swappiness='memory_swappiness',
|
|
shm_size='shm_size',
|
|
group_add='groups',
|
|
devices='devices',
|
|
pid_mode='pid_mode'
|
|
)
|
|
params = dict()
|
|
for key, value in host_config_params.iteritems():
|
|
if getattr(self, value, None) is not None:
|
|
params[key] = getattr(self, value)
|
|
|
|
if self.restart_policy:
|
|
params['restart_policy'] = dict(Name=self.restart_policy,
|
|
MaximumRetryCount=self.restart_retries)
|
|
|
|
return self.client.create_host_config(**params)
|
|
|
|
@property
|
|
def default_host_ip(self):
|
|
ip = '0.0.0.0'
|
|
if not self.networks:
|
|
return ip
|
|
for net in self.networks:
|
|
if net.get('name'):
|
|
network = self.client.inspect_network(net['name'])
|
|
if network.get('Driver') == 'bridge' and \
|
|
network.get('Options', {}).get('com.docker.network.bridge.host_binding_ipv4'):
|
|
ip = network['Options']['com.docker.network.bridge.host_binding_ipv4']
|
|
break
|
|
return ip
|
|
|
|
def _parse_publish_ports(self):
|
|
'''
|
|
Parse ports from docker CLI syntax
|
|
'''
|
|
if self.published_ports is None:
|
|
return None
|
|
|
|
if 'all' in self.published_ports:
|
|
return 'all'
|
|
|
|
default_ip = self.default_host_ip
|
|
|
|
binds = {}
|
|
for port in self.published_ports:
|
|
parts = str(port).split(':')
|
|
container_port = parts[-1]
|
|
if '/' not in container_port:
|
|
container_port = int(parts[-1])
|
|
|
|
p_len = len(parts)
|
|
if p_len == 1:
|
|
bind = (default_ip,)
|
|
elif p_len == 2:
|
|
bind = (default_ip, int(parts[0]))
|
|
elif p_len == 3:
|
|
bind = (parts[0], int(parts[1])) if parts[1] else (parts[0],)
|
|
|
|
if container_port in binds:
|
|
old_bind = binds[container_port]
|
|
if isinstance(old_bind, list):
|
|
old_bind.append(bind)
|
|
else:
|
|
binds[container_port] = [binds[container_port], bind]
|
|
else:
|
|
binds[container_port] = bind
|
|
return binds
|
|
|
|
@staticmethod
|
|
def _get_volume_binds(volumes):
|
|
'''
|
|
Extract host bindings, if any, from list of volume mapping strings.
|
|
|
|
:return: dictionary of bind mappings
|
|
'''
|
|
result = dict()
|
|
if volumes:
|
|
for vol in volumes:
|
|
host = None
|
|
if ':' in vol:
|
|
if len(vol.split(':')) == 3:
|
|
host, container, mode = vol.split(':')
|
|
if len(vol.split(':')) == 2:
|
|
parts = vol.split(':')
|
|
if parts[1] not in VOLUME_PERMISSIONS:
|
|
host, container, mode = (vol.split(':') + ['rw'])
|
|
if host is not None:
|
|
result[host] = dict(
|
|
bind=container,
|
|
mode=mode
|
|
)
|
|
return result
|
|
|
|
def _parse_exposed_ports(self, published_ports):
|
|
'''
|
|
Parse exposed ports from docker CLI-style ports syntax.
|
|
'''
|
|
exposed = []
|
|
if self.exposed_ports:
|
|
for port in self.exposed_ports:
|
|
port = str(port).strip()
|
|
protocol = 'tcp'
|
|
match = re.search(r'(/.+$)', port)
|
|
if match:
|
|
protocol = match.group(1).replace('/', '')
|
|
port = re.sub(r'/.+$', '', port)
|
|
exposed.append((port, protocol))
|
|
if published_ports:
|
|
# Any published port should also be exposed
|
|
for publish_port in published_ports:
|
|
match = False
|
|
if isinstance(publish_port, basestring) and '/' in publish_port:
|
|
port, protocol = publish_port.split('/')
|
|
port = int(port)
|
|
else:
|
|
protocol = 'tcp'
|
|
port = int(publish_port)
|
|
for exposed_port in exposed:
|
|
if isinstance(exposed_port[0], basestring) and '-' in exposed_port[0]:
|
|
start_port, end_port = exposed_port[0].split('-')
|
|
if int(start_port) <= port <= int(end_port):
|
|
match = True
|
|
elif exposed_port[0] == port:
|
|
match = True
|
|
if not match:
|
|
exposed.append((port, protocol))
|
|
return exposed
|
|
|
|
@staticmethod
|
|
def _parse_links(links):
|
|
'''
|
|
Turn links into a dictionary
|
|
'''
|
|
if links is None:
|
|
return None
|
|
|
|
result = {}
|
|
for link in links:
|
|
parsed_link = link.split(':', 1)
|
|
if len(parsed_link) == 2:
|
|
result[parsed_link[0]] = parsed_link[1]
|
|
else:
|
|
result[parsed_link[0]] = parsed_link[0]
|
|
return result
|
|
|
|
def _parse_ulimits(self):
|
|
'''
|
|
Turn ulimits into an array of Ulimit objects
|
|
'''
|
|
if self.ulimits is None:
|
|
return None
|
|
|
|
results = []
|
|
for limit in self.ulimits:
|
|
limits = dict()
|
|
pieces = limit.split(':')
|
|
if len(pieces) >= 2:
|
|
limits['name'] = pieces[0]
|
|
limits['soft'] = int(pieces[1])
|
|
limits['hard'] = int(pieces[1])
|
|
if len(pieces) == 3:
|
|
limits['hard'] = int(pieces[2])
|
|
try:
|
|
results.append(Ulimit(**limits))
|
|
except ValueError as exc:
|
|
self.fail("Error parsing ulimits value %s - %s" % (limit, exc))
|
|
return results
|
|
|
|
def _parse_log_config(self):
|
|
'''
|
|
Create a LogConfig object
|
|
'''
|
|
if self.log_driver is None:
|
|
return None
|
|
|
|
options = dict(
|
|
Type=self.log_driver,
|
|
Config = dict()
|
|
)
|
|
|
|
if self.log_options is not None:
|
|
options['Config'] = self.log_options
|
|
|
|
try:
|
|
return LogConfig(**options)
|
|
except ValueError as exc:
|
|
self.fail('Error parsing logging options - %s' % (exc))
|
|
|
|
def _get_environment(self):
|
|
"""
|
|
If environment file is combined with explicit environment variables, the explicit environment variables
|
|
take precedence.
|
|
"""
|
|
final_env = {}
|
|
if self.env_file:
|
|
parsed_env_file = utils.parse_env_file(self.env_file)
|
|
for name, value in parsed_env_file.iteritems():
|
|
final_env[name] = str(value)
|
|
if self.env:
|
|
for name, value in self.env.iteritems():
|
|
final_env[name] = str(value)
|
|
return final_env
|
|
|
|
def _get_network_id(self, network_name):
|
|
network_id = None
|
|
try:
|
|
for network in self.client.networks(names=[network_name]):
|
|
if network['Name'] == network_name:
|
|
network_id = network['Id']
|
|
break
|
|
except Exception as exc:
|
|
self.fail("Error getting network id for %s - %s" % (network_name, str(exc)))
|
|
return network_id
|
|
|
|
|
|
|
|
class Container(DockerBaseClass):
|
|
|
|
def __init__(self, container, parameters):
|
|
super(Container, self).__init__()
|
|
self.raw = container
|
|
self.Id = None
|
|
self.container = container
|
|
if container:
|
|
self.Id = container['Id']
|
|
self.Image = container['Image']
|
|
self.log(self.container, pretty_print=True)
|
|
self.parameters = parameters
|
|
self.parameters.expected_links = None
|
|
self.parameters.expected_ports = None
|
|
self.parameters.expected_exposed = None
|
|
self.parameters.expected_volumes = None
|
|
self.parameters.expected_ulimits = None
|
|
self.parameters.expected_etc_hosts = None
|
|
self.parameters.expected_env = None
|
|
|
|
def fail(self, msg):
|
|
self.parameters.client.module.fail_json(msg=msg)
|
|
|
|
@property
|
|
def exists(self):
|
|
return True if self.container else False
|
|
|
|
@property
|
|
def running(self):
|
|
if self.container and self.container.get('State'):
|
|
if self.container['State'].get('Running') and not self.container['State'].get('Ghost', False):
|
|
return True
|
|
return False
|
|
|
|
def has_different_configuration(self, image):
|
|
'''
|
|
Diff parameters vs existing container config. Returns tuple: (True | False, List of differences)
|
|
'''
|
|
self.log('Starting has_different_configuration')
|
|
self.parameters.expected_entrypoint = self._get_expected_entrypoint()
|
|
self.parameters.expected_links = self._get_expected_links()
|
|
self.parameters.expected_ports = self._get_expected_ports()
|
|
self.parameters.expected_exposed = self._get_expected_exposed(image)
|
|
self.parameters.expected_volumes = self._get_expected_volumes(image)
|
|
self.parameters.expected_binds = self._get_expected_binds(image)
|
|
self.parameters.expected_ulimits = self._get_expected_ulimits(self.parameters.ulimits)
|
|
self.parameters.expected_etc_hosts = self._convert_simple_dict_to_list('etc_hosts')
|
|
self.parameters.expected_env = self._get_expected_env(image)
|
|
self.parameters.expected_cmd = self._get_expected_cmd()
|
|
|
|
if not self.container.get('HostConfig'):
|
|
self.fail("has_config_diff: Error parsing container properties. HostConfig missing.")
|
|
if not self.container.get('Config'):
|
|
self.fail("has_config_diff: Error parsing container properties. Config missing.")
|
|
if not self.container.get('NetworkSettings'):
|
|
self.fail("has_config_diff: Error parsing container properties. NetworkSettings missing.")
|
|
|
|
host_config = self.container['HostConfig']
|
|
log_config = host_config.get('LogConfig', dict())
|
|
restart_policy = host_config.get('RestartPolicy', dict())
|
|
config = self.container['Config']
|
|
network = self.container['NetworkSettings']
|
|
|
|
# The previous version of the docker module ignored the detach state by
|
|
# assuming if the container was running, it must have been detached.
|
|
detach = not (config.get('AttachStderr') and config.get('AttachStdout'))
|
|
|
|
# Map parameters to container inspect results
|
|
config_mapping = dict(
|
|
image=config.get('Image'),
|
|
expected_cmd=config.get('Cmd'),
|
|
hostname=config.get('Hostname'),
|
|
user=config.get('User'),
|
|
detach=detach,
|
|
interactive=config.get('OpenStdin'),
|
|
capabilities=host_config.get('CapAdd'),
|
|
devices=host_config.get('Devices'),
|
|
dns_servers=host_config.get('Dns'),
|
|
dns_opts=host_config.get('DnsOptions'),
|
|
dns_search_domains=host_config.get('DnsSearch'),
|
|
expected_env=(config.get('Env') or []),
|
|
expected_entrypoint=config.get('Entrypoint'),
|
|
expected_etc_hosts=host_config['ExtraHosts'],
|
|
expected_exposed=[re.sub(r'/.+$', '', p) for p in config.get('ExposedPorts', dict()).keys()],
|
|
groups=host_config.get('GroupAdd'),
|
|
ipc_mode=host_config.get("IpcMode"),
|
|
labels=config.get('Labels'),
|
|
expected_links=host_config.get('Links'),
|
|
log_driver=log_config.get('Type'),
|
|
log_options=log_config.get('Config'),
|
|
mac_address=network.get('MacAddress'),
|
|
memory_swappiness=host_config.get('MemorySwappiness'),
|
|
network_mode=host_config.get('NetworkMode'),
|
|
oom_killer=host_config.get('OomKillDisable'),
|
|
pid_mode=host_config.get('PidMode'),
|
|
privileged=host_config.get('Privileged'),
|
|
expected_ports=host_config.get('PortBindings'),
|
|
read_only=host_config.get('ReadonlyRootfs'),
|
|
restart_policy=restart_policy.get('Name'),
|
|
restart_retries=restart_policy.get('MaximumRetryCount'),
|
|
# Cannot test shm_size, as shm_size is not included in container inspection results.
|
|
# shm_size=host_config.get('ShmSize'),
|
|
security_opts=host_config.get("SecuriytOpt"),
|
|
stop_signal=config.get("StopSignal"),
|
|
tty=config.get('Tty'),
|
|
expected_ulimits=host_config.get('Ulimits'),
|
|
uts=host_config.get('UTSMode'),
|
|
expected_volumes=config.get('Volumes'),
|
|
expected_binds=host_config.get('Binds'),
|
|
volumes_from=host_config.get('VolumesFrom'),
|
|
volume_driver=host_config.get('VolumeDriver')
|
|
)
|
|
|
|
differences = []
|
|
for key, value in config_mapping.iteritems():
|
|
self.log('check differences %s %s vs %s' % (key, getattr(self.parameters, key), str(value)))
|
|
if getattr(self.parameters, key, None) is not None:
|
|
if isinstance(getattr(self.parameters, key), list) and isinstance(value, list):
|
|
if len(getattr(self.parameters, key)) > 0 and isinstance(getattr(self.parameters, key)[0], dict):
|
|
# compare list of dictionaries
|
|
self.log("comparing list of dict: %s" % key)
|
|
match = self._compare_dictionary_lists(getattr(self.parameters, key), value)
|
|
else:
|
|
# compare two lists. Is list_a in list_b?
|
|
self.log("comparing lists: %s" % key)
|
|
set_a = set(getattr(self.parameters, key))
|
|
set_b = set(value)
|
|
match = (set_a <= set_b)
|
|
elif isinstance(getattr(self.parameters, key), dict) and isinstance(value, dict):
|
|
# compare two dicts
|
|
self.log("comparing two dicts: %s" % key)
|
|
match = self._compare_dicts(getattr(self.parameters, key), value)
|
|
else:
|
|
# primitive compare
|
|
self.log("primitive compare: %s" % key)
|
|
match = (getattr(self.parameters, key) == value)
|
|
|
|
if not match:
|
|
# no match. record the differences
|
|
item = dict()
|
|
item[key] = dict(
|
|
parameter=getattr(self.parameters, key),
|
|
container=value
|
|
)
|
|
differences.append(item)
|
|
|
|
has_differences = True if len(differences) > 0 else False
|
|
return has_differences, differences
|
|
|
|
def _compare_dictionary_lists(self, list_a, list_b):
|
|
'''
|
|
If all of list_a exists in list_b, return True
|
|
'''
|
|
if not isinstance(list_a, list) or not isinstance(list_b, list):
|
|
return False
|
|
matches = 0
|
|
for dict_a in list_a:
|
|
for dict_b in list_b:
|
|
if self._compare_dicts(dict_a, dict_b):
|
|
matches += 1
|
|
break
|
|
result = (matches == len(list_a))
|
|
return result
|
|
|
|
def _compare_dicts(self, dict_a, dict_b):
|
|
'''
|
|
If dict_a in dict_b, return True
|
|
'''
|
|
if not isinstance(dict_a, dict) or not isinstance(dict_b, dict):
|
|
return False
|
|
for key, value in dict_a.iteritems():
|
|
if isinstance(value, dict):
|
|
match = self._compare_dicts(value, dict_b.get(key))
|
|
elif isinstance(value, list):
|
|
if len(value) > 0 and isinstance(value[0], dict):
|
|
match = self._compare_dictionary_lists(value, dict_b.get(key))
|
|
else:
|
|
set_a = set(value)
|
|
set_b = set(dict_b.get(key))
|
|
match = (set_a == set_b)
|
|
else:
|
|
match = (value == dict_b.get(key))
|
|
if not match:
|
|
return False
|
|
return True
|
|
|
|
def has_different_resource_limits(self):
|
|
'''
|
|
Diff parameters and container resource limits
|
|
'''
|
|
if not self.container.get('HostConfig'):
|
|
self.fail("limits_differ_from_container: Error parsing container properties. HostConfig missing.")
|
|
|
|
host_config = self.container['HostConfig']
|
|
|
|
config_mapping = dict(
|
|
cpu_period=host_config.get('CpuPeriod'),
|
|
cpu_quota=host_config.get('CpuQuota'),
|
|
cpuset_cpus=host_config.get('CpusetCpus'),
|
|
cpuset_mems=host_config.get('CpusetMems'),
|
|
cpu_shares=host_config.get('CpuShares'),
|
|
kernel_memory=host_config.get("KernelMemory"),
|
|
memory=host_config.get('Memory'),
|
|
memory_reservation=host_config.get('MemoryReservation'),
|
|
memory_swap=host_config.get('MemorySwap'),
|
|
)
|
|
|
|
differences = []
|
|
for key, value in config_mapping.iteritems():
|
|
if getattr(self.parameters, key, None) and getattr(self.parameters, key) != value:
|
|
# no match. record the differences
|
|
item = dict()
|
|
item[key] = dict(
|
|
parameter=getattr(self.parameters, key),
|
|
container=value
|
|
)
|
|
differences.append(item)
|
|
different = (len(differences) > 0)
|
|
return different, differences
|
|
|
|
def has_network_differences(self):
|
|
'''
|
|
Check if the container is connected to requested networks with expected options: links, aliases, ipv4, ipv6
|
|
'''
|
|
different = False
|
|
differences = []
|
|
|
|
if not self.parameters.networks:
|
|
return different, differences
|
|
|
|
if not self.container.get('NetworkSettings'):
|
|
self.fail("has_missing_networks: Error parsing container properties. NetworkSettings missing.")
|
|
|
|
connected_networks = self.container['NetworkSettings']['Networks']
|
|
for network in self.parameters.networks:
|
|
if connected_networks.get(network['name'], None) is None:
|
|
different = True
|
|
differences.append(dict(
|
|
parameter=network,
|
|
container=None
|
|
))
|
|
else:
|
|
diff = False
|
|
if network.get('ipv4_address') and network['ipv4_address'] != connected_networks[network['name']].get('IPAddress'):
|
|
diff = True
|
|
if network.get('ipv6_address') and network['ipv6_address'] != connected_networks[network['name']].get('GlobalIPv6Address'):
|
|
diff = True
|
|
if network.get('aliases') and not connected_networks[network['name']].get('Aliases'):
|
|
diff = True
|
|
if network.get('aliases') and connected_networks[network['name']].get('Aliases'):
|
|
for alias in network.get('aliases'):
|
|
if alias not in connected_networks[network['name']].get('Aliases', []):
|
|
diff = True
|
|
if network.get('links') and not connected_networks[network['name']].get('Links'):
|
|
diff = True
|
|
if network.get('links') and connected_networks[network['name']].get('Links'):
|
|
expected_links = []
|
|
for link, alias in network['links'].iteritems():
|
|
expected_links.append("%s:%s" % (link, alias))
|
|
for link in expected_links:
|
|
if link not in connected_networks[network['name']].get('Links', []):
|
|
diff = True
|
|
if diff:
|
|
different = True
|
|
differences.append(dict(
|
|
parameter=network,
|
|
container=dict(
|
|
name=network['name'],
|
|
ipv4_address=connected_networks[network['name']].get('IPAddress'),
|
|
ipv6_address=connected_networks[network['name']].get('GlobalIPv6Address'),
|
|
aliases=connected_networks[network['name']].get('Aliases'),
|
|
links=connected_networks[network['name']].get('Links')
|
|
)
|
|
))
|
|
return different, differences
|
|
|
|
def has_extra_networks(self):
|
|
'''
|
|
Check if the container is connected to non-requested networks
|
|
'''
|
|
extra_networks = []
|
|
extra = False
|
|
|
|
if not self.container.get('NetworkSettings'):
|
|
self.fail("has_extra_networks: Error parsing container properties. NetworkSettings missing.")
|
|
|
|
connected_networks = self.container['NetworkSettings'].get('Networks')
|
|
if connected_networks:
|
|
for network, network_config in connected_networks.iteritems():
|
|
keep = False
|
|
if self.parameters.networks:
|
|
for expected_network in self.parameters.networks:
|
|
if expected_network['name'] == network:
|
|
keep = True
|
|
if not keep:
|
|
extra = True
|
|
extra_networks.append(dict(name=network, id=network_config['NetworkID']))
|
|
return extra, extra_networks
|
|
|
|
def _get_expected_entrypoint(self):
|
|
self.log('_get_expected_entrypoint')
|
|
if not self.parameters.entrypoint:
|
|
return None
|
|
return shlex.split(self.parameters.entrypoint)
|
|
|
|
def _get_expected_ports(self):
|
|
if not self.parameters.published_ports:
|
|
return None
|
|
expected_bound_ports = {}
|
|
for container_port, config in self.parameters.published_ports.iteritems():
|
|
if isinstance(container_port, int):
|
|
container_port = "%s/tcp" % container_port
|
|
if len(config) == 1:
|
|
expected_bound_ports[container_port] = [{'HostIp': "0.0.0.0", 'HostPort': ""}]
|
|
elif isinstance(config[0], tuple):
|
|
expected_bound_ports[container_port] = []
|
|
for host_ip, host_port in config:
|
|
expected_bound_ports[container_port].append({'HostIp': host_ip, 'HostPort': str(host_port)})
|
|
else:
|
|
expected_bound_ports[container_port] = [{'HostIp': config[0], 'HostPort': str(config[1])}]
|
|
return expected_bound_ports
|
|
|
|
def _get_expected_links(self):
|
|
if self.parameters.links is None:
|
|
return None
|
|
self.log('parameter links:')
|
|
self.log(self.parameters.links, pretty_print=True)
|
|
exp_links = []
|
|
for link, alias in self.parameters.links.iteritems():
|
|
exp_links.append("/%s:%s/%s" % (link, ('/' + self.parameters.name), alias))
|
|
return exp_links
|
|
|
|
def _get_expected_binds(self, image):
|
|
self.log('_get_expected_binds')
|
|
image_vols = []
|
|
if image:
|
|
image_vols = self._get_image_binds(image['ContainerConfig'].get('Volumes'))
|
|
param_vols = []
|
|
if self.parameters.volumes:
|
|
for vol in self.parameters.volumes:
|
|
host = None
|
|
if ':' in vol:
|
|
if len(vol.split(':')) == 3:
|
|
host, container, mode = vol.split(':')
|
|
if len(vol.split(':')) == 2:
|
|
parts = vol.split(':')
|
|
if parts[1] not in VOLUME_PERMISSIONS:
|
|
host, container, mode = vol.split(':') + ['rw']
|
|
if host:
|
|
param_vols.append("%s:%s:%s" % (host, container, mode))
|
|
result = list(set(image_vols + param_vols))
|
|
self.log("expected_binds:")
|
|
self.log(result, pretty_print=True)
|
|
return result
|
|
|
|
def _get_image_binds(self, volumes):
|
|
'''
|
|
Convert array of binds to array of strings with format host_path:container_path:mode
|
|
|
|
:param volumes: array of bind dicts
|
|
:return: array of strings
|
|
'''
|
|
results = []
|
|
if isinstance(volumes, dict):
|
|
results += self._get_bind_from_dict(volumes)
|
|
elif isinstance(volumes, list):
|
|
for vol in volumes:
|
|
results += self._get_bind_from_dict(vol)
|
|
return results
|
|
|
|
@staticmethod
|
|
def _get_bind_from_dict(volume_dict):
|
|
results = []
|
|
if volume_dict:
|
|
for host_path, config in volume_dict.items():
|
|
if isinstance(config, dict) and config.get('bind'):
|
|
container_path = config.get('bind')
|
|
mode = config.get('mode', 'rw')
|
|
results.append("%s:%s:%s" % (host_path, container_path, mode))
|
|
return results
|
|
|
|
def _get_expected_volumes(self, image):
|
|
self.log('_get_expected_volumes')
|
|
expected_vols = dict()
|
|
if image and image['ContainerConfig'].get('Volumes'):
|
|
expected_vols.update(image['ContainerConfig'].get('Volumes'))
|
|
|
|
if self.parameters.volumes:
|
|
for vol in self.parameters.volumes:
|
|
container = None
|
|
if ':' in vol:
|
|
if len(vol.split(':')) == 3:
|
|
host, container, mode = vol.split(':')
|
|
if len(vol.split(':')) == 2:
|
|
parts = vol.split(':')
|
|
if parts[1] not in VOLUME_PERMISSIONS:
|
|
host, container, mode = vol.split(':') + ['rw']
|
|
new_vol = dict()
|
|
if container:
|
|
new_vol[container] = dict()
|
|
else:
|
|
new_vol[vol] = dict()
|
|
expected_vols.update(new_vol)
|
|
|
|
if not expected_vols:
|
|
expected_vols = None
|
|
self.log("expected_volumes:")
|
|
self.log(expected_vols, pretty_print=True)
|
|
return expected_vols
|
|
|
|
def _get_expected_env(self, image):
|
|
self.log('_get_expected_env')
|
|
expected_env = dict()
|
|
if image and image['ContainerConfig'].get('Env'):
|
|
for env_var in image['ContainerConfig']['Env']:
|
|
parts = env_var.split('=', 1)
|
|
expected_env[parts[0]] = parts[1]
|
|
if self.parameters.env:
|
|
expected_env.update(self.parameters.env)
|
|
param_env = []
|
|
for key, value in expected_env.items():
|
|
param_env.append("%s=%s" % (key, value))
|
|
return param_env
|
|
|
|
def _get_expected_exposed(self, image):
|
|
self.log('_get_expected_exposed')
|
|
image_ports = []
|
|
if image:
|
|
image_ports = [re.sub(r'/.+$', '', p) for p in (image['ContainerConfig'].get('ExposedPorts') or {}).keys()]
|
|
param_ports = []
|
|
if self.parameters.ports:
|
|
param_ports = [str(p[0]) for p in self.parameters.ports]
|
|
result = list(set(image_ports + param_ports))
|
|
self.log(result, pretty_print=True)
|
|
return result
|
|
|
|
def _get_expected_ulimits(self, config_ulimits):
|
|
self.log('_get_expected_ulimits')
|
|
if config_ulimits is None:
|
|
return None
|
|
results = []
|
|
for limit in config_ulimits:
|
|
results.append(dict(
|
|
Name=limit.name,
|
|
Soft=limit.soft,
|
|
Hard=limit.hard
|
|
))
|
|
return results
|
|
|
|
def _get_expected_cmd(self):
|
|
self.log('_get_expected_cmd')
|
|
if not self.parameters.command:
|
|
return None
|
|
return shlex.split(self.parameters.command)
|
|
|
|
def _convert_simple_dict_to_list(self, param_name, join_with=':'):
|
|
if getattr(self.parameters, param_name, None) is None:
|
|
return None
|
|
results = []
|
|
for key, value in getattr(self.parameters, param_name).iteritems():
|
|
results.append("%s%s%s" % (key, join_with, value))
|
|
return results
|
|
|
|
|
|
class ContainerManager(DockerBaseClass):
|
|
'''
|
|
Perform container management tasks
|
|
'''
|
|
|
|
def __init__(self, client):
|
|
|
|
super(ContainerManager, self).__init__()
|
|
|
|
self.client = client
|
|
self.parameters = TaskParameters(client)
|
|
self.check_mode = self.client.check_mode
|
|
self.results = {'changed': False, 'actions': []}
|
|
self.diff = {}
|
|
self.facts = {}
|
|
|
|
state = self.parameters.state
|
|
if state in ('stopped', 'started', 'present'):
|
|
self.present(state)
|
|
elif state == 'absent':
|
|
self.absent()
|
|
|
|
if not self.check_mode and not self.parameters.debug:
|
|
self.results.pop('actions')
|
|
|
|
if self.client.module._diff or self.parameters.debug:
|
|
self.results['diff'] = self.diff
|
|
|
|
if self.facts:
|
|
self.results['ansible_facts'] = {'ansible_docker_container': self.facts}
|
|
|
|
def present(self, state):
|
|
container = self._get_container(self.parameters.name)
|
|
image = self._get_image()
|
|
|
|
if not container.exists:
|
|
# New container
|
|
self.log('No container found')
|
|
new_container = self.container_create(self.parameters.image, self.parameters.create_parameters)
|
|
if new_container:
|
|
container = new_container
|
|
else:
|
|
# Existing container
|
|
different, differences = container.has_different_configuration(image)
|
|
image_different = False
|
|
if not self.parameters.ignore_image:
|
|
image_different = self._image_is_different(image, container)
|
|
if image_different or different or self.parameters.recreate:
|
|
self.diff['differences'] = differences
|
|
if image_different:
|
|
self.diff['image_different'] = True
|
|
self.log("differences")
|
|
self.log(differences, pretty_print=True)
|
|
if container.running:
|
|
self.container_stop(container.Id)
|
|
self.container_remove(container.Id)
|
|
new_container = self.container_create(self.parameters.image, self.parameters.create_parameters)
|
|
if new_container:
|
|
container = new_container
|
|
|
|
if container and container.exists:
|
|
container = self.update_limits(container)
|
|
container = self.update_networks(container)
|
|
|
|
if state == 'started' and not container.running:
|
|
container = self.container_start(container.Id)
|
|
elif state == 'started' and self.parameters.restart:
|
|
self.container_stop(container.Id)
|
|
container = self.container_start(container.Id)
|
|
elif state == 'stopped' and container.running:
|
|
self.container_stop(container.Id)
|
|
container = self._get_container(container.Id)
|
|
|
|
self.facts = container.raw
|
|
|
|
def absent(self):
|
|
container = self._get_container(self.parameters.name)
|
|
if container.exists:
|
|
if container.running:
|
|
self.container_stop(container.Id)
|
|
self.container_remove(container.Id)
|
|
|
|
def fail(self, msg, **kwargs):
|
|
self.client.module.fail_json(msg=msg, **kwargs)
|
|
|
|
def _get_container(self, container):
|
|
'''
|
|
Expects container ID or Name. Returns a container object
|
|
'''
|
|
return Container(self.client.get_container(container), self.parameters)
|
|
|
|
def _get_image(self):
|
|
if not self.parameters.image:
|
|
self.log('No image specified')
|
|
return None
|
|
repository, tag = utils.parse_repository_tag(self.parameters.image)
|
|
if not tag:
|
|
tag = "latest"
|
|
image = self.client.find_image(repository, tag)
|
|
if not self.check_mode:
|
|
if not image or self.parameters.pull:
|
|
self.log("Pull the image.")
|
|
image = self.client.pull_image(repository, tag)
|
|
self.results['actions'].append(dict(pulled_image="%s:%s" % (repository, tag)))
|
|
self.results['changed'] = True
|
|
self.log("image")
|
|
self.log(image, pretty_print=True)
|
|
return image
|
|
|
|
def _image_is_different(self, image, container):
|
|
if image and image.get('Id'):
|
|
if container and container.Image:
|
|
if image.get('Id') != container.Image:
|
|
return True
|
|
return False
|
|
|
|
def update_limits(self, container):
|
|
limits_differ, different_limits = container.has_different_resource_limits()
|
|
if limits_differ:
|
|
self.log("limit differences:")
|
|
self.log(different_limits, pretty_print=True)
|
|
if limits_differ and not self.check_mode:
|
|
self.container_update(container.Id, self.parameters.update_parameters)
|
|
return self._get_container(container.Id)
|
|
return container
|
|
|
|
def update_networks(self, container):
|
|
has_network_differences, network_differences = container.has_network_differences()
|
|
updated_container = container
|
|
if has_network_differences:
|
|
if self.diff.get('differences'):
|
|
self.diff['differences'].append(dict(network_differences=network_differences))
|
|
else:
|
|
self.diff['differences'] = [dict(network_differences=network_differences)]
|
|
self.results['changed'] = True
|
|
updated_container = self._add_networks(container, network_differences)
|
|
|
|
if self.parameters.purge_networks:
|
|
has_extra_networks, extra_networks = container.has_extra_networks()
|
|
if has_extra_networks:
|
|
if self.diff.get('differences'):
|
|
self.diff['differences'].append(dict(purge_networks=extra_networks))
|
|
else:
|
|
self.diff['differences'] = [dict(purge_networks=extra_networks)]
|
|
self.results['changed'] = True
|
|
updated_container = self._purge_networks(container, extra_networks)
|
|
return updated_container
|
|
|
|
def _add_networks(self, container, differences):
|
|
for diff in differences:
|
|
# remove the container from the network, if connected
|
|
if diff.get('container'):
|
|
self.results['actions'].append(dict(removed_from_network=diff['parameter']['name']))
|
|
if not self.check_mode:
|
|
try:
|
|
self.client.disconnect_container_from_network(container.Id, diff['parameter']['id'])
|
|
except Exception as exc:
|
|
self.fail("Error disconnecting container from network %s - %s" % (diff['parameter']['name'],
|
|
str(exc)))
|
|
# connect to the network
|
|
params = dict(
|
|
ipv4_address=diff['parameter'].get('ipv4_address', None),
|
|
ipv6_address=diff['parameter'].get('ipv6_address', None),
|
|
links=diff['parameter'].get('links', None),
|
|
aliases=diff['parameter'].get('aliases', None)
|
|
)
|
|
self.results['actions'].append(dict(added_to_network=diff['parameter']['name'], network_parameters=params))
|
|
if not self.check_mode:
|
|
try:
|
|
self.log("Connecting conainer to network %s" % diff['parameter']['id'])
|
|
self.log(params, pretty_print=True)
|
|
self.client.connect_container_to_network(container.Id, diff['parameter']['id'], **params)
|
|
except Exception as exc:
|
|
self.fail("Error connecting container to network %s - %s" % (diff['parameter']['name'], str(exc)))
|
|
return self._get_container(container.Id)
|
|
|
|
def _purge_networks(self, container, networks):
|
|
for network in networks:
|
|
self.results['actions'].append(dict(removed_from_network=network['name']))
|
|
if not self.check_mode and network.get('id'):
|
|
try:
|
|
self.client.disconnect_container_from_network(container.Id, network['id'])
|
|
except Exception as exc:
|
|
self.fail("Error disconnecting container from network %s - %s" % (network['name'],
|
|
str(exc)))
|
|
return self._get_container(container.Id)
|
|
|
|
def container_create(self, image, create_parameters):
|
|
self.log("create container")
|
|
self.log("image: %s parameters:" % image)
|
|
self.log(create_parameters, pretty_print=True)
|
|
self.results['actions'].append(dict(created="Created container", create_parameters=create_parameters))
|
|
self.results['changed'] = True
|
|
new_container = None
|
|
if not self.check_mode:
|
|
try:
|
|
new_container = self.client.create_container(image, **create_parameters)
|
|
except Exception as exc:
|
|
self.fail("Error creating container: %s" % str(exc))
|
|
return self._get_container(new_container['Id'])
|
|
return new_container
|
|
|
|
def container_start(self, container_id):
|
|
self.log("start container %s" % (container_id))
|
|
self.results['actions'].append(dict(started=container_id))
|
|
self.results['changed'] = True
|
|
if not self.check_mode:
|
|
try:
|
|
self.client.start(container=container_id)
|
|
except Exception as exc:
|
|
self.fail("Error starting container %s: %s" % (container_id, str(exc)))
|
|
|
|
if not self.parameters.detach:
|
|
status = self.client.wait(container_id)
|
|
output = self.client.logs(container_id, stdout=True, stderr=True, stream=False, timestamps=False)
|
|
if status != 0:
|
|
self.fail(output, status=status)
|
|
if self.parameters.cleanup:
|
|
self.container_remove(container_id, force=True)
|
|
insp = self._get_container(container_id)
|
|
if insp.raw:
|
|
insp.raw['Output'] = output
|
|
else:
|
|
insp.raw = dict(Output=output)
|
|
return insp
|
|
return self._get_container(container_id)
|
|
|
|
def container_remove(self, container_id, link=False, force=False):
|
|
volume_state = (not self.parameters.keep_volumes)
|
|
self.log("remove container container:%s v:%s link:%s force%s" % (container_id, volume_state, link, force))
|
|
self.results['actions'].append(dict(removed=container_id, volume_state=volume_state, link=link, force=force))
|
|
self.results['changed'] = True
|
|
response = None
|
|
if not self.check_mode:
|
|
try:
|
|
response = self.client.remove_container(container_id, v=volume_state, link=link, force=force)
|
|
except Exception as exc:
|
|
self.fail("Error removing container %s: %s" % (container_id, str(exc)))
|
|
return response
|
|
|
|
def container_update(self, container_id, update_parameters):
|
|
if update_parameters:
|
|
self.log("update container %s" % (container_id))
|
|
self.log(update_parameters, pretty_print=True)
|
|
self.results['actions'].append(dict(updated=container_id, update_parameters=update_parameters))
|
|
self.results['changed'] = True
|
|
if not self.check_mode and callable(getattr(self.client, 'update_container')):
|
|
try:
|
|
self.client.update_container(container_id, **update_parameters)
|
|
except Exception as exc:
|
|
self.fail("Error updating container %s: %s" % (container_id, str(exc)))
|
|
return self._get_container(container_id)
|
|
|
|
def container_kill(self, container_id):
|
|
self.results['actions'].append(dict(killed=container_id, signal=self.parameters.kill_signal))
|
|
self.results['changed'] = True
|
|
response = None
|
|
if not self.check_mode:
|
|
try:
|
|
if self.parameters.kill_signal:
|
|
response = self.client.kill(container_id, signal=self.parameters.kill_signal)
|
|
else:
|
|
response = self.client.kill(container_id)
|
|
except Exception as exc:
|
|
self.fail("Error killing container %s: %s" % (container_id, exc))
|
|
return response
|
|
|
|
def container_stop(self, container_id):
|
|
if self.parameters.force_kill:
|
|
self.container_kill(container_id)
|
|
return
|
|
self.results['actions'].append(dict(stopped=container_id, timeout=self.parameters.stop_timeout))
|
|
self.results['changed'] = True
|
|
response = None
|
|
if not self.check_mode:
|
|
try:
|
|
if self.parameters.stop_timeout:
|
|
response = self.client.stop(container_id, timeout=self.parameters.stop_timeout)
|
|
else:
|
|
response = self.client.stop(container_id)
|
|
except Exception as exc:
|
|
self.fail("Error stopping container %s: %s" % (container_id, str(exc)))
|
|
return response
|
|
|
|
|
|
def main():
|
|
argument_spec = dict(
|
|
blkio_weight=dict(type='int'),
|
|
capabilities=dict(type='list'),
|
|
cleanup=dict(type='bool', default=False),
|
|
command=dict(type='str'),
|
|
cpu_period=dict(type='int'),
|
|
cpu_quota=dict(type='int'),
|
|
cpuset_cpus=dict(type='str'),
|
|
cpuset_mems=dict(type='str'),
|
|
cpu_shares=dict(type='int'),
|
|
detach=dict(type='bool', default=True),
|
|
devices=dict(type='list'),
|
|
dns_servers=dict(type='list'),
|
|
dns_opts=dict(type='list'),
|
|
dns_search_domains=dict(type='list'),
|
|
env=dict(type='dict'),
|
|
env_file=dict(type='path'),
|
|
entrypoint=dict(type='str'),
|
|
etc_hosts=dict(type='dict'),
|
|
exposed_ports=dict(type='list', aliases=['exposed', 'expose']),
|
|
force_kill=dict(type='bool', default=False, aliases=['forcekill']),
|
|
groups=dict(type='list'),
|
|
hostname=dict(type='str'),
|
|
ignore_image=dict(type='bool', default=False),
|
|
image=dict(type='str'),
|
|
interactive=dict(type='bool', default=False),
|
|
ipc_mode=dict(type='str'),
|
|
keep_volumes=dict(type='bool', default=True),
|
|
kernel_memory=dict(type='str'),
|
|
kill_signal=dict(type='str'),
|
|
labels=dict(type='dict'),
|
|
links=dict(type='list'),
|
|
log_driver=dict(type='str', choices=['json-file', 'syslog', 'journald', 'gelf', 'fluentd', 'awslogs', 'splunk'], default='json-file'),
|
|
log_options=dict(type='dict', aliases=['log_opt']),
|
|
mac_address=dict(type='str'),
|
|
memory=dict(type='str', default='0'),
|
|
memory_reservation=dict(type='str'),
|
|
memory_swap=dict(type='str'),
|
|
memory_swappiness=dict(type='int'),
|
|
name=dict(type='str', required=True),
|
|
network_mode=dict(type='str'),
|
|
networks=dict(type='list'),
|
|
oom_killer=dict(type='bool'),
|
|
paused=dict(type='bool', default=False),
|
|
pid_mode=dict(type='str'),
|
|
privileged=dict(type='bool', default=False),
|
|
published_ports=dict(type='list', aliases=['ports']),
|
|
pull=dict(type='bool', default=False),
|
|
purge_networks=dict(type='bool', deault=False),
|
|
read_only=dict(type='bool', default=False),
|
|
recreate=dict(type='bool', default=False),
|
|
restart=dict(type='bool', default=False),
|
|
restart_policy=dict(type='str', choices=['no', 'on-failure', 'always', 'unless-stopped']),
|
|
restart_retries=dict(type='int', default=0),
|
|
shm_size=dict(type='str'),
|
|
security_opts=dict(type='list'),
|
|
state=dict(type='str', choices=['absent', 'present', 'started', 'stopped'], default='started'),
|
|
stop_signal=dict(type='str'),
|
|
stop_timeout=dict(type='int'),
|
|
trust_image_content=dict(type='bool', default=False),
|
|
tty=dict(type='bool', default=False),
|
|
ulimits=dict(type='list'),
|
|
user=dict(type='str'),
|
|
uts=dict(type='str'),
|
|
volumes=dict(type='list'),
|
|
volumes_from=dict(type='list'),
|
|
volume_driver=dict(type='str'),
|
|
)
|
|
|
|
required_if = [
|
|
('state', 'present', ['image'])
|
|
]
|
|
|
|
client = AnsibleDockerClient(
|
|
argument_spec=argument_spec,
|
|
required_if=required_if,
|
|
supports_check_mode=True
|
|
)
|
|
|
|
cm = ContainerManager(client)
|
|
client.module.exit_json(**cm.results)
|
|
|
|
# import module snippets
|
|
from ansible.module_utils.basic import *
|
|
|
|
if __name__ == '__main__':
|
|
main()
|