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

# (c) 2013, Matt Hite <mhite@hotmail.com>
#
# 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/>.

ANSIBLE_METADATA = {'metadata_version': '1.0',
                    'status': ['preview'],
                    'supported_by': 'community'}


DOCUMENTATION = '''
---
module: bigip_pool
short_description: "Manages F5 BIG-IP LTM pools"
description:
  - Manages F5 BIG-IP LTM pools via iControl SOAP API
version_added: 1.2
author:
  - Matt Hite (@mhite)
  - Tim Rupp (@caphrim007)
notes:
  - Requires BIG-IP software version >= 11
  - F5 developed module 'bigsuds' required (see http://devcentral.f5.com)
  - Best run as a local_action in your playbook
requirements:
  - bigsuds
options:
  description:
    description:
      - Specifies descriptive text that identifies the pool.
    required: false
    version_added: "2.3"
  state:
    description:
      - Pool/pool member state
    required: false
    default: present
    choices:
      - present
      - absent
    aliases: []
  name:
    description:
      - Pool name
    required: true
    default: null
    choices: []
    aliases:
      - pool
  partition:
    description:
      - Partition of pool/pool member
    required: false
    default: 'Common'
    choices: []
    aliases: []
  lb_method:
    description:
      - Load balancing method
    version_added: "1.3"
    required: False
    default: 'round_robin'
    choices:
      - round_robin
      - ratio_member
      - least_connection_member
      - observed_member
      - predictive_member
      - ratio_node_address
      - least_connection_node_address
      - fastest_node_address
      - observed_node_address
      - predictive_node_address
      - dynamic_ratio
      - fastest_app_response
      - least_sessions
      - dynamic_ratio_member
      - l3_addr
      - weighted_least_connection_member
      - weighted_least_connection_node_address
      - ratio_session
      - ratio_least_connection_member
      - ratio_least_connection_node_address
    aliases: []
  monitor_type:
    description:
      - Monitor rule type when monitors > 1
    version_added: "1.3"
    required: False
    default: null
    choices: ['and_list', 'm_of_n']
    aliases: []
  quorum:
    description:
      - Monitor quorum value when monitor_type is m_of_n
    version_added: "1.3"
    required: False
    default: null
    choices: []
    aliases: []
  monitors:
    description:
      - Monitor template name list. Always use the full path to the monitor.
    version_added: "1.3"
    required: False
    default: null
    choices: []
    aliases: []
  slow_ramp_time:
    description:
      - Sets the ramp-up time (in seconds) to gradually ramp up the load on
        newly added or freshly detected up pool members
    version_added: "1.3"
    required: False
    default: null
    choices: []
    aliases: []
  reselect_tries:
    description:
      - Sets the number of times the system tries to contact a pool member
        after a passive failure
    version_added: "2.2"
    required: False
    default: null
    choices: []
    aliases: []
  service_down_action:
    description:
      - Sets the action to take when node goes down in pool
    version_added: "1.3"
    required: False
    default: null
    choices:
      - none
      - reset
      - drop
      - reselect
    aliases: []
  host:
    description:
      - "Pool member IP"
    required: False
    default: null
    choices: []
    aliases:
      - address
  port:
    description:
      - Pool member port
    required: False
    default: null
    choices: []
    aliases: []
extends_documentation_fragment: f5
'''

EXAMPLES = '''
- name: Create pool
  bigip_pool:
      server: "lb.mydomain.com"
      user: "admin"
      password: "secret"
      state: "present"
      name: "my-pool"
      partition: "Common"
      lb_method: "least_connection_member"
      slow_ramp_time: 120
  delegate_to: localhost

- name: Modify load balancer method
  bigip_pool:
      server: "lb.mydomain.com"
      user: "admin"
      password: "secret"
      state: "present"
      name: "my-pool"
      partition: "Common"
      lb_method: "round_robin"

- name: Add pool member
  bigip_pool:
      server: "lb.mydomain.com"
      user: "admin"
      password: "secret"
      state: "present"
      name: "my-pool"
      partition: "Common"
      host: "{{ ansible_default_ipv4['address'] }}"
      port: 80

- name: Remove pool member from pool
  bigip_pool:
      server: "lb.mydomain.com"
      user: "admin"
      password: "secret"
      state: "absent"
      name: "my-pool"
      partition: "Common"
      host: "{{ ansible_default_ipv4['address'] }}"
      port: 80

- name: Delete pool
  bigip_pool:
      server: "lb.mydomain.com"
      user: "admin"
      password: "secret"
      state: "absent"
      name: "my-pool"
      partition: "Common"
'''

