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

# Copyright (c) 2015 Hewlett-Packard Development Company, L.P.
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)

DOCUMENTATION = '''
---
module: compute_flavor
short_description: Manage OpenStack compute flavors
author: OpenStack Ansible SIG
description:
   - Add or remove compute flavors from OpenStack.
   - Updating a flavor consists of deleting and (re)creating a flavor.
options:
  description:
    description:
       - Description of the flavor.
    type: str
  disk:
    description:
       - Size of local disk, in GB.
       - Required when I(state) is C(present).
    type: int
  ephemeral:
    description:
       - Ephemeral space size, in GB.
    type: int
  extra_specs:
    description:
       - Metadata dictionary
    type: dict
  id:
    description:
       - ID for the flavor. This is optional as a unique UUID will be
         assigned if a value is not specified.
       - Note that this ID will only be used when first creating the flavor.
       - The ID of an existing flavor cannot be changed.
       - When I(id) is set to C(auto), a new id will be autogenerated.
         C(auto) is kept for backward compatibility and
         will be dropped in the next major release.
    type: str
    aliases: ['flavorid']
  is_public:
    description:
       - Make flavor accessible to the public.
    type: bool
  name:
    description:
       - Flavor name.
    required: true
    type: str
  ram:
    description:
       - Amount of memory, in MB.
       - Required when I(state) is C(present).
    type: int
  rxtx_factor:
    description:
       - RX/TX factor.
    type: float
  state:
    description:
       - Indicate desired state of the resource.
       - When I(state) is C(present), then I(ram), I(vcpus), and I(disk) are
         required. There are no default values for those parameters.
    choices: ['present', 'absent']
    default: present
    type: str
  swap:
    description:
       - Swap space size, in MB.
    type: int
  vcpus:
    description:
       - Number of virtual CPUs.
       - Required when I(state) is C(present).
    type: int
extends_documentation_fragment:
- openstack.cloud.openstack
'''

EXAMPLES = '''
- name: Create tiny flavor with 1024MB RAM, 1 vCPU, 10GB disk, 10GB ephemeral
  openstack.cloud.compute_flavor:
    cloud: mycloud
    state: present
    name: tiny
    ram: 1024
    vcpus: 1
    disk: 10
    ephemeral: 10
    description: "I am flavor mycloud"

- name: Delete tiny flavor
  openstack.cloud.compute_flavor:
    cloud: mycloud
    state: absent
    name: tiny

- name: Create flavor with metadata
  openstack.cloud.compute_flavor:
    cloud: mycloud
    state: present
    name: tiny
    ram: 1024
    vcpus: 1
    disk: 10
    extra_specs:
      "quota:disk_read_iops_sec": 5000
      "aggregate_instance_extra_specs:pinned": false
'''

RETURN = '''
flavor:
  description: Dictionary describing the flavor.
  returned: On success when I(state) is 'present'
  type: dict
  contains:
    description:
      description: Description attached to flavor
      returned: success
      type: str
      sample: Example description
    disk:
      description: Size of local disk, in GB.
      returned: success
      type: int
      sample: 10
    ephemeral:
      description: Ephemeral space size, in GB.
      returned: success
      type: int
      sample: 10
    extra_specs:
      description: Flavor metadata
      returned: success
      type: dict
      sample:
        "quota:disk_read_iops_sec": 5000
        "aggregate_instance_extra_specs:pinned": false
    id:
      description: Flavor ID.
      returned: success
      type: str
      sample: "515256b8-7027-4d73-aa54-4e30a4a4a339"
    is_disabled:
      description: Whether the flavor is disabled
      returned: success
      type: bool
      sample: true
    is_public:
      description: Make flavor accessible to the public.
      returned: success
      type: bool
      sample: true
    name:
      description: Flavor name.
      returned: success
      type: str
      sample: "tiny"
    original_name:
      description: The name of this flavor when returned by server list/show
      type: str
      returned: success
    ram:
      description: Amount of memory, in MB.
      returned: success
      type: int
      sample: 1024
    rxtx_factor:
      description: |
        The bandwidth scaling factor this flavor receives on the network
      returned: success
      type: int
      sample: 100
    swap:
      description: Swap space size, in MB.
      returned: success
      type: int
      sample: 100
    vcpus:
      description: Number of virtual CPUs.
      returned: success
      type: int
      sample: 2
'''

from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule


