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:
jctanner 2014-02-28 08:31:56 -05:00
commit 42a2586dfa

View file

@ -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)