#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright Ansible Project
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later

from __future__ import absolute_import, division, print_function
__metaclass__ = type


DOCUMENTATION = '''
---
module: rax_scaling_group
short_description: Manipulate Rackspace Cloud Autoscale Groups
description:
    - Manipulate Rackspace Cloud Autoscale Groups
options:
  config_drive:
    description:
      - Attach read-only configuration drive to server as label config-2
    type: bool
    default: false
  cooldown:
    type: int
    description:
      - The period of time, in seconds, that must pass before any scaling can
        occur after the previous scaling. Must be an integer between 0 and
        86400 (24 hrs).
    default: 300
  disk_config:
    type: str
    description:
      - Disk partitioning strategy
      - If not specified, it will fallback to C(auto).
    choices:
      - auto
      - manual
  files:
    type: dict
    default: {}
    description:
      - 'Files to insert into the instance. Hash of C(remotepath: localpath)'
  flavor:
    type: str
    description:
      - flavor to use for the instance
    required: true
  image:
    type: str
    description:
      - image to use for the instance. Can be an C(id), C(human_id) or C(name)
    required: true
  key_name:
    type: str
    description:
      - key pair to use on the instance
  loadbalancers:
    type: list
    elements: dict
    description:
      - List of load balancer C(id) and C(port) hashes
  max_entities:
    type: int
    description:
      - The maximum number of entities that are allowed in the scaling group.
        Must be an integer between 0 and 1000.
    required: true
  meta:
    type: dict
    default: {}
    description:
      - A hash of metadata to associate with the instance
  min_entities:
    type: int
    description:
      - The minimum number of entities that are allowed in the scaling group.
        Must be an integer between 0 and 1000.
    required: true
  name:
    type: str
    description:
      - Name to give the scaling group
    required: true
  networks:
    type: list
    elements: str
    description:
      - The network to attach to the instances. If specified, you must include
        ALL networks including the public and private interfaces. Can be C(id)
        or C(label).
    default:
      - public
      - private
  server_name:
    type: str
    description:
      - The base name for servers created by Autoscale
    required: true
  state:
    type: str
    description:
      - Indicate desired state of the resource
    choices:
      - present
      - absent
    default: present
  user_data:
    type: str
    description:
      - Data to be uploaded to the servers config drive. This option implies
        I(config_drive). Can be a file path or a string
  wait:
    description:
      - wait for the scaling group to finish provisioning the minimum amount of
        servers
    type: bool
    default: false
  wait_timeout:
    type: int
    description:
      - how long before wait gives up, in seconds
    default: 300
author: "Matt Martz (@sivel)"
extends_documentation_fragment:
- community.general.rackspace
- community.general.rackspace.openstack

'''

EXAMPLES = '''
---
- hosts: localhost
  gather_facts: false
  connection: local
  tasks:
    - community.general.rax_scaling_group:
        credentials: ~/.raxpub
        region: ORD
        cooldown: 300
        flavor: performance1-1
        image: bb02b1a3-bc77-4d17-ab5b-421d89850fca
        min_entities: 5
        max_entities: 10
        name: ASG Test
        server_name: asgtest
        loadbalancers:
            - id: 228385
              port: 80
      register: asg
'''

import base64
import json
import os
import time

try:
    import pyrax
    HAS_PYRAX = True
except ImportError:
    HAS_PYRAX = False

from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.rax import (rax_argument_spec, rax_find_image, rax_find_network,
                                                                            rax_required_together, rax_to_dict, setup_rax_module)
from ansible.module_utils.six import string_types