class ComputeFlavorModule(OpenStackModule):
    argument_spec = dict(
        description=dict(),
        disk=dict(type='int'),
        ephemeral=dict(type='int'),
        extra_specs=dict(type='dict'),
        id=dict(aliases=['flavorid']),
        is_public=dict(type='bool'),
        name=dict(required=True),
        ram=dict(type='int'),
        rxtx_factor=dict(type='float'),
        state=dict(default='present', choices=['absent', 'present']),
        swap=dict(type='int'),
        vcpus=dict(type='int'),
    )

    module_kwargs = dict(
        required_if=[
            ('state', 'present', ['ram', 'vcpus', 'disk'])
        ],
        supports_check_mode=True
    )

    def run(self):
        state = self.params['state']
        id = self.params['id']
        name = self.params['name']
        name_or_id = id if id and id != 'auto' else name
        flavor = self.conn.compute.find_flavor(name_or_id,
                                               get_extra_specs=True)

        if self.ansible.check_mode:
            self.exit_json(changed=self._will_change(state, flavor))

        if state == 'present' and not flavor:
            # Create flavor
            flavor = self._create()
            self.exit_json(changed=True,
                           flavor=flavor.to_dict(computed=False))

        elif state == 'present' and flavor:
            # Update flavor
            update = self._build_update(flavor)
            if update:
                flavor = self._update(flavor, update)

            self.exit_json(changed=bool(update),
                           flavor=flavor.to_dict(computed=False))

        elif state == 'absent' and flavor:
            # Delete flavor
            self._delete(flavor)
            self.exit_json(changed=True)

        elif state == 'absent' and not flavor:
            # Do nothing
            self.exit_json(changed=False)

    def _build_update(self, flavor):
        return {
            **self._build_update_extra_specs(flavor),
            **self._build_update_flavor(flavor)}

    def _build_update_extra_specs(self, flavor):
        update = {}

        old_extra_specs = flavor['extra_specs']
        new_extra_specs = self.params['extra_specs'] or {}
        if flavor['swap'] == '':
            flavor['swap'] = 0

        delete_extra_specs_keys = \
            set(old_extra_specs.keys()) - set(new_extra_specs.keys())

        if delete_extra_specs_keys:
            update['delete_extra_specs_keys'] = delete_extra_specs_keys

        stringified = dict([(k, str(v))
                            for k, v in new_extra_specs.items()])

        if old_extra_specs != stringified:
            update['create_extra_specs'] = new_extra_specs

        return update

    def _build_update_flavor(self, flavor):
        update = {}

        flavor_attributes = dict(
            (k, self.params[k])
            for k in ['ram', 'vcpus', 'disk', 'ephemeral', 'swap',
                      'rxtx_factor', 'is_public', 'description']
            if k in self.params and self.params[k] is not None
            and self.params[k] != flavor[k])

        if flavor_attributes:
            update['flavor_attributes'] = flavor_attributes

        return update

    def _create(self):
        kwargs = dict((k, self.params[k])
                      for k in ['name', 'ram', 'vcpus', 'disk', 'ephemeral',
                                'swap', 'rxtx_factor', 'is_public',
                                'description']
                      if self.params[k] is not None)

        # Keep for backward compatibility
        id = self.params['id']
        if id is not None and id != 'auto':
            kwargs['id'] = id

        flavor = self.conn.compute.create_flavor(**kwargs)

        extra_specs = self.params['extra_specs']
        if extra_specs:
            flavor = self.conn.compute.create_flavor_extra_specs(flavor.id,
                                                                 extra_specs)

        return flavor

    def _delete(self, flavor):
        self.conn.compute.delete_flavor(flavor)

    def _update(self, flavor, update):
        flavor = self._update_flavor(flavor, update)
        flavor = self._update_extra_specs(flavor, update)
        return flavor

    def _update_extra_specs(self, flavor, update):
        if update.get('flavor_attributes'):
            # No need to update extra_specs since flavor will be recreated
            return flavor

        delete_extra_specs_keys = update.get('delete_extra_specs_keys')
        if delete_extra_specs_keys:
            self.conn.unset_flavor_specs(flavor.id, delete_extra_specs_keys)
            # Update flavor after extra_specs removal
            flavor = self.conn.compute.fetch_flavor_extra_specs(flavor)

        create_extra_specs = update.get('create_extra_specs')
        if create_extra_specs:
            flavor = self.conn.compute.create_flavor_extra_specs(
                flavor.id, create_extra_specs)

        return flavor

    def _update_flavor(self, flavor, update):
        flavor_attributes = update.get('flavor_attributes')
        if flavor_attributes:
            # Because only flavor descriptions are updateable,
            # flavor has to be recreated to "update" it
            self._delete(flavor)
            flavor = self._create()

        return flavor

    def _will_change(self, state, flavor):
        if state == 'present' and not flavor:
            return True
        elif state == 'present' and flavor:
            return bool(self._build_update(flavor))
        elif state == 'absent' and flavor:
            return True
        else:
            # state == 'absent' and not flavor:
            return False


def main():
    module = ComputeFlavorModule()
    module()


if __name__ == '__main__':
    main()
