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

# (c) 2013, serge van Ginderachter <serge@vanginderachter.be>
#
# 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: bigip_monitor_tcp
short_description: "Manages F5 BIG-IP LTM tcp monitors"
description:
    - "Manages F5 BIG-IP LTM tcp monitors via iControl SOAP API"
version_added: "1.4"
author: Serge van Ginderachter
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"
    - "Monitor API documentation: https://devcentral.f5.com/wiki/iControl.LocalLB__Monitor.ashx"
requirements:
    - bigsuds
options:
    server:
        description:
            - BIG-IP host
        required: true
        default: null
    user:
        description:
            - BIG-IP username
        required: true
        default: null
    password:
        description:
            - BIG-IP password
        required: true
        default: null
    state:
        description:
            - Monitor state
        required: false
        default: 'present'
        choices: ['present', 'absent']
    name:
        description:
            - Monitor name
        required: true
        default: null
        aliases: ['monitor']
    partition:
        description:
            - Partition for the monitor
        required: false
        default: 'Common'
    type:
        description:
            - The template type of this monitor template
        required: false
        default: 'tcp'
        choices: [ 'TTYPE_TCP', 'TTYPE_TCP_ECHO', 'TTYPE_TCP_HALF_OPEN']
    parent:
        description:
            - The parent template of this monitor template
        required: false
        default: 'tcp'
        choices: [ 'tcp', 'tcp_echo', 'tcp_half_open']
    parent_partition:
        description:
            - Partition for the parent monitor
        required: false
        default: 'Common'
    send:
        description:
            - The send string for the monitor call
        required: true
        default: none
    receive:
        description:
            - The receive string for the monitor call
        required: true
        default: none
    ip:
        description: 
            - IP address part of the ipport definition. The default API setting
              is "0.0.0.0".
        required: false
        default: none
    port:
        description: 
            - port address part op the ipport definition. Tyhe default API
              setting is 0.
        required: false
        default: none
    interval:
        description: 
            - The interval specifying how frequently the monitor instance
              of this template will run. By default, this interval is used for up and
              down states. The default API setting is 5.
        required: false
        default: none
    timeout:
        description:
            - The number of seconds in which the node or service must respond to
              the monitor request. If the target responds within the set time
              period, it is considered up. If the target does not respond within
              the set time period, it is considered down. You can change this
              number to any number you want, however, it should be 3 times the
              interval number of seconds plus 1 second. The default API setting
              is 16.
        required: false
        default: none
    time_until_up:
        description:
            - Specifies the amount of time in seconds after the first successful
              response before a node will be marked up. A value of 0 will cause a
              node to be marked up immediately after a valid response is received
              from the node. The default API setting is 0.
        required: false
        default: none
'''

EXAMPLES = '''

- name: BIGIP F5 | Create TCP Monitor
  local_action:
    module:             bigip_monitor_tcp
    state:              present
    server:             "{{ f5server }}"
    user:               "{{ f5user }}"
    password:           "{{ f5password }}"
    name:               "{{ item.monitorname }}"
    type:               tcp
    send:               "{{ item.send }}"
    receive:            "{{ item.receive }}"
  with_items: f5monitors-tcp
- name: BIGIP F5 | Create TCP half open Monitor
  local_action:
    module:             bigip_monitor_tcp
    state:              present
    server:             "{{ f5server }}"
    user:               "{{ f5user }}"
    password:           "{{ f5password }}"
    name:               "{{ item.monitorname }}"
    type:               tcp
    send:               "{{ item.send }}"
    receive:            "{{ item.receive }}"
  with_items: f5monitors-halftcp
- name: BIGIP F5 | Remove TCP Monitor
  local_action:
    module:             bigip_monitor_tcp
    state:              absent
    server:             "{{ f5server }}"
    user:               "{{ f5user }}"
    password:           "{{ f5password }}"
    name:               "{{ monitorname }}"
  with_flattened:
  - f5monitors-tcp
  - f5monitors-halftcp