RETURN = '''
'''


def pool_exists(api, pool):
    # hack to determine if pool exists
    result = False
    try:
        api.LocalLB.Pool.get_object_status(pool_names=[pool])
        result = True
    except bigsuds.OperationFailed as e:
        if "was not found" in str(e):
            result = False
        else:
            # genuine exception
            raise
    return result


def create_pool(api, pool, lb_method):
    # create requires lb_method but we don't want to default
    # to a value on subsequent runs
    if not lb_method:
        lb_method = 'round_robin'
    lb_method = "LB_METHOD_%s" % lb_method.strip().upper()
    api.LocalLB.Pool.create_v2(pool_names=[pool], lb_methods=[lb_method],
                               members=[[]])


def remove_pool(api, pool):
    api.LocalLB.Pool.delete_pool(pool_names=[pool])


def get_lb_method(api, pool):
    lb_method = api.LocalLB.Pool.get_lb_method(pool_names=[pool])[0]
    lb_method = lb_method.strip().replace('LB_METHOD_', '').lower()
    return lb_method


def set_lb_method(api, pool, lb_method):
    lb_method = "LB_METHOD_%s" % lb_method.strip().upper()
    api.LocalLB.Pool.set_lb_method(pool_names=[pool], lb_methods=[lb_method])


def get_monitors(api, pool):
    result = api.LocalLB.Pool.get_monitor_association(pool_names=[pool])[0]['monitor_rule']
    monitor_type = result['type'].split("MONITOR_RULE_TYPE_")[-1].lower()
    quorum = result['quorum']
    monitor_templates = result['monitor_templates']
    return (monitor_type, quorum, monitor_templates)


def set_monitors(api, pool, monitor_type, quorum, monitor_templates):
    monitor_type = "MONITOR_RULE_TYPE_%s" % monitor_type.strip().upper()
    monitor_rule = {'type': monitor_type, 'quorum': quorum, 'monitor_templates': monitor_templates}
    monitor_association = {'pool_name': pool, 'monitor_rule': monitor_rule}
    api.LocalLB.Pool.set_monitor_association(monitor_associations=[monitor_association])


def get_slow_ramp_time(api, pool):
    result = api.LocalLB.Pool.get_slow_ramp_time(pool_names=[pool])[0]
    return result


def set_slow_ramp_time(api, pool, seconds):
    api.LocalLB.Pool.set_slow_ramp_time(pool_names=[pool], values=[seconds])


def get_reselect_tries(api, pool):
    result = api.LocalLB.Pool.get_reselect_tries(pool_names=[pool])[0]
    return result


def set_reselect_tries(api, pool, tries):
    api.LocalLB.Pool.set_reselect_tries(pool_names=[pool], values=[tries])


def get_action_on_service_down(api, pool):
    result = api.LocalLB.Pool.get_action_on_service_down(pool_names=[pool])[0]
    result = result.split("SERVICE_DOWN_ACTION_")[-1].lower()
    return result


def set_action_on_service_down(api, pool, action):
    action = "SERVICE_DOWN_ACTION_%s" % action.strip().upper()
    api.LocalLB.Pool.set_action_on_service_down(pool_names=[pool], actions=[action])


def member_exists(api, pool, address, port):
    # hack to determine if member exists
    result = False
    try:
        members = [{'address': address, 'port': port}]
        api.LocalLB.Pool.get_member_object_status(pool_names=[pool],
                                                  members=[members])
        result = True
    except bigsuds.OperationFailed as e:
        if "was not found" in str(e):
            result = False
        else:
            # genuine exception
            raise
    return result


def delete_node_address(api, address):
    result = False
    try:
        api.LocalLB.NodeAddressV2.delete_node_address(nodes=[address])
        result = True
    except bigsuds.OperationFailed as e:
        if "is referenced by a member of pool" in str(e):
            result = False
        else:
            # genuine exception
            raise
    return result


