#!/usr/bin/python
# -*- coding: utf-8 -*-

# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
DOCUMENTATION = '''
---
module: digital_ocean
short_description: Create/delete a droplet/SSH_key in DigitalOcean
description:
     - Create/delete a droplet in DigitalOcean and optionally waits for it to be 'running', or deploy an SSH key.
version_added: "1.3"
options:
  command:
    description:
     - Which target you want to operate on.
    default: droplet
    choices: ['droplet', 'ssh']
  state:
    description:
     - Indicate desired state of the target.
    default: present
    choices: ['present', 'active', 'absent', 'deleted']
  client_id: 
     description:
     - Digital Ocean manager id.
  api_key:
    description:
     - Digital Ocean api key.
  id:
    description:
     - Numeric, the droplet id you want to operate on.
  name:
    description:
     - String, this is the name of the droplet - must be formatted by hostname rules, or the name of a SSH key.
  size_id:
    description:
     - Numeric, this is the id of the size you would like the droplet created at.
  image_id:
    description:
     - Numeric, this is the id of the image you would like the droplet created with.
  region_id:
    description:
     - "Numeric, this is the id of the region you would like your server"
  ssh_key_ids:
    description:
     - Optional, comma separated list of ssh_key_ids that you would like to be added to the server
  wait:
    description: 
     - Wait for the droplet to be in state 'running' before returning.
    default: "yes"
    choices: [ "yes", "no" ]
  wait_timeout:
    description:
     - How long before wait gives up, in seconds.
    default: 300
  ssh_pub_key:
    description:
     - The public SSH key you want to add to your account.

notes:
  - Two environment variables can be used, DO_CLIENT_ID and DO_API_KEY.
'''


EXAMPLES = '''
# Ensure a SSH key is present
# If a key matches this name, will return the ssh key id and changed = False
# If no existing key matches this name, a new key is created, the ssh key id is returned and changed = False

- digital_ocean: >
      state=present 
      command=ssh 
      name=my_ssh_key 
      ssh_pub_key='ssh-rsa AAAA...' 
      client_id=XXX 
      api_key=XXX

# Create a new Droplet
# Will return the droplet details including the droplet id (used for idempotence)

- digital_ocean: >
      state=present 
      command=droplet 
      name=my_new_droplet 
      client_id=XXX 
      api_key=XXX 
      size_id=1 
      region_id=2 
      image_id=3 
      wait_timeout=500

# Ensure a droplet is present
# If droplet id already exist, will return the droplet details and changed = False
# If no droplet matches the id, a new droplet will be created and the droplet details (including the new id) are returned, changed = True.

- digital_ocean: >
      state=present 
      command=droplet 
      id=123 
      name=my_new_droplet 
      client_id=XXX 
      api_key=XXX 
      size_id=1 
      region_id=2 
      image_id=3 
      wait_timeout=500

# Create a droplet with ssh key
# The ssh key id can be passed as argument at the creation of a droplet (see ssh_key_ids). 
# Several keys can be added to ssh_key_ids as id1,id2,id3
# The keys are used to connect as root to the droplet.

- digital_ocean: >
      state=present 
      ssh_key_ids=id1,id2
      name=my_new_droplet 
      client_id=XXX 
      api_key=XXX 
      size_id=1 
      region_id=2 
      image_id=3
=======
- digital_ocean: state=present command=ssh name=my_ssh_key ssh_pub_key='ssh-rsa AAAA...' client_id=XXX api_key=XXX

If a key matches this name, will return the ssh key id and changed = False
If no existing key matches this name, a new key is created, the ssh key id is returned and changed = False

# Create a new Droplet
- digital_ocean: state=present command=droplet name=my_new_droplet client_id=XXX api_key=XXX size_id=1 region_id=2 image_id=3 wait_timeout=500

Will return the droplet details including the droplet id (used for idempotence)

# Ensure a droplet is present
- digital_ocean: state=present command=droplet id=123 name=my_new_droplet client_id=XXX api_key=XXX size_id=1 region_id=2 image_id=3 wait_timeout=500

If droplet id already exist, will return the droplet details and changed = False
If no droplet matches the id, a new droplet will be created and the droplet details (including the new id) are returned, changed = True.

# Create a droplet with ssh key
- digital_ocean: state=present ssh_key_ids=id name=my_new_droplet client_id=XXX api_key=XXX size_id=1 region_id=2 image_id=3

The ssh key id can be passed as argument at the creation of a droplet (see ssh_key_ids). 
Several keys can be added to ssh_key_ids as id1,id2,id3

The keys are used to connect as root to the droplet.
'''

import sys
import os
import time

try:
    from dopy.manager import DoError, DoManager
except ImportError as e:
    print "failed=True msg='dopy required for this module'"
    sys.exit(1)

class TimeoutError(DoError):
    def __init__(self, msg, id):
        super(TimeoutError, self).__init__(msg)
        self.id = id

class JsonfyMixIn(object):
    def to_json(self):
        return self.__dict__

