#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright: (c) 2018, F5 Networks Inc.
# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)

from __future__ import absolute_import, division, print_function
__metaclass__ = type


ANSIBLE_METADATA = {'metadata_version': '1.1',
                    'status': ['preview'],
                    'supported_by': 'certified'}

DOCUMENTATION = r'''
---
module: bigip_profile_oneconnect
short_description: Manage OneConnect profiles on a BIG-IP
description:
  - Manage OneConnect profiles on a BIG-IP.
version_added: 2.7
options:
  name:
    description:
      - Specifies the name of the OneConnect profile.
    required: True
  parent:
    description:
      - Specifies the profile from which this profile inherits settings.
      - When creating a new profile, if this parameter is not specified, the default
        is the system-supplied C(oneconnect) profile.
  source_mask:
    description:
      - Specifies a value that the system applies to the source address to determine
        its eligibility for reuse.
      - When creating a new profile, if this parameter is not specified, the
        default is provided by the parent profile.
      - The system applies the value of this setting to the server-side source address to
        determine its eligibility for reuse.
      - A mask of C(0) causes the system to share reused connections across all source
        addresses. A host mask of C(32) causes the system to share only those reused
        connections originating from the same source address.
      - When you are using a SNAT or SNAT pool, the server-side source address is
        translated first and then the OneConnect mask is applied to the translated address.
  description:
    description:
      - Description of the profile.
  maximum_size:
    description:
      - Specifies the maximum number of connections that the system holds in the
        connection reuse pool.
      - If the pool is already full, then a server-side connection closes after the
        response is completed.
      - When creating a new profile, if this parameter is not specified, the
        default is provided by the parent profile.
  maximum_age:
    description:
      - Specifies the maximum number of seconds allowed for a connection in the connection
        reuse pool.
      - For any connection with an age higher than this value, the system removes that
        connection from the re-use pool.
      - When creating a new profile, if this parameter is not specified, the
        default is provided by the parent profile.
  maximum_reuse:
    description:
      - Specifies the maximum number of times that a server-side connection can be reused.
      - When creating a new profile, if this parameter is not specified, the
        default is provided by the parent profile.
  idle_timeout_override:
    description:
      - Specifies the number of seconds that a connection is idle before the connection
        flow is eligible for deletion.
      - When creating a new profile, if this parameter is not specified, the default
        is provided by the parent profile.
      - You may specify a number of seconds for the timeout override.
      - When C(disabled), specifies that there is no timeout override for the connection.
      - When C(indefinite), Specifies that a connection may be idle with no timeout
        override.
  limit_type:
    description:
      - When C(none), simultaneous in-flight requests and responses over TCP connections
        to a pool member are counted toward the limit. This is the historical behavior.
      - When C(idle), idle connections will be dropped as the TCP connection limit is
        reached. For short intervals, during the overlap of the idle connection being
        dropped and the new connection being established, the TCP connection limit may
        be exceeded.
      - When C(strict), the TCP connection limit is honored with no exceptions. This means
        that idle connections will prevent new TCP connections from being made until
        they expire, even if they could otherwise be reused.
      - C(strict) is not a recommended configuration except in very special cases with
        short expiration timeouts.
      - When creating a new profile, if this parameter is not specified, the default
        is provided by the parent profile.
    choices:
      - none
      - idle
      - strict
  share_pools:
    description:
      - Indicates that connections may be shared not only within a virtual server, but
        also among similar virtual servers
      - When C(yes), all virtual servers that use the same OneConnect and other internal
        network profiles can share connections.
      - When creating a new profile, if this parameter is not specified, the default
        is provided by the parent profile.
    type: bool
  partition:
    description:
      - Device partition to manage resources on.
    default: Common
  state:
    description:
      - When C(present), ensures that the profile exists.
      - When C(absent), ensures the profile is removed.
    default: present
    choices:
      - present
      - absent
extends_documentation_fragment: f5
author:
  - Tim Rupp (@caphrim007)
'''

EXAMPLES = r'''
- name: Create a OneConnect profile
  bigip_profile_oneconnect:
    name: foo
    state: present
    provider:
      user: admin
      password: secret
      server: lb.mydomain.com
  delegate_to: localhost
'''