def rax_asg(module, cooldown=300, disk_config=None, files=None, flavor=None,
            image=None, key_name=None, loadbalancers=None, meta=None,
            min_entities=0, max_entities=0, name=None, networks=None,
            server_name=None, state='present', user_data=None,
            config_drive=False, wait=True, wait_timeout=300):
    files = {} if files is None else files
    loadbalancers = [] if loadbalancers is None else loadbalancers
    meta = {} if meta is None else meta
    networks = [] if networks is None else networks

    changed = False

    au = pyrax.autoscale
    if not au:
        module.fail_json(msg='Failed to instantiate clients. This '
                             'typically indicates an invalid region or an '
                             'incorrectly capitalized region name.')

    if user_data:
        config_drive = True

    if user_data and os.path.isfile(user_data):
        try:
            f = open(user_data)
            user_data = f.read()
            f.close()
        except Exception as e:
            module.fail_json(msg='Failed to load %s' % user_data)

    if state == 'present':
        # Normalize and ensure all metadata values are strings
        if meta:
            for k, v in meta.items():
                if isinstance(v, list):
                    meta[k] = ','.join(['%s' % i for i in v])
                elif isinstance(v, dict):
                    meta[k] = json.dumps(v)
                elif not isinstance(v, string_types):
                    meta[k] = '%s' % v

        if image:
            image = rax_find_image(module, pyrax, image)

        nics = []
        if networks:
            for network in networks:
                nics.extend(rax_find_network(module, pyrax, network))

            for nic in nics:
                # pyrax is currently returning net-id, but we need uuid
                # this check makes this forward compatible for a time when
                # pyrax uses uuid instead
                if nic.get('net-id'):
                    nic.update(uuid=nic['net-id'])
                    del nic['net-id']

        # Handle the file contents
        personality = []
        if files:
            for rpath in files.keys():
                lpath = os.path.expanduser(files[rpath])
                try:
                    f = open(lpath, 'r')
                    personality.append({
                        'path': rpath,
                        'contents': f.read()
                    })
                    f.close()
                except Exception as e:
                    module.fail_json(msg='Failed to load %s' % lpath)

        lbs = []
        if loadbalancers:
            for lb in loadbalancers:
                try:
                    lb_id = int(lb.get('id'))
                except (ValueError, TypeError):
                    module.fail_json(msg='Load balancer ID is not an integer: '
                                         '%s' % lb.get('id'))
                try:
                    port = int(lb.get('port'))
                except (ValueError, TypeError):
                    module.fail_json(msg='Load balancer port is not an '
                                         'integer: %s' % lb.get('port'))
                if not lb_id or not port:
                    continue
                lbs.append((lb_id, port))

        try:
            sg = au.find(name=name)
        except pyrax.exceptions.NoUniqueMatch as e:
            module.fail_json(msg='%s' % e.message)
        except pyrax.exceptions.NotFound:
            try:
                sg = au.create(name, cooldown=cooldown,
                               min_entities=min_entities,
                               max_entities=max_entities,
                               launch_config_type='launch_server',
                               server_name=server_name, image=image,
                               flavor=flavor, disk_config=disk_config,
                               metadata=meta, personality=personality,
                               networks=nics, load_balancers=lbs,
                               key_name=key_name, config_drive=config_drive,
                               user_data=user_data)
                changed = True
            except Exception as e:
                module.fail_json(msg='%s' % e.message)

        if not changed:
            # Scaling Group Updates
            group_args = {}
            if cooldown != sg.cooldown:
                group_args['cooldown'] = cooldown

            if min_entities != sg.min_entities:
                group_args['min_entities'] = min_entities

            if max_entities != sg.max_entities:
                group_args['max_entities'] = max_entities

            if group_args:
                changed = True
                sg.update(**group_args)

            # Launch Configuration Updates
            lc = sg.get_launch_config()
            lc_args = {}
            if server_name != lc.get('name'):
                lc_args['server_name'] = server_name

            if image != lc.get('image'):
                lc_args['image'] = image

            if flavor != lc.get('flavor'):
                lc_args['flavor'] = flavor

            disk_config = disk_config or 'AUTO'
            if ((disk_config or lc.get('disk_config')) and
                    disk_config != lc.get('disk_config', 'AUTO')):
                lc_args['disk_config'] = disk_config

            if (meta or lc.get('meta')) and meta != lc.get('metadata'):
                lc_args['metadata'] = meta

            test_personality = []
            for p in personality:
                test_personality.append({
                    'path': p['path'],
                    'contents': base64.b64encode(p['contents'])
                })
            if ((test_personality or lc.get('personality')) and
                    test_personality != lc.get('personality')):
                lc_args['personality'] = personality

            if nics != lc.get('networks'):
                lc_args['networks'] = nics

            if lbs != lc.get('load_balancers'):
                # Work around for https://github.com/rackspace/pyrax/pull/393
                lc_args['load_balancers'] = sg.manager._resolve_lbs(lbs)

            if key_name != lc.get('key_name'):
                lc_args['key_name'] = key_name

            if config_drive != lc.get('config_drive', False):
                lc_args['config_drive'] = config_drive

            if (user_data and
                    base64.b64encode(user_data) != lc.get('user_data')):
                lc_args['user_data'] = user_data

            if lc_args:
                # Work around for https://github.com/rackspace/pyrax/pull/389
                if 'flavor' not in lc_args:
                    lc_args['flavor'] = lc.get('flavor')
                changed = True
                sg.update_launch_config(**lc_args)

            sg.get()

        if wait:
            end_time = time.time() + wait_timeout
            infinite = wait_timeout == 0
            while infinite or time.time() < end_time:
                state = sg.get_state()
                if state["pending_capacity"] == 0:
                    break

                time.sleep(5)

        module.exit_json(changed=changed, autoscale_group=rax_to_dict(sg))

    else:
        try:
            sg = au.find(name=name)
            sg.delete()
            changed = True
        except pyrax.exceptions.NotFound as e:
            sg = {}
        except Exception as e:
            module.fail_json(msg='%s' % e.message)

        module.exit_json(changed=changed, autoscale_group=rax_to_dict(sg))


