#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright: (c) 2017, F5 Networks Inc.
# Copyright: (c) 2013, Matt Hite <mhite@hotmail.com>
# 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

DOCUMENTATION = r'''
---
module: bigip_pool_member
short_description: Manages F5 BIG-IP LTM pool members
description:
  - Manages F5 BIG-IP LTM pool members via the REST API.
version_added: "1.0.0"
options:
  aggregate:
    description:
      - List of pool member definitions to be created, modified, or removed.
      - When using C(aggregates), if one of the aggregate definitions is invalid, the aggregate run will fail,
        indicating the error it last encountered.
      - The module will B(NOT) rollback any changes it has made prior to encountering the error.
      - The module also will not indicate what changes were made prior to failure. Therefore we strong advise
        you run the module in C(check) mode to ensure basic validation prior to executing this module.
    type: list
    elements: dict
    suboptions:
      name:
        description:
          - Name of the node to create or re-use when creating a new pool member.
          - While this parameter is optional, we recommend specifying this parameter
            at all times to mitigate any unexpected behavior.
          - If not specified, a node name is created automatically from either the specified C(address) or C(fqdn).
          - The C(enabled) state is an alias of C(present).
        type: str
      address:
        description:
          - IP address of the pool member. This can be either IPv4 or IPv6. When creating a
            new pool member, one of either C(address) or C(fqdn) must be provided. This
            parameter cannot be updated after it is set.
        type: str
        aliases:
          - ip
          - host
      fqdn:
        description:
          - FQDN name of the pool member. This can be any name that is a valid RFC 1123 DNS
            name. Therefore, the only usable characters are "A" to "Z",
            "a" to "z", "0" to "9", the hyphen ("-") and the period (".").
          - FQDN names must include at least one period; delineating the host from
            the domain. For example, C(host.domain).
          - FQDN names must end with a letter or a number.
          - When creating a new pool member, one of either C(address) or C(fqdn) must be
            provided. This parameter cannot be updated after it is set.
        type: str
        aliases:
          - hostname
      port:
        description:
          - Pool member port.
          - This value cannot be changed after it has been set.
          - Parameter must be provided when using aggregates.
        type: int
      connection_limit:
        description:
          - Pool member connection limit. Setting this to C(0) disables the limit.
        type: int
      description:
        description:
          - Pool member description.
        type: str
      rate_limit:
        description:
          - Pool member rate limit (connections-per-second). Setting this to C(0)
            disables the limit.
        type: int
      ratio:
        description:
          - Pool member ratio weight. Valid values range from 1 through 100.
            New pool members -- unless overridden with this value -- default
            to 1.
        type: int
      preserve_node:
        description:
          - When state is C(absent), the system attempts to remove the node the pool
            member references.
          - The node will not be removed if it is still referenced by other pool
            members. If this happens, the module will not raise an error.
          - Setting this to C(true) disables this behavior.
        type: bool
      priority_group:
        description:
          - Specifies a number representing the priority group for the pool member.
          - When adding a new member, the default is C(0), meaning the member has no priority.
          - To specify a priority, you must activate priority group usage when you
            create a new pool or when adding or removing pool members. When activated,
            the system load balances traffic according to the priority group number
            assigned to the pool member.
          - The higher the number, the higher the priority. So a member with a priority
            of 3 has higher priority than a member with a priority of 1.
        type: int
      fqdn_auto_populate:
        description:
          - Specifies whether the system automatically creates ephemeral nodes using
            the IP addresses returned by the resolution of a DNS query for a node
            defined by an FQDN.
          - When C(true), the system generates an ephemeral node for each IP address
            returned in response to a DNS query for the FQDN of the node. Additionally,
            when a DNS response indicates the IP address of an ephemeral node no longer
            exists, the system deletes the ephemeral node.
          - When C(false), the system resolves a DNS query for the FQDN of the node
            with the single IP address associated with the FQDN.
          - When creating a new pool member, the default for this parameter is C(true).
          - Once set this parameter cannot be changed afterwards.
          - This parameter is ignored when C(reuse_nodes) is C(true).
        type: bool
      reuse_nodes:
        description:
          - Reuses node definitions if requested.
        type: bool
        default: true
      monitors:
        description:
          - Specifies the health monitors the system currently uses to monitor
            this resource.
        type: list
        elements: str
      availability_requirements:
        description:
          - If you activate more than one health monitor, specifies the number of health
            monitors that must receive successful responses in order for the link to be
            considered available.
          - Specifying an empty string will remove the monitors and revert to inheriting from the pool (default).
          - Specifying C(none) will remove any health monitoring from the member completely.
        type: dict
        suboptions:
          type:
            description:
              - Monitor rule type when C(monitors) is specified.
              - When creating a new pool, if this value is not specified, the default of
                C(all) will be used.
            type: str
            required: True
            choices:
              - all
              - at_least
          at_least:
            description:
              - Specifies the minimum number of active health monitors that must be successful
                before the link is considered up.
              - This parameter is only relevant when a C(type) of C(at_least) is used.
              - This parameter will be ignored if a type of C(all) is used.
            type: int
      ip_encapsulation:
        description:
          - Specifies the IP encapsulation using either IPIP (IP encapsulation within IP,
            RFC 2003) or GRE (Generic Router Encapsulation, RFC 2784) on outbound packets
            (from BIG-IP system to server-pool member).
          - When C(none), disables IP encapsulation.
          - When C(inherit), inherits the IP encapsulation setting from the member's pool.
          - When any other value, the options are None, Inherit from Pool, and Member Specific.
        type: str
      state:
        description:
          - Pool member state.
        type: str
        choices:
          - present
          - absent
          - enabled
          - disabled
          - forced_offline
        default: present
      partition:
        description:
          - Partition to manage resources on.
        type: str
        default: Common
    aliases:
      - members
  pool:
    description:
      - Pool name. This pool must exist.
    type: str
    required: True
  name:
    description:
      - Name of the node to create or re-use when creating a new pool member.
      - While this parameter is optional, we recommend specifying this parameter
        at all times to mitigate any unexpected behavior.
      - If not specified, a node name is created automatically from either the specified C(address) or C(fqdn).
      - The C(enabled) state is an alias of C(present).
    type: str
  address:
    description:
      - IP address of the pool member. This can be either IPv4 or IPv6. When creating a
        new pool member, one of either C(address) or C(fqdn) must be provided. This
        parameter cannot be updated after it is set.
    type: str
    aliases:
      - ip
      - host
  fqdn:
    description:
      - FQDN name of the pool member. This can be any name that is a valid RFC 1123 DNS
        name. Therefore, the only usable characters are "A" to "Z",
        "a" to "z", "0" to "9", the hyphen ("-") and the period (".").
      - FQDN names must include at least one period; delineating the host from
        the domain. For example, C(host.domain).
      - FQDN names must end with a letter or a number.
      - When creating a new pool member, one of either C(address) or C(fqdn) must be
        provided. This parameter cannot be updated after it is set.
    type: str
    aliases:
      - hostname
  port:
    description:
      - Pool member port.
      - This value cannot be changed after it has been set.
      - Parameter must be provided when using aggregates.
    type: int
  connection_limit:
    description:
      - Pool member connection limit. Setting this to C(0) disables the limit.
    type: int
  description:
    description:
      - Pool member description.
    type: str
  rate_limit:
    description:
      - Pool member rate limit (connections-per-second). Setting this to C(0)
        disables the limit.
    type: int
  ratio:
    description:
      - Pool member ratio weight. Valid values range from 1 through 100.
        New pool members -- unless overridden with this value -- default
        to 1.
    type: int
  preserve_node:
    description:
      - When state is C(absent), the system attempts to remove the node the pool
        member references.
      - The node will not be removed if it is still referenced by other pool
        members. If this happens, the module will not raise an error.
      - Setting this to C(true) disables this behavior.
    type: bool
  priority_group:
    description:
      - Specifies a number representing the priority group for the pool member.
      - When adding a new member, the default is C(0), meaning the member has no priority.
      - To specify a priority, you must activate priority group usage when you
        create a new pool or when adding or removing pool members. When activated,
        the system load balances traffic according to the priority group number
        assigned to the pool member.
      - The higher the number, the higher the priority. So a member with a priority
        of 3 has higher priority than a member with a priority of 1.
    type: int
  fqdn_auto_populate:
    description:
      - Specifies whether the system automatically creates ephemeral nodes using
        the IP addresses returned by the resolution of a DNS query for a node
        defined by an FQDN.
      - When C(true), the system generates an ephemeral node for each IP address
        returned in response to a DNS query for the FQDN of the node. Additionally,
        when a DNS response indicates the IP address of an ephemeral node no longer
        exists, the system deletes the ephemeral node.
      - When C(false), the system resolves a DNS query for the FQDN of the node
        with the single IP address associated with the FQDN.
      - When creating a new pool member, the default for this parameter is C(true).
      - Once set this parameter cannot be changed afterwards.
      - This parameter is ignored when C(reuse_nodes) is C(true).
    type: bool
  reuse_nodes:
    description:
      - Reuses node definitions if requested.
    type: bool
    default: true
  monitors:
    description:
      - Specifies the health monitors the system currently uses to monitor
        this resource.
    type: list
    elements: str
  availability_requirements:
    description:
      - If you activate more than one health monitor, specifies the number of health
        monitors that must receive successful responses in order for the link to be
        considered available.
      - Specifying an empty string will remove the monitors and revert to inheriting from the pool (default).
      - Specifying C(none) will remove any health monitoring from the member completely.
    type: dict
    suboptions:
      type:
        description:
          - Monitor rule type when C(monitors) is specified.
          - When creating a new pool, if this value is not specified, the default of
            C(all) will be used.
        type: str
        required: True
        choices:
          - all
          - at_least
      at_least:
        description:
          - Specifies the minimum number of active health monitors that must be successful
            before the link is considered up.
          - This parameter is only relevant when a C(type) of C(at_least) is used.
          - This parameter will be ignored if a type of C(all) is used.
        type: int
  ip_encapsulation:
    description:
      - Specifies the IP encapsulation using either IPIP (IP encapsulation within IP,
        RFC 2003) or GRE (Generic Router Encapsulation, RFC 2784) on outbound packets
        (from BIG-IP system to server-pool member).
      - When C(none), disables IP encapsulation.
      - When C(inherit), inherits the IP encapsulation setting from the member's pool.
      - When any other value, the options are None, Inherit from Pool, and Member Specific.
    type: str
  state:
    description:
      - Pool member state.
    type: str
    choices:
      - present
      - absent
      - enabled
      - disabled
      - forced_offline
    default: present
  partition:
    description:
      - Partition to manage resources on.
    type: str
    default: Common
  replace_all_with:
    description:
      - Removes members not defined in the C(aggregate) parameter.
      - This operation is all or none, meaning it will stop if there are some pool members
        that cannot be removed.
    type: bool
    default: false
    aliases:
      - purge
extends_documentation_fragment: f5networks.f5_modules.f5
author:
  - Tim Rupp (@caphrim007)
  - Wojciech Wypior (@wojtek0806)
'''

