mirror of https://github.com/ansible/ansible.git
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1642 lines
58 KiB
Python
1642 lines
58 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.
|
|
|
|
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
|
|
command:
|
|
description:
|
|
- Command or list of commands to execute in the container when it 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
|
|
entrypoint:
|
|
description:
|
|
- String or list of commands that overwrite the default ENTRYPOINT of the image.
|
|
default: null
|
|
required: false
|
|
etc_hosts:
|
|
description:
|
|
- Dict of host-to-IP mappings, where each host name is key in the dictionary.
|
|
Hostname 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 xposed again.
|
|
default: null
|
|
required: false
|
|
aliases:
|
|
- exposed
|
|
force_kill:
|
|
description:
|
|
- Use with absent, present, started and stopped states to use the kill command rather than the stop command.
|
|
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 state is present or started the module compares the configuration of an existing
|
|
container to requested configuration. The evaluation includes the image version. If
|
|
the image vesion in the registry does not match the container, the container will be
|
|
rebuilt. To stop this behavior set ignore_image to true.
|
|
default: false
|
|
required: false
|
|
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
|
|
defult: 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
|
|
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.
|
|
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
|
|
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
|
|
registry:
|
|
description:
|
|
- Registry URL from which to pull images. If not specified, images will be pulled from
|
|
the default registry found in the local docker config.json file.
|
|
default: null
|
|
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.
|
|
choices:
|
|
- on-failure
|
|
- always
|
|
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:
|
|
- '"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.'
|
|
- '"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.'
|
|
- '"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.'
|
|
- '"stopped" - a container matching the specified name will be stopped. 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's 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
|
|
recreate: yes
|
|
force_kill: yes
|
|
image: someplace/image
|
|
command: echo "I'm here!"
|
|
|
|
- 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
|
|
|
|
'''
|
|
|
|
RETURN = '''
|
|
ansible_docker_container:
|
|
description: Facts representing the current state of the container. Note that facts are not part of registred vars but accessible directly.
|
|
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/": {}
|
|
},
|
|
...
|
|
}'
|
|
'''
|
|
|
|
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'
|
|
]
|
|
|
|
|
|
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.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.devices = None
|
|
self.dns_servers = None
|
|
self.dns_opts = None
|
|
self.dns_search_domains = None
|
|
self.env = None
|
|
self.entrypoint = None
|
|
self.etc_hosts = None
|
|
self.exposed_ports = None
|
|
self.force_kill = None
|
|
self.groups = None
|
|
self.hostname = 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.pull = None
|
|
self.read_only = None
|
|
self.recreate = None
|
|
self.restart = None
|
|
self.restart_retries = 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, exc:
|
|
self.fail("Failed to convert %s to bytes: %s" % (param_name, exc))
|
|
|
|
self.ports = self._parse_exposed_ports()
|
|
self.published_ports = self._parse_publish_ports()
|
|
self.publish_all_ports = None
|
|
if self.published_ports == 'all':
|
|
self.publish_all_ports = True
|
|
self.published_ports = None
|
|
|
|
self.links = self._parse_links()
|
|
|
|
if self.restart_policy is not None:
|
|
self.restart_policy = dict(Name=self.restart_policy,
|
|
MaximumRetryCount=self.restart_retries)
|
|
if self.volumes:
|
|
self.volumes = self._expand_host_paths()
|
|
|
|
self.log("volumes:")
|
|
self.log(self.volumes, pretty_print=True)
|
|
|
|
self.ulimits = self._parse_ulimits()
|
|
self.log_config = self._parse_log_config()
|
|
self.exp_links = None
|
|
self._parse_volumes()
|
|
|
|
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',
|
|
dns='dns_servers',
|
|
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(':')
|
|
host = os.path.abspath(host)
|
|
new_vols.append("%s:%s:%s" % (host, container, mode))
|
|
else:
|
|
host, container = vol.split(':')
|
|
host = os.path.abspath(host)
|
|
new_vols.append("%s:%s:rw" % (host, container))
|
|
else:
|
|
new_vols.append("%s:%s:rw" % (os.path.abspath(vol), vol))
|
|
return new_vols
|
|
|
|
def _get_mounts(self):
|
|
result = []
|
|
if self.volumes:
|
|
for vol in self.volumes:
|
|
if ':' in vol:
|
|
host, container, _ = vol.split(':')
|
|
result.append(host)
|
|
else:
|
|
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',
|
|
restart_policy='restart_policy',
|
|
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)
|
|
return self.client.create_host_config(**params)
|
|
|
|
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'
|
|
|
|
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 = ('0.0.0.0',)
|
|
elif p_len == 2:
|
|
bind = ('0.0.0.0', 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
|
|
|
|
def _parse_volumes(self):
|
|
'''
|
|
Convert volumes parameter to host_config bind format.
|
|
https://docker-py.readthedocs.org/en/latest/volumes/
|
|
:return: array of binds
|
|
'''
|
|
if self.volumes:
|
|
for vol in self.volumes:
|
|
if ':' in vol:
|
|
if len(vol.split(':')) == 3:
|
|
host, container, mode = vol.split(':')
|
|
else:
|
|
host, container, mode = (vol.split(':') + ['rw'])
|
|
self.volume_binds[host] = dict(
|
|
bind=container,
|
|
mode=mode
|
|
)
|
|
else:
|
|
self.volume_binds[vol] = dict(
|
|
bind=vol,
|
|
mode='rw'
|
|
)
|
|
|
|
|
|
def _parse_exposed_ports(self):
|
|
'''
|
|
Parse exposed ports from docker CLI-style ports syntax.
|
|
'''
|
|
if self.exposed_ports is None:
|
|
return None
|
|
|
|
exposed = []
|
|
for port in self.exposed_ports:
|
|
port = str(port).strip()
|
|
if port.endswith('/tcp') or port.endswith('/udp'):
|
|
port_with_proto = tuple(port.split('/'))
|
|
else:
|
|
# assume tcp protocol if not specified
|
|
port_with_proto = (port, 'tcp')
|
|
exposed.append(port_with_proto)
|
|
return exposed
|
|
|
|
def _parse_links(self):
|
|
'''
|
|
Turn links into a dictionary
|
|
'''
|
|
if self.links is None:
|
|
return None
|
|
|
|
links = {}
|
|
for link in self.links:
|
|
parsed_link = link.split(':', 1)
|
|
if len(parsed_link) == 2:
|
|
links[parsed_link[0]] = parsed_link[1]
|
|
else:
|
|
links[parsed_link[0]] = parsed_link[0]
|
|
return links
|
|
|
|
def _parse_ulimits(self):
|
|
'''
|
|
Turn ulimits into a dictionary
|
|
'''
|
|
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])
|
|
if len(pieces) == 3:
|
|
limits['hard'] = int(pieces[2])
|
|
try:
|
|
results.append(Ulimit(**limits))
|
|
except ValueError, 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, exc:
|
|
self.fail('Error parsing logging options - %s' % (exc))
|
|
|
|
|
|
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 found(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(image)
|
|
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_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']
|
|
host_config['Ulimits'] = self._get_expected_ulimits(host_config['Ulimits'])
|
|
|
|
# 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=host_config['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_missing_networks(self):
|
|
'''
|
|
Check if the container is connected to requested networks
|
|
'''
|
|
missing_networks = []
|
|
missing = False
|
|
|
|
if not self.parameters.networks:
|
|
return missing, missing_networks
|
|
|
|
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, config in self.parameters.networks.iteritems():
|
|
if connected_networks.get(network, None) is None:
|
|
missing_networks.append(network)
|
|
if len(missing_networks) > 0:
|
|
missing = True
|
|
return missing, missing_networks
|
|
|
|
def has_extra_networks(self):
|
|
'''
|
|
Check if the container is connected to non-requested networks
|
|
'''
|
|
extra_networks = []
|
|
extra = False
|
|
|
|
if not self.parameters.networks:
|
|
return extra, extra_networks
|
|
|
|
if not self.container.get('NetworkSettings'):
|
|
self.fail("has_extra_networks: Error parsing container properties. NetworkSettings missing.")
|
|
|
|
connected_networks = self.container['NetworkSettings']['Networks']
|
|
for network in connected_networks:
|
|
if network not in ('bridge', 'host') and not network.startswith('container:'):
|
|
if network not in self.parameters.networks:
|
|
extra_networks.append(network)
|
|
if len(extra_networks) > 0:
|
|
extra = True
|
|
return extra, extra_networks
|
|
|
|
def _get_expected_entrypoint(self, image):
|
|
self.log('_get_expected_entrypoint')
|
|
if isinstance(self.parameters.entrypoint, list):
|
|
entrypoint = self.parameters.entrypoint
|
|
else:
|
|
entrypoint = []
|
|
if image and image['ContainerConfig'].get('Entrypoint'):
|
|
entrypoint = list(set(entrypoint + image['ContainerConfig'].get('Entrypoint')))
|
|
if len(entrypoint) == 0:
|
|
return None
|
|
return entrypoint
|
|
|
|
def _get_expected_ports(self):
|
|
if self.parameters.published_ports is None:
|
|
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.iteritems():
|
|
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_volumes(self, image):
|
|
self.log('_get_expected_volumes')
|
|
image_vols = []
|
|
if image:
|
|
image_vols = self._get_volumes_from_binds(image['ContainerConfig'].get('Volumes'))
|
|
param_vols = []
|
|
if self.parameters.volumes:
|
|
for vol in self.parameters.volumes:
|
|
if ':' in vol:
|
|
if len(vol.split(':')) == 3:
|
|
host, container, mode = vol.split(':')
|
|
else:
|
|
host, container, mode = vol.split(':') + ['rw']
|
|
param_vols.append("%s:%s:%s" % (host, container, mode))
|
|
else:
|
|
param_vols.append(vol)
|
|
# flip to container first
|
|
return list(set(image_vols + param_vols))
|
|
|
|
def _get_volumes_from_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_volume_from_dict(volumes)
|
|
elif isinstance(volumes, list):
|
|
for vol in volumes:
|
|
results += self._get_volume_from_dict(vol)
|
|
return results
|
|
|
|
def _get_volume_from_dict(self, 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_env(self, image):
|
|
self.log('_get_expected_env')
|
|
param_env = (self._convert_simple_dict_to_list('env', '=') or [])
|
|
if image and image['ContainerConfig'].get('Env'):
|
|
image_env = image['ContainerConfig'].get('Env')
|
|
param_env = list(set(param_env + image_env))
|
|
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()]
|
|
if self.parameters.exposed_ports:
|
|
param_ports = [str(p) for p in self.parameters.exposed_ports]
|
|
else:
|
|
param_ports = []
|
|
return list(set(image_ports + param_ports))
|
|
|
|
def _get_expected_ulimits(self, config_ulimits):
|
|
self.log('_get_expected_ulimits')
|
|
if config_ulimits is None:
|
|
return None
|
|
|
|
results = []
|
|
if isinstance(config_ulimits[0], Ulimit):
|
|
for limit in config_ulimits:
|
|
if limit.hard:
|
|
results.append("%s:%s" % (limit.name, limit.soft, limit.hard))
|
|
else:
|
|
results.append("%s:%s" % (limit.name, limit.soft))
|
|
else:
|
|
for limit in config_ulimits:
|
|
if limit.get('hard'):
|
|
results.append("%s:%s" % (limit.get('name'), limit.get('hard')))
|
|
else:
|
|
results.append("%s:%s" % (limit.get('name'), limit.get('soft')))
|
|
return results
|
|
|
|
def _get_expected_cmd(self):
|
|
self.log('_get_expected_cmd')
|
|
if not self.parameters.command:
|
|
return None
|
|
# expected_commands = []
|
|
# commands = self.parameters.command
|
|
# for cmd in commands:
|
|
# expected_commands = expected_commands + shlex.split(cmd)
|
|
return 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 ('started', 'present'):
|
|
self.present(state)
|
|
elif state == 'absent':
|
|
self.absent()
|
|
|
|
# remove for now, until we decide about general framework
|
|
try:
|
|
del self.results['actions']
|
|
except:
|
|
pass
|
|
|
|
if self.client.module._diff:
|
|
self.results['diff'] = self.diff
|
|
|
|
if self.facts:
|
|
self.results['ansbile_facts'] = {'ansible_docker_container': self.facts}
|
|
|
|
def present(self, state):
|
|
container = self._get_container(self.parameters.name)
|
|
image = self._get_image()
|
|
|
|
if not container.found:
|
|
self.log('No container found')
|
|
# New container
|
|
new_container = self.container_create(self.parameters.image, self.parameters.create_parameters)
|
|
if new_container:
|
|
container = new_container
|
|
container = self.update_limits(container)
|
|
container = self.update_networks(container)
|
|
if state == 'started':
|
|
container = self.container_start(container.Id)
|
|
self.facts = container.raw
|
|
return True
|
|
|
|
# Existing container
|
|
self.log(container.raw, pretty_print=True)
|
|
different, differences = container.has_different_configuration(image)
|
|
image_different = self._image_is_different(image, container)
|
|
if image_different or different or self.parameters.recreate:
|
|
self.diff['differences'] = differences
|
|
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 image_different:
|
|
self.diff['image_different'] = True
|
|
|
|
container = self.update_limits(container)
|
|
container = self.update_networks(container)
|
|
|
|
# TODO implement has_extra_networks
|
|
|
|
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 == 'present' and container.running:
|
|
self.container_stop(container.Id)
|
|
container = self._get_container(container.Id)
|
|
|
|
self.facts = container.raw
|
|
|
|
def absent(self):
|
|
container = Container(self.client.get_container(self.parameters.name), self.parameters)
|
|
if container.found:
|
|
if container.running:
|
|
self.container_stop(container.Id)
|
|
self.container_remove(container.Id)
|
|
|
|
def fail(self, msg):
|
|
self.client.module.fail_json(msg=msg)
|
|
|
|
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):
|
|
networks_missing, missing_networks = container.has_missing_networks()
|
|
if networks_missing:
|
|
self.log("networks missing")
|
|
self.log(missing_networks, pretty_print=True)
|
|
if networks_missing and not self.check_mode:
|
|
for network in missing_networks:
|
|
self.connect_container_to_network(container.Id, network)
|
|
return self._get_container(container.Id)
|
|
return container
|
|
|
|
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, 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, exc:
|
|
self.fail("Error starting container %s: %s" % (container_id, str(exc)))
|
|
return self._get_container(container_id)
|
|
|
|
def container_remove(self, container_id, v=False, link=False, force=False):
|
|
volume_state = (True if self.parameters.keep_volumes else False)
|
|
self.log("remove container container:%s v:%s link:%s force%s" % (container_id, v, link, force))
|
|
self.results['actions'].append(dict(removed=container_id, volume_state=volume_state))
|
|
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, 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, 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, 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, exc:
|
|
self.fail("Error stopping container %s: %s" % (container_id, str(exc)))
|
|
return response
|
|
|
|
def connect_container_to_network(self, container_id, network ):
|
|
# TODO - Implement network connecions
|
|
pass
|
|
|
|
|
|
def main():
|
|
argument_spec = dict(
|
|
blkio_weight=dict(type='int'),
|
|
capabilities=dict(type='list'),
|
|
command=dict(type='list'),
|
|
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'),
|
|
entrypoint=dict(type='list'),
|
|
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'),
|
|
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='dict'),
|
|
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),
|
|
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=['on-failure', 'always']),
|
|
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()
|