class Droplet(JsonfyMixIn):
    manager = None

    def __init__(self, droplet_json):
        self.status = 'new'
        self.__dict__.update(droplet_json)

    def is_powered_on(self):
        return self.status == 'active'

    def update_attr(self, attrs=None, times=5):
        if attrs:
            for k, v in attrs.iteritems():
                setattr(self, k, v)
        else:
            json = self.manager.show_droplet(self.id)
            if not json['ip_address']:
                if times > 0:
                    time.sleep(2)
                    self.update_attr(times=times-1)
                else:
                    raise TimeoutError('No ip is found.', self.id)
            else:
                self.update_attr(json)

    def power_on(self):
        assert self.status == 'off', 'Can only power on a closed one.'
        json = self.manager.power_on_droplet(self.id)
        self.update_attr(json)

    def ensure_powered_on(self, wait=True, wait_timeout=300):
        if self.is_powered_on():
            return
        if self.status == 'off':  # powered off
            self.power_on()

        if wait:
            end_time = time.time()+wait_timeout
            while time.time() < end_time:
                time.sleep(min(20, end_time-time.time()))
                self.update_attr()
                if self.is_powered_on():
                    return
            raise TimeoutError('Wait for droplet running timeout', self.id)

    def destroy(self):
        return self.manager.destroy_droplet(self.id)
    
    @classmethod
    def setup(cls, client_id, api_key):
        cls.manager = DoManager(client_id, api_key)

    @classmethod
    def add(cls, name, size_id, image_id, region_id, ssh_key_ids=None):
        json = cls.manager.new_droplet(name, size_id, image_id, region_id, ssh_key_ids)
        droplet = cls(json)
        droplet.update_attr()
        return droplet

    @classmethod
    def find(cls, id):
        if not id:
            return False
        droplets = cls.list_all()
        for droplet in droplets:
            if droplet.id == id:
                return droplet
        return False

    @classmethod
    def list_all(cls):
        json = cls.manager.all_active_droplets()
        return map(cls, json)

class SSH(JsonfyMixIn):
    manager = None

    def __init__(self, ssh_key_json):
        self.__dict__.update(ssh_key_json)
    update_attr = __init__

    def destroy(self):
        self.manager.destroy_ssh_key(self.id)
        return True

    @classmethod
    def setup(cls, client_id, api_key):
        cls.manager = DoManager(client_id, api_key)

    @classmethod
    def find(cls, name):
        if not name:
            return False
        keys = cls.list_all()
        for key in keys:
            if key.name == name:
                return key
        return False

    @classmethod
    def list_all(cls):
        json = cls.manager.all_ssh_keys()
        return map(cls, json)

    @classmethod
    def add(cls, name, key_pub):
        json = cls.manager.new_ssh_key(name, key_pub)
        return cls(json)

def core(module):
    def getkeyordie(k):
        v = module.params[k]
        if v is None:
            module.fail_json(msg='Unable to load %s' % k)
        return v

    try:
        # params['client_id'] will be None even if client_id is not passed in
        client_id = module.params['client_id'] or os.environ['DO_CLIENT_ID']
        api_key = module.params['api_key'] or os.environ['DO_API_KEY']
    except KeyError, e:
        module.fail_json(msg='Unable to load %s' % e.message)

    changed = True
    command = module.params['command']
    state = module.params['state']

    if command == 'droplet':
        Droplet.setup(client_id, api_key)
        if state in ('active', 'present'):
            droplet = Droplet.find(module.params['id'])
            if not droplet:
                droplet = Droplet.add(
                        name=getkeyordie('name'), 
                        size_id=getkeyordie('size_id'), 
                        image_id=getkeyordie('image_id'), 
                        region_id=getkeyordie('region_id'), 
                        ssh_key_ids=module.params['ssh_key_ids']
                    )
            if droplet.is_powered_on():
                changed = False
            droplet.ensure_powered_on(
                        wait=getkeyordie('wait'),
                        wait_timeout=getkeyordie('wait_timeout')
                    )
            module.exit_json(changed=changed, droplet=droplet.to_json())
       
        elif state in ('absent', 'deleted'):
            droplet = Droplet.find(getkeyordie('id'))
            if not droplet:
                module.exit_json(changed=False, msg='The droplet is not found.')
            event_json = droplet.destroy()
            module.exit_json(changed=True, event_id=event_json['event_id'])

    elif command == 'ssh':
        SSH.setup(client_id, api_key)
        name = getkeyordie('name')
        if state in ('active', 'present'):
            key = SSH.find(name)
            if key:
                module.exit_json(changed=False, ssh_key=key.to_json())
            key = SSH.add(name, getkeyordie('ssh_pub_key'))
            module.exit_json(changed=True, ssh_key=key.to_json())

        elif state in ('absent', 'deleted'):
            key = SSH.find(name)
            if not key:
                module.exit_json(changed=False, msg='SSH key with the name of %s is not found.' % name)
            key.destroy()
            module.exit_json(changed=True)


def main():
    module = AnsibleModule(
        argument_spec = dict(
            command = dict(choices=['droplet', 'ssh'], default='droplet'),
            state = dict(choices=['active', 'present', 'absent', 'deleted'], default='present'),
            client_id = dict(aliases=['CLIENT_ID'], no_log=True),
            api_key = dict(aliases=['API_KEY'], no_log=True),
            name = dict(type='str'),
            size_id = dict(type='int'),
            image_id = dict(type='int'),
            region_id = dict(type='int'),
            ssh_key_ids = dict(default=''),
            id = dict(aliases=['droplet_id'], type='int'),
            wait = dict(type='bool', choices=BOOLEANS, default='yes'),
            wait_timeout = dict(default=300, type='int'),
            ssh_pub_key = dict(type='str'),
        ),
        required_together = (
            ['size_id', 'image_id', 'region_id'],
        ),
        mutually_exclusive = (
            ['size_id', 'ssh_pub_key'],
            ['image_id', 'ssh_pub_key'],
            ['region_id', 'ssh_pub_key'],
        ),
        required_one_of = (
            ['id', 'name'],
        ),
    )

    try:
        core(module)
    except TimeoutError as e:
        module.fail_json(msg=str(e), id=e.id)
    except (DoError, Exception) as e:
        module.fail_json(msg=str(e))

# this is magic, see lib/ansible/module_common.py
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>

main()