EXAMPLES = r'''
- name: Add pool member
  bigip_pool_member:
    pool: my-pool
    partition: Common
    name: my-member
    host: "{{ ansible_default_ipv4['address'] }}"
    port: 80
    description: web server
    connection_limit: 100
    rate_limit: 50
    ratio: 2
    provider:
      server: lb.mydomain.com
      user: admin
      password: secret
  delegate_to: localhost

- name: Modify pool member ratio and description
  bigip_pool_member:
    pool: my-pool
    partition: Common
    name: my-member
    host: "{{ ansible_default_ipv4['address'] }}"
    port: 80
    ratio: 1
    description: nginx server
    provider:
      server: lb.mydomain.com
      user: admin
      password: secret
  delegate_to: localhost

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

- name: Force pool member offline
  bigip_pool_member:
    state: forced_offline
    pool: my-pool
    partition: Common
    name: my-member
    host: "{{ ansible_default_ipv4['address'] }}"
    port: 80
    provider:
      server: lb.mydomain.com
      user: admin
      password: secret
  delegate_to: localhost

- name: Create members with priority groups
  bigip_pool_member:
    pool: my-pool
    partition: Common
    host: "{{ item.address }}"
    name: "{{ item.name }}"
    priority_group: "{{ item.priority_group }}"
    port: 80
    provider:
      server: lb.mydomain.com
      user: admin
      password: secret
  delegate_to: localhost
  loop:
    - address: 1.1.1.1
      name: web1
      priority_group: 4
    - address: 2.2.2.2
      name: web2
      priority_group: 3
    - address: 3.3.3.3
      name: web3
      priority_group: 2
    - address: 4.4.4.4
      name: web4
      priority_group: 1

- name: Add pool members aggregate
  bigip_pool_member:
    pool: my-pool
    aggregate:
      - host: 192.168.1.1
        partition: Common
        port: 80
        description: web server
        connection_limit: 100
        rate_limit: 50
        ratio: 2
      - host: 192.168.1.2
        partition: Common
        port: 80
        description: web server
        connection_limit: 100
        rate_limit: 50
        ratio: 2
      - host: 192.168.1.3
        partition: Common
        port: 80
        description: web server
        connection_limit: 100
        rate_limit: 50
        ratio: 2
    provider:
      server: lb.mydomain.com
      user: admin
      password: secret
  delegate_to: localhost

- name: Add pool members aggregate, remove non aggregates
  bigip_pool_member:
    pool: my-pool
    aggregate:
      - host: 192.168.1.1
        partition: Common
        port: 80
        description: web server
        connection_limit: 100
        rate_limit: 50
        ratio: 2
      - host: 192.168.1.2
        partition: Common
        port: 80
        description: web server
        connection_limit: 100
        rate_limit: 50
        ratio: 2
      - host: 192.168.1.3
        partition: Common
        port: 80
        description: web server
        connection_limit: 100
        rate_limit: 50
        ratio: 2
    replace_all_with: true
    provider:
      server: lb.mydomain.com
      user: admin
      password: secret
  delegate_to: localhost
'''

