#!/usr/bin/python

DOCUMENTATION = '''
---
module: docker_compose
version_added: 1.8.4
short_description: Manage docker containers with docker-compose
description:
     - Manage the lifecycle of groups of docker containers with docker-compose
options:
  command:
    description:
      - Select the compose action to perform
    required: true
    choices: ['build', 'kill', 'pull', 'rm', 'scale', 'start', 'stop', 'restart', 'up']
  compose_file:
    description:
      - Specify the compose file to build from
    required: true
    type: bool
  insecure_registry:
    description:
      - Allow access to insecure registry (HTTP or TLS with a CA not known by the Docker daemon)
    type: bool
  kill_signal:
    description:
      - The SIG to send to the docker container process when killing it
    default: SIGKILL
  no_build:
    description:
      - Do not build an image, even if it's missing
    type: bool
  no_deps:
    description:
      - Don't start linked services
    type: bool
  no_recreate:
    description:
      - If containers already exist, don't recreate them
    type: bool
  project_name:
    description:
      - Specify project name (defaults to directory name of the compose file)
    type: str
  service_names:
    description:
      - Only modify services in this list (can be used in conjunction with no_deps)
    type: list
  stop_timeout:
    description:
      - The amount of time in seconds to wait for an instance to cleanly terminate before killing it (can be used in conjunction with stop_timeout)
    default: 10
    type: int

author: Sam Yaple
requirements: [ "docker-compose", "docker >= 1.3" ]
'''

EXAMPLES = '''
Compose web application:

- hosts: web
  tasks:
    - name: compose super important weblog
      docker_compose: command="up" compose_file="/opt/compose/weblog.yml"

Compose only mysql server from previous example:

- hosts: web
  tasks:
    - name: compose mysql
      docker_compose: command="up" compose_file="/opt/compose/weblog.yml" service_names=['mysql']

Compose project with specified prefix:

- hosts: web
  tasks:
    - name: compose weblog named "myproject_weblog_1"
      docker_compose: command="up" compose_file="/opt/compose/weblog.yml" project_name="myproject"

Compose project only if already built (or no build instructions). Explicitly refuse to build the container image(s):

- hosts: web
  tasks:
    - name: compose weblog
      docker_compose: command="up" compose_file="/opt/compose/weblog.yml" no_build=True

Allow the container image to be pulled from an insecure registry (this requires the docker daemon to allow the insecure registry as well):

- hosts: web
  tasks:
    - name: compose weblog from local registry
      docker_compose: command="up" compose_file="/opt/compose/weblog.yml" insecure_registry=True

Start the containers in the compose project, but do not create any:

- hosts: web
  tasks:
    - name: compose weblog
      docker_compose: command="start" compose_file="/opt/compose/weblog.yml"

Removed all the containers associated with a project; only wait 5 seconds for the container to respond to a SIGTERM:

- hosts: web
  tasks:
    - name: Destroy ALL containers for project "devweblog"
      docker_compose: command="rm" stop_timeout=5 compose_file="/opt/compose/weblog.yml" project_name="devweblog"
'''

HAS_DOCKER_COMPOSE = True

import re

try:
    from compose import config
    from compose.cli.command import Command as compose_command
    from compose.cli.docker_client import docker_client
except ImportError, e:
    HAS_DOCKER_COMPOSE = False

TIMEMAP = {
        'second': 0,
        'seconds': 0,
        'minute': 1,
        'minutes': 1,
        'hour': 2,
        'hours': 2,
        'weeks': 3,
        'months': 4,
        'years': 5,
}

