From 8085c38e054b543fc233efae3f3391c13ecffbe0 Mon Sep 17 00:00:00 2001
From: Tim Rupp <caphrim007@gmail.com>
Date: Fri, 20 Oct 2017 18:05:45 -0700
Subject: [PATCH] Refactors the bigip_gtm_facts module (#31917)

Includes pep fixes and inlining code with current conventions
---
 .../modules/network/f5/bigip_gtm_facts.py     | 1206 ++++++++++++-----
 test/sanity/pep8/legacy-files.txt             |    1 -
 .../fixtures/load_gtm_pool_a_collection.json  |   44 +
 .../load_gtm_pool_a_example_stats.json        |   48 +
 .../network/f5/test_bigip_gtm_facts.py        |  157 +++
 5 files changed, 1088 insertions(+), 368 deletions(-)
 create mode 100644 test/units/modules/network/f5/fixtures/load_gtm_pool_a_collection.json
 create mode 100644 test/units/modules/network/f5/fixtures/load_gtm_pool_a_example_stats.json
 create mode 100644 test/units/modules/network/f5/test_bigip_gtm_facts.py

diff --git a/lib/ansible/modules/network/f5/bigip_gtm_facts.py b/lib/ansible/modules/network/f5/bigip_gtm_facts.py
index 5103aa73817..8551203c903 100644
--- a/lib/ansible/modules/network/f5/bigip_gtm_facts.py
+++ b/lib/ansible/modules/network/f5/bigip_gtm_facts.py
@@ -4,22 +4,26 @@
 # Copyright (c) 2017 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': 'community'}
 
-DOCUMENTATION = '''
+DOCUMENTATION = r'''
 ---
 module: bigip_gtm_facts
-short_description: Collect facts from F5 BIG-IP GTM devices.
+short_description: Collect facts from F5 BIG-IP GTM devices
 description:
   - Collect facts from F5 BIG-IP GTM devices.
 version_added: "2.3"
 options:
   include:
     description:
-      - Fact category to collect
-    required: true
+      - Fact category to collect.
+    required: True
     choices:
       - pool
       - wide_ip
@@ -29,11 +33,9 @@ options:
       - Perform regex filter of response. Filtering is done on the name of
         the resource. Valid filters are anything that can be provided to
         Python's C(re) module.
-    required: false
-    default: None
 notes:
-   - Requires the f5-sdk Python package on the host. This is as easy as
-     pip install f5-sdk
+  - Requires the f5-sdk Python package on the host. This is as easy as
+    pip install f5-sdk
 extends_documentation_fragment: f5
 requirements:
   - f5-sdk
@@ -41,156 +43,162 @@ author:
   - Tim Rupp (@caphrim007)
 '''
 
-EXAMPLES = '''
+EXAMPLES = r'''
 - name: Get pool facts
   bigip_gtm_facts:
-      server: "lb.mydomain.com"
-      user: "admin"
-      password: "secret"
-      include: "pool"
-      filter: "my_pool"
+    server: lb.mydomain.com
+    user: admin
+    password: secret
+    include: pool
+    filter: my_pool
   delegate_to: localhost
 '''
 
-RETURN = '''
+RETURN = r'''
 wide_ip:
-    description:
-        Contains the lb method for the wide ip and the pools
-        that are within the wide ip.
-    returned: changed
-    type: dict
-    sample:
-        wide_ip:
-            - enabled: "True"
-              failure_rcode: "noerror"
-              failure_rcode_response: "disabled"
-              failure_rcode_ttl: "0"
-              full_path: "/Common/foo.ok.com"
-              last_resort_pool: ""
-              minimal_response: "enabled"
-              name: "foo.ok.com"
-              partition: "Common"
-              persist_cidr_ipv4: "32"
-              persist_cidr_ipv6: "128"
-              persistence: "disabled"
-              pool_lb_mode: "round-robin"
-              pools:
-                  - name: "d3qw"
-                    order: "0"
-                    partition: "Common"
-                    ratio: "1"
-              ttl_persistence: "3600"
-              type: "naptr"
+  description:
+    Contains the lb method for the wide ip and the pools that are within the wide ip.
+  returned: changed
+  type: list
+  sample:
+    wide_ip:
+      - enabled: True
+        failure_rcode: noerror
+        failure_rcode_response: disabled
+        failure_rcode_ttl: 0
+        full_path: /Common/foo.ok.com
+        last_resort_pool: ""
+        minimal_response: enabled
+        name: foo.ok.com
+        partition: Common
+        persist_cidr_ipv4: 32
+        persist_cidr_ipv6: 128
+        persistence: disabled
+        pool_lb_mode: round-robin
+        pools:
+          - name: d3qw
+            order: 0
+            partition: Common
+            ratio: 1
+        ttl_persistence: 3600
+        type: naptr
 pool:
-    description: Contains the pool object status and enabled status.
-    returned: changed
-    type: dict
-    sample:
-        pool:
-            - alternate_mode: "round-robin"
-              dynamic_ratio: "disabled"
-              enabled: "True"
-              fallback_mode: "return-to-dns"
-              full_path: "/Common/d3qw"
-              load_balancing_mode: "round-robin"
-              manual_resume: "disabled"
-              max_answers_returned: "1"
-              members:
-                  - disabled: "True"
-                    flags: "a"
-                    full_path: "ok3.com"
-                    member_order: "0"
-                    name: "ok3.com"
-                    order: "10"
-                    preference: "10"
-                    ratio: "1"
-                    service: "80"
-              name: "d3qw"
-              partition: "Common"
-              qos_hit_ratio: "5"
-              qos_hops: "0"
-              qos_kilobytes_second: "3"
-              qos_lcs: "30"
-              qos_packet_rate: "1"
-              qos_rtt: "50"
-              qos_topology: "0"
-              qos_vs_capacity: "0"
-              qos_vs_score: "0"
-              ttl: "30"
-              type: "naptr"
-              verify_member_availability: "disabled"
+  description: Contains the pool object status and enabled status.
+  returned: changed
+  type: list
+  sample:
+    pool:
+      - alternate_mode: round-robin
+        dynamic_ratio: disabled
+        enabled: True
+        fallback_mode: return-to-dns
+        full_path: /Common/d3qw
+        load_balancing_mode: round-robin
+        manual_resume: disabled
+        max_answers_returned: 1
+        members:
+          - disabled: True
+            flags: a
+            full_path: ok3.com
+            member_order: 0
+            name: ok3.com
+            order: 10
+            preference: 10
+            ratio: 1
+            service: 80
+        name: d3qw
+        partition: Common
+        qos_hit_ratio: 5
+        qos_hops: 0
+        qos_kilobytes_second: 3
+        qos_lcs: 30
+        qos_packet_rate: 1
+        qos_rtt: 50
+        qos_topology: 0
+        qos_vs_capacity: 0
+        qos_vs_score: 0
+        availability_state: offline
+        enabled_state: disabled
+        ttl: 30
+        type: naptr
+        verify_member_availability: disabled
 virtual_server:
-    description:
-        Contains the virtual server enabled and availability
-        status, and address
-    returned: changed
-    type: dict
-    sample:
-        virtual_server:
-            - addresses:
-                  - device_name: "/Common/qweqwe"
-                    name: "10.10.10.10"
-                    translation: "none"
-              datacenter: "/Common/xfxgh"
-              enabled: "True"
-              expose_route_domains: "no"
-              full_path: "/Common/qweqwe"
-              iq_allow_path: "yes"
-              iq_allow_service_check: "yes"
-              iq_allow_snmp: "yes"
-              limit_cpu_usage: "0"
-              limit_cpu_usage_status: "disabled"
-              limit_max_bps: "0"
-              limit_max_bps_status: "disabled"
-              limit_max_connections: "0"
-              limit_max_connections_status: "disabled"
-              limit_max_pps: "0"
-              limit_max_pps_status: "disabled"
-              limit_mem_avail: "0"
-              limit_mem_avail_status: "disabled"
-              link_discovery: "disabled"
-              monitor: "/Common/bigip "
-              name: "qweqwe"
-              partition: "Common"
-              product: "single-bigip"
-              virtual_server_discovery: "disabled"
-              virtual_servers:
-                  - destination: "10.10.10.10:0"
-                    enabled: "True"
-                    full_path: "jsdfhsd"
-                    limit_max_bps: "0"
-                    limit_max_bps_status: "disabled"
-                    limit_max_connections: "0"
-                    limit_max_connections_status: "disabled"
-                    limit_max_pps: "0"
-                    limit_max_pps_status: "disabled"
-                    name: "jsdfhsd"
-                    translation_address: "none"
-                    translation_port: "0"
+  description:
+    Contains the virtual server enabled and availability status, and address.
+  returned: changed
+  type: list
+  sample:
+    virtual_server:
+      - addresses:
+          - device_name: /Common/qweqwe
+            name: 10.10.10.10
+            translation: none
+        datacenter: /Common/xfxgh
+        enabled: True
+        expose_route_domains: no
+        full_path: /Common/qweqwe
+        iq_allow_path: yes
+        iq_allow_service_check: yes
+        iq_allow_snmp: yes
+        limit_cpu_usage: 0
+        limit_cpu_usage_status: disabled
+        limit_max_bps: 0
+        limit_max_bps_status: disabled
+        limit_max_connections: 0
+        limit_max_connections_status: disabled
+        limit_max_pps: 0
+        limit_max_pps_status: disabled
+        limit_mem_avail: 0
+        limit_mem_avail_status: disabled
+        link_discovery: disabled
+        monitor: /Common/bigip
+        name: qweqwe
+        partition: Common
+        product: single-bigip
+        virtual_server_discovery: disabled
+        virtual_servers:
+          - destination: 10.10.10.10:0
+            enabled: True
+            full_path: jsdfhsd
+            limit_max_bps: 0
+            limit_max_bps_status: disabled
+            limit_max_connections: 0
+            limit_max_connections_status: disabled
+            limit_max_pps: 0
+            limit_max_pps_status: disabled
+            name: jsdfhsd
+            translation_address: none
+            translation_port: 0
 '''
 
-try:
-    from distutils.version import LooseVersion
-    from f5.bigip.contexts import TransactionContextManager
-    from f5.bigip import ManagementRoot
-    from icontrol.session import iControlUnexpectedHTTPError
-
-    HAS_F5SDK = True
-except ImportError:
-    HAS_F5SDK = False
-
 import re
 
+try:
+    import json
+except ImportError:
+    import simplejson as json
 
-class BigIpGtmFactsCommon(object):
-    def __init__(self):
-        self.api = None
-        self.attributes_to_remove = [
-            'kind', 'generation', 'selfLink', '_meta_data',
-            'membersReference', 'datacenterReference',
-            'virtualServersReference', 'nameReference'
-        ]
-        self.gtm_types = dict(
+from ansible.module_utils.f5_utils import AnsibleF5Client
+from ansible.module_utils.f5_utils import AnsibleF5Parameters
+from ansible.module_utils.f5_utils import HAS_F5SDK
+from ansible.module_utils.f5_utils import F5ModuleError
+from ansible.module_utils.parsing.convert_bool import BOOLEANS_TRUE
+from ansible.module_utils.parsing.convert_bool import BOOLEANS_FALSE
+from ansible.module_utils.six import iteritems
+from collections import defaultdict
+from distutils.version import LooseVersion
+
+try:
+    from f5.utils.responses.handlers import Stats
+    from ansible.module_utils.f5_utils import iControlUnexpectedHTTPError
+except ImportError:
+    HAS_F5SDK = False
+
+
+class BaseManager(object):
+    def __init__(self, client):
+        self.client = client
+        self.types = dict(
             a_s='a',
             aaaas='aaaa',
             cnames='cname',
@@ -198,30 +206,23 @@ class BigIpGtmFactsCommon(object):
             naptrs='naptr',
             srvs='srv'
         )
-        self.request_params = dict(
-            params='expandSubcollections=true'
-        )
 
-    def is_version_less_than_12(self):
-        version = self.api.tmos_version
-        if LooseVersion(version) < LooseVersion('12.0.0'):
+    def exec_module(self):
+        result = self.read_current_from_device()
+        return result
+
+    def filter_matches_name(self, name):
+        if self.want.filter is None:
+            return True
+        matches = re.match(self.want.filter, str(name))
+        if matches:
             return True
         else:
             return False
 
-    def format_string_facts(self, parameters):
-        result = dict()
-        for attribute in self.attributes_to_remove:
-            parameters.pop(attribute, None)
-        for key, val in parameters.items():
-            result[key] = str(val)
-        return result
-
-    def filter_matches_name(self, name):
-        if not self.params['filter']:
-            return True
-        matches = re.match(self.params['filter'], str(name))
-        if matches:
+    def version_is_less_than_12(self):
+        version = self.client.api.tmos_version
+        if LooseVersion(version) < LooseVersion('12.0.0'):
             return True
         else:
             return False
@@ -235,247 +236,718 @@ class BigIpGtmFactsCommon(object):
             results.append(facts)
         return results
 
-    def connect_to_bigip(self, **kwargs):
-        return ManagementRoot(kwargs['server'],
-                              kwargs['user'],
-                              kwargs['password'],
-                              port=kwargs['server_port'])
+    def read_stats_from_device(self, resource):
+        stats = Stats(resource.stats.load())
+        return stats.stat
 
 
-class BigIpGtmFactsPools(BigIpGtmFactsCommon):
-    def __init__(self, *args, **kwargs):
-        super(BigIpGtmFactsPools, self).__init__()
-        self.params = kwargs
+class UntypedManager(BaseManager):
+    def exec_module(self):
+        results = []
+        facts = self.read_facts()
+        for item in facts:
+            filtered = [(k, v) for k, v in iteritems(item) if self.filter_matches_name(k)]
+            if filtered:
+                results.append(dict(filtered))
+        return results
 
-    def get_facts(self):
-        self.api = self.connect_to_bigip(**self.params)
-        return self.get_facts_from_device()
 
-    def get_facts_from_device(self):
+class TypedManager(BaseManager):
+    def exec_module(self):
+        results = []
+        for collection, type in iteritems(self.types):
+            facts = self.read_facts(collection)
+            if not facts:
+                continue
+            for x in facts:
+                x.update({'type': type})
+            for item in facts:
+                attrs = item.to_return()
+                filtered = [(k, v) for k, v in iteritems(attrs) if self.filter_matches_name(k)]
+                if filtered:
+                    results.append(dict(filtered))
+        return results
+
+
+class Parameters(AnsibleF5Parameters):
+    def __init__(self, params=None):
+        super(Parameters, self).__init__(params)
+        self._values['__warnings'] = []
+
+    @property
+    def include(self):
+        requested = self._values['include']
+        valid = ['pool', 'wide_ip', 'virtual_server', 'server', 'all']
+
+        if any(x for x in requested if x not in valid):
+            raise F5ModuleError(
+                "The valid 'include' choices are {0}".format(', '.join(valid))
+            )
+        if any(x for x in requested if x == 'virtual_server'):
+            self._values['__warnings'].append(
+                dict(
+                    msg="The 'virtual_server' param is deprecated. Use 'server' instead",
+                    version='2.5'
+                )
+            )
+
+        if 'all' in requested:
+            return ['all']
+        else:
+            return requested
+
+
+class BaseParameters(AnsibleF5Parameters):
+    def __init__(self, params=None):
+        self._values = defaultdict(lambda: None)
+        if params:
+            self.update(params=params)
+        self._values['__warnings'] = []
+
+    def update(self, params=None):
+        if params:
+            for k, v in iteritems(params):
+                if self.api_map is not None and k in self.api_map:
+                    map_key = self.api_map[k]
+                else:
+                    map_key = k
+
+                # Handle weird API parameters like `dns.proxy.__iter__` by
+                # using a map provided by the module developer
+                class_attr = getattr(type(self), map_key, None)
+                if isinstance(class_attr, property):
+                    # There is a mapped value for the api_map key
+                    if class_attr.fset is None:
+                        # If the mapped value does not have an associated setter
+                        self._values[map_key] = v
+                    else:
+                        # The mapped value has a setter
+                        setattr(self, map_key, v)
+                else:
+                    # If the mapped value is not a @property
+                    self._values[map_key] = v
+
+    @property
+    def enabled(self):
+        if self._values['enabled'] is None:
+            return None
+        elif self._values['enabled'] in BOOLEANS_TRUE:
+            return True
+        else:
+            return False
+
+    @property
+    def disabled(self):
+        if self._values['disabled'] is None:
+            return None
+        elif self._values['disabled'] in BOOLEANS_TRUE:
+            return True
+        else:
+            return False
+
+    def _remove_internal_keywords(self, resource):
+        del resource['kind']
+        del resource['generation']
+        del resource['selfLink']
+
+    def to_return(self):
+        result = {}
+        for returnable in self.returnables:
+            result[returnable] = getattr(self, returnable)
+        result = self._filter_params(result)
+        return result
+
+
+class PoolParameters(BaseParameters):
+    api_map = {
+        'alternateMode': 'alternate_mode',
+        'dynamicRatio': 'dynamic_ratio',
+        'fallbackMode': 'fallback_mode',
+        'fullPath': 'full_path',
+        'loadBalancingMode': 'load_balancing_mode',
+        'manualResume': 'manual_resume',
+        'maxAnswersReturned': 'max_answers_returned',
+        'qosHitRatio': 'qos_hit_ratio',
+        'qosHops': 'qos_hops',
+        'qosKilobytesSecond': 'qos_kilobytes_second',
+        'qosLcs': 'qos_lcs',
+        'qosPacketRate': 'qos_packet_rate',
+        'qosRtt': 'qos_rtt',
+        'qosTopology': 'qos_topology',
+        'qosVsCapacity': 'qos_vs_capacity',
+        'qosVsScore': 'qos_vs_score',
+        'verifyMemberAvailability': 'verify_member_availability',
+        'membersReference': 'members'
+    }
+
+    returnables = [
+        'alternate_mode', 'dynamic_ratio', 'enabled', 'disabled', 'fallback_mode',
+        'load_balancing_mode', 'manual_resume', 'max_answers_returned', 'members',
+        'name', 'partition', 'qos_hit_ratio', 'qos_hops', 'qos_kilobytes_second',
+        'qos_lcs', 'qos_packet_rate', 'qos_rtt', 'qos_topology', 'qos_vs_capacity',
+        'qos_vs_score', 'ttl', 'type', 'full_path', 'availability_state',
+        'enabled_state', 'availability_status'
+    ]
+
+    @property
+    def max_answers_returned(self):
+        if self._values['max_answers_returned'] is None:
+            return None
+        return int(self._values['max_answers_returned'])
+
+    @property
+    def members(self):
+        result = []
+        if self._values['members'] is None or 'items' not in self._values['members']:
+            return result
+        for item in self._values['members']['items']:
+            self._remove_internal_keywords(item)
+            if 'disabled' in item:
+                if item['disabled'] in BOOLEANS_TRUE:
+                    item['disabled'] = True
+                else:
+                    item['disabled'] = False
+            if 'enabled' in item:
+                if item['enabled'] in BOOLEANS_TRUE:
+                    item['enabled'] = True
+                else:
+                    item['enabled'] = False
+            if 'fullPath' in item:
+                item['full_path'] = item.pop('fullPath')
+            if 'memberOrder' in item:
+                item['member_order'] = int(item.pop('memberOrder'))
+            # Cast some attributes to integer
+            for x in ['order', 'preference', 'ratio', 'service']:
+                if x in item:
+                    item[x] = int(item[x])
+            result.append(item)
+        return result
+
+    @property
+    def qos_hit_ratio(self):
+        if self._values['qos_hit_ratio'] is None:
+            return None
+        return int(self._values['qos_hit_ratio'])
+
+    @property
+    def qos_hops(self):
+        if self._values['qos_hops'] is None:
+            return None
+        return int(self._values['qos_hops'])
+
+    @property
+    def qos_kilobytes_second(self):
+        if self._values['qos_kilobytes_second'] is None:
+            return None
+        return int(self._values['qos_kilobytes_second'])
+
+    @property
+    def qos_lcs(self):
+        if self._values['qos_lcs'] is None:
+            return None
+        return int(self._values['qos_lcs'])
+
+    @property
+    def qos_packet_rate(self):
+        if self._values['qos_packet_rate'] is None:
+            return None
+        return int(self._values['qos_packet_rate'])
+
+    @property
+    def qos_rtt(self):
+        if self._values['qos_rtt'] is None:
+            return None
+        return int(self._values['qos_rtt'])
+
+    @property
+    def qos_topology(self):
+        if self._values['qos_topology'] is None:
+            return None
+        return int(self._values['qos_topology'])
+
+    @property
+    def qos_vs_capacity(self):
+        if self._values['qos_vs_capacity'] is None:
+            return None
+        return int(self._values['qos_vs_capacity'])
+
+    @property
+    def qos_vs_score(self):
+        if self._values['qos_vs_score'] is None:
+            return None
+        return int(self._values['qos_vs_score'])
+
+    @property
+    def availability_state(self):
+        if self._values['stats'] is None:
+            return None
         try:
-            if self.is_version_less_than_12():
-                return self.get_facts_without_types()
+            result = self._values['stats']['status_availabilityState']
+            return result['description']
+        except AttributeError:
+            return None
+
+    @property
+    def enabled_state(self):
+        if self._values['stats'] is None:
+            return None
+        try:
+            result = self._values['stats']['status_enabledState']
+            return result['description']
+        except AttributeError:
+            return None
+
+    @property
+    def availability_status(self):
+        # This fact is a combination of the availability_state and enabled_state
+        #
+        # The purpose of the fact is to give a higher-level view of the availability
+        # of the pool, that can be used in playbooks. If you need further detail,
+        # consider using the following facts together.
+        #
+        # - availability_state
+        # - enabled_state
+        #
+        if self.enabled_state == 'enabled':
+            if self.availability_state == 'offline':
+                return 'red'
+            elif self.availability_state == 'available':
+                return 'green'
+            elif self.availability_state == 'unknown':
+                return 'blue'
             else:
-                return self.get_facts_with_types()
-        except iControlUnexpectedHTTPError as e:
-            raise F5ModuleError(str(e))
+                return 'none'
+        else:
+            # disabled
+            return 'black'
 
-    def get_facts_with_types(self):
+
+class WideIpParameters(BaseParameters):
+    api_map = {
+        'fullPath': 'full_path',
+        'failureRcode': 'failure_return_code',
+        'failureRcodeResponse': 'failure_return_code_response',
+        'failureRcodeTtl': 'failure_return_code_ttl',
+        'lastResortPool': 'last_resort_pool',
+        'minimalResponse': 'minimal_response',
+        'persistCidrIpv4': 'persist_cidr_ipv4',
+        'persistCidrIpv6': 'persist_cidr_ipv6',
+        'poolLbMode': 'pool_lb_mode',
+        'ttlPersistence': 'ttl_persistence'
+    }
+
+    returnables = [
+        'full_path', 'description', 'enabled', 'disabled', 'failure_return_code',
+        'failure_return_code_response', 'failure_return_code_ttl', 'last_resort_pool',
+        'minimal_response', 'persist_cidr_ipv4', 'persist_cidr_ipv6', 'pool_lb_mode',
+        'ttl_persistence', 'pools'
+    ]
+
+    @property
+    def pools(self):
         result = []
-        for key, type in self.gtm_types.items():
-            facts = self.get_all_facts_by_type(key, type)
-            if facts:
-                result.append(facts)
+        if self._values['pools'] is None:
+            return []
+        for pool in self._values['pools']:
+            del pool['nameReference']
+            for x in ['order', 'ratio']:
+                if x in pool:
+                    pool[x] = int(pool[x])
+            result.append(pool)
         return result
 
-    def get_facts_without_types(self):
-        pools = self.api.tm.gtm.pools.get_collection(**self.request_params)
-        return self.get_facts_from_collection(pools)
+    @property
+    def failure_return_code_ttl(self):
+        if self._values['failure_return_code_ttl'] is None:
+            return None
+        return int(self._values['failure_return_code_ttl'])
 
-    def get_all_facts_by_type(self, key, type):
-        collection = getattr(self.api.tm.gtm.pools, key)
-        pools = collection.get_collection(**self.request_params)
-        return self.get_facts_from_collection(pools, type)
+    @property
+    def persist_cidr_ipv4(self):
+        if self._values['persist_cidr_ipv4'] is None:
+            return None
+        return int(self._values['persist_cidr_ipv4'])
 
-    def format_facts(self, pool, collection_type):
-        result = dict()
-        pool_dict = pool.to_dict()
-        result.update(self.format_string_facts(pool_dict))
-        result.update(self.format_member_facts(pool))
-        if collection_type:
-            result['type'] = collection_type
-        return camel_dict_to_snake_dict(result)
+    @property
+    def persist_cidr_ipv6(self):
+        if self._values['persist_cidr_ipv6'] is None:
+            return None
+        return int(self._values['persist_cidr_ipv6'])
 
-    def format_member_facts(self, pool):
+    @property
+    def ttl_persistence(self):
+        if self._values['ttl_persistence'] is None:
+            return None
+        return int(self._values['ttl_persistence'])
+
+
+class ServerParameters(BaseParameters):
+    api_map = {
+        'fullPath': 'full_path',
+        'exposeRouteDomains': 'expose_route_domains',
+        'iqAllowPath': 'iq_allow_path',
+        'iqAllowServiceCheck': 'iq_allow_service_check',
+        'iqAllowSnmp': 'iq_allow_snmp',
+        'limitCpuUsage': 'limit_cpu_usage',
+        'limitCpuUsageStatus': 'limit_cpu_usage_status',
+        'limitMaxBps': 'limit_max_bps',
+        'limitMaxBpsStatus': 'limit_max_bps_status',
+        'limitMaxConnections': 'limit_max_connections',
+        'limitMaxConnectionsStatus': 'limit_max_connections_status',
+        'limitMaxPps': 'limit_max_pps',
+        'limitMaxPpsStatus': 'limit_max_pps_status',
+        'limitMemAvail': 'limit_mem_available',
+        'limitMemAvailStatus': 'limit_mem_available_status',
+        'linkDiscovery': 'link_discovery',
+        'proberFallback': 'prober_fallback',
+        'proberPreference': 'prober_preference',
+        'virtualServerDiscovery': 'virtual_server_discovery',
+        'devicesReference': 'devices',
+        'virtualServersReference': 'virtual_servers'
+    }
+
+    returnables = [
+        'datacenter', 'enabled', 'disabled', 'expose_route_domains', 'iq_allow_path',
+        'full_path', 'iq_allow_service_check', 'iq_allow_snmp', 'limit_cpu_usage',
+        'limit_cpu_usage_status', 'limit_max_bps', 'limit_max_bps_status',
+        'limit_max_connections', 'limit_max_connections_status', 'limit_max_pps',
+        'limit_max_pps_status', 'limit_mem_available', 'limit_mem_available_status',
+        'link_discovery', 'monitor', 'product', 'prober_fallback', 'prober_preference',
+        'virtual_server_discovery', 'addresses', 'devices', 'virtual_servers'
+    ]
+
+    @property
+    def devices(self):
         result = []
-        if not 'items' in pool.membersReference:
-            return dict(members=[])
-        for member in pool.membersReference['items']:
-            member_facts = self.format_string_facts(member)
-            result.append(member_facts)
-        return dict(members=result)
-
-
-class BigIpGtmFactsWideIps(BigIpGtmFactsCommon):
-    def __init__(self, *args, **kwargs):
-        super(BigIpGtmFactsWideIps, self).__init__()
-        self.params = kwargs
-
-    def get_facts(self):
-        self.api = self.connect_to_bigip(**self.params)
-        return self.get_facts_from_device()
-
-    def get_facts_from_device(self):
-        try:
-            if self.is_version_less_than_12():
-                return self.get_facts_without_types()
-            else:
-                return self.get_facts_with_types()
-        except iControlUnexpectedHTTPError as e:
-            raise F5ModuleError(str(e))
-
-    def get_facts_with_types(self):
-        result = []
-        for key, type in self.gtm_types.items():
-            facts = self.get_all_facts_by_type(key, type)
-            if facts:
-                result.append(facts)
+        if self._values['devices'] is None or 'items' not in self._values['devices']:
+            return result
+        for item in self._values['devices']['items']:
+            self._remove_internal_keywords(item)
+            if 'fullPath' in item:
+                item['full_path'] = item.pop('fullPath')
+            result.append(item)
         return result
 
-    def get_facts_without_types(self):
-        wideips = self.api.tm.gtm.wideips.get_collection(
-            **self.request_params
+    @property
+    def virtual_servers(self):
+        result = []
+        if self._values['virtual_servers'] is None or 'items' not in self._values['virtual_servers']:
+            return result
+        for item in self._values['virtual_servers']['items']:
+            self._remove_internal_keywords(item)
+            if 'disabled' in item:
+                if item['disabled'] in BOOLEANS_TRUE:
+                    item['disabled'] = True
+                else:
+                    item['disabled'] = False
+            if 'enabled' in item:
+                if item['enabled'] in BOOLEANS_TRUE:
+                    item['enabled'] = True
+                else:
+                    item['enabled'] = False
+            if 'fullPath' in item:
+                item['full_path'] = item.pop('fullPath')
+            if 'limitMaxBps' in item:
+                item['limit_max_bps'] = int(item.pop('limitMaxBps'))
+            if 'limitMaxBpsStatus' in item:
+                item['limit_max_bps_status'] = item.pop('limitMaxBpsStatus')
+            if 'limitMaxConnections' in item:
+                item['limit_max_connections'] = int(item.pop('limitMaxConnections'))
+            if 'limitMaxConnectionsStatus' in item:
+                item['limit_max_connections_status'] = item.pop('limitMaxConnectionsStatus')
+            if 'limitMaxPps' in item:
+                item['limit_max_pps'] = int(item.pop('limitMaxPps'))
+            if 'limitMaxPpsStatus' in item:
+                item['limit_max_pps_status'] = item.pop('limitMaxPpsStatus')
+            if 'translationAddress' in item:
+                item['translation_address'] = item.pop('translationAddress')
+            if 'translationPort' in item:
+                item['translation_port'] = int(item.pop('translation_port'))
+            result.append(item)
+        return result
+
+    @property
+    def limit_cpu_usage(self):
+        if self._values['limit_cpu_usage'] is None:
+            return None
+        return int(self._values['limit_cpu_usage'])
+
+    @property
+    def limit_max_bps(self):
+        if self._values['limit_max_bps'] is None:
+            return None
+        return int(self._values['limit_max_bps'])
+
+    @property
+    def limit_max_connections(self):
+        if self._values['limit_max_connections'] is None:
+            return None
+        return int(self._values['limit_max_connections'])
+
+    @property
+    def limit_max_pps(self):
+        if self._values['limit_max_pps'] is None:
+            return None
+        return int(self._values['limit_max_pps'])
+
+    @property
+    def limit_mem_available(self):
+        if self._values['limit_mem_available'] is None:
+            return None
+        return int(self._values['limit_mem_available'])
+
+
+class PoolFactManager(BaseManager):
+    def exec_module(self):
+        if self.version_is_less_than_12():
+            manager = self.get_manager('untyped')
+        else:
+            manager = self.get_manager('typed')
+        facts = manager.exec_module()
+        result = dict(pool=facts)
+        return result
+
+    def get_manager(self, type):
+        if type == 'typed':
+            return TypedPoolFactManager(self.client)
+        elif type == 'untyped':
+            return UntypedPoolFactManager(self.client)
+
+
+class TypedPoolFactManager(TypedManager):
+    def __init__(self, client):
+        super(TypedPoolFactManager, self).__init__(client)
+        self.want = PoolParameters(self.client.module.params)
+
+    def read_facts(self, collection):
+        results = []
+        collection = self.read_collection_from_device(collection)
+        for resource in collection:
+            attrs = resource.attrs
+            attrs['stats'] = self.read_stats_from_device(resource)
+            params = PoolParameters(attrs)
+            results.append(params)
+        return results
+
+    def read_collection_from_device(self, collection_name):
+        pools = self.client.api.tm.gtm.pools
+        collection = getattr(pools, collection_name)
+        result = collection.get_collection(
+            requests_params=dict(
+                params='expandSubcollections=true'
+            )
         )
-        return self.get_facts_from_collection(wideips)
-
-    def get_all_facts_by_type(self, key, type):
-        collection = getattr(self.api.tm.gtm.wideips, key)
-        wideips = collection.get_collection(**self.request_params)
-        return self.get_facts_from_collection(wideips, type)
-
-    def format_facts(self, wideip, collection_type):
-        result = dict()
-        wideip_dict = wideip.to_dict()
-        result.update(self.format_string_facts(wideip_dict))
-        result.update(self.format_pool_facts(wideip))
-        if collection_type:
-            result['type'] = collection_type
-        return camel_dict_to_snake_dict(result)
-
-    def format_pool_facts(self, wideip):
-        result = []
-        if not hasattr(wideip, 'pools'):
-            return dict(pools=[])
-        for pool in wideip.pools:
-            pool_facts = self.format_string_facts(pool)
-            result.append(pool_facts)
-        return dict(pools=result)
-
-
-class BigIpGtmFactsVirtualServers(BigIpGtmFactsCommon):
-    def __init__(self, *args, **kwargs):
-        super(BigIpGtmFactsVirtualServers, self).__init__()
-        self.params = kwargs
-
-    def get_facts(self):
-        try:
-            self.api = self.connect_to_bigip(**self.params)
-            return self.get_facts_from_device()
-        except iControlUnexpectedHTTPError as e:
-            raise F5ModuleError(str(e))
-
-    def get_facts_from_device(self):
-        servers = self.api.tm.gtm.servers.get_collection(
-            **self.request_params
-        )
-        return self.get_facts_from_collection(servers)
-
-    def format_facts(self, server, collection_type=None):
-        result = dict()
-        server_dict = server.to_dict()
-        result.update(self.format_string_facts(server_dict))
-        result.update(self.format_address_facts(server))
-        result.update(self.format_virtual_server_facts(server))
-        return camel_dict_to_snake_dict(result)
-
-    def format_address_facts(self, server):
-        result = []
-        if not hasattr(server, 'addresses'):
-            return dict(addresses=[])
-        for address in server.addresses:
-            address_facts = self.format_string_facts(address)
-            result.append(address_facts)
-        return dict(addresses=result)
-
-    def format_virtual_server_facts(self, server):
-        result = []
-        if not 'items' in server.virtualServersReference:
-            return dict(virtual_servers=[])
-        for server in server.virtualServersReference['items']:
-            server_facts = self.format_string_facts(server)
-            result.append(server_facts)
-        return dict(virtual_servers=result)
-
-class BigIpGtmFactsManager(object):
-    def __init__(self, *args, **kwargs):
-        self.params = kwargs
-        self.api = None
-
-    def get_facts(self):
-        result = dict()
-        facts = dict()
-
-        if 'pool' in self.params['include']:
-            facts['pool'] = self.get_pool_facts()
-        if 'wide_ip' in self.params['include']:
-            facts['wide_ip'] = self.get_wide_ip_facts()
-        if 'virtual_server' in self.params['include']:
-            facts['virtual_server'] = self.get_virtual_server_facts()
-
-        result.update(**facts)
-        result.update(dict(changed=True))
         return result
 
-    def get_pool_facts(self):
-        pools = BigIpGtmFactsPools(**self.params)
-        return pools.get_facts()
 
-    def get_wide_ip_facts(self):
-        wide_ips = BigIpGtmFactsWideIps(**self.params)
-        return wide_ips.get_facts()
+class UntypedPoolFactManager(UntypedManager):
+    def __init__(self, client):
+        super(UntypedPoolFactManager, self).__init__(client)
+        self.want = PoolParameters(self.client.module.params)
 
-    def get_virtual_server_facts(self):
-        wide_ips = BigIpGtmFactsVirtualServers(**self.params)
-        return wide_ips.get_facts()
+    def read_facts(self):
+        results = []
+        collection = self.read_collection_from_device()
+        for resource in collection:
+            attrs = resource.attrs
+            attrs['stats'] = self.read_stats_from_device(resource)
+            params = PoolParameters(attrs)
+            results.append(params)
+        return results
+
+    def read_collection_from_device(self):
+        result = self.client.api.tm.gtm.pools.get_collection(
+            requests_params=dict(
+                params='expandSubcollections=true'
+            )
+        )
+        return result
 
 
-class BigIpGtmFactsModuleConfig(object):
+class WideIpFactManager(BaseManager):
+    def exec_module(self):
+        if self.version_is_less_than_12():
+            manager = self.get_manager('untyped')
+        else:
+            manager = self.get_manager('typed')
+        facts = manager.exec_module()
+        result = dict(wide_ip=facts)
+        return result
+
+    def get_manager(self, type):
+        if type == 'typed':
+            return TypedWideIpFactManager(self.client)
+        elif type == 'untyped':
+            return UntypedWideIpFactManager(self.client)
+
+
+class TypedWideIpFactManager(TypedManager):
+    def __init__(self, client):
+        super(TypedWideIpFactManager, self).__init__(client)
+        self.want = WideIpParameters(self.client.module.params)
+
+    def read_facts(self, collection):
+        results = []
+        collection = self.read_collection_from_device(collection)
+        for resource in collection:
+            attrs = resource.attrs
+            params = WideIpParameters(attrs)
+            results.append(params)
+        return results
+
+    def read_collection_from_device(self, collection_name):
+        wideips = self.client.api.tm.gtm.wideips
+        collection = getattr(wideips, collection_name)
+        result = collection.get_collection(
+            requests_params=dict(
+                params='expandSubcollections=true'
+            )
+        )
+        return result
+
+
+class UntypedWideIpFactManager(UntypedManager):
+    def __init__(self, client):
+        super(UntypedWideIpFactManager, self).__init__(client)
+        self.want = WideIpParameters(self.client.module.params)
+
+    def read_facts(self):
+        results = []
+        collection = self.read_collection_from_device()
+        for resource in collection:
+            attrs = resource.attrs
+            params = WideIpParameters(attrs)
+            results.append(params)
+        return results
+
+    def read_collection_from_device(self):
+        result = self.client.api.tm.gtm.wideips.get_collection(
+            requests_params=dict(
+                params='expandSubcollections=true'
+            )
+        )
+        return result
+
+
+class ServerFactManager(UntypedManager):
+    def __init__(self, client):
+        super(ServerFactManager, self).__init__(client)
+        self.want = ServerParameters(self.client.module.params)
+
+    def exec_module(self):
+        facts = super(ServerFactManager, self).exec_module()
+        result = dict(server=facts, virtual_server=facts)
+        return result
+
+    def read_facts(self):
+        results = []
+        collection = self.read_collection_from_device()
+        for resource in collection:
+            attrs = resource.attrs
+            params = WideIpParameters(attrs)
+            results.append(params)
+        return results
+
+    def read_collection_from_device(self):
+        result = self.client.api.tm.gtm.servers.get_collection(
+            requests_params=dict(
+                params='expandSubcollections=true'
+            )
+        )
+        return result
+
+
+class ModuleManager(object):
+    def __init__(self, client):
+        self.client = client
+        self.want = Parameters(self.client.module.params)
+
+    def exec_module(self):
+        if not self.gtm_provisioned():
+            raise F5ModuleError(
+                "GTM must be provisioned to use this module."
+            )
+
+        if 'all' in self.want.include:
+            names = ['pool', 'wide_ip', 'server']
+        else:
+            names = self.want.include
+            # The virtual_server parameter is deprecated
+            if 'virtual_server' in names:
+                names.append('server')
+                names.remove('virtual_server')
+        managers = [self.get_manager(name) for name in names]
+        result = self.execute_managers(managers)
+        if result:
+            result['changed'] = True
+        else:
+            result['changed'] = False
+        self._announce_deprecations()
+        return result
+
+    def _announce_deprecations(self):
+        warnings = []
+        if self.want:
+            warnings += self.want._values.get('__warnings', [])
+        for warning in warnings:
+            self.client.module.deprecate(
+                msg=warning['msg'],
+                version=warning['version']
+            )
+
+    def execute_managers(self, managers):
+        results = dict()
+        for manager in managers:
+            result = manager.exec_module()
+            results.update(result)
+        return results
+
+    def get_manager(self, which):
+        if 'pool' == which:
+            return PoolFactManager(self.client)
+        if 'wide_ip' == which:
+            return WideIpFactManager(self.client)
+        if 'server' == which:
+            return ServerFactManager(self.client)
+
+    def gtm_provisioned(self):
+        resource = self.client.api.tm.sys.dbs.db.load(
+            name='provisioned.cpu.gtm'
+        )
+        if int(resource.value) == 0:
+            return False
+        return True
+
+
+class ArgumentSpec(object):
     def __init__(self):
-        self.argument_spec = dict()
-        self.meta_args = dict()
         self.supports_check_mode = False
-        self.valid_includes = ['pool', 'wide_ip', 'virtual_server']
-        self.initialize_meta_args()
-        self.initialize_argument_spec()
-
-    def initialize_meta_args(self):
-        args = dict(
+        self.argument_spec = dict(
             include=dict(type='list', required=True),
             filter=dict(type='str', required=False)
         )
-        self.meta_args = args
-
-    def initialize_argument_spec(self):
-        self.argument_spec = f5_argument_spec()
-        self.argument_spec.update(self.meta_args)
-
-    def create(self):
-        return AnsibleModule(
-            argument_spec=self.argument_spec,
-            supports_check_mode=self.supports_check_mode
-        )
+        self.f5_product_name = 'bigip'
 
 
 def main():
     if not HAS_F5SDK:
         raise F5ModuleError("The python f5-sdk module is required")
 
-    config = BigIpGtmFactsModuleConfig()
-    module = config.create()
+    spec = ArgumentSpec()
+
+    client = AnsibleF5Client(
+        argument_spec=spec.argument_spec,
+        supports_check_mode=spec.supports_check_mode,
+        f5_product_name=spec.f5_product_name,
+    )
 
     try:
-        obj = BigIpGtmFactsManager(
-            check_mode=module.check_mode, **module.params
-        )
-        result = obj.get_facts()
-
-        module.exit_json(**result)
+        mm = ModuleManager(client)
+        results = mm.exec_module()
+        client.module.exit_json(**results)
     except F5ModuleError as e:
-        module.fail_json(msg=str(e))
+        client.module.fail_json(msg=str(e))
 
-from ansible.module_utils.basic import *
-from ansible.module_utils.ec2 import camel_dict_to_snake_dict
-from ansible.module_utils.f5_utils import *
 
 if __name__ == '__main__':
     main()
diff --git a/test/sanity/pep8/legacy-files.txt b/test/sanity/pep8/legacy-files.txt
index 63f66226ef2..dc9ed820fd3 100644
--- a/test/sanity/pep8/legacy-files.txt
+++ b/test/sanity/pep8/legacy-files.txt
@@ -206,7 +206,6 @@ lib/ansible/modules/network/eos/eos_eapi.py
 lib/ansible/modules/network/eos/eos_facts.py
 lib/ansible/modules/network/eos/eos_system.py
 lib/ansible/modules/network/eos/eos_user.py
-lib/ansible/modules/network/f5/bigip_gtm_facts.py
 lib/ansible/modules/network/f5/bigip_virtual_server.py
 lib/ansible/modules/network/fortios/fortios_config.py
 lib/ansible/modules/network/fortios/fortios_ipv4_policy.py
diff --git a/test/units/modules/network/f5/fixtures/load_gtm_pool_a_collection.json b/test/units/modules/network/f5/fixtures/load_gtm_pool_a_collection.json
new file mode 100644
index 00000000000..7ed5344503e
--- /dev/null
+++ b/test/units/modules/network/f5/fixtures/load_gtm_pool_a_collection.json
@@ -0,0 +1,44 @@
+{
+    "kind": "tm:gtm:pool:a:acollectionstate",
+    "selfLink": "https://localhost/mgmt/tm/gtm/pool/a?expandSubcollections=true&ver=13.0.0",
+    "items": [
+        {
+            "kind": "tm:gtm:pool:a:astate",
+            "name": "foo.pool",
+            "partition": "Common",
+            "fullPath": "/Common/foo.pool",
+            "generation": 216,
+            "selfLink": "https://localhost/mgmt/tm/gtm/pool/a/~Common~foo.pool?ver=13.0.0",
+            "alternateMode": "round-robin",
+            "dynamicRatio": "disabled",
+            "enabled": true,
+            "fallbackIp": "any",
+            "fallbackMode": "return-to-dns",
+            "limitMaxBps": 0,
+            "limitMaxBpsStatus": "disabled",
+            "limitMaxConnections": 0,
+            "limitMaxConnectionsStatus": "disabled",
+            "limitMaxPps": 0,
+            "limitMaxPpsStatus": "disabled",
+            "loadBalancingMode": "round-robin",
+            "manualResume": "disabled",
+            "maxAnswersReturned": 1,
+            "monitor": "default",
+            "qosHitRatio": 5,
+            "qosHops": 0,
+            "qosKilobytesSecond": 3,
+            "qosLcs": 30,
+            "qosPacketRate": 1,
+            "qosRtt": 50,
+            "qosTopology": 0,
+            "qosVsCapacity": 0,
+            "qosVsScore": 0,
+            "ttl": 30,
+            "verifyMemberAvailability": "enabled",
+            "membersReference": {
+                "link": "https://localhost/mgmt/tm/gtm/pool/a/~Common~foo.pool/members?ver=13.0.0",
+                "isSubcollection": true
+            }
+        }
+    ]
+}
diff --git a/test/units/modules/network/f5/fixtures/load_gtm_pool_a_example_stats.json b/test/units/modules/network/f5/fixtures/load_gtm_pool_a_example_stats.json
new file mode 100644
index 00000000000..70388c91440
--- /dev/null
+++ b/test/units/modules/network/f5/fixtures/load_gtm_pool_a_example_stats.json
@@ -0,0 +1,48 @@
+{
+    "kind": "tm:gtm:pool:a:astats",
+    "generation": 216,
+    "selfLink": "https://localhost/mgmt/tm/gtm/pool/a/~Common~foo.pool/stats?ver=13.0.0",
+    "entries": {
+        "https://localhost/mgmt/tm/gtm/pool/a/~Common~foo.pool/~Common~foo.pool:A/stats": {
+            "nestedStats": {
+                "kind": "tm:gtm:pool:a:astats",
+                "selfLink": "https://localhost/mgmt/tm/gtm/pool/a/~Common~foo.pool/~Common~foo.pool:A/stats?ver=13.0.0",
+                "entries": {
+                    "alternate": {
+                        "value": 0
+                    },
+                    "dropped": {
+                        "value": 0
+                    },
+                    "fallback": {
+                        "value": 0
+                    },
+                    "tmName": {
+                        "description": "/Common/foo.pool"
+                    },
+                    "poolType": {
+                        "description": "A"
+                    },
+                    "preferred": {
+                        "value": 0
+                    },
+                    "returnFromDns": {
+                        "value": 0
+                    },
+                    "returnToDns": {
+                        "value": 0
+                    },
+                    "status.availabilityState": {
+                        "description": "offline"
+                    },
+                    "status.enabledState": {
+                        "description": "enabled"
+                    },
+                    "status.statusReason": {
+                        "description": "No enabled pool members available"
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/test/units/modules/network/f5/test_bigip_gtm_facts.py b/test/units/modules/network/f5/test_bigip_gtm_facts.py
new file mode 100644
index 00000000000..e4d2b891170
--- /dev/null
+++ b/test/units/modules/network/f5/test_bigip_gtm_facts.py
@@ -0,0 +1,157 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 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
+
+import os
+import json
+import sys
+
+from nose.plugins.skip import SkipTest
+if sys.version_info < (2, 7):
+    raise SkipTest("F5 Ansible modules require Python >= 2.7")
+
+from ansible.compat.tests import unittest
+from ansible.compat.tests.mock import patch, Mock
+from ansible.module_utils import basic
+from ansible.module_utils._text import to_bytes
+from ansible.module_utils.f5_utils import AnsibleF5Client
+from ansible.module_utils.six import iteritems
+
+try:
+    from library.bigip_gtm_facts import Parameters
+    from library.bigip_gtm_facts import ServerParameters
+    from library.bigip_gtm_facts import PoolParameters
+    from library.bigip_gtm_facts import WideIpParameters
+    from library.bigip_gtm_facts import ModuleManager
+    from library.bigip_gtm_facts import ServerFactManager
+    from library.bigip_gtm_facts import PoolFactManager
+    from library.bigip_gtm_facts import TypedPoolFactManager
+    from library.bigip_gtm_facts import UntypedPoolFactManager
+    from library.bigip_gtm_facts import WideIpFactManager
+    from library.bigip_gtm_facts import TypedWideIpFactManager
+    from library.bigip_gtm_facts import UntypedWideIpFactManager
+    from library.bigip_gtm_facts import ArgumentSpec
+    from ansible.module_utils.f5_utils import iControlUnexpectedHTTPError
+    from f5.bigip.tm.gtm.pool import A
+    from f5.utils.responses.handlers import Stats
+except ImportError:
+    try:
+        from ansible.modules.network.f5.bigip_gtm_pool import Parameters
+        from ansible.modules.network.f5.bigip_gtm_pool import ServerParameters
+        from ansible.modules.network.f5.bigip_gtm_pool import PoolParameters
+        from ansible.modules.network.f5.bigip_gtm_pool import WideIpParameters
+        from ansible.modules.network.f5.bigip_gtm_pool import ModuleManager
+        from ansible.modules.network.f5.bigip_gtm_pool import ServerFactManager
+        from ansible.modules.network.f5.bigip_gtm_pool import PoolFactManager
+        from ansible.modules.network.f5.bigip_gtm_pool import TypedPoolFactManager
+        from ansible.modules.network.f5.bigip_gtm_pool import UntypedPoolFactManager
+        from ansible.modules.network.f5.bigip_gtm_pool import WideIpFactManager
+        from ansible.modules.network.f5.bigip_gtm_pool import TypedWideIpFactManager
+        from ansible.modules.network.f5.bigip_gtm_pool import UntypedWideIpFactManager
+        from ansible.modules.network.f5.bigip_gtm_pool import ArgumentSpec
+        from ansible.module_utils.f5_utils import iControlUnexpectedHTTPError
+        from f5.bigip.tm.gtm.pool import A
+        from f5.utils.responses.handlers import Stats
+    except ImportError:
+        raise SkipTest("F5 Ansible modules require the f5-sdk Python library")
+
+fixture_path = os.path.join(os.path.dirname(__file__), 'fixtures')
+fixture_data = {}
+
+
+def set_module_args(args):
+    args = json.dumps({'ANSIBLE_MODULE_ARGS': args})
+    basic._ANSIBLE_ARGS = to_bytes(args)
+
+
+def load_fixture(name):
+    path = os.path.join(fixture_path, name)
+
+    if path in fixture_data:
+        return fixture_data[path]
+
+    with open(path) as f:
+        data = f.read()
+
+    try:
+        data = json.loads(data)
+    except Exception:
+        pass
+
+    fixture_data[path] = data
+    return data
+
+
+class FakeStatResource(object):
+    def __init__(self, obj):
+        self.entries = obj
+
+
+class FakeARecord(A):
+    def __init__(self, *args, **kwargs):
+        attrs = kwargs.pop('attrs', {})
+        for key, value in iteritems(attrs):
+            setattr(self, key, value)
+
+
+class TestParameters(unittest.TestCase):
+    def test_module_parameters(self):
+        args = dict(
+            include=['pool'],
+            filter='name.*'
+        )
+        p = Parameters(args)
+        assert p.include == ['pool']
+        assert p.filter == 'name.*'
+
+
+@patch('ansible.module_utils.f5_utils.AnsibleF5Client._get_mgmt_root',
+       return_value=True)
+class TestManager(unittest.TestCase):
+
+    def setUp(self):
+        self.spec = ArgumentSpec()
+
+    def test_get_typed_pool_facts(self, *args):
+        set_module_args(dict(
+            include='pool',
+            password='passsword',
+            server='localhost',
+            user='admin'
+        ))
+
+        fixture1 = load_fixture('load_gtm_pool_a_collection.json')
+        fixture2 = load_fixture('load_gtm_pool_a_example_stats.json')
+        collection = [FakeARecord(attrs=x) for x in fixture1['items']]
+        stats = Stats(FakeStatResource(fixture2['entries']))
+
+        client = AnsibleF5Client(
+            argument_spec=self.spec.argument_spec,
+            supports_check_mode=self.spec.supports_check_mode,
+            f5_product_name=self.spec.f5_product_name
+        )
+
+        # Override methods in the specific type of manager
+        tfm = TypedPoolFactManager(client)
+        tfm.read_collection_from_device = Mock(return_value=collection)
+        tfm.read_stats_from_device = Mock(return_value=stats.stat)
+
+        tm = PoolFactManager(client)
+        tm.version_is_less_than_12 = Mock(return_value=False)
+        tm.get_manager = Mock(return_value=tfm)
+
+        # Override methods to force specific logic in the module to happen
+        mm = ModuleManager(client)
+        mm.get_manager = Mock(return_value=tm)
+        mm.gtm_provisioned = Mock(return_value=True)
+
+        results = mm.exec_module()
+
+        assert results['changed'] is True
+        assert 'pool' in results
+        assert len(results['pool']) > 0
+        assert 'load_balancing_mode' in results['pool'][0]