aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJenkins <jenkins@review.openstack.org>2015-12-31 16:03:07 +0000
committerGerrit Code Review <review@openstack.org>2015-12-31 16:03:07 +0000
commitad262534d30f54930c2fc3d9efe986044bedf131 (patch)
tree4a1fc508f728e5c827daaa57e31616caa82e312f
parentbf21fba7e31f9fe39cfbe90da905b09bac02f937 (diff)
parent412a53dde1b74a4d1dd591fe663c6be66cf6b7bb (diff)
downloadkolla-ad262534d30f54930c2fc3d9efe986044bedf131.tar.gz
kolla-ad262534d30f54930c2fc3d9efe986044bedf131.zip
Merge "Add docker module in Kolla"
-rw-r--r--ansible/group_vars/all.yml13
-rw-r--r--ansible/library/kolla_docker.py436
2 files changed, 449 insertions, 0 deletions
diff --git a/ansible/group_vars/all.yml b/ansible/group_vars/all.yml
index 30f90a459..71c1e53c6 100644
--- a/ansible/group_vars/all.yml
+++ b/ansible/group_vars/all.yml
@@ -40,6 +40,7 @@ database_user: "root"
####################
# Docker options
####################
+docker_registry_email:
docker_registry:
docker_namespace: "kollaglue"
docker_registry_username:
@@ -54,6 +55,18 @@ docker_restart_policy: "always"
# '0' means unlimited retries
docker_restart_policy_retry: "10"
+# Common options used throughout docker
+docker_common_options:
+ auth_email: "{{ docker_registry_email }}"
+ auth_password: "{{ docker_registry_password }}"
+ auth_registry: "{{ docker_registry }}"
+ auth_username: "{{ docker_registry_username }}"
+ environment:
+ KOLLA_CONFIG_STRATEGY: "{{ config_strategy }}"
+ insecure_registry: "{{ docker_insecure_registry }}"
+ restart_policy: "{{ docker_restart_policy }}"
+ restart_retries: "{{ docker_restart_policy_retry }}"
+
####################
# Networking options
diff --git a/ansible/library/kolla_docker.py b/ansible/library/kolla_docker.py
new file mode 100644
index 000000000..b2006b435
--- /dev/null
+++ b/ansible/library/kolla_docker.py
@@ -0,0 +1,436 @@
+#!/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 controling Docker
+description:
+ - A module targeting at controling Docker as used by Kolla.
+options:
+ example:
+ description:
+ - example
+ required: True
+ type: bool
+author: Sam Yaple
+'''
+
+EXAMPLES = '''
+- hosts: kolla_docker
+ tasks:
+ - name: Start container
+ kolla_docker:
+ example: False
+'''
+
+import os
+
+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']:
+ 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 check_container_differs(self):
+ container = self.check_container()
+ if not container:
+ return True
+ container_info = self.dc.inspect_container(self.params.get('name'))
+
+ return (
+ self.compare_image(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_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):
+ new_image = self.check_image()
+ current_image = container_info['Image']
+ if new_image['Id'] != current_image:
+ 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()
+
+ status = [
+ json.loads(line.strip()) for line in self.dc.pull(
+ repository=image, tag=tag, stream=True
+ )
+ ]
+
+ if "Downloaded newer image for" in status[-1].get('status'):
+ self.changed = True
+ elif "Image is up to date for" in status[-1].get('status'):
+ # No new layer was pulled, no change
+ pass
+ else:
+ self.module.fail_json(
+ msg="Invalid status returned from pull",
+ changed=True,
+ 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',
+ '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 build_container_options(self):
+ volumes, binds = self.generate_volumes()
+ return {
+ 'detach': self.params.get('detach'),
+ 'environment': self.params.get('environment'),
+ 'host_config': self.build_host_config(binds),
+ '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 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=['create_volume',
+ 'pull_image',
+ 'remove_container',
+ 'remove_volume',
+ 'start_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),
+ name=dict(required=True, type='str'),
+ environment=dict(required=False, type='dict'),
+ image=dict(required=False, type='str'),
+ insecure_registry=dict(required=False, type='bool', default=False),
+ 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']
+ ]
+ return AnsibleModule(
+ argument_spec=argument_spec,
+ required_together=required_together
+ )
+
+
+def generate_nested_module():
+ module = generate_module()
+
+ # We unnest the common dict and the update it with the other options
+ new_args = module.params.get('common_options')
+ new_args.update(module._load_params()[0])
+ module.params = new_args
+
+ # Override ARGS to ensure new args are used
+ global MODULE_ARGS
+ global MODULE_COMPLEX_ARGS
+ MODULE_ARGS = ''
+ MODULE_COMPLEX_ARGS = json.dumps(module.params)
+
+ # Reprocess the args now that the common dict has been unnested
+ return generate_module()
+
+
+def main():
+ module = generate_nested_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')):
+ self.module.fail_json(
+ msg="missing required arguments: image",
+ failed=True
+ )
+
+ try:
+ dw = DockerWorker(module)
+ getattr(dw, module.params.get('action'))()
+ module.exit_json(changed=dw.changed)
+ except Exception as e:
+ module.exit_json(failed=True, changed=True, msg=repr(e))
+
+# import module snippets
+from ansible.module_utils.basic import * # noqa
+if __name__ == '__main__':
+ main()