kolla-ansible/ansible/library/kolla_docker.py
Jeffrey Zhang 476da0f43a Log the traceback in the kolla libraries
The traceback is logged also and will be helpful when the
library failed. We can get which line is failed from the log.

TrivialFix

Change-Id: Ia417232007e2f34c83d2e7cad6ed08dee49ce836
2016-05-31 01:40:55 +00:00

740 lines
24 KiB
Python

#!/usr/bin/python
# Copyright 2015 Sam Yaple
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
DOCUMENTATION = '''
---
module: kolla_docker
short_description: Module for controlling Docker
description:
- A module targeting at controlling Docker as used by Kolla.
options:
common_options:
description:
- A dict containing common params such as login info
required: False
type: dict
default: dict()
action:
description:
- The action the module should take
required: True
type: str
choices:
- compare_image
- create_volume
- get_container_env
- get_container_state
- pull_image
- remove_container
- remove_volume
- restart_container
- start_container
- stop_container
api_version:
description:
- The version of the api for docker-py to use when contacting docker
required: False
type: str
default: auto
auth_email:
description:
- The email address used to authenticate
required: False
type: str
auth_password:
description:
- The password used to authenticate
required: False
type: str
auth_registry:
description:
- The registry to authenticate to
required: False
type: str
auth_username:
description:
- The username used to authenticate
required: False
type: str
detach:
description:
- Detach from the container after it is created
required: False
default: True
type: bool
name:
description:
- Name of the container or volume to manage
required: False
type: str
environment:
description:
- The environment to set for the container
required: False
type: dict
image:
description:
- Name of the docker image
required: False
type: str
ipc_mode:
description:
- Set docker ipc namespace
required: False
type: str
default: None
choices:
- host
cap_add:
description:
- Add capabilities to docker container
required: False
type: list
default: list()
security_opt:
description:
- Set container security profile
required: False
type: list
default: list()
labels:
description:
- List of labels to apply to container
required: False
type: dict
default: dict()
pid_mode:
description:
- Set docker pid namespace
required: False
type: str
default: None
choices:
- host
privileged:
description:
- Set the container to privileged
required: False
default: False
type: bool
remove_on_exit:
description:
- When not detaching from container, remove on successful exit
required: False
default: True
type: bool
restart_policy:
description:
- Determine what docker does when the container exits
required: False
type: str
choices:
- never
- on-failure
- always
restart_retries:
description:
- How many times to attempt a restart if restart_policy is set
type: int
default: 10
volumes:
description:
- Set volumes for docker to use
required: False
type: list
volumes_from:
description:
- Name or id of container(s) to use volumes from
required: True
type: list
author: Sam Yaple
'''
EXAMPLES = '''
- hosts: kolla_docker
tasks:
- name: Start container
kolla_docker:
image: ubuntu
name: test_container
action: start_container
- name: Remove container
kolla_docker:
name: test_container
action: remove_container
- name: Pull image without starting container
kolla_docker:
action: pull_container
image: private-registry.example.com:5000/ubuntu
- name: Create named volume
action: create_volume
name: name_of_volume
- name: Remove named volume
action: remove_volume
name: name_of_volume
'''
import os
import traceback
import docker
class DockerWorker(object):
def __init__(self, module):
self.module = module
self.params = self.module.params
self.changed = False
# TLS not fully implemented
# tls_config = self.generate_tls()
options = {
'version': self.params.get('api_version')
}
self.dc = docker.Client(**options)
def generate_tls(self):
tls = {'verify': self.params.get('tls_verify')}
tls_cert = self.params.get('tls_cert'),
tls_key = self.params.get('tls_key'),
tls_cacert = self.params.get('tls_cacert')
if tls['verify']:
if tlscert:
self.check_file(tls['tls_cert'])
self.check_file(tls['tls_key'])
tls['client_cert'] = (tls_cert, tls_key)
if tlscacert:
self.check_file(tls['tls_cacert'])
tls['verify'] = tls_cacert
return docker.tls.TLSConfig(**tls)
def check_file(self, path):
if not os.path.isfile(path):
self.module.fail_json(
failed=True,
msg='There is no file at "{}"'.format(path)
)
if not os.access(path, os.R_OK):
self.module.fail_json(
failed=True,
msg='Permission denied for file at "{}"'.format(path)
)
def check_image(self):
find_image = ':'.join(self.parse_image())
for image in self.dc.images():
for image_name in image['RepoTags']:
if image_name == find_image:
return image
def check_volume(self):
for vol in self.dc.volumes()['Volumes'] or list():
if vol['Name'] == self.params.get('name'):
return vol
def check_container(self):
find_name = '/{}'.format(self.params.get('name'))
for cont in self.dc.containers(all=True):
if find_name in cont['Names']:
return cont
def get_container_info(self):
container = self.check_container()
if not container:
return None
return self.dc.inspect_container(self.params.get('name'))
def check_container_differs(self):
container_info = self.get_container_info()
return (
self.compare_cap_add(container_info) or
self.compare_security_opt(container_info) or
self.compare_image(container_info) or
self.compare_ipc_mode(container_info) or
self.compare_labels(container_info) or
self.compare_privileged(container_info) or
self.compare_pid_mode(container_info) or
self.compare_volumes(container_info) or
self.compare_volumes_from(container_info) or
self.compare_environment(container_info)
)
def compare_ipc_mode(self, container_info):
new_ipc_mode = self.params.get('ipc_mode')
current_ipc_mode = container_info['HostConfig'].get('IpcMode')
if not current_ipc_mode:
current_ipc_mode = None
if new_ipc_mode != current_ipc_mode:
return True
def compare_cap_add(self, container_info):
new_cap_add = self.params.get('cap_add', list())
current_cap_add = container_info['HostConfig'].get('CapAdd',
list())
if not current_cap_add:
current_cap_add = list()
if set(new_cap_add).symmetric_difference(set(current_cap_add)):
return True
def compare_security_opt(self, container_info):
new_sec_opt = self.params.get('security_opt', list())
current_sec_opt = container_info['HostConfig'].get('SecurityOpt',
list())
if not current_sec_opt:
current_sec_opt = list()
if set(new_sec_opt).symmetric_difference(set(current_sec_opt)):
return True
def compare_pid_mode(self, container_info):
new_pid_mode = self.params.get('pid_mode')
current_pid_mode = container_info['HostConfig'].get('PidMode')
if not current_pid_mode:
current_pid_mode = None
if new_pid_mode != current_pid_mode:
return True
def compare_privileged(self, container_info):
new_privileged = self.params.get('privileged')
current_privileged = container_info['HostConfig']['Privileged']
if new_privileged != current_privileged:
return True
def compare_image(self, container_info=None):
container_info = container_info or self.get_container_info()
parse_repository_tag = docker.utils.parse_repository_tag
if not container_info:
return True
new_image = self.check_image()
current_image = container_info['Image']
if not new_image:
return True
if new_image['Id'] != current_image:
return True
# NOTE(Jeffrey4l) when new image and the current image have
# the same id, but the tag name different.
elif (parse_repository_tag(container_info['Config']['Image']) !=
parse_repository_tag(self.params.get('image'))):
return True
def compare_labels(self, container_info):
new_labels = self.params.get('labels')
current_labels = container_info['Config'].get('Labels', dict())
image_labels = self.check_image().get('Labels', dict())
for k, v in image_labels.iteritems():
if k in new_labels:
if v != new_labels[k]:
return True
else:
del current_labels[k]
if new_labels != current_labels:
return True
def compare_volumes_from(self, container_info):
new_vols_from = self.params.get('volumes_from')
current_vols_from = container_info['HostConfig'].get('VolumesFrom')
if not new_vols_from:
new_vols_from = list()
if not current_vols_from:
current_vols_from = list()
if set(current_vols_from).symmetric_difference(set(new_vols_from)):
return True
def compare_volumes(self, container_info):
volumes, binds = self.generate_volumes()
current_vols = container_info['Config'].get('Volumes')
current_binds = container_info['HostConfig'].get('Binds')
if not volumes:
volumes = list()
if not current_vols:
current_vols = list()
if not current_binds:
current_binds = list()
if set(volumes).symmetric_difference(set(current_vols)):
return True
new_binds = list()
if binds:
for k, v in binds.iteritems():
new_binds.append("{}:{}:{}".format(k, v['bind'], v['mode']))
if set(new_binds).symmetric_difference(set(current_binds)):
return True
def compare_environment(self, container_info):
if self.params.get('environment'):
current_env = dict()
for kv in container_info['Config'].get('Env', list()):
k, v = kv.split('=', 1)
current_env.update({k: v})
for k, v in self.params.get('environment').iteritems():
if k not in current_env:
return True
if current_env[k] != v:
return True
def parse_image(self):
full_image = self.params.get('image')
if '/' in full_image:
registry, image = full_image.split('/', 1)
else:
image = full_image
if ':' in image:
return full_image.rsplit(':', 1)
else:
return full_image, 'latest'
def pull_image(self):
if self.params.get('auth_username'):
self.dc.login(
username=self.params.get('auth_username'),
password=self.params.get('auth_password'),
registry=self.params.get('auth_registry'),
email=self.params.get('auth_email')
)
image, tag = self.parse_image()
statuses = [
json.loads(line.strip()) for line in self.dc.pull(
repository=image, tag=tag, stream=True
)
]
for status in reversed(statuses):
if 'error' in status:
if status['error'].endswith('not found'):
self.module.fail_json(
msg="The requested image does not exist: {}:{}".format(
image, tag),
failed=True
)
else:
self.module.fail_json(
msg="Unknown error message: {}".format(
status['error']),
failed=True
)
if status and status.get('status'):
# NOTE(SamYaple): This allows us to use v1 and v2 docker
# registries. Eventually docker will stop supporting v1
# registries and when that happens we can remove this.
if 'legacy registry' in status['status']:
continue
elif 'Downloaded newer image for' in status['status']:
self.changed = True
return
elif 'Image is up to date for' in status['status']:
return
else:
self.module.fail_json(
msg="Unknown status message: {}".format(
status['status']),
failed=True
)
def remove_container(self):
if self.check_container():
self.changed = True
self.dc.remove_container(
container=self.params.get('name'),
force=True
)
def generate_volumes(self):
volumes = self.params.get('volumes')
if not volumes:
return None, None
vol_list = list()
vol_dict = dict()
for vol in volumes:
if ':' not in vol:
vol_list.append(vol)
continue
split_vol = vol.split(':')
if (len(split_vol) == 2
and ('/' not in split_vol[0] or '/' in split_vol[1])):
split_vol.append('rw')
vol_list.append(split_vol[1])
vol_dict.update({
split_vol[0]: {
'bind': split_vol[1],
'mode': split_vol[2]
}
})
return vol_list, vol_dict
def build_host_config(self, binds):
options = {
'network_mode': 'host',
'ipc_mode': self.params.get('ipc_mode'),
'cap_add': self.params.get('cap_add'),
'security_opt': self.params.get('security_opt'),
'pid_mode': self.params.get('pid_mode'),
'privileged': self.params.get('privileged'),
'volumes_from': self.params.get('volumes_from')
}
if self.params.get('restart_policy') in ['on-failure', 'always']:
options['restart_policy'] = {
'Name': self.params.get('restart_policy'),
'MaximumRetryCount': self.params.get('restart_retries')
}
if binds:
options['binds'] = binds
return self.dc.create_host_config(**options)
def _inject_env_var(self, environment_info):
newenv = {
'KOLLA_SERVICE_NAME': self.params.get('name').replace('_', '-')
}
environment_info.update(newenv)
return environment_info
def _format_env_vars(self):
env = self._inject_env_var(self.params.get('environment'))
return {k: "" if env[k] is None else env[k] for k in env}
def build_container_options(self):
volumes, binds = self.generate_volumes()
return {
'detach': self.params.get('detach'),
'environment': self._format_env_vars(),
'host_config': self.build_host_config(binds),
'labels': self.params.get('labels'),
'image': self.params.get('image'),
'name': self.params.get('name'),
'volumes': volumes,
'tty': True
}
def create_container(self):
self.changed = True
options = self.build_container_options()
self.dc.create_container(**options)
def start_container(self):
if not self.check_image():
self.pull_image()
container = self.check_container()
if container and self.check_container_differs():
self.remove_container()
container = self.check_container()
if not container:
self.create_container()
container = self.check_container()
if not container['Status'].startswith('Up '):
self.changed = True
self.dc.start(container=self.params.get('name'))
# We do not want to detach so we wait around for container to exit
if not self.params.get('detach'):
rc = self.dc.wait(self.params.get('name'))
if rc != 0:
self.module.fail_json(
failed=True,
changed=True,
msg="Container exited with non-zero return code"
)
if self.params.get('remove_on_exit'):
self.remove_container()
def get_container_env(self):
name = self.params.get('name')
info = self.get_container_info()
if not info:
self.module.fail_json(msg="No such container: {}".format(name))
else:
envs = dict()
for env in info['Config']['Env']:
if '=' in env:
key, value = env.split('=', 1)
else:
key, value = env, ''
envs[key] = value
self.module.exit_json(**envs)
def get_container_state(self):
name = self.params.get('name')
info = self.get_container_info()
if not info:
self.module.fail_json(msg="No such container: {}".format(name))
else:
self.module.exit_json(**info['State'])
def stop_container(self):
name = self.params.get('name')
container = self.check_container()
if not container['Status'].startswith('Exited '):
self.changed = True
self.dc.stop(name)
def restart_container(self):
name = self.params.get('name')
info = self.get_container_info()
if not info:
self.module.fail_json(
msg="No such container: {}".format(name))
else:
self.changed = True
self.dc.restart(name)
def create_volume(self):
if not self.check_volume():
self.changed = True
self.dc.create_volume(name=self.params.get('name'), driver='local')
def remove_volume(self):
if self.check_volume():
self.changed = True
try:
self.dc.remove_volume(name=self.params.get('name'))
except docker.errors.APIError as e:
if e.response.status_code == 409:
self.module.fail_json(
failed=True,
msg="Volume named '{}' is currently in-use".format(
self.params.get('name')
)
)
raise
def generate_module():
argument_spec = dict(
common_options=dict(required=False, type='dict', default=dict()),
action=dict(requried=True, type='str', choices=['compare_image',
'create_volume',
'get_container_env',
'get_container_state',
'pull_image',
'remove_container',
'remove_volume',
'restart_container',
'start_container',
'stop_container']),
api_version=dict(required=False, type='str', default='auto'),
auth_email=dict(required=False, type='str'),
auth_password=dict(required=False, type='str'),
auth_registry=dict(required=False, type='str'),
auth_username=dict(required=False, type='str'),
detach=dict(required=False, type='bool', default=True),
labels=dict(required=False, type='dict', default=dict()),
name=dict(required=False, type='str'),
environment=dict(required=False, type='dict'),
image=dict(required=False, type='str'),
ipc_mode=dict(required=False, type='str', choices=['host']),
cap_add=dict(required=False, type='list', default=list()),
security_opt=dict(required=False, type='list', default=list()),
pid_mode=dict(required=False, type='str', choices=['host']),
privileged=dict(required=False, type='bool', default=False),
remove_on_exit=dict(required=False, type='bool', default=True),
restart_policy=dict(required=False, type='str', choices=['no',
'never',
'on-failure',
'always']),
restart_retries=dict(required=False, type='int', default=10),
tls_verify=dict(required=False, type='bool', default=False),
tls_cert=dict(required=False, type='str'),
tls_key=dict(required=False, type='str'),
tls_cacert=dict(required=False, type='str'),
volumes=dict(required=False, type='list'),
volumes_from=dict(required=False, type='list')
)
required_together = [
['tls_cert', 'tls_key']
]
module = AnsibleModule(
argument_spec=argument_spec,
required_together=required_together,
bypass_checks=True
)
new_args = module.params.pop('common_options', dict())
# NOTE(jeffrey4l): merge the environment
env = module.params.pop('environment', dict())
if env:
new_args['environment'].update(env)
for key, value in module.params.iteritems():
if key in new_args and value is None:
continue
new_args[key] = value
module.params = new_args
return module
def main():
module = generate_module()
# TODO(SamYaple): Replace with required_if when Ansible 2.0 lands
if (module.params.get('action') in ['pull_image', 'start_container']
and not module.params.get('image')):
module.fail_json(
msg="missing required arguments: image",
failed=True
)
# TODO(SamYaple): Replace with required_if when Ansible 2.0 lands
if (module.params.get('action') != 'pull_image'
and not module.params.get('name')):
module.fail_json(
msg="missing required arguments: name",
failed=True
)
try:
dw = DockerWorker(module)
# TODO(inc0): We keep it bool to have ansible deal with consistent
# types. If we ever add method that will have to return some
# meaningful data, we need to refactor all methods to return dicts.
result = bool(getattr(dw, module.params.get('action'))())
module.exit_json(changed=dw.changed, result=result)
except Exception:
module.exit_json(failed=True, changed=True,
msg=repr(traceback.format_exc()))
# import module snippets
from ansible.module_utils.basic import * # noqa
if __name__ == '__main__':
main()