RETURN = r'''
source_mask:
  description: Value that the system applies to the source address to determine its eligibility for reuse.
  returned: changed
  type: string
  sample: 255.255.255.255
description:
  description: Description of the profile.
  returned: changed
  type: string
  sample: My profile
maximum_size:
  description: Maximum number of connections that the system holds in the connection reuse pool.
  returned: changed
  type: int
  sample: 3000
maximum_age:
  description: Maximum number of seconds allowed for a connection in the connection reuse pool.
  returned: changed
  type: int
  sample: 2000
maximum_reuse:
  description: Maximum number of times that a server-side connection can be reused.
  returned: changed
  type: int
  sample: 1000
idle_timeout_override:
  description: The new idle timeout override.
  returned: changed
  type: string
  sample: disabled
limit_type:
  description: New limit type of the profile.
  returned: changed
  type: string
  sample: idle
share_pools:
  description: Share connections among similar virtual servers.
  returned: changed
  type: bool
  sample: yes
'''

from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.basic import env_fallback

try:
    from library.module_utils.network.f5.bigip import F5RestClient
    from library.module_utils.network.f5.common import F5ModuleError
    from library.module_utils.network.f5.common import AnsibleF5Parameters
    from library.module_utils.network.f5.common import cleanup_tokens
    from library.module_utils.network.f5.common import fq_name
    from library.module_utils.network.f5.common import f5_argument_spec
    from library.module_utils.network.f5.common import transform_name
    from library.module_utils.network.f5.common import exit_json
    from library.module_utils.network.f5.common import fail_json
    from library.module_utils.network.f5.ipaddress import is_valid_ip
except ImportError:
    from ansible.module_utils.network.f5.bigip import F5RestClient
    from ansible.module_utils.network.f5.common import F5ModuleError
    from ansible.module_utils.network.f5.common import AnsibleF5Parameters
    from ansible.module_utils.network.f5.common import cleanup_tokens
    from ansible.module_utils.network.f5.common import fq_name
    from ansible.module_utils.network.f5.common import f5_argument_spec
    from ansible.module_utils.network.f5.common import transform_name
    from ansible.module_utils.network.f5.common import exit_json
    from ansible.module_utils.network.f5.common import fail_json
    from ansible.module_utils.network.f5.ipaddress import is_valid_ip


class Parameters(AnsibleF5Parameters):
    api_map = {
        'sourceMask': 'source_mask',
        'maxSize': 'maximum_size',
        'maxReuse': 'maximum_reuse',
        'maxAge': 'maximum_age',
        'defaultsFrom': 'parent',
        'limitType': 'limit_type',
        'idleTimeoutOverride': 'idle_timeout_override',
        'sharePools': 'share_pools',
    }

    api_attributes = [
        'sourceMask',
        'maxSize',
        'defaultsFrom',
        'description',
        'limitType',
        'idleTimeoutOverride',
        'maxAge',
        'maxReuse',
        'sharePools',
    ]

    returnables = [
        'description',
        'source_mask',
        'maximum_size',
        'maximum_age',
        'maximum_reuse',
        'limit_type',
        'idle_timeout_override',
        'share_pools',
        'parent',
    ]

    updatables = [
        'description',
        'source_mask',
        'maximum_size',
        'maximum_age',
        'maximum_reuse',
        'limit_type',
        'idle_timeout_override',
        'share_pools',
    ]


class ApiParameters(Parameters):
    @property
    def source_mask(self):
        if self._values['source_mask'] is None:
            return None
        elif self._values['source_mask'] == 'any':
            return 0
        return self._values['source_mask']

    @property
    def idle_timeout_override(self):
        if self._values['idle_timeout_override'] is None:
            return None
        try:
            return int(self._values['idle_timeout_override'])
        except ValueError:
            return self._values['idle_timeout_override']