RETURN = r'''
rate_limit:
  description: The new rate limit, in connections per second, of the pool member.
  returned: changed
  type: int
  sample: 100
connection_limit:
  description: The new connection limit of the pool member.
  returned: changed
  type: int
  sample: 1000
description:
  description: The new description of pool member.
  returned: changed
  type: str
  sample: My pool member
ratio:
  description: The new pool member ratio weight.
  returned: changed
  type: int
  sample: 50
priority_group:
  description: The new priority group.
  returned: changed
  type: int
  sample: 3
fqdn_auto_populate:
  description: Whether FQDN auto population was set on the member or not.
  returned: changed
  type: bool
  sample: true
fqdn:
  description: The FQDN of the pool member.
  returned: changed
  type: str
  sample: foo.bar.com
address:
  description: The address of the pool member.
  returned: changed
  type: str
  sample: 1.2.3.4
monitors:
  description: The new list of monitors for the resource.
  returned: changed
  type: list
  sample: ['/Common/monitor1', '/Common/monitor2']
replace_all_with:
  description: Purges all non-aggregate pool members from device.
  returned: changed
  type: bool
  sample: true
'''

import os
import re

from copy import deepcopy
from datetime import datetime

from ansible.module_utils.urls import urlparse
from ansible.module_utils.basic import (
    AnsibleModule, env_fallback
)
from ansible.module_utils.six import iteritems

from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import remove_default_spec

from ..module_utils.bigip import F5RestClient
from ..module_utils.common import (
    F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, fq_name, flatten_boolean, is_valid_hostname
)
from ..module_utils.ipaddress import is_valid_ip, validate_ip_v6_address
from ..module_utils.compare import cmp_str_with_none
from ..module_utils.icontrol import (
    TransactionContextManager, tmos_version
)
from ..module_utils.teem import send_teem


class Parameters(AnsibleF5Parameters):
    api_map = {
        'rateLimit': 'rate_limit',
        'connectionLimit': 'connection_limit',
        'priorityGroup': 'priority_group',
        'monitor': 'monitors',
        'inheritProfile': 'inherit_profile',
        'profiles': 'ip_encapsulation',
    }

    api_attributes = [
        'rateLimit',
        'connectionLimit',
        'description',
        'ratio',
        'priorityGroup',
        'address',
        'fqdn',
        'session',
        'state',
        'monitor',

        # These two settings are for IP Encapsulation
        'inheritProfile',
        'profiles',
    ]

    returnables = [
        'rate_limit',
        'connection_limit',
        'description',
        'ratio',
        'priority_group',
        'fqdn_auto_populate',
        'session',
        'fqdn',
        'address',
        'monitors',

        # IP Encapsulation related
        'inherit_profile',
        'ip_encapsulation',
    ]

    updatables = [
        'rate_limit',
        'connection_limit',
        'description',
        'ratio',
        'priority_group',
        'fqdn_auto_populate',
        'state',
        'monitors',
        'inherit_profile',
        'ip_encapsulation',
    ]


