Merge pull request #6171 from joshuaconner/update_to_docker-py_0.3.0
docker: update to docker-py 0.3.0, plus full port binding support etc. from #5639
This commit is contained in:
commit
42a2586dfa
1 changed files with 257 additions and 71 deletions
|
@ -1,6 +1,7 @@
|
|||
#!/usr/bin/python
|
||||
|
||||
# (c) 2013, Cove Schneider
|
||||
# (c) 2014, Joshua Conner <joshua.conner@gmail.com>
|
||||
# (c) 2014, Pavel Antonov <antonov@adwz.ru>
|
||||
#
|
||||
# This file is part of Ansible,
|
||||
|
@ -52,18 +53,28 @@ options:
|
|||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
version_added: "1.5"
|
||||
ports:
|
||||
description:
|
||||
- Set private to public port mapping specification (e.g. ports=22,80 or ports=:8080 maps 8080 directly to host)
|
||||
- Set private to public port mapping specification using docker CLI-style syntax [([<host_interface>:[host_port]])|(<host_port>):]<container_port>[/udp]
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
links:
|
||||
version_added: "1.5"
|
||||
expose:
|
||||
description:
|
||||
- Add links to another container (e.g. links=redis,postgresql:db)
|
||||
- Set container ports to expose for port mappings or links. (If the port is already exposed using EXPOSE in a Dockerfile, you don't need to expose it again.)
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
version_added: "1.5"
|
||||
publish_all_ports:
|
||||
description:
|
||||
- Publish all exposed ports to the host interfaces
|
||||
required: false
|
||||
default: false
|
||||
aliases: []
|
||||
version_added: "1.5"
|
||||
volumes:
|
||||
description:
|
||||
- Set volume(s) to mount on the container
|
||||
|
@ -76,6 +87,13 @@ options:
|
|||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
links:
|
||||
description:
|
||||
- Link container(s) to other container(s) (e.g. links=redis,postgresql:db)
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
version_added: "1.5"
|
||||
memory_limit:
|
||||
description:
|
||||
- Set RAM allocated to container
|
||||
|
@ -144,8 +162,15 @@ options:
|
|||
required: false
|
||||
default:
|
||||
aliases: []
|
||||
author: Cove Schneider, Pavel Antonov
|
||||
requirements: [ "docker-py" ]
|
||||
name:
|
||||
description:
|
||||
- Set the name of the container (cannot use with count)
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
version_added: "1.5"
|
||||
author: Cove Schneider, Joshua Conner, Pavel Antonov
|
||||
requirements: [ "docker-py >= 0.3.0" ]
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
|
@ -156,7 +181,7 @@ on the host:
|
|||
sudo: yes
|
||||
tasks:
|
||||
- name: run tomcat servers
|
||||
docker: image=centos command="service tomcat6 start" ports=:8080
|
||||
docker: image=centos command="service tomcat6 start" ports=8080
|
||||
|
||||
The tomcat server's port is NAT'ed to a dynamic port on the host, but you can determine which port the server was
|
||||
mapped to using docker_containers:
|
||||
|
@ -167,7 +192,7 @@ mapped to using docker_containers:
|
|||
- name: run tomcat servers
|
||||
docker: image=centos command="service tomcat6 start" ports=8080 count=5
|
||||
- name: Display IP address and port mappings for containers
|
||||
debug: msg={{inventory_hostname}}:{{item.NetworkSettings.Ports['8080/tcp'][0].HostPort}}
|
||||
debug: msg={{inventory_hostname}}:{{item['HostConfig']['PortBindings']['8080/tcp'][0]['HostPort']}}
|
||||
with_items: docker_containers
|
||||
|
||||
Just as in the previous example, but iterates over the list of docker containers with a sequence:
|
||||
|
@ -180,7 +205,7 @@ Just as in the previous example, but iterates over the list of docker containers
|
|||
- name: run tomcat servers
|
||||
docker: image=centos command="service tomcat6 start" ports=8080 count={{start_containers_count}}
|
||||
- name: Display IP address and port mappings for containers
|
||||
debug: msg={{inventory_hostname}}:{{docker_containers[{{item}}].NetworkSettings.Ports['8080/tcp'][0].HostPort}}"
|
||||
debug: msg="{{inventory_hostname}}:{{docker_containers[{{item}}]['HostConfig']['PortBindings']['8080/tcp'][0]['HostPort']}}"
|
||||
with_sequence: start=0 end={{start_containers_count - 1}}
|
||||
|
||||
Stop, remove all of the running tomcat containers and list the exit code from the stopped containers:
|
||||
|
@ -193,16 +218,79 @@ Stop, remove all of the running tomcat containers and list the exit code from th
|
|||
- name: Display return codes from stopped containers
|
||||
debug: msg="Returned {{inventory_hostname}}:{{item}}"
|
||||
with_items: docker_containers
|
||||
|
||||
Create a named container:
|
||||
|
||||
- hosts: web
|
||||
sudo: yes
|
||||
tasks:
|
||||
- name: run tomcat server
|
||||
docker: image=centos name=tomcat command="service tomcat6 start" ports=8080
|
||||
|
||||
Create multiple named containers:
|
||||
|
||||
- hosts: web
|
||||
sudo: yes
|
||||
tasks:
|
||||
- name: run tomcat servers
|
||||
docker: image=centos name={{item}} command="service tomcat6 start" ports=8080
|
||||
with_items:
|
||||
- crookshank
|
||||
- snowbell
|
||||
- heathcliff
|
||||
- felix
|
||||
- sylvester
|
||||
|
||||
Create containers named in a sequence:
|
||||
|
||||
- hosts: web
|
||||
sudo: yes
|
||||
tasks:
|
||||
- name: run tomcat servers
|
||||
docker: image=centos name={{item}} command="service tomcat6 start" ports=8080
|
||||
with_sequence: start=1 end=5 format=tomcat_%d.example.com
|
||||
|
||||
Create two linked containers:
|
||||
|
||||
- hosts: web
|
||||
sudo: yes
|
||||
tasks:
|
||||
- name: ensure redis container is running
|
||||
docker: image=crosbymichael/redis name=redis
|
||||
|
||||
- name: ensure redis_ambassador container is running
|
||||
docker: image=svendowideit/ambassador ports=6379:6379 links=redis:redis name=redis_ambassador_ansible
|
||||
|
||||
Create containers with options specified as key-value pairs and lists:
|
||||
|
||||
- hosts: web
|
||||
sudo: yes
|
||||
tasks:
|
||||
- docker:
|
||||
image: namespace/image_name
|
||||
links:
|
||||
- postgresql:db
|
||||
- redis:redis
|
||||
|
||||
|
||||
Create containers with options specified as strings and lists as comma-separated strings:
|
||||
|
||||
- hosts: web
|
||||
sudo: yes
|
||||
tasks:
|
||||
docker: image=namespace/image_name links=postgresql:db,redis:redis
|
||||
'''
|
||||
|
||||
try:
|
||||
HAS_DOCKER_PY = True
|
||||
|
||||
import sys
|
||||
from urlparse import urlparse
|
||||
try:
|
||||
import docker.client
|
||||
from requests.exceptions import *
|
||||
from urlparse import urlparse
|
||||
except ImportError, e:
|
||||
print "failed=True msg='failed to import python module: %s'" % e
|
||||
sys.exit(1)
|
||||
HAS_DOCKER_PY = False
|
||||
|
||||
|
||||
def _human_to_bytes(number):
|
||||
suffixes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']
|
||||
|
@ -243,9 +331,7 @@ class DockerManager:
|
|||
if self.module.params.get('volumes'):
|
||||
self.binds = {}
|
||||
self.volumes = {}
|
||||
vols = self.module.params.get('volumes')
|
||||
if not isinstance(vols, list):
|
||||
vols = vols.split(" ")
|
||||
vols = self.parse_list_from_param('volumes')
|
||||
for vol in vols:
|
||||
parts = vol.split(":")
|
||||
# host mount (e.g. /mnt:/tmp, bind mounts host's /tmp to /mnt in the container)
|
||||
|
@ -259,50 +345,106 @@ class DockerManager:
|
|||
self.lxc_conf = None
|
||||
if self.module.params.get('lxc_conf'):
|
||||
self.lxc_conf = []
|
||||
options = self.module.params.get('lxc_conf').split(" ")
|
||||
options = self.parse_list_from_param('lxc_conf')
|
||||
for option in options:
|
||||
parts = option.split(':')
|
||||
self.lxc_conf.append({"Key": parts[0], "Value": parts[1]})
|
||||
|
||||
self.ports = None
|
||||
if self.module.params.get('ports'):
|
||||
ports = self.module.params.get('ports')
|
||||
self.ports = ports if isinstance(ports, list) else ports.split(",")
|
||||
self.exposed_ports = None
|
||||
if self.module.params.get('expose'):
|
||||
expose = self.parse_list_from_param('expose')
|
||||
self.exposed_ports = self.get_exposed_ports(expose)
|
||||
|
||||
self.port_bindings = None
|
||||
if self.module.params.get('port_bindings'):
|
||||
self.port_bindings = {}
|
||||
bindings = self.module.params.get('port_bindings')
|
||||
bindings = bindings if isinstance(bindings, list) else bindings.split(",")
|
||||
for binding in bindings:
|
||||
parts = binding.split(":")
|
||||
if len(parts) == 3:
|
||||
self.port_bindings[parts[0]] = (parts[1], parts[2])
|
||||
elif len(parts) == 2:
|
||||
self.port_bindings[parts[0]] = parts[1]
|
||||
else:
|
||||
self.port_bindings[parts[0]] = None
|
||||
if self.module.params.get('ports'):
|
||||
ports = self.parse_list_from_param('ports')
|
||||
self.port_bindings = self.get_port_bindings(ports)
|
||||
|
||||
self.links = None
|
||||
if self.module.params.get('links'):
|
||||
self.links = {}
|
||||
links = self.module.params.get('links')
|
||||
if not isinstance(links, list):
|
||||
links = links.split(",")
|
||||
for link in links:
|
||||
parts = link.split(":")
|
||||
self.links[parts[0]] = parts[1] if len(parts) == 2 else parts[0]
|
||||
links = self.parse_list_from_param('links')
|
||||
self.links = dict(map(lambda x: x.split(':'), links))
|
||||
|
||||
self.env = None
|
||||
if self.module.params.get('env'):
|
||||
env = self.module.params.get('env')
|
||||
self.env = dict(map(lambda x: x.split("="), env if isinstance(env, list) else env.split(",")))
|
||||
env = self.parse_list_from_param('env')
|
||||
self.env = dict(map(lambda x: x.split("="), env))
|
||||
|
||||
# connect to docker server
|
||||
docker_url = urlparse(module.params.get('docker_url'))
|
||||
self.client = docker.Client(base_url=docker_url.geturl())
|
||||
|
||||
|
||||
def parse_list_from_param(self, param_name, delimiter=','):
|
||||
"""
|
||||
Get a list from a module parameter, whether it's specified as a delimiter-separated string or is already in list form.
|
||||
"""
|
||||
param_list = self.module.params.get(param_name)
|
||||
if not isinstance(param_list, list):
|
||||
param_list = param_list.split(delimiter)
|
||||
return param_list
|
||||
|
||||
|
||||
def get_exposed_ports(self, expose_list):
|
||||
"""
|
||||
Parse the ports and protocols (TCP/UDP) to expose in the docker-py `create_container` call from the docker CLI-style syntax.
|
||||
"""
|
||||
if expose_list:
|
||||
exposed = []
|
||||
for port in expose_list:
|
||||
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
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def get_port_bindings(self, ports):
|
||||
"""
|
||||
Parse the `ports` string into a port bindings dict for the `start_container` call.
|
||||
"""
|
||||
binds = {}
|
||||
for port in ports:
|
||||
parts = 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 `container_port` of the container to a dynamically
|
||||
# allocated TCP port on all available interfaces of the host
|
||||
# machine.
|
||||
bind = ('0.0.0.0',)
|
||||
elif p_len == 2:
|
||||
# Bind `container_port` of the container to port `parts[0]` on
|
||||
# all available interfaces of the host machine.
|
||||
bind = ('0.0.0.0', int(parts[0]))
|
||||
elif p_len == 3:
|
||||
# Bind `container_port` of the container to port `parts[1]` on
|
||||
# IP `parts[0]` of the host machine. If `parts[1]` empty bind
|
||||
# to a dynamically allocacted port of IP `parts[0]`.
|
||||
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):
|
||||
# append to list if it already exists
|
||||
old_bind.append(bind)
|
||||
else:
|
||||
# otherwise create list that contains the old and new binds
|
||||
binds[container_port] = [binds[container_port], bind]
|
||||
else:
|
||||
binds[container_port] = bind
|
||||
|
||||
return binds
|
||||
|
||||
|
||||
|
||||
def get_split_image_tag(self, image):
|
||||
if '/' in image:
|
||||
image = image.split('/')[1]
|
||||
|
@ -378,8 +520,7 @@ class DockerManager:
|
|||
def create_containers(self, count=1):
|
||||
params = {'image': self.module.params.get('image'),
|
||||
'command': self.module.params.get('command'),
|
||||
'name': self.module.params.get('name'),
|
||||
'ports': self.ports,
|
||||
'ports': self.exposed_ports,
|
||||
'volumes': self.volumes,
|
||||
'volumes_from': self.module.params.get('volumes_from'),
|
||||
'mem_limit': _human_to_bytes(self.module.params.get('memory_limit')),
|
||||
|
@ -387,7 +528,7 @@ class DockerManager:
|
|||
'dns': self.module.params.get('dns'),
|
||||
'hostname': self.module.params.get('hostname'),
|
||||
'detach': self.module.params.get('detach'),
|
||||
'privileged': self.module.params.get('privileged'),
|
||||
'name': self.module.params.get('name'),
|
||||
}
|
||||
|
||||
def do_create(count, params):
|
||||
|
@ -409,9 +550,16 @@ class DockerManager:
|
|||
return containers
|
||||
|
||||
def start_containers(self, containers):
|
||||
params = {
|
||||
'lxc_conf': self.lxc_conf,
|
||||
'binds': self.binds,
|
||||
'port_bindings': self.port_bindings,
|
||||
'publish_all_ports': self.module.params.get('publish_all_ports'),
|
||||
'privileged': self.module.params.get('privileged'),
|
||||
'links': self.links,
|
||||
}
|
||||
for i in containers:
|
||||
self.client.start(i['Id'], lxc_conf=self.lxc_conf, binds=self.binds,
|
||||
port_bindings = self.port_bindings, links=self.links)
|
||||
self.client.start(i['Id'], **params)
|
||||
self.increment_counter('started')
|
||||
|
||||
def stop_containers(self, containers):
|
||||
|
@ -436,18 +584,44 @@ class DockerManager:
|
|||
self.client.restart(i['Id'])
|
||||
self.increment_counter('restarted')
|
||||
|
||||
|
||||
def check_dependencies(module):
|
||||
"""
|
||||
Ensure `docker-py` >= 0.3.0 is installed, and call module.fail_json with a
|
||||
helpful error message if it isn't.
|
||||
"""
|
||||
if not HAS_DOCKER_PY:
|
||||
module.fail_json(msg="`docker-py` doesn't seem to be installed, but is required for the Ansible Docker module.")
|
||||
else:
|
||||
HAS_NEW_ENOUGH_DOCKER_PY = False
|
||||
if hasattr(docker, '__version__'):
|
||||
# a '__version__' attribute was added to the module but not until
|
||||
# after 0.3.0 was added pushed to pip. If it's there, use it.
|
||||
if docker.__version__ >= '0.3.0':
|
||||
HAS_NEW_ENOUGH_DOCKER_PY = True
|
||||
else:
|
||||
# HACK: if '__version__' isn't there, we check for the existence of
|
||||
# `_get_raw_response_socket` in the docker.Client class, which was
|
||||
# added in 0.3.0
|
||||
if hasattr(docker.Client, '_get_raw_response_socket'):
|
||||
HAS_NEW_ENOUGH_DOCKER_PY = True
|
||||
|
||||
if not HAS_NEW_ENOUGH_DOCKER_PY:
|
||||
module.fail_json(msg="The Ansible Docker module requires `docker-py` >= 0.3.0.")
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict(
|
||||
count = dict(default=1),
|
||||
image = dict(required=True),
|
||||
command = dict(required=False, default=None),
|
||||
name = dict(required=False, default=None),
|
||||
expose = dict(required=False, default=None),
|
||||
ports = dict(required=False, default=None),
|
||||
port_bindings = dict(required=False, default=None),
|
||||
links = dict(required=False, default=None),
|
||||
publish_all_ports = dict(default=False, type='bool'),
|
||||
volumes = dict(default=None),
|
||||
volumes_from = dict(default=None),
|
||||
links = dict(default=None),
|
||||
memory_limit = dict(default=0),
|
||||
memory_swap = dict(default=0),
|
||||
docker_url = dict(default='unix://var/run/docker.sock'),
|
||||
|
@ -461,17 +635,23 @@ def main():
|
|||
state = dict(default='present', choices=['absent', 'present', 'stopped', 'killed', 'restarted']),
|
||||
debug = dict(default=False, type='bool'),
|
||||
privileged = dict(default=False, type='bool'),
|
||||
lxc_conf = dict(default=None)
|
||||
lxc_conf = dict(default=None),
|
||||
name = dict(default=None)
|
||||
)
|
||||
)
|
||||
|
||||
check_dependencies(module)
|
||||
|
||||
try:
|
||||
manager = DockerManager(module)
|
||||
state = module.params.get('state')
|
||||
count = int(module.params.get('count'))
|
||||
name = module.params.get('name')
|
||||
|
||||
if count < 1:
|
||||
module.fail_json(msg="Count must be positive number")
|
||||
if count < 0:
|
||||
module.fail_json(msg="Count must be greater than zero")
|
||||
if count > 1 and name:
|
||||
module.fail_json(msg="Count and name must not be used together")
|
||||
|
||||
running_containers = manager.get_running_containers()
|
||||
running_count = len(running_containers)
|
||||
|
@ -483,8 +663,14 @@ def main():
|
|||
|
||||
# start/stop containers
|
||||
if state == "present":
|
||||
|
||||
# make sure a container with `name` is running
|
||||
if name and "/" + name not in map(lambda x: x.get('Name'), running_containers):
|
||||
containers = manager.create_containers(1)
|
||||
manager.start_containers(containers)
|
||||
|
||||
# start more containers if we don't have enough
|
||||
if delta > 0:
|
||||
elif delta > 0:
|
||||
containers = manager.create_containers(delta)
|
||||
manager.start_containers(containers)
|
||||
|
||||
|
|
Loading…
Reference in a new issue