'''

try:
    import bigsuds
except ImportError:
    bigsuds_found = False
else:
    bigsuds_found = True

TEMPLATE_TYPE = DEFAULT_TEMPLATE_TYPE = 'TTYPE_TCP'
TEMPLATE_TYPE_CHOICES = ['tcp', 'tcp_echo', 'tcp_half_open']
DEFAULT_PARENT = DEFAULT_TEMPLATE_TYPE_CHOICE = DEFAULT_TEMPLATE_TYPE.replace('TTYPE_', '').lower()


# ===========================================
# bigip_monitor module generic methods.
# these should be re-useable for other monitor types
#

def bigip_api(bigip, user, password):

    api = bigsuds.BIGIP(hostname=bigip, username=user, password=password)
    return api


def check_monitor_exists(module, api, monitor, parent):

    # hack to determine if monitor exists
    result = False
    try:
        ttype = api.LocalLB.Monitor.get_template_type(template_names=[monitor])[0]
        parent2 = api.LocalLB.Monitor.get_parent_template(template_names=[monitor])[0]
        if ttype == TEMPLATE_TYPE and parent == parent2:
            result = True
        else:
            module.fail_json(msg='Monitor already exists, but has a different type (%s) or parent(%s)' % (ttype, parent))
    except bigsuds.OperationFailed, e:
        if "was not found" in str(e):
            result = False
        else:
            # genuine exception
            raise
    return result


def create_monitor(api, monitor, template_attributes):

    try: 
        api.LocalLB.Monitor.create_template(templates=[{'template_name': monitor, 'template_type': TEMPLATE_TYPE}], template_attributes=[template_attributes])
    except bigsuds.OperationFailed, e:
        if "already exists" in str(e):
            return False
        else:
            # genuine exception
            raise
    return True


def delete_monitor(api, monitor):

    try:
        api.LocalLB.Monitor.delete_template(template_names=[monitor])
    except bigsuds.OperationFailed, e:
        # maybe it was deleted since we checked
        if "was not found" in str(e):
            return False
        else:
            # genuine exception
            raise
    return True


def check_string_property(api, monitor, str_property):

    return str_property == api.LocalLB.Monitor.get_template_string_property([monitor], [str_property['type']])[0]


def set_string_property(api, monitor, str_property):

    api.LocalLB.Monitor.set_template_string_property(template_names=[monitor], values=[str_property])


def check_integer_property(api, monitor, int_property):

    return int_property == api.LocalLB.Monitor.get_template_integer_property([monitor], [int_property['type']])[0]


def set_integer_property(api, monitor, int_property):

    api.LocalLB.Monitor.set_template_int_property(template_names=[monitor], values=[int_property])


def update_monitor_properties(api, module, monitor, template_string_properties, template_integer_properties):

    changed = False
    for str_property in template_string_properties:
        if str_property['value'] is not None and not check_string_property(api, monitor, str_property):
            if not module.check_mode:
                set_string_property(api, monitor, str_property)
            changed = True
    for int_property in template_integer_properties:
        if int_property['value'] is not None and not check_integer_property(api, monitor, int_property):
            if not module.check_mode:
                set_integer_property(api, monitor, int_property)
            changed = True

    return changed


def get_ipport(api, monitor):

    return api.LocalLB.Monitor.get_template_destination(template_names=[monitor])[0]


def set_ipport(api, monitor, ipport):

    try:
        api.LocalLB.Monitor.set_template_destination(template_names=[monitor], destinations=[ipport])
        return True, ""

    except bigsuds.OperationFailed, e:
        if "Cannot modify the address type of monitor" in str(e):
            return False, "Cannot modify the address type of monitor if already assigned to a pool."
        else:
            # genuine exception
            raise

# ===========================================
# main loop
#
# writing a module for other monitor types should 
# only need an updated main() (and monitor specific functions)

def main():

    # begin monitor specific stuff

    module = AnsibleModule(
        argument_spec = dict(
            server    = dict(required=True),
            user      = dict(required=True),
            password  = dict(required=True),
            partition = dict(default='Common'),
            state     = dict(default='present', choices=['present', 'absent']),
            name      = dict(required=True),
            type      = dict(default=DEFAULT_TEMPLATE_TYPE_CHOICE, choices=TEMPLATE_TYPE_CHOICES),
            parent    = dict(default=DEFAULT_PARENT),
            parent_partition = dict(default='Common'),
            send      = dict(required=False),
            receive   = dict(required=False),
            ip        = dict(required=False),
            port      = dict(required=False, type='int'),
            interval  = dict(required=False, type='int'),
            timeout   = dict(required=False, type='int'),
            time_until_up = dict(required=False, type='int', default=0)
        ),
        supports_check_mode=True
    )

    server = module.params['server']
    user = module.params['user']
    password = module.params['password']
    partition = module.params['partition']
    parent_partition = module.params['parent_partition']
    state = module.params['state']
    name = module.params['name']
    type = 'TTYPE_' + module.params['type'].upper()
    parent = "/%s/%s" % (parent_partition, module.params['parent'])
    monitor = "/%s/%s" % (partition, name)
    send = module.params['send']
    receive = module.params['receive']
    ip = module.params['ip']
    port = module.params['port']
    interval = module.params['interval']
    timeout = module.params['timeout']
    time_until_up = module.params['time_until_up']

    # tcp monitor has multiple types, so overrule
    global TEMPLATE_TYPE
    TEMPLATE_TYPE = type

    # end monitor specific stuff

    if not bigsuds_found:
        module.fail_json(msg="the python bigsuds module is required")
    api = bigip_api(server, user, password)
    monitor_exists = check_monitor_exists(module, api, monitor, parent)


    # ipport is a special setting
    if monitor_exists: # make sure to not update current settings if not asked
        cur_ipport = get_ipport(api, monitor)
        if ip is None:
            ip = cur_ipport['ipport']['address']
        if port is None:
            port = cur_ipport['ipport']['port']
    else: # use API defaults if not defined to create it
        if interval is None:        
            interval = 5
        if timeout is None:         
            timeout = 16
        if ip is None:              
            ip = '0.0.0.0'
        if port is None:            
            port = 0
        if send is None:            
            send = ''
        if receive is None:         
            receive = ''

    # define and set address type
    if ip == '0.0.0.0' and port == 0:
        address_type = 'ATYPE_STAR_ADDRESS_STAR_PORT'
    elif ip == '0.0.0.0' and port != 0:
        address_type = 'ATYPE_STAR_ADDRESS_EXPLICIT_PORT'
    elif ip != '0.0.0.0' and port != 0:
        address_type = 'ATYPE_EXPLICIT_ADDRESS_EXPLICIT_PORT'
    else:
        address_type = 'ATYPE_UNSET'

    ipport = {'address_type': address_type,
              'ipport': {'address': ip,
                         'port': port}}

    template_attributes = {'parent_template': parent,
                           'interval': interval,
                           'timeout': timeout,
                           'dest_ipport': ipport,
                           'is_read_only': False,
                           'is_directly_usable': True}

    # monitor specific stuff
    if type == 'TTYPE_TCP':
        template_string_properties = [{'type': 'STYPE_SEND',
                                       'value': send},
                                      {'type': 'STYPE_RECEIVE',
                                       'value': receive}]
    else:
        template_string_properties = []

    template_integer_properties = [{'type': 'ITYPE_INTERVAL',
                                     'value': interval},
                                   {'type': 'ITYPE_TIMEOUT',
                                    'value': timeout},
                                   {'type': 'ITYPE_TIME_UNTIL_UP',
                                    'value': interval}]

    # main logic, monitor generic

    try:
        result = {'changed': False}  # default


        if state == 'absent':
            if monitor_exists:
                if not module.check_mode:
                    # possible race condition if same task 
                    # on other node deleted it first
                    result['changed'] |= delete_monitor(api, monitor)
                else:
                    result['changed'] |= True

        else: # state present
            ## check for monitor itself
            if not monitor_exists: # create it
                if not module.check_mode: 
                    # again, check changed status here b/c race conditions
                    # if other task already created it
                    result['changed'] |= create_monitor(api, monitor, template_attributes)
                else: 
                    result['changed'] |= True

            ## check for monitor parameters
            # whether it already existed, or was just created, now update
            # the update functions need to check for check mode but
            # cannot update settings if it doesn't exist which happens in check mode
            if monitor_exists and not module.check_mode:
                result['changed'] |= update_monitor_properties(api, module, monitor,
                                                               template_string_properties,
                                                               template_integer_properties)
            # else assume nothing changed

            # we just have to update the ipport if monitor already exists and it's different
            if monitor_exists and cur_ipport != ipport:
                set_ipport(api, monitor, ipport)    
                result['changed'] |= True
            #else: monitor doesn't exist (check mode) or ipport is already ok


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

    module.exit_json(**result)

# include magic from lib/ansible/module_common.py
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
main()

