###################################################################
# Copyright 2013-2015 All Rights Reserved
# Authors: The Paradrop Team
###################################################################
"""
Functions associated with deploying and cleaning up docker containers.
"""
from pdtools.lib.output import out
import docker
import json
import os
import subprocess
from paradrop.lib import settings
DOCKER_CONF = """
# Docker systemd configuration
#
# This configuration file was automatically generated by Paradrop. Any changes
# will be overwritten on startup.
# Tell docker not to start containers automatically on startup.
DOCKER_OPTIONS="--restart=false"
"""
[docs]def writeDockerConfig():
"""
Write options to Docker configuration.
Mainly, we want to tell Docker not to start containers automatically on
system boot.
"""
# First we have to find the configuration file. On Snappy, it should be in
# "/var/lib/apps/docker/{version}/etc/docker.conf", but version could
# change.
path = "/var/lib/apps/docker"
if not os.path.exists(path):
out.warn('No directory "{}" found'.format(path))
return
written = False
for d in os.listdir(path):
finalPath = os.path.join(path, d, "etc/docker.conf")
if not os.path.exists(finalPath):
continue
try:
with open(finalPath, "w") as output:
output.write(DOCKER_CONF)
written = True
except Exception as e:
out.warn('Error writing to {}: {}'.format(finalPath, str(e)))
if not written:
out.warn('Could not write docker configuration.')
[docs]def startChute(update):
"""
Build and deploy a docker container based on the passed in update.
:param update: The update object containing information about the chute.
:type update: obj
:returns: None
"""
out.info('Attempting to start new Chute %s \n' % (update.name))
repo = update.name + ":latest"
dockerfile = update.dockerfile
name = update.name
host_config = build_host_config(update)
c = docker.Client(base_url="unix://var/run/docker.sock", version='auto')
#Get Id's of current images for comparison upon failure
validImages = c.images(quiet=True, all=False)
validContainers = c.containers(quiet=True, all=True)
buildFailed = False
for line in c.build(rm=True, tag=repo, fileobj=dockerfile):
#if we encountered an error make note of it
if 'errorDetail' in line:
buildFailed = True
for key, value in json.loads(line).iteritems():
if isinstance(value, dict):
continue
elif key == 'stream':
update.pkg.request.write(str(value))
else:
update.pkg.request.write(str(value) + '\n')
#If we failed to build skip creating and starting clean up and fail
if buildFailed:
failAndCleanUpDocker(validImages, validContainers)
try:
container = c.create_container(
image=repo, name=name, host_config=host_config
)
c.start(container.get('Id'))
except Exception as e:
failAndCleanUpDocker(validImages, validContainers)
out.info("Successfully started chute with Id: %s\n" % (str(container.get('Id'))))
setup_net_interfaces(update)
[docs]def removeChute(update):
"""
Remove a docker container and the image it was built on based on the passed in update.
:param update: The update object containing information about the chute.
:type update: obj
:returns: None
"""
out.info('Attempting to remove chute %s\n' % (update.name))
c = docker.Client(base_url='unix://var/run/docker.sock', version='auto')
repo = update.name + ":latest"
name = update.name
try:
c.remove_container(container=name, force=True)
c.remove_image(image=repo)
except Exception as e:
update.complete(success=False, message= e.explanation)
[docs]def stopChute(update):
"""
Stop a docker container based on the passed in update.
:param update: The update object containing information about the chute.
:type update: obj
:returns: None
"""
out.info('Attempting to stop chute %s\n' % (update.name))
c = docker.Client(base_url='unix://var/run/docker.sock', version='auto')
try:
c.stop(container=update.name)
except Exception as e:
update.complete(success=False, message= e.explanation)
raise e
[docs]def restartChute(update):
"""
Start a docker container based on the passed in update.
:param update: The update object containing information about the chute.
:type update: obj
:returns: None
"""
out.info('Attempting to restart chute %s\n' % (update.name))
c = docker.Client(base_url='unix://var/run/docker.sock', version='auto')
try:
c.start(container=update.name)
except Exception as e:
update.complete(success=False, message= e.explanation)
raise e
setup_net_interfaces(update)
[docs]def failAndCleanUpDocker(validImages, validContainers):
"""
Clean up any intermediate containers that may have resulted from a failure and throw an Exception so that
the abort process is called.
:param validImages: A list of dicts containing the Id's of all the images that should exist on the system.
:type validImages: list
:param validContainers: A list of the Id's of all the containers that should exist on the system.
:type validContainers: list
:returns: None
"""
c = docker.Client(base_url="unix://var/run/docker.sock", version='auto')
#Clean up containers from failed build/start
currContainers = c.containers(quiet=True, all=True)
for cntr in currContainers:
if not cntr in validContainers:
out.info('Removing Invalid container with id: %s' % str(cntr.get('Id')))
c.remove_container(container=cntr.get('Id'))
#Clean up images from failed build
currImages = c.images(quiet=True, all=False)
for img in currImages:
if not img in validImages:
out.info('Removing Invalid image with id: %s' % str(img))
c.remove_image(image=img)
#Throw exception so abort plan is called and user is notifie
raise Exception('Building or starting of docker image failed check your Dockerfile for errors.')
[docs]def build_host_config(update):
"""
Build the host_config dict for a docker container based on the passed in update.
:param update: The update object containing information about the chute.
:type update: obj
:returns: (dict) The host_config dict which docker needs in order to create the container.
"""
if not hasattr(update.new, 'host_config') or update.new.host_config == None:
config = dict()
else:
config = update.new.host_config
host_conf = docker.utils.create_host_config(
#TO support
port_bindings=config.get('port_bindings'),
binds=config.get('binds'),
links=config.get('links'),
dns=config.get('dns'),
#not supported/managed by us
#network_mode=update.host_config.get('network_mode'),
#extra_hosts=update.host_config.get('extra_hosts'),
restart_policy={'MaximumRetryCount': 5, 'Name': 'on-failure'},
devices=[],
lxc_conf={},
publish_all_ports=False,
privileged=False,
dns_search=[],
volumes_from=None,
cap_add=['NET_ADMIN'],
cap_drop=[]
)
return host_conf
[docs]def setup_net_interfaces(update):
"""
Link interfaces in the host to the internal interface in the docker container using pipework.
:param update: The update object containing information about the chute.
:type update: obj
:returns: None
"""
interfaces = update.new.getCache('networkInterfaces')
for iface in interfaces:
if iface.get('netType') == 'wifi':
IP = iface.get('ipaddrWithPrefix')
internalIntf = iface.get('internalIntf')
externalIntf = iface.get('externalIntf')
else:
continue
# Construct environment for pipework call. It only seems to require
# the PATH variable to include the directory containing the docker
# client. On Snappy this was not happening by default, which is why
# this code is here.
env = {"PATH": os.environ.get("PATH", "")}
if settings.DOCKER_BIN_DIR not in env['PATH']:
env['PATH'] += ":" + settings.DOCKER_BIN_DIR
cmd = ['/apps/paradrop/current/bin/pipework', externalIntf, '-i',
internalIntf, update.name, IP]
out.info("Calling: {}\n".format(" ".join(cmd)))
try:
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, env=env)
for line in proc.stdout:
out.info("pipework: {}\n".format(line.strip()))
for line in proc.stderr:
out.warn("pipework: {}\n".format(line.strip()))
except OSError as e:
out.warn('Command "{}" failed\n'.format(" ".join(cmd)))
out.exception(e, True)
raise e