class ModuleParameters(Parameters):
    @property
    def full_name(self):
        delimiter = ':'
        try:
            if validate_ip_v6_address(self.full_name_dict['name']):
                delimiter = '.'
        except TypeError:
            pass
        return '{0}{1}{2}'.format(self.full_name_dict['name'], delimiter, self.port)

    @property
    def full_name_dict(self):
        if self._values['name'] is None:
            if self._values['address'] and not self._values['fqdn']:
                name = self._values['address']
            else:
                name = self._values['fqdn']
        else:
            name = self._values['name']
        return dict(
            name=name,
            port=self.port
        )

    @property
    def node_name(self):
        return self.full_name_dict['name']

    @property
    def fqdn_name(self):
        return self._values['fqdn']

    @property
    def fqdn(self):
        result = {}
        if self.fqdn_auto_populate:
            result['autopopulate'] = 'enabled'
        else:
            result['autopopulate'] = 'disabled'
        if self._values['fqdn'] is None:
            return result
        if not is_valid_hostname(self._values['fqdn']):
            raise F5ModuleError(
                "The specified 'fqdn' value of: {0} is not a valid hostname.".format(self._values['fqdn'])
            )
        result['tmName'] = self._values['fqdn']
        return result

    @property
    def pool(self):
        return fq_name(self.want.partition, self._values['pool'])

    @property
    def port(self):
        if self._values['port'] is None:
            raise F5ModuleError(
                "Port value must be specified."
            )
        if 0 > int(self._values['port']) or int(self._values['port']) > 65535:
            raise F5ModuleError(
                "Valid ports must be in range 0 - 65535"
            )
        return int(self._values['port'])

    @property
    def address(self):
        if self._values['address'] is None:
            return None
        elif self._values['address'] == 'any6':
            return 'any6'
        address = self._values['address'].split('%')[0]
        if is_valid_ip(address):
            return self._values['address']
        raise F5ModuleError(
            "The specified 'address' value of: {0} is not a valid IP address.".format(address)
        )

    @property
    def state(self):
        if self._values['state'] == 'enabled':
            return 'present'
        return self._values['state']

    @property
    def monitors_list(self):
        if self._values['monitors'] is None:
            return []
        try:
            result = re.findall(r'/\w+/[^\s}]+', self._values['monitors'])
            result.sort()
            return result
        except Exception:
            return self._values['monitors']

    @property
    def monitors(self):
        if self._values['monitors'] is None:
            return None
        if len(self._values['monitors']) == 1 and self._values['monitors'][0] == '':
            return 'default'
        if len(self._values['monitors']) == 1 and self._values['monitors'][0] == 'none':
            return '/Common/none'
        monitors = [fq_name(self.partition, x) for x in self.monitors_list]
        if self.availability_requirement_type == 'at_least':
            if self.at_least > len(self.monitors_list):
                raise F5ModuleError(
                    "The 'at_least' value must not exceed the number of 'monitors'."
                )
            monitors = ' '.join(monitors)
            result = 'min {0} of {{ {1} }}'.format(self.at_least, monitors)
        else:
            result = ' and '.join(monitors).strip()
        return result

    @property
    def availability_requirement_type(self):
        if self._values['availability_requirements'] is None:
            return None
        return self._values['availability_requirements']['type']

    @property
    def at_least(self):
        return self._get_availability_value('at_least')

    @property
    def ip_encapsulation(self):
        if self._values['ip_encapsulation'] is None:
            return None
        if self._values['ip_encapsulation'] == 'inherit':
            return 'inherit'
        if self._values['ip_encapsulation'] in ['', 'none']:
            return ''
        return fq_name(self.partition, self._values['ip_encapsulation'])

    def _get_availability_value(self, type):
        if self._values['availability_requirements'] is None:
            return None
        if self._values['availability_requirements'][type] is None:
            return None
        return int(self._values['availability_requirements'][type])


class ApiParameters(Parameters):
    @property
    def ip_encapsulation(self):
        """Returns a simple name for the tunnel.

        The API stores the data like so

            "profiles": [
                {
                    "name": "gre",
                    "partition": "Common",
                    "nameReference": {
                        "link": "https://localhost/mgmt/tm/net/tunnels/gre/~Common~gre?ver=13.1.0.7"
                    }
                }
            ]

        This method returns that data as a simple profile name. For instance,

            /Common/gre

        This allows us to do comparisons of it in the Difference class and then,
        as needed, translate it back to the more complex form in the UsableChanges
        class.

        Returns:
            string: The simple form representation of the tunnel
        """
        if self._values['ip_encapsulation'] is None and self.inherit_profile == 'yes':
            return 'inherit'
        if self._values['ip_encapsulation'] is None and self.inherit_profile == 'no':
            return ''
        if self._values['ip_encapsulation'] is None:
            return None

        # There can be only one
        tunnel = self._values['ip_encapsulation'][0]

        return fq_name(tunnel['partition'], tunnel['name'])

    @property
    def inherit_profile(self):
        return flatten_boolean(self._values['inherit_profile'])

    @property
    def allow(self):
        if self._values['allow'] is None:
            return ''
        if self._values['allow'][0] == 'All':
            return 'all'
        allow = self._values['allow']
        result = list(set([str(x) for x in allow]))
        result = sorted(result)
        return result

    @property
    def rate_limit(self):
        if self._values['rate_limit'] is None:
            return None
        if self._values['rate_limit'] == 'disabled':
            return 0
        return int(self._values['rate_limit'])

    @property
    def state(self):
        if (self._values['state'] in ['user-up', 'unchecked', 'fqdn-up-no-addr', 'fqdn-up', 'fqdn-down']
           and self._values['session'] in ['user-enabled', 'monitor-enabled']):
            return 'present'
        elif self._values['state'] in ['down', 'up', 'checking'] and self._values['session'] == 'monitor-enabled':
            # monitor-enabled + checking:
            #   Monitor is checking to see state of pool member. For instance,
            #   whether it is up or down
            #
            # monitor-enabled + down:
            #   Monitor returned and determined that pool member is down.
            #
            # monitor-enabled + up
            #   Monitor returned and determined that pool member is up.
            return 'present'
        elif self._values['state'] in ['user-down'] and self._values['session'] in ['user-disabled']:
            return 'forced_offline'
        else:
            return 'disabled'

    @property
    def availability_requirement_type(self):
        if self._values['monitors'] is None:
            return None
        if 'min ' in self._values['monitors']:
            return 'at_least'
        else:
            return 'all'

    @property
    def monitors_list(self):
        if self._values['monitors'] is None:
            return []
        try:
            result = re.findall(r'/\w+/[^\s}]+', self._values['monitors'])
            result.sort()
            return result
        except Exception:
            return self._values['monitors']

    @property
    def monitors(self):
        if self._values['monitors'] is None:
            return None
        if self._values['monitors'] == 'default':
            return 'default'
        monitors = [fq_name(self.partition, x) for x in self.monitors_list]
        if self.availability_requirement_type == 'at_least':
            monitors = ' '.join(monitors)
            result = 'min {0} of {{ {1} }}'.format(self.at_least, monitors)
        else:
            result = ' and '.join(monitors).strip()

        return result

    @property
    def at_least(self):
        """Returns the 'at least' value from the monitor string.
        The monitor string for a Require monitor looks like this.
            min 1 of { /Common/gateway_icmp }
        This method parses out the first of the numeric values. This values represents
        the "at_least" value that can be updated in the module.
        Returns:
             int: The at_least value if found. None otherwise.
        """
        if self._values['monitors'] is None:
            return None
        pattern = r'min\s+(?P<least>\d+)\s+of\s+'
        matches = re.search(pattern, self._values['monitors'])
        if matches is None:
            return None
        return matches.group('least')

    @property
    def fqdn_auto_populate(self):
        if self._values['fqdn'] is None:
            return None
        if 'autopopulate' in self._values['fqdn']:
            if self._values['fqdn']['autopopulate'] == 'enabled':
                return True
            return False

    @property
    def fqdn(self):
        if self._values['fqdn'] is None:
            return None
        if 'tmName' in self._values['fqdn']:
            return self._values['fqdn']['tmName']