class DockerComposer:
    def __init__(self, module):
        self.module = module

        self.compose_file = self.module.params.get('compose_file')
        self.insecure_registry = self.module.params.get('insecure_registry')
        self.no_build = self.module.params.get('no_build')
        self.no_cache = self.module.params.get('no_cache')
        self.no_deps = self.module.params.get('no_deps')
        self.no_recreate = self.module.params.get('no_recreate')
        self.project_name = self.module.params.get('project_name')
        self.stop_timeout = self.module.params.get('stop_timeout')
        self.scale = self.module.params.get('scale')
        self.service_names = self.module.params.get('service_names')


        self.project = compose_command.get_project(compose_command(),
                                                   self.compose_file,
                                                   self.project_name,
                                                   )
        self.containers = self.project.client.containers(all=True)


    def build(self):
        self.project.build(no_cache = self.no_cache,
                           service_names = self.service_names,
                           )

    def kill(self):
        self.project.kill(signal = self.kill_signal,
                          service_names = self.service_names,
                          )

    def pull(self):
        self.project.pull(insecure_registry = self.insecure_registry,
                          service_names = self.service_names,
                          )

    def rm(self):
        self.stop()
        self.project.remove_stopped(service_names = self.service_names)

    def scale(self):
        for s in self.service_names:
            try:
                num = int(self.scale[s])
            except ValueError:
                msg = ('Value for scaling service "%s" should be an int, but '
                       'value "%s" was recieved' % (s, self.scale[s]))
                module.fail_json(msg=msg, failed=True)

            try:
                project.get_service(s).scale(num)
            except CannotBeScaledError:
                msg = ('Service "%s" cannot be scaled because it specifies a '
                       'port on the host.' % s)
                module.fail_json(msg=msg, failed=True)

    def start(self):
        self.project.start(service_names = self.service_names)

    def stop(self):
        self.project.stop(service_names = self.service_names,
                          timeout = self.stop_timeout,
                          )

    def restart(self):
        self.project.restart(service_names = self.service_names)

    def up(self):
        self.project_contains = self.project.up(
                                    do_build = not self.no_build,
                                    insecure_registry = self.insecure_registry,
                                    allow_recreate = not self.no_recreate,
                                    service_names = self.service_names,
                                    start_deps = not self.no_deps,
                                    )

    def check_if_changed(self):
        new_containers = self.project.client.containers(all=True)

        for pc in self.project_contains:
            old_container = None
            new_container = None
            name = '/' + re.split(' |>',str(pc))[1]

            for c in self.containers:
                if c['Names'][0] == name:
                    old_container = c

            for c in new_containers:
                if c['Names'][0] == name:
                    new_container = c

            if not old_container or not new_container:
                return True

            if old_container['Created'] != new_container['Created']:
                return True

            old_status = re.split(' ', old_container['Status'])
            new_status = re.split(' ', new_container['Status'])

            if old_status[0] != new_status[0]:
                return True

            if old_status[0] == 'Up':
                if TIMEMAP[old_status[-1]] > TIMEMAP[new_status[-1]]:
                    return True
            else:
                if TIMEMAP[old_status[-2]] < TIMEMAP[new_status[-2]]:
                    return True

        return False

def check_dependencies(module):
    if not HAS_DOCKER_COMPOSE:
        msg = ('`docker-compose` does not seem to be installed, but is required '
               'by the Ansible docker-compose module.')
        module.fail_json(failed=True, msg=msg)

def main():
    module = AnsibleModule(
        argument_spec = dict(
            command         = dict(required=True,
                                   choices=['build', 'kill', 'pull', 'rm',
                                            'scale', 'start', 'stop',
                                            'restart', 'up']
                                   ),
            compose_file    = dict(required=True, type='str'),
            insecure_registry = dict(type='bool'),
            kill_signal     = dict(default='SIGKILL'),
            no_build        = dict(type='bool'),
            no_cache        = dict(type='bool'),
            no_deps         = dict(type='bool'),
            no_recreate     = dict(type='bool'),
            project_name    = dict(type='str'),
            scale           = dict(type='dict'),
            service_names   = dict(type='list'),
            stop_timeout    = dict(default=10, type='int')
        )
    )

    check_dependencies(module)


    try:
        composer = DockerComposer(module)
        command = getattr(composer, module.params.get('command'))

        command()

        changed = composer.check_if_changed()

        module.exit_json(changed=changed)
    except Exception, e:
        try:
            changed = composer.check_if_changed()
        except Exception:
            changed = True

        module.exit_json(failed=True, changed=changed, msg=repr(e))


# import module snippets
from ansible.module_utils.basic import *

if __name__ == '__main__':
    main()