class ModuleParameters(Parameters):
    @property
    def parent(self):
        if self._values['parent'] is None:
            return None
        result = fq_name(self.partition, self._values['parent'])
        return result

    @property
    def idle_timeout_override(self):
        if self._values['idle_timeout_override'] is None:
            return None
        try:
            return int(self._values['idle_timeout_override'])
        except ValueError:
            return self._values['idle_timeout_override']

    @property
    def source_mask(self):
        if self._values['source_mask'] is None:
            return None
        elif self._values['source_mask'] == 'any':
            return 0
        try:
            int(self._values['source_mask'])
            raise F5ModuleError(
                "'source_mask' must not be in CIDR format."
            )
        except ValueError:
            pass

        if is_valid_ip(self._values['source_mask']):
            return self._values['source_mask']

    @property
    def share_pools(self):
        if self._values['share_pools'] is None:
            return None
        elif self._values['share_pools'] is True:
            return 'enabled'
        return 'disabled'


class Changes(Parameters):
    def to_return(self):
        result = {}
        try:
            for returnable in self.returnables:
                result[returnable] = getattr(self, returnable)
            result = self._filter_params(result)
        except Exception:
            pass
        return result


class UsableChanges(Changes):
    pass


class ReportableChanges(Changes):
    @property
    def idle_timeout_override(self):
        try:
            return int(self._values['idle_timeout_override'])
        except ValueError:
            return self._values['idle_timeout_override']

    @property
    def share_pools(self):
        if self._values['idle_timeout_override'] is None:
            return None
        elif self._values['idle_timeout_override'] == 'enabled':
            return 'yes'
        elif self._values['idle_timeout_override'] == 'disabled':
            return 'no'


class Difference(object):
    def __init__(self, want, have=None):
        self.want = want
        self.have = have

    def compare(self, param):
        try:
            result = getattr(self, param)
            return result
        except AttributeError:
            return self.__default(param)

    def __default(self, param):
        attr1 = getattr(self.want, param)
        try:
            attr2 = getattr(self.have, param)
            if attr1 != attr2:
                return attr1
        except AttributeError:
            return attr1

    @property
    def parent(self):
        if self.want.parent != self.have.parent:
            raise F5ModuleError(
                "The parent profile cannot be changed"
            )