class NodeApiParameters(Parameters):
    pass


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):
    @property
    def monitors(self):
        monitor_string = self._values['monitors']
        if monitor_string is None:
            return None
        if '{' in monitor_string and '}' in monitor_string:
            tmp = monitor_string.strip('}').split('{')
            monitor = ''.join(tmp).rstrip()
            return monitor
        return monitor_string


class ReportableChanges(Changes):
    @property
    def ssl_cipher_suite(self):
        default = ':'.join(sorted(Parameters._ciphers.split(':')))
        if self._values['ssl_cipher_suite'] == default:
            return 'default'
        else:
            return self._values['ssl_cipher_suite']

    @property
    def fqdn_auto_populate(self):
        if self._values['fqdn'] is None:
            return None
        if 'autopopulate' in self._values['fqdn']:
            if self._values['fqdn']['autopopulate'] == 'enabled':
                return True
            return False

    @property
    def fqdn(self):
        if self._values['fqdn'] is None:
            return None
        if 'tmName' in self._values['fqdn']:
            return self._values['fqdn']['tmName']

    @property
    def state(self):
        if (self._values['state'] in ['user-up', 'unchecked', 'fqdn-up-no-addr', 'fqdn-up'] and
           self._values['session'] in ['user-enabled']):
            return 'present'
        elif self._values['state'] in ['down', 'up', 'checking'] and self._values['session'] == 'monitor-enabled':
            return 'present'
        elif self._values['state'] in ['user-down'] and self._values['session'] in ['user-disabled']:
            return 'forced_offline'
        else:
            return 'disabled'

    @property
    def monitors(self):
        if self._values['monitors'] is None:
            return []
        try:
            result = re.findall(r'/\w+/[^\s}]+', self._values['monitors'])
            result.sort()
            return result
        except Exception:
            return self._values['monitors']

    @property
    def availability_requirement_type(self):
        if self._values['monitors'] is None:
            return None
        if 'min ' in self._values['monitors']:
            return 'at_least'
        else:
            return 'all'

    @property
    def at_least(self):
        """Returns the 'at least' value from the monitor string.
        The monitor string for a Require monitor looks like this.
            min 1 of { /Common/gateway_icmp }
        This method parses out the first of the numeric values. This values represents
        the "at_least" value that can be updated in the module.
        Returns:
             int: The at_least value if found. None otherwise.
        """
        if self._values['monitors'] is None:
            return None
        pattern = r'min\s+(?P<least>\d+)\s+of\s+'
        matches = re.search(pattern, self._values['monitors'])
        if matches is None:
            return None
        return int(matches.group('least'))

    @property
    def availability_requirements(self):
        if self._values['monitors'] is None:
            return None
        result = dict()
        result['type'] = self.availability_requirement_type
        result['at_least'] = self.at_least
        return result


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 state(self):
        if self.want.state == self.have.state:
            return None
        if self.want.state == 'forced_offline':
            return {
                'state': 'user-down',
                'session': 'user-disabled'
            }
        elif self.want.state == 'disabled':
            return {
                'state': 'user-up',
                'session': 'user-disabled'
            }
        elif self.want.state in ['present', 'enabled']:
            return {
                'state': 'user-up',
                'session': 'user-enabled'
            }

    @property
    def fqdn_auto_populate(self):
        if self.want.fqdn_auto_populate is not None:
            if self.want.fqdn_auto_populate != self.have.fqdn_auto_populate:
                raise F5ModuleError(
                    "The fqdn_auto_populate cannot be changed once it has been set."
                )

    @property
    def monitors(self):
        if self.want.monitors is None:
            return None
        if self.want.monitors == 'default' and self.have.monitors == 'default':
            return None
        if self.want.monitors == 'default' and self.have.monitors is None:
            return None
        if self.want.monitors == 'default' and len(self.have.monitors) > 0:
            return 'default'
        # this is necessary as in v12 there is a bug where returned value has a space at the end
        if self.want.monitors == '/Common/none' and self.have.monitors in ['/Common/none', '/Common/none ']:
            return None
        if self.have.monitors is None:
            return self.want.monitors
        if self.have.monitors != self.want.monitors:
            return self.want.monitors

    @property
    def ip_encapsulation(self):
        result = cmp_str_with_none(self.want.ip_encapsulation, self.have.ip_encapsulation)
        if result is None:
            return None
        if result == 'inherit':
            return dict(
                inherit_profile='enabled',
                ip_encapsulation=[]
            )
        elif result in ['', 'none']:
            return dict(
                inherit_profile='disabled',
                ip_encapsulation=[]
            )
        else:
            return dict(
                inherit_profile='disabled',
                ip_encapsulation=[
                    dict(
                        name=os.path.basename(result).strip('/'),
                        partition=os.path.dirname(result)
                    )
                ]
            )