def main():
    argument_spec = rax_argument_spec()
    argument_spec.update(
        dict(
            config_drive=dict(default=False, type='bool'),
            cooldown=dict(type='int', default=300),
            disk_config=dict(choices=['auto', 'manual']),
            files=dict(type='dict', default={}),
            flavor=dict(required=True),
            image=dict(required=True),
            key_name=dict(),
            loadbalancers=dict(type='list', elements='dict'),
            meta=dict(type='dict', default={}),
            min_entities=dict(type='int', required=True),
            max_entities=dict(type='int', required=True),
            name=dict(required=True),
            networks=dict(type='list', elements='str', default=['public', 'private']),
            server_name=dict(required=True),
            state=dict(default='present', choices=['present', 'absent']),
            user_data=dict(no_log=True),
            wait=dict(default=False, type='bool'),
            wait_timeout=dict(default=300, type='int'),
        )
    )

    module = AnsibleModule(
        argument_spec=argument_spec,
        required_together=rax_required_together(),
    )

    if not HAS_PYRAX:
        module.fail_json(msg='pyrax is required for this module')

    config_drive = module.params.get('config_drive')
    cooldown = module.params.get('cooldown')
    disk_config = module.params.get('disk_config')
    if disk_config:
        disk_config = disk_config.upper()
    files = module.params.get('files')
    flavor = module.params.get('flavor')
    image = module.params.get('image')
    key_name = module.params.get('key_name')
    loadbalancers = module.params.get('loadbalancers')
    meta = module.params.get('meta')
    min_entities = module.params.get('min_entities')
    max_entities = module.params.get('max_entities')
    name = module.params.get('name')
    networks = module.params.get('networks')
    server_name = module.params.get('server_name')
    state = module.params.get('state')
    user_data = module.params.get('user_data')

    if not 0 <= min_entities <= 1000 or not 0 <= max_entities <= 1000:
        module.fail_json(msg='min_entities and max_entities must be an '
                             'integer between 0 and 1000')

    if not 0 <= cooldown <= 86400:
        module.fail_json(msg='cooldown must be an integer between 0 and 86400')

    setup_rax_module(module, pyrax)

    rax_asg(module, cooldown=cooldown, disk_config=disk_config,
            files=files, flavor=flavor, image=image, meta=meta,
            key_name=key_name, loadbalancers=loadbalancers,
            min_entities=min_entities, max_entities=max_entities,
            name=name, networks=networks, server_name=server_name,
            state=state, config_drive=config_drive, user_data=user_data)


if __name__ == '__main__':
    main()