class ModuleManager(object):
    def __init__(self, *args, **kwargs):
        self.module = kwargs.get('module', None)
        self.client = kwargs.get('client', None)
        self.want = ModuleParameters(params=self.module.params)
        self.have = ApiParameters()
        self.changes = UsableChanges()

    def _set_changed_options(self):
        changed = {}
        for key in Parameters.returnables:
            if getattr(self.want, key) is not None:
                changed[key] = getattr(self.want, key)
        if changed:
            self.changes = UsableChanges(params=changed)

    def _update_changed_options(self):
        diff = Difference(self.want, self.have)
        updatables = Parameters.updatables
        changed = dict()
        for k in updatables:
            change = diff.compare(k)
            if change is None:
                continue
            else:
                if isinstance(change, dict):
                    changed.update(change)
                else:
                    changed[k] = change
        if changed:
            self.changes = UsableChanges(params=changed)
            return True
        return False

    def should_update(self):
        result = self._update_changed_options()
        if result:
            return True
        return False

    def exec_module(self):
        changed = False
        result = dict()
        state = self.want.state

        if state == "present":
            changed = self.present()
        elif state == "absent":
            changed = self.absent()

        reportable = ReportableChanges(params=self.changes.to_return())
        changes = reportable.to_return()
        result.update(**changes)
        result.update(dict(changed=changed))
        self._announce_deprecations(result)
        return result

    def _announce_deprecations(self, result):
        warnings = result.pop('__warnings', [])
        for warning in warnings:
            self.client.module.deprecate(
                msg=warning['msg'],
                version=warning['version']
            )

    def present(self):
        if self.exists():
            return self.update()
        else:
            return self.create()

    def exists(self):
        uri = "https://{0}:{1}/mgmt/tm/ltm/profile/one-connect/{2}".format(
            self.client.provider['server'],
            self.client.provider['server_port'],
            transform_name(self.want.partition, self.want.name)
        )
        resp = self.client.api.get(uri)
        try:
            response = resp.json()
        except ValueError:
            return False
        if resp.status == 404 or 'code' in response and response['code'] == 404:
            return False
        return True

    def update(self):
        self.have = self.read_current_from_device()
        if not self.should_update():
            return False
        if self.module.check_mode:
            return True
        self.update_on_device()
        return True

    def remove(self):
        if self.module.check_mode:
            return True
        self.remove_from_device()
        if self.exists():
            raise F5ModuleError("Failed to delete the resource.")
        return True

    def create(self):
        self._set_changed_options()
        if self.module.check_mode:
            return True
        self.create_on_device()
        return True

    def create_on_device(self):
        params = self.changes.api_params()
        params['name'] = self.want.name
        params['partition'] = self.want.partition
        uri = "https://{0}:{1}/mgmt/tm/ltm/profile/one-connect/".format(
            self.client.provider['server'],
            self.client.provider['server_port']
        )
        resp = self.client.api.post(uri, json=params)
        try:
            response = resp.json()
        except ValueError as ex:
            raise F5ModuleError(str(ex))

        if 'code' in response and response['code'] in [400, 403, 404]:
            if 'message' in response:
                raise F5ModuleError(response['message'])
            else:
                raise F5ModuleError(resp.content)
        return response['selfLink']

    def update_on_device(self):
        params = self.changes.api_params()
        uri = "https://{0}:{1}/mgmt/tm/ltm/profile/one-connect/{2}".format(
            self.client.provider['server'],
            self.client.provider['server_port'],
            transform_name(self.want.partition, self.want.name)
        )
        resp = self.client.api.patch(uri, json=params)
        try:
            response = resp.json()
        except ValueError as ex:
            raise F5ModuleError(str(ex))

        if 'code' in response and response['code'] in [400, 404]:
            if 'message' in response:
                raise F5ModuleError(response['message'])
            else:
                raise F5ModuleError(resp.content)

    def absent(self):
        if self.exists():
            return self.remove()
        return False

    def remove_from_device(self):
        uri = "https://{0}:{1}/mgmt/tm/ltm/profile/one-connect/{2}".format(
            self.client.provider['server'],
            self.client.provider['server_port'],
            transform_name(self.want.partition, self.want.name)
        )
        response = self.client.api.delete(uri)
        if response.status == 200:
            return True
        raise F5ModuleError(response.content)

    def read_current_from_device(self):
        uri = "https://{0}:{1}/mgmt/tm/ltm/profile/one-connect/{2}".format(
            self.client.provider['server'],
            self.client.provider['server_port'],
            transform_name(self.want.partition, self.want.name)
        )
        resp = self.client.api.get(uri)
        try:
            response = resp.json()
        except ValueError as ex:
            raise F5ModuleError(str(ex))

        if 'code' in response and response['code'] == 400:
            if 'message' in response:
                raise F5ModuleError(response['message'])
            else:
                raise F5ModuleError(resp.content)
        return ApiParameters(params=response)


class ArgumentSpec(object):
    def __init__(self):
        self.supports_check_mode = True
        argument_spec = dict(
            name=dict(required=True),
            description=dict(),
            parent=dict(),
            source_mask=dict(),
            maximum_size=dict(type='int'),
            maximum_reuse=dict(type='int'),
            maximum_age=dict(type='int'),
            limit_type=dict(
                choices=['none', 'idle', 'strict']
            ),
            idle_timeout_override=dict(),
            share_pools=dict(type='bool'),
            state=dict(
                default='present',
                choices=['present', 'absent']
            ),
            partition=dict(
                default='Common',
                fallback=(env_fallback, ['F5_PARTITION'])
            )
        )
        self.argument_spec = {}
        self.argument_spec.update(f5_argument_spec)
        self.argument_spec.update(argument_spec)


def main():
    spec = ArgumentSpec()

    module = AnsibleModule(
        argument_spec=spec.argument_spec,
        supports_check_mode=spec.supports_check_mode,
    )

    client = F5RestClient(**module.params)

    try:
        mm = ModuleManager(module=module, client=client)
        results = mm.exec_module()
        cleanup_tokens(client)
        exit_json(module, results, client)
    except F5ModuleError as ex:
        cleanup_tokens(client)
        fail_json(module, ex, client)


if __name__ == '__main__':
    main()