class ModuleManager(object):
    def __init__(self, *args, **kwargs):
        self.module = kwargs.get('module', None)
        self.client = F5RestClient(**self.module.params)
        self.want = None
        self.have = None
        self.changes = None
        self.replace_all_with = False
        self.purge_links = list()
        self.on_device = None

    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 _announce_deprecations(self, result):
        warnings = result.pop('__warnings', [])
        for warning in warnings:
            self.module.deprecate(
                msg=warning['msg'],
                version=warning['version']
            )

    def exec_module(self):
        start = datetime.now().isoformat()
        version = tmos_version(self.client)
        wants = None
        if self.module.params['replace_all_with']:
            self.replace_all_with = True

        if self.module.params['aggregate']:
            wants = self.merge_defaults_for_aggregate(self.module.params)

        result = dict()
        changed = False
        if self.replace_all_with and self.purge_links:
            self.purge()
            changed = True

        if self.module.params['aggregate']:
            result['aggregate'] = list()
            for want in wants:
                output = self.execute(want)
                if output['changed']:
                    changed = output['changed']
                result['aggregate'].append(output)
        else:
            output = self.execute(self.module.params)
            if output['changed']:
                changed = output['changed']
            result.update(output)
        if changed:
            result['changed'] = True
        send_teem(start, self.client, self.module, version)
        return result

    def merge_defaults_for_aggregate(self, params):
        defaults = deepcopy(params)
        aggregate = defaults.pop('aggregate')

        for i, j in enumerate(aggregate):
            for k, v in iteritems(defaults):
                if k != 'replace_all_with':
                    if j.get(k, None) is None and v is not None:
                        aggregate[i][k] = v

        if self.replace_all_with:
            self.compare_aggregate_names(aggregate)

        return aggregate

    def _filter_ephemerals(self):
        on_device = self._read_purge_collection()
        if not on_device:
            self.on_device = []
            return
        self.on_device = [member for member in on_device if member['ephemeral'] != "true"]

    def compare_fqdns(self, items):
        if any('fqdn' in item for item in items):
            aggregates = [item['fqdn'] for item in items if 'fqdn' in item and item['fqdn']]
            collection = [member['fqdn']['tmName'] for member in self.on_device if 'tmName' in member['fqdn']]

            diff = set(collection) - set(aggregates)

            if diff:
                fqdns = [
                    member['selfLink'] for member in self.on_device
                    if 'tmName' in member['fqdn'] and member['fqdn']['tmName'] in diff
                ]
                self.purge_links.extend(fqdns)
                return True
            return False
        return False

    def _join_address_port(self, item):
        if 'port' not in item:
            raise F5ModuleError(
                "Aggregates must be provided with both address and port."
            )
        delimiter = ':'
        # If user provides us a name for the aggregate element, we use its name with port, this is
        # how F5 BIG-IP behaves as well.
        if 'name' in item and item['name']:
            return '{0}{1}{2}'.format(item['name'], delimiter, item['port'])
        try:
            if validate_ip_v6_address(item['address']):
                delimiter = '.'
        except TypeError:
            pass
        return '{0}{1}{2}'.format(item['address'], delimiter, item['port'])

    def compare_addresses(self, items):
        if any('address' in item for item in items):
            aggregates = [self._join_address_port(item) for item in items if 'address' in item and item['address']]
            collection = [member['name'] for member in self.on_device]
            diff = set(collection) - set(aggregates)
            if diff:
                addresses = [item['selfLink'] for item in self.on_device if item['name'] in diff]
                self.purge_links.extend(addresses)
                return True
            return False
        return False

    def compare_aggregate_names(self, items):
        self._filter_ephemerals()
        if not self.on_device:
            return False
        fqdns = self.compare_fqdns(items)
        addresses = self.compare_addresses(items)

        if self.purge_links:
            if fqdns:
                if not addresses:
                    self.purge_links.extend([item['selfLink'] for item in self.on_device if 'tmName' not in item['fqdn']])

    def execute(self, params=None):
        self.want = ModuleParameters(params=params)
        self.have = ApiParameters()
        self.changes = UsableChanges()

        changed = False
        result = dict()
        state = params['state']
        if state in ['present', 'enabled', 'disabled', 'forced_offline']:
            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 present(self):
        if self.exists():
            return self.update()
        else:
            return self.create()

    def absent(self):
        if self.exists():
            return self.remove()
        elif not self.want.preserve_node and self.node_exists():
            return self.remove_node_from_device()
        return False

    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 should_update(self):
        result = self._update_changed_options()
        if result:
            return True
        return False

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

    def purge(self):
        if self.module.check_mode:
            return True
        if not self.pool_exist():
            raise F5ModuleError('The specified pool does not exist')
        self.purge_from_device()
        return True

    def create(self):
        if self.want.reuse_nodes:
            self._update_address_with_existing_nodes()

        if self.want.name and not any(x for x in [self.want.address, self.want.fqdn_name]):
            self._set_host_by_name()

        if self.want.ip_encapsulation == '':
            self.changes.update({'inherit_profile': 'enabled'})
            self.changes.update({'profiles': []})
        elif self.want.ip_encapsulation:
            # Read the current list of tunnels so that IP encapsulation
            # checking can take place.
            tunnels_gre = self.read_current_tunnels_from_device('gre')
            tunnels_ipip = self.read_current_tunnels_from_device('ipip')
            tunnels = tunnels_gre + tunnels_ipip
            if self.want.ip_encapsulation not in tunnels:
                raise F5ModuleError(
                    "The specified 'ip_encapsulation' tunnel was not found on the system."
                )
            self.changes.update({'inherit_profile': 'disabled'})

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

    def exists(self):
        if not self.pool_exist():
            raise F5ModuleError('The specified pool does not exist')

        uri = "https://{0}:{1}/mgmt/tm/ltm/pool/{2}/members/{3}".format(
            self.client.provider['server'],
            self.client.provider['server_port'],
            transform_name(name=fq_name(self.want.partition, self.want.pool)),
            transform_name(self.want.partition, self.want.full_name)
        )
        resp = self.client.api.get(uri)
        try:
            response = resp.json()
        except ValueError as ex:
            raise F5ModuleError(str(ex))
        if resp.status == 404 or 'code' in response and response['code'] == 404:
            return False
        if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]:
            return True

        errors = [401, 403, 409, 500, 501, 502, 503, 504]

        if resp.status in errors or 'code' in response and response['code'] in errors:
            if 'message' in response:
                raise F5ModuleError(response['message'])
            else:
                raise F5ModuleError(resp.content)

    def pool_exist(self):
        if self.module.check_mode:
            return True
        if self.replace_all_with:
            pool_name = transform_name(name=fq_name(self.module.params['partition'], self.module.params['pool']))
        else:
            pool_name = transform_name(name=fq_name(self.want.partition, self.want.pool))

        uri = "https://{0}:{1}/mgmt/tm/ltm/pool/{2}".format(
            self.client.provider['server'],
            self.client.provider['server_port'],
            pool_name

        )
        resp = self.client.api.get(uri)
        try:
            response = resp.json()
        except ValueError as ex:
            raise F5ModuleError(str(ex))

        if resp.status == 404 or 'code' in response and response['code'] == 404:
            return False
        if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]:
            return True

        errors = [401, 403, 409, 500, 501, 502, 503, 504]

        if resp.status in errors or 'code' in response and response['code'] in errors:
            if 'message' in response:
                raise F5ModuleError(response['message'])
            else:
                raise F5ModuleError(resp.content)

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

        if resp.status == 404 or 'code' in response and response['code'] == 404:
            return False
        if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]:
            return True

        errors = [401, 403, 409, 500, 501, 502, 503, 504]

        if resp.status in errors or 'code' in response and response['code'] in errors:
            if 'message' in response:
                raise F5ModuleError(response['message'])
            else:
                raise F5ModuleError(resp.content)

    def _set_host_by_name(self):
        if is_valid_ip(self.want.name):
            self.want.update({
                'fqdn': None,
                'address': self.want.name
            })
        else:
            if not is_valid_hostname(self.want.name):
                raise F5ModuleError(
                    "'name' is neither a valid IP address or FQDN name."
                )
            self.want.update({
                'fqdn': self.want.name,
                'address': None
            })

    def _update_api_state_attributes(self):
        if self.want.state == 'forced_offline':
            self.want.update({
                'state': 'user-down',
                'session': 'user-disabled',
            })
        elif self.want.state == 'disabled':
            self.want.update({
                'state': 'user-up',
                'session': 'user-disabled',
            })
        elif self.want.state in ['present', 'enabled']:
            self.want.update({
                'state': 'user-up',
                'session': 'user-enabled',
            })

    def _update_address_with_existing_nodes(self):
        try:
            have = self.read_current_node_from_device(self.want.node_name)
            if self.want.fqdn_auto_populate and self.want.reuse_nodes:
                self.module.warn(
                    "'fqdn_auto_populate' is discarded in favor of the re-used node's auto-populate setting."
                )
            self.want.update({
                'fqdn_auto_populate': True if have.fqdn['autopopulate'] == 'enabled' else False
            })
            if 'tmName' in have.fqdn:
                self.want.update({
                    'fqdn': have.fqdn['tmName'],
                    'address': 'any6'
                })
            else:
                self.want.update({
                    'address': have.address
                })
        except Exception:
            return None

    def _read_purge_collection(self):
        uri = "https://{0}:{1}/mgmt/tm/ltm/pool/{2}/members".format(
            self.client.provider['server'],
            self.client.provider['server_port'],
            transform_name(name=fq_name(self.module.params['partition'], self.module.params['pool']))
        )

        query = '?$select=name,selfLink,fqdn,address,ephemeral'
        resp = self.client.api.get(uri + query)

        try:
            response = resp.json()
        except ValueError as ex:
            raise F5ModuleError(str(ex))

        if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]:
            if 'items' in response:
                return response['items']
            return []
        raise F5ModuleError(resp.content)

    def create_on_device(self):
        params = self.changes.api_params()
        params['name'] = self.want.full_name
        params['partition'] = self.want.partition
        uri = "https://{0}:{1}/mgmt/tm/ltm/pool/{2}/members".format(
            self.client.provider['server'],
            self.client.provider['server_port'],
            transform_name(name=fq_name(self.want.partition, self.want.pool)),

        )
        resp = self.client.api.post(uri, json=params)
        try:
            response = resp.json()
        except ValueError as ex:
            raise F5ModuleError(str(ex))

        if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]:
            return True
        raise F5ModuleError(resp.content)

    def update_on_device(self):
        params = self.changes.api_params()
        uri = "https://{0}:{1}/mgmt/tm/ltm/pool/{2}/members/{3}".format(
            self.client.provider['server'],
            self.client.provider['server_port'],
            transform_name(name=fq_name(self.want.partition, self.want.pool)),
            transform_name(self.want.partition, self.want.full_name)

        )
        resp = self.client.api.patch(uri, json=params)
        try:
            response = resp.json()
        except ValueError as ex:
            raise F5ModuleError(str(ex))

        if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]:
            return True
        raise F5ModuleError(resp.content)

    def remove_from_device(self):
        uri = "https://{0}:{1}/mgmt/tm/ltm/pool/{2}/members/{3}".format(
            self.client.provider['server'],
            self.client.provider['server_port'],
            transform_name(name=fq_name(self.want.partition, self.want.pool)),
            transform_name(self.want.partition, self.want.full_name)

        )
        response = self.client.api.delete(uri)
        if response.status == 200:
            return True
        raise F5ModuleError(response.content)

    def remove_node_from_device(self):
        uri = "https://{0}:{1}/mgmt/tm/ltm/node/{2}".format(
            self.client.provider['server'],
            self.client.provider['server_port'],
            transform_name(self.want.partition, self.want.node_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/pool/{2}/members/{3}".format(
            self.client.provider['server'],
            self.client.provider['server_port'],
            transform_name(name=fq_name(self.want.partition, self.want.pool)),
            transform_name(self.want.partition, self.want.full_name)

        )
        resp = self.client.api.get(uri)
        try:
            response = resp.json()
        except ValueError as ex:
            raise F5ModuleError(str(ex))

        if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]:
            raise F5ModuleError(resp.content)

        # Read the current list of tunnels so that IP encapsulation
        # checking can take place.
        tunnels_gre = self.read_current_tunnels_from_device('gre')
        tunnels_ipip = self.read_current_tunnels_from_device('ipip')
        response['tunnels'] = tunnels_gre + tunnels_ipip

        return ApiParameters(params=response)

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

        if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]:
            return NodeApiParameters(params=response)
        raise F5ModuleError(resp.content)

    def read_current_tunnels_from_device(self, tunnel_type):
        uri = "https://{0}:{1}/mgmt/tm/net/tunnels/{2}".format(
            self.client.provider['server'],
            self.client.provider['server_port'],
            tunnel_type
        )
        resp = self.client.api.get(uri)
        try:
            response = resp.json()
        except ValueError as ex:
            raise F5ModuleError(str(ex))

        if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]:
            if 'items' not in response:
                return []
            return [x['fullPath'] for x in response['items']]
        raise F5ModuleError(resp.content)

    def _prepare_links(self, collection):
        # this is to ensure no duplicates are in the provided collection
        no_dupes = list(set(collection))
        links = list()
        purge_paths = [urlparse(link).path for link in no_dupes]

        for path in purge_paths:
            link = "https://{0}:{1}{2}".format(
                self.client.provider['server'],
                self.client.provider['server_port'],
                path
            )
            links.append(link)
        return links

    def purge_from_device(self):
        links = self._prepare_links(self.purge_links)

        with TransactionContextManager(self.client) as transact:
            for link in links:
                resp = transact.api.delete(link)

                try:
                    response = resp.json()
                except ValueError as ex:
                    raise F5ModuleError(str(ex))

                if resp.status not in [200, 201] or 'code' in response and response['code'] not in [200, 201]:
                    raise F5ModuleError(resp.content)
        return True