def remove_pool_member(api, pool, address, port):
    members = [{'address': address, 'port': port}]
    api.LocalLB.Pool.remove_member_v2(pool_names=[pool], members=[members])


def add_pool_member(api, pool, address, port):
    members = [{'address': address, 'port': port}]
    api.LocalLB.Pool.add_member_v2(pool_names=[pool], members=[members])


def set_description(api, pool, description):
    api.LocalLB.Pool.set_description(
        pool_names=[pool], descriptions=[description]
    )


def get_description(api, pool):
    return api.LocalLB.Pool.get_description(pool_names=[pool])[0]


def main():
    lb_method_choices = ['round_robin', 'ratio_member',
                         'least_connection_member', 'observed_member',
                         'predictive_member', 'ratio_node_address',
                         'least_connection_node_address',
                         'fastest_node_address', 'observed_node_address',
                         'predictive_node_address', 'dynamic_ratio',
                         'fastest_app_response', 'least_sessions',
                         'dynamic_ratio_member', 'l3_addr',
                         'weighted_least_connection_member',
                         'weighted_least_connection_node_address',
                         'ratio_session', 'ratio_least_connection_member',
                         'ratio_least_connection_node_address']

    monitor_type_choices = ['and_list', 'm_of_n']

    service_down_choices = ['none', 'reset', 'drop', 'reselect']

    argument_spec = f5_argument_spec()

    meta_args = dict(
        name=dict(type='str', required=True, aliases=['pool']),
        lb_method=dict(type='str', choices=lb_method_choices),
        monitor_type=dict(type='str', choices=monitor_type_choices),
        quorum=dict(type='int'),
        monitors=dict(type='list'),
        slow_ramp_time=dict(type='int'),
        reselect_tries=dict(type='int'),
        service_down_action=dict(type='str', choices=service_down_choices),
        host=dict(type='str', aliases=['address']),
        port=dict(type='int'),
        description=dict(type='str')
    )
    argument_spec.update(meta_args)

    module = AnsibleModule(
        argument_spec=argument_spec,
        supports_check_mode=True
    )

    if not bigsuds_found:
        module.fail_json(msg="the python bigsuds module is required")

    if module.params['validate_certs']:
        import ssl
        if not hasattr(ssl, 'SSLContext'):
            module.fail_json(msg='bigsuds does not support verifying certificates with python < 2.7.9.  Either update python or set validate_certs=False on the task')

    server = module.params['server']
    server_port = module.params['server_port']
    user = module.params['user']
    password = module.params['password']
    state = module.params['state']
    partition = module.params['partition']
    validate_certs = module.params['validate_certs']
    description = module.params['description']

    name = module.params['name']
    pool = fq_name(partition, name)
    lb_method = module.params['lb_method']
    if lb_method:
        lb_method = lb_method.lower()
    monitor_type = module.params['monitor_type']
    if monitor_type:
        monitor_type = monitor_type.lower()
    quorum = module.params['quorum']
    monitors = module.params['monitors']
    if monitors:
        monitors = []
        for monitor in module.params['monitors']:
            monitors.append(fq_name(partition, monitor))
    slow_ramp_time = module.params['slow_ramp_time']
    reselect_tries = module.params['reselect_tries']
    service_down_action = module.params['service_down_action']
    if service_down_action:
        service_down_action = service_down_action.lower()
    host = module.params['host']
    address = fq_name(partition, host)
    port = module.params['port']

    # sanity check user supplied values

    if (host and port is None) or (port is not None and not host):
        module.fail_json(msg="both host and port must be supplied")

    if port is not None and (0 > port or port > 65535):
        module.fail_json(msg="valid ports must be in range 0 - 65535")

    if monitors:
        if len(monitors) == 1:
            # set default required values for single monitor
            quorum = 0
            monitor_type = 'single'
        elif len(monitors) > 1:
            if not monitor_type:
                module.fail_json(msg="monitor_type required for monitors > 1")
            if monitor_type == 'm_of_n' and not quorum:
                module.fail_json(msg="quorum value required for monitor_type m_of_n")
            if monitor_type != 'm_of_n':
                quorum = 0
    elif monitor_type:
        # no monitors specified but monitor_type exists
        module.fail_json(msg="monitor_type require monitors parameter")
    elif quorum is not None:
        # no monitors specified but quorum exists
        module.fail_json(msg="quorum requires monitors parameter")

    try:
        api = bigip_api(server, user, password, validate_certs, port=server_port)
        result = {'changed': False}  # default

        if state == 'absent':
            if host and port and pool:
                # member removal takes precedent
                if pool_exists(api, pool) and member_exists(api, pool, address, port):
                    if not module.check_mode:
                        remove_pool_member(api, pool, address, port)
                        deleted = delete_node_address(api, address)
                        result = {'changed': True, 'deleted': deleted}
                    else:
                        result = {'changed': True}
            elif pool_exists(api, pool):
                # no host/port supplied, must be pool removal
                if not module.check_mode:
                    # hack to handle concurrent runs of module
                    # pool might be gone before we actually remove it
                    try:
                        remove_pool(api, pool)
                        result = {'changed': True}
                    except bigsuds.OperationFailed as e:
                        if "was not found" in str(e):
                            result = {'changed': False}
                        else:
                            # genuine exception
                            raise
                else:
                    # check-mode return value
                    result = {'changed': True}

        elif state == 'present':
            update = False
            if not pool_exists(api, pool):
                # pool does not exist -- need to create it
                if not module.check_mode:
                    # a bit of a hack to handle concurrent runs of this module.
                    # even though we've checked the pool doesn't exist,
                    # it may exist by the time we run create_pool().
                    # this catches the exception and does something smart
                    # about it!
                    try:
                        create_pool(api, pool, lb_method)
                        result = {'changed': True}
                    except bigsuds.OperationFailed as e:
                        if "already exists" in str(e):
                            update = True
                        else:
                            # genuine exception
                            raise
                    else:
                        if monitors:
                            set_monitors(api, pool, monitor_type, quorum, monitors)
                        if slow_ramp_time:
                            set_slow_ramp_time(api, pool, slow_ramp_time)
                        if reselect_tries:
                            set_reselect_tries(api, pool, reselect_tries)
                        if service_down_action:
                            set_action_on_service_down(api, pool, service_down_action)
                        if host and port:
                            add_pool_member(api, pool, address, port)
                        if description:
                            set_description(api, pool, description)
                else:
                    # check-mode return value
                    result = {'changed': True}
            else:
                # pool exists -- potentially modify attributes
                update = True

            if update:
                if lb_method and lb_method != get_lb_method(api, pool):
                    if not module.check_mode:
                        set_lb_method(api, pool, lb_method)
                    result = {'changed': True}
                if monitors:
                    t_monitor_type, t_quorum, t_monitor_templates = get_monitors(api, pool)
                    if (t_monitor_type != monitor_type) or (t_quorum != quorum) or (set(t_monitor_templates) != set(monitors)):
                        if not module.check_mode:
                            set_monitors(api, pool, monitor_type, quorum, monitors)
                        result = {'changed': True}
                if slow_ramp_time and slow_ramp_time != get_slow_ramp_time(api, pool):
                    if not module.check_mode:
                        set_slow_ramp_time(api, pool, slow_ramp_time)
                    result = {'changed': True}
                if reselect_tries and reselect_tries != get_reselect_tries(api, pool):
                    if not module.check_mode:
                        set_reselect_tries(api, pool, reselect_tries)
                    result = {'changed': True}
                if service_down_action and service_down_action != get_action_on_service_down(api, pool):
                    if not module.check_mode:
                        set_action_on_service_down(api, pool, service_down_action)
                    result = {'changed': True}
                if (host and port) and not member_exists(api, pool, address, port):
                    if not module.check_mode:
                        add_pool_member(api, pool, address, port)
                    result = {'changed': True}
                if (host and port == 0) and not member_exists(api, pool, address, port):
                    if not module.check_mode:
                        add_pool_member(api, pool, address, port)
                    result = {'changed': True}
                if description and description != get_description(api, pool):
                    if not module.check_mode:
                        set_description(api, pool, description)
                    result = {'changed': True}

    except Exception as e:
        module.fail_json(msg="received exception: %s" % e)

    module.exit_json(**result)

from ansible.module_utils.basic import *
from ansible.module_utils.f5_utils import *

if __name__ == '__main__':
    main()