class ArgumentSpec(object):
    def __init__(self):
        self.supports_check_mode = True
        element_spec = dict(
            address=dict(aliases=['host', 'ip']),
            fqdn=dict(
                aliases=['hostname']
            ),
            name=dict(),
            port=dict(type='int'),
            connection_limit=dict(type='int'),
            description=dict(),
            rate_limit=dict(type='int'),
            ratio=dict(type='int'),
            preserve_node=dict(type='bool'),
            priority_group=dict(type='int'),
            state=dict(
                default='present',
                choices=['absent', 'present', 'enabled', 'disabled', 'forced_offline']
            ),
            fqdn_auto_populate=dict(type='bool'),
            reuse_nodes=dict(type='bool', default=True),
            availability_requirements=dict(
                type='dict',
                options=dict(
                    type=dict(
                        choices=['all', 'at_least'],
                        required=True
                    ),
                    at_least=dict(type='int'),
                ),
                required_if=[
                    ['type', 'at_least', ['at_least']],
                ]
            ),
            monitors=dict(
                type='list',
                elements='str',
            ),
            ip_encapsulation=dict(),
            partition=dict(
                default='Common',
                fallback=(env_fallback, ['F5_PARTITION'])
            ),
        )
        aggregate_spec = deepcopy(element_spec)

        # remove default in aggregate spec, to handle common arguments
        remove_default_spec(aggregate_spec)
        aggregate_spec["state"].update(default="present")
        aggregate_spec["partition"].update(default="Common")
        aggregate_spec["reuse_nodes"].update(default=True)

        self.argument_spec = dict(
            aggregate=dict(
                type='list',
                elements='dict',
                options=aggregate_spec,
                aliases=['members'],
                mutually_exclusive=[
                    ['address', 'fqdn']
                ],
                required_one_of=[
                    ['address', 'fqdn']
                ],
            ),
            replace_all_with=dict(
                type='bool',
                aliases=['purge'],
                default='no'
            ),
            pool=dict(required=True),
            partition=dict(
                default='Common',
                fallback=(env_fallback, ['F5_PARTITION'])
            ),
        )

        self.argument_spec.update(element_spec)
        self.argument_spec.update(f5_argument_spec)

        self.mutually_exclusive = [
            ['address', 'aggregate'],
            ['fqdn', 'aggregate']
        ]
        self.required_one_of = [
            ['address', 'fqdn', 'aggregate'],
        ]


def main():
    spec = ArgumentSpec()

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

    try:
        mm = ModuleManager(module=module)
        results = mm.exec_module()
        module.exit_json(**results)
    except F5ModuleError as ex:
        module.fail_json(msg=str(ex))


if __name__ == '__main__':
    main()
