From fb264281ded12a1819338b53dbb9bc33e2a9b591 Mon Sep 17 00:00:00 2001 From: Tim Rupp Date: Wed, 25 Apr 2018 07:16:11 -0700 Subject: [PATCH] Adds various features and fixes (#39271) * a refactor of pool member and node modules to be inline with current f5 conventions * Added priority_group_activation to pools * various other small convention fixes and bug fixes --- .../modules/network/f5/bigip_monitor_udp.py | 14 +- lib/ansible/modules/network/f5/bigip_node.py | 31 +- .../modules/network/f5/bigip_partition.py | 9 +- .../modules/network/f5/bigip_policy.py | 9 +- .../modules/network/f5/bigip_policy_rule.py | 38 +- lib/ansible/modules/network/f5/bigip_pool.py | 108 +- .../modules/network/f5/bigip_pool_member.py | 1083 +++++++++++------ .../network/f5/bigip_profile_client_ssl.py | 30 +- test/sanity/validate-modules/ignore.txt | 1 - .../f5/fixtures/load_net_node_with_fqdn.json | 25 + .../load_net_node_with_ipv4_address.json | 24 + .../network/f5/test_bigip_monitor_udp.py | 6 +- .../modules/network/f5/test_bigip_node.py | 6 +- .../network/f5/test_bigip_partition.py | 6 +- .../modules/network/f5/test_bigip_policy.py | 10 +- .../network/f5/test_bigip_policy_rule.py | 10 +- .../modules/network/f5/test_bigip_pool.py | 8 +- .../network/f5/test_bigip_pool_member.py | 338 +++++ .../f5/test_bigip_profile_client_ssl.py | 8 +- 19 files changed, 1232 insertions(+), 532 deletions(-) create mode 100644 test/units/modules/network/f5/fixtures/load_net_node_with_fqdn.json create mode 100644 test/units/modules/network/f5/fixtures/load_net_node_with_ipv4_address.json create mode 100644 test/units/modules/network/f5/test_bigip_pool_member.py diff --git a/lib/ansible/modules/network/f5/bigip_monitor_udp.py b/lib/ansible/modules/network/f5/bigip_monitor_udp.py index 1ab9293328..ff5a87ff18 100644 --- a/lib/ansible/modules/network/f5/bigip_monitor_udp.py +++ b/lib/ansible/modules/network/f5/bigip_monitor_udp.py @@ -17,7 +17,7 @@ DOCUMENTATION = r''' module: bigip_monitor_udp short_description: Manages F5 BIG-IP LTM udp monitors description: Manages F5 BIG-IP LTM udp monitors. -version_added: "2.5" +version_added: 2.5 options: name: description: @@ -150,30 +150,23 @@ import os from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.basic import env_fallback -HAS_DEVEL_IMPORTS = False - try: - # Sideband repository used for dev from library.module_utils.network.f5.bigip import HAS_F5SDK from library.module_utils.network.f5.bigip import F5Client from library.module_utils.network.f5.common import F5ModuleError from library.module_utils.network.f5.common import AnsibleF5Parameters from library.module_utils.network.f5.common import cleanup_tokens - from library.module_utils.network.f5.common import fqdn_name from library.module_utils.network.f5.common import f5_argument_spec try: from library.module_utils.network.f5.common import iControlUnexpectedHTTPError except ImportError: HAS_F5SDK = False - HAS_DEVEL_IMPORTS = True except ImportError: - # Upstream Ansible from ansible.module_utils.network.f5.bigip import HAS_F5SDK from ansible.module_utils.network.f5.bigip import F5Client from ansible.module_utils.network.f5.common import F5ModuleError from ansible.module_utils.network.f5.common import AnsibleF5Parameters from ansible.module_utils.network.f5.common import cleanup_tokens - from ansible.module_utils.network.f5.common import fqdn_name from ansible.module_utils.network.f5.common import f5_argument_spec try: from ansible.module_utils.network.f5.common import iControlUnexpectedHTTPError @@ -208,11 +201,6 @@ class Parameters(AnsibleF5Parameters): 'destination', 'send', 'receive', 'interval', 'timeout', 'time_until_up' ] - def _fqdn_name(self, value): - if value is not None and not value.startswith('/'): - return '/{0}/{1}'.format(self.partition, value) - return value - def to_return(self): result = {} try: diff --git a/lib/ansible/modules/network/f5/bigip_node.py b/lib/ansible/modules/network/f5/bigip_node.py index 020c7e073a..6a33312398 100644 --- a/lib/ansible/modules/network/f5/bigip_node.py +++ b/lib/ansible/modules/network/f5/bigip_node.py @@ -18,7 +18,7 @@ module: bigip_node short_description: Manages F5 BIG-IP LTM nodes description: - Manages F5 BIG-IP LTM nodes. -version_added: "1.4" +version_added: 1.4 options: state: description: @@ -59,12 +59,12 @@ options: quorum: description: - Monitor quorum value when C(monitor_type) is C(m_of_n). - version_added: "2.2" + version_added: 2.2 monitors: description: - Specifies the health monitors that the system currently uses to monitor this node. - version_added: "2.2" + version_added: 2.2 address: description: - IP address of the node. This can be either IPv4 or IPv6. When creating a @@ -73,7 +73,7 @@ options: aliases: - ip - host - version_added: "2.2" + version_added: 2.2 fqdn: description: - FQDN name of the node. This can be any name that is a valid RFC 1123 DNS @@ -86,7 +86,7 @@ options: provided. This parameter cannot be updated after it is set. aliases: - hostname - version_added: "2.5" + version_added: 2.5 description: description: - Specifies descriptive text that identifies the node. @@ -97,7 +97,9 @@ options: version_added: 2.5 notes: - Requires the netaddr Python package on the host. This is as easy as - pip install netaddr + C(pip install netaddr). +requirements: + - netaddr extends_documentation_fragment: f5 author: - Tim Rupp (@caphrim007) @@ -216,30 +218,25 @@ import time from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.basic import env_fallback -HAS_DEVEL_IMPORTS = False - try: - # Sideband repository used for dev from library.module_utils.network.f5.bigip import HAS_F5SDK from library.module_utils.network.f5.bigip import F5Client from library.module_utils.network.f5.common import F5ModuleError from library.module_utils.network.f5.common import AnsibleF5Parameters from library.module_utils.network.f5.common import cleanup_tokens - from library.module_utils.network.f5.common import fqdn_name + from library.module_utils.network.f5.common import fq_name from library.module_utils.network.f5.common import f5_argument_spec try: from library.module_utils.network.f5.common import iControlUnexpectedHTTPError except ImportError: HAS_F5SDK = False - HAS_DEVEL_IMPORTS = True except ImportError: - # Upstream Ansible from ansible.module_utils.network.f5.bigip import HAS_F5SDK from ansible.module_utils.network.f5.bigip import F5Client from ansible.module_utils.network.f5.common import F5ModuleError from ansible.module_utils.network.f5.common import AnsibleF5Parameters from ansible.module_utils.network.f5.common import cleanup_tokens - from ansible.module_utils.network.f5.common import fqdn_name + from ansible.module_utils.network.f5.common import fq_name from ansible.module_utils.network.f5.common import f5_argument_spec try: from ansible.module_utils.network.f5.common import iControlUnexpectedHTTPError @@ -291,11 +288,6 @@ class Parameters(AnsibleF5Parameters): except Exception: return result - def _fqdn_name(self, value): - if value is not None and not value.startswith('/'): - return '/{0}/{1}'.format(self.partition, value) - return value - @property def monitors_list(self): if self._values['monitors'] is None: @@ -310,13 +302,12 @@ class Parameters(AnsibleF5Parameters): def monitors(self): if self._values['monitors'] is None: return None - monitors = [self._fqdn_name(x) for x in self.monitors_list] + monitors = [fq_name(self.partition, x) for x in self.monitors_list] if self.monitor_type == 'm_of_n': monitors = ' '.join(monitors) result = 'min %s of { %s }' % (self.quorum, monitors) else: result = ' and '.join(monitors).strip() - return result @property diff --git a/lib/ansible/modules/network/f5/bigip_partition.py b/lib/ansible/modules/network/f5/bigip_partition.py index 452fe245c2..fb2ade5ad9 100644 --- a/lib/ansible/modules/network/f5/bigip_partition.py +++ b/lib/ansible/modules/network/f5/bigip_partition.py @@ -18,7 +18,7 @@ module: bigip_partition short_description: Manage BIG-IP partitions description: - Manage BIG-IP partitions. -version_added: "2.5" +version_added: 2.5 options: name: description: @@ -107,30 +107,23 @@ description: from ansible.module_utils.basic import AnsibleModule -HAS_DEVEL_IMPORTS = False - try: - # Sideband repository used for dev from library.module_utils.network.f5.bigip import HAS_F5SDK from library.module_utils.network.f5.bigip import F5Client from library.module_utils.network.f5.common import F5ModuleError from library.module_utils.network.f5.common import AnsibleF5Parameters from library.module_utils.network.f5.common import cleanup_tokens - from library.module_utils.network.f5.common import fqdn_name from library.module_utils.network.f5.common import f5_argument_spec try: from library.module_utils.network.f5.common import iControlUnexpectedHTTPError except ImportError: HAS_F5SDK = False - HAS_DEVEL_IMPORTS = True except ImportError: - # Upstream Ansible from ansible.module_utils.network.f5.bigip import HAS_F5SDK from ansible.module_utils.network.f5.bigip import F5Client from ansible.module_utils.network.f5.common import F5ModuleError from ansible.module_utils.network.f5.common import AnsibleF5Parameters from ansible.module_utils.network.f5.common import cleanup_tokens - from ansible.module_utils.network.f5.common import fqdn_name from ansible.module_utils.network.f5.common import f5_argument_spec try: from ansible.module_utils.network.f5.common import iControlUnexpectedHTTPError diff --git a/lib/ansible/modules/network/f5/bigip_policy.py b/lib/ansible/modules/network/f5/bigip_policy.py index 20c864b981..bf1af427c9 100644 --- a/lib/ansible/modules/network/f5/bigip_policy.py +++ b/lib/ansible/modules/network/f5/bigip_policy.py @@ -23,7 +23,7 @@ description: the description, and things unrelated to the policy rules themselves. It is also the first module that should be used when creating rules as the C(bigip_policy_rule) module requires a policy parameter. -version_added: "2.5" +version_added: 2.5 options: description: description: @@ -168,30 +168,23 @@ from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.basic import env_fallback from distutils.version import LooseVersion -HAS_DEVEL_IMPORTS = False - try: - # Sideband repository used for dev from library.module_utils.network.f5.bigip import HAS_F5SDK from library.module_utils.network.f5.bigip import F5Client from library.module_utils.network.f5.common import F5ModuleError from library.module_utils.network.f5.common import AnsibleF5Parameters from library.module_utils.network.f5.common import cleanup_tokens - from library.module_utils.network.f5.common import fqdn_name from library.module_utils.network.f5.common import f5_argument_spec try: from library.module_utils.network.f5.common import iControlUnexpectedHTTPError except ImportError: HAS_F5SDK = False - HAS_DEVEL_IMPORTS = True except ImportError: - # Upstream Ansible from ansible.module_utils.network.f5.bigip import HAS_F5SDK from ansible.module_utils.network.f5.bigip import F5Client from ansible.module_utils.network.f5.common import F5ModuleError from ansible.module_utils.network.f5.common import AnsibleF5Parameters from ansible.module_utils.network.f5.common import cleanup_tokens - from ansible.module_utils.network.f5.common import fqdn_name from ansible.module_utils.network.f5.common import f5_argument_spec try: from ansible.module_utils.network.f5.common import iControlUnexpectedHTTPError diff --git a/lib/ansible/modules/network/f5/bigip_policy_rule.py b/lib/ansible/modules/network/f5/bigip_policy_rule.py index 0e7c93c609..af09d71832 100644 --- a/lib/ansible/modules/network/f5/bigip_policy_rule.py +++ b/lib/ansible/modules/network/f5/bigip_policy_rule.py @@ -41,7 +41,7 @@ options: - When C(type) is C(ignore), will remove all existing actions from this rule. required: true - choices: [ 'forward', 'enable', 'ignore' ] + choices: ['forward', 'enable', 'ignore'] pool: description: - Pool that you want to forward traffic to. @@ -77,7 +77,7 @@ options: list will provide a match. - When C(type) is C(all_traffic), will remove all existing conditions from this rule. - required: true + required: True choices: [ 'http_uri', 'all_traffic' ] path_begins_with_any: description: @@ -120,6 +120,7 @@ EXAMPLES = r''' actions: - type: forward pool: pool-svrs + delegate_to: localhost - name: Add multiple rules to the new policy bigip_policy_rule: @@ -127,6 +128,7 @@ EXAMPLES = r''' name: "{{ item.name }}" conditions: "{{ item.conditions }}" actions: "{{ item.actions }}" + delegate_to: localhost loop: - name: rule1 actions: @@ -151,6 +153,7 @@ EXAMPLES = r''' - type: all_traffic actions: - type: ignore + delegate_to: localhost ''' RETURN = r''' @@ -197,30 +200,25 @@ from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.basic import env_fallback from ansible.module_utils.six import iteritems -HAS_DEVEL_IMPORTS = False - try: - # Sideband repository used for dev from library.module_utils.network.f5.bigip import HAS_F5SDK from library.module_utils.network.f5.bigip import F5Client from library.module_utils.network.f5.common import F5ModuleError from library.module_utils.network.f5.common import AnsibleF5Parameters from library.module_utils.network.f5.common import cleanup_tokens - from library.module_utils.network.f5.common import fqdn_name + from library.module_utils.network.f5.common import fq_name from library.module_utils.network.f5.common import f5_argument_spec try: from library.module_utils.network.f5.common import iControlUnexpectedHTTPError except ImportError: HAS_F5SDK = False - HAS_DEVEL_IMPORTS = True except ImportError: - # Upstream Ansible from ansible.module_utils.network.f5.bigip import HAS_F5SDK from ansible.module_utils.network.f5.bigip import F5Client from ansible.module_utils.network.f5.common import F5ModuleError from ansible.module_utils.network.f5.common import AnsibleF5Parameters from ansible.module_utils.network.f5.common import cleanup_tokens - from ansible.module_utils.network.f5.common import fqdn_name + from ansible.module_utils.network.f5.common import fq_name from ansible.module_utils.network.f5.common import f5_argument_spec try: from ansible.module_utils.network.f5.common import iControlUnexpectedHTTPError @@ -241,10 +239,9 @@ class Parameters(AnsibleF5Parameters): 'actions', 'conditions', 'description' ] - def _fqdn_name(self, value): - if value is not None and not value.startswith('/'): - return '/{0}/{1}'.format(self.partition, value) - return value + returnable = [ + 'description' + ] @property def name(self): @@ -254,13 +251,6 @@ class Parameters(AnsibleF5Parameters): def description(self): return self._values.get('description', None) - @property - def strategy(self): - if self._values['strategy'] is None: - return None - result = self._fqdn_name(self._values['strategy']) - return result - @property def policy(self): if self._values['policy'] is None: @@ -407,7 +397,7 @@ class ModuleParameters(Parameters): raise F5ModuleError( "A 'pool' must be specified when the 'forward' type is used." ) - action['pool'] = self._fqdn_name(item['pool']) + action['pool'] = fq_name(self.partition, item['pool']) def _handle_enable_action(self, action, item): """Handle the nuances of the enable type @@ -422,7 +412,7 @@ class ModuleParameters(Parameters): "An 'asm_policy' must be specified when the 'enable' type is used." ) action.update(dict( - policy=self._fqdn_name(item['asm_policy']), + policy=fq_name(self.partition, item['asm_policy']), asm=True )) @@ -836,9 +826,9 @@ class ArgumentSpec(object): 'all_traffic' ], required=True - ) + ), + path_begins_with_any=dict() ), - path_begins_with_any=dict() ), name=dict(required=True), policy=dict(required=True), diff --git a/lib/ansible/modules/network/f5/bigip_pool.py b/lib/ansible/modules/network/f5/bigip_pool.py index 1db6899046..22e3737d0d 100644 --- a/lib/ansible/modules/network/f5/bigip_pool.py +++ b/lib/ansible/modules/network/f5/bigip_pool.py @@ -23,7 +23,7 @@ options: description: description: - Specifies descriptive text that identifies the pool. - version_added: "2.3" + version_added: 2.3 name: description: - Pool name @@ -34,7 +34,7 @@ options: description: - Load balancing method. When creating a new pool, if this value is not specified, the default of C(round-robin) will be used. - version_added: "1.3" + version_added: 1.3 choices: - dynamic-ratio-member - dynamic-ratio-node @@ -54,7 +54,7 @@ options: - ratio-session - round-robin - weighted-least-connections-member - - weighted-least-connections-nod + - weighted-least-connections-node monitor_type: description: - Monitor rule type when C(monitors) is specified. @@ -69,32 +69,32 @@ options: or already existing on the device. - Both C(single) and C(and_list) are functionally identical since BIG-IP considers all monitors as "a list". - version_added: "1.3" + version_added: 1.3 choices: ['and_list', 'm_of_n', 'single'] quorum: description: - Monitor quorum value when C(monitor_type) is C(m_of_n). - Quorum must be a value of 1 or greater when C(monitor_type) is C(m_of_n). - version_added: "1.3" + version_added: 1.3 monitors: description: - Monitor template name list. If the partition is not provided as part of the monitor name, then the C(partition) option will be used instead. - version_added: "1.3" + version_added: 1.3 slow_ramp_time: description: - Sets the ramp-up time (in seconds) to gradually ramp up the load on newly added or freshly detected up pool members. - version_added: "1.3" + version_added: 1.3 reselect_tries: description: - Sets the number of times the system tries to contact a pool member after a passive failure. - version_added: "2.2" + version_added: 2.2 service_down_action: description: - Sets the action to take when node goes down in pool. - version_added: "1.3" + version_added: 1.3 choices: - none - reset @@ -124,6 +124,25 @@ options: that are numbers. - Data will be persisted, not ephemeral. version_added: 2.5 + priority_group_activation: + description: + - Specifies whether the system load balances traffic according to the priority + number assigned to the pool member. + - When creating a new pool, if this parameter is not specified, the default of + C(0) will be used. + - To disable this setting, provide the value C(0). + - Once you enable this setting, you can specify pool member priority when you + create a new pool or on a pool member's properties screen. + - The system treats same-priority pool members as a group. + - To enable priority group activation, provide a number from C(0) to C(65535) + that represents the minimum number of members that must be available in one + priority group before the system directs traffic to members in a lower + priority group. + - When a sufficient number of members become available in the higher priority + group, the system again directs traffic to the higher priority group. + aliases: + - minimum_active_members + version_added: 2.6 notes: - Requires BIG-IP software version >= 12. - To add members do a pool, use the C(bigip_pool_member) module. Previously, the @@ -144,7 +163,7 @@ EXAMPLES = r''' state: present name: my-pool partition: Common - lb_method: least-connection-member + lb_method: least-connections-member slow_ramp_time: 120 delegate_to: localhost @@ -307,6 +326,11 @@ metadata: returned: changed type: dict sample: {'key1': 'foo', 'key2': 'bar'} +priority_group_activation: + description: The new minimum number of members to activate the priorty group. + returned: changed + type: int + sample: 10 ''' import re @@ -315,30 +339,25 @@ from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.basic import env_fallback from ansible.module_utils.six import iteritems -HAS_DEVEL_IMPORTS = False - try: - # Sideband repository used for dev from library.module_utils.network.f5.bigip import HAS_F5SDK from library.module_utils.network.f5.bigip import F5Client from library.module_utils.network.f5.common import F5ModuleError from library.module_utils.network.f5.common import AnsibleF5Parameters from library.module_utils.network.f5.common import cleanup_tokens - from library.module_utils.network.f5.common import fqdn_name + from library.module_utils.network.f5.common import fq_name from library.module_utils.network.f5.common import f5_argument_spec try: from library.module_utils.network.f5.common import iControlUnexpectedHTTPError except ImportError: HAS_F5SDK = False - HAS_DEVEL_IMPORTS = True except ImportError: - # Upstream Ansible from ansible.module_utils.network.f5.bigip import HAS_F5SDK from ansible.module_utils.network.f5.bigip import F5Client from ansible.module_utils.network.f5.common import F5ModuleError from ansible.module_utils.network.f5.common import AnsibleF5Parameters from ansible.module_utils.network.f5.common import cleanup_tokens - from ansible.module_utils.network.f5.common import fqdn_name + from ansible.module_utils.network.f5.common import fq_name from ansible.module_utils.network.f5.common import f5_argument_spec try: from ansible.module_utils.network.f5.common import iControlUnexpectedHTTPError @@ -358,24 +377,26 @@ class Parameters(AnsibleF5Parameters): 'slowRampTime': 'slow_ramp_time', 'reselectTries': 'reselect_tries', 'serviceDownAction': 'service_down_action', - 'monitor': 'monitors' + 'monitor': 'monitors', + 'minActiveMembers': 'priority_group_activation' } api_attributes = [ 'description', 'name', 'loadBalancingMode', 'monitor', 'slowRampTime', - 'reselectTries', 'serviceDownAction', 'metadata' + 'reselectTries', 'serviceDownAction', 'metadata', 'minActiveMembers' ] returnables = [ 'monitor_type', 'quorum', 'monitors', 'service_down_action', 'description', 'lb_method', 'slow_ramp_time', - 'reselect_tries', 'monitor', 'name', 'partition', 'metadata' + 'reselect_tries', 'monitor', 'name', 'partition', 'metadata', + 'priority_group_activation' ] updatables = [ 'monitor_type', 'quorum', 'monitors', 'service_down_action', 'description', 'lb_method', 'slow_ramp_time', 'reselect_tries', - 'metadata' + 'metadata', 'priority_group_activation' ] @property @@ -389,23 +410,6 @@ class Parameters(AnsibleF5Parameters): raise F5ModuleError('Provided lb_method is unknown') return lb_method - def _fqdn_name(self, value): - if value is not None and not value.startswith('/'): - return '/{0}/{1}'.format(self.partition, value) - return value - - @property - def monitors(self): - if self._values['monitors'] is None: - return None - monitors = [self._fqdn_name(x) for x in self.monitors_list] - if self.monitor_type == 'm_of_n': - monitors = ' '.join(monitors) - result = 'min %s of { %s }' % (self.quorum, monitors) - else: - result = ' and '.join(monitors).strip() - return result - def _verify_quorum_type(self, quorum): try: if quorum is None: @@ -416,6 +420,24 @@ class Parameters(AnsibleF5Parameters): "The specified 'quorum' must be an integer." ) + @property + def monitors(self): + if self._values['monitors'] is None: + return None + monitors = [fq_name(self.partition, x) for x in self.monitors_list] + if self.monitor_type == 'm_of_n': + monitors = ' '.join(monitors) + result = 'min %s of { %s }' % (self.quorum, monitors) + else: + result = ' and '.join(monitors).strip() + return result + + @property + def priority_group_activation(self): + if self._values['priority_group_activation'] is None: + return None + return int(self._values['priority_group_activation']) + class ApiParameters(Parameters): @property @@ -447,7 +469,7 @@ class ApiParameters(Parameters): if self._values['monitors'] is None: return [] try: - result = re.findall(r'/\w+/[^\s}]+', self._values['monitors']) + result = re.findall(r'/[\w-]+/[^\s}]+', self._values['monitors']) return result except Exception: return self._values['monitors'] @@ -534,7 +556,7 @@ class UsableChanges(Changes): class ReportableChanges(Changes): @property def monitors(self): - result = sorted(re.findall(r'/\w+/[^\s}]+', self._values['monitors'])) + result = sorted(re.findall(r'/[\w-]+/[^\s}]+', self._values['monitors'])) return result @property @@ -794,6 +816,8 @@ class ModuleManager(object): raise F5ModuleError( "When using a 'monitor_type' of 'single', only one monitor may be provided" ) + if self.want.priority_group_activation is None: + self.want.update({'priority_group_activation': 0}) self._set_changed_options() if self.module.check_mode: @@ -903,6 +927,10 @@ class ArgumentSpec(object): partition=dict( default='Common', fallback=(env_fallback, ['F5_PARTITION']) + ), + priority_group_activation=dict( + type='int', + aliases=['minimum_active_members'] ) ) self.argument_spec = {} diff --git a/lib/ansible/modules/network/f5/bigip_pool_member.py b/lib/ansible/modules/network/f5/bigip_pool_member.py index 5e6407c972..58456bae11 100644 --- a/lib/ansible/modules/network/f5/bigip_pool_member.py +++ b/lib/ansible/modules/network/f5/bigip_pool_member.py @@ -20,17 +20,13 @@ short_description: Manages F5 BIG-IP LTM pool members description: - Manages F5 BIG-IP LTM pool members via iControl SOAP API. version_added: 1.4 -author: - - Matt Hite (@mhite) - - Tim Rupp (@caphrim007) -notes: - - Requires BIG-IP software version >= 11 - - F5 developed module 'bigsuds' required (see http://devcentral.f5.com) - - Best run as a local_action in your playbook - - Supersedes bigip_pool for managing pool members -requirements: - - bigsuds options: + name: + description: + - Name of the node to create, or re-use, when creating a new pool member. + - This parameter is optional and, if not specified, a node name will be + created automatically from either the specified C(address) or C(fqdn). + version_added: 2.6 state: description: - Pool member state. @@ -39,20 +35,9 @@ options: choices: - present - absent - session_state: - description: - - Set new session availability status for pool member. - version_added: 2.0 - choices: - - enabled - - disabled - monitor_state: - description: - - Set monitor availability status for pool member. - version_added: 2.0 - choices: - enabled - disabled + - forced_offline pool: description: - Pool name. This pool must exist. @@ -61,16 +46,32 @@ options: description: - Partition default: Common - host: + address: description: - - Pool member IP. - required: True + - 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. aliases: - - address - - name + - ip + - host + version_added: 2.2 + fqdn: + description: + - FQDN name of the pool member. This can be any name that is a valid RFC 1123 DNS + name. Therefore, the only characters that can be used are "A" to "Z", + "a" to "z", "0" to "9", the hyphen ("-") and the period ("."). + - FQDN names must include at lease one period; delineating the host from + the domain. ex. 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. + aliases: + - hostname + version_added: 2.6 port: description: - Pool member port. + - This value cannot be changed after it has been set. required: True connection_limit: description: @@ -89,10 +90,11 @@ options: to 1. preserve_node: description: - - When state is absent and the pool member is no longer referenced - in other pools, the default behavior removes the unused node - object. Setting this to 'yes' disables this behavior. - default: no + - When state is C(absent) attempts to remove the node that 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(yes) disables this behavior. type: bool version_added: 2.1 priority_group: @@ -106,7 +108,48 @@ options: - 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. version_added: 2.5 + 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(enabled), 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(disabled), 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(yes). + - This parameter is ignored when C(reuse_nodes) is C(yes). + type: bool + version_added: 2.6 + reuse_nodes: + description: + - Reuses node definitions if requested. + default: yes + type: bool + version_added: 2.6 + session_state: + description: + - Set new session availability status for pool member. + - This parameter is deprecated and will be removed in Ansible 2.7. Use C(state) + C(enabled) or C(disabled). + version_added: 2.0 + choices: + - enabled + - disabled + monitor_state: + description: + - Set monitor availability status for pool member. + - This parameter is deprecated and will be removed in Ansible 2.7. Use C(state) + C(enabled) or C(disabled). + version_added: 2.0 + choices: + - enabled + - disabled extends_documentation_fragment: f5 +author: + - Tim Rupp (@caphrim007) ''' EXAMPLES = ''' @@ -152,398 +195,694 @@ EXAMPLES = ''' port: 80 delegate_to: localhost - -# The BIG-IP GUI doesn't map directly to the API calls for "Pool -> -# Members -> State". The following states map to API monitor -# and session states. -# -# Enabled (all traffic allowed): -# monitor_state=enabled, session_state=enabled -# Disabled (only persistent or active connections allowed): -# monitor_state=enabled, session_state=disabled -# Forced offline (only active connections allowed): -# monitor_state=disabled, session_state=disabled -# -# See https://devcentral.f5.com/questions/icontrol-equivalent-call-for-b-node-down - - name: Force pool member offline bigip_pool_member: server: lb.mydomain.com user: admin password: secret - state: present - session_state: disabled - monitor_state: disabled + state: forced_offline pool: my-pool partition: Common host: "{{ ansible_default_ipv4['address'] }}" port: 80 delegate_to: localhost + +- name: Create members with priority groups + bigip_pool_member: + server: lb.mydomain.com + user: admin + password: secret + pool: my-pool + partition: Common + host: "{{ item.address }}" + name: "{{ item.name }}" + priority_group: "{{ item.priority_group }}" + port: 80 + delegate_to: localhost + loop: + - host: 1.1.1.1 + name: web1 + priority_group: 4 + - host: 2.2.2.2 + name: web2 + priority_group: 3 + - host: 3.3.3.3 + name: web3 + priority_group: 2 + - host: 4.4.4.4 + name: web4 + priority_group: 1 ''' -try: - import bigsuds - HAS_BIGSUDS = True -except ImportError: - pass # Handled by f5_utils.bigsuds_found +RETURN = ''' +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: string + 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: string + sample: foo.bar.com +address: + description: The address of the pool member. + returned: changed + type: string + sample: 1.2.3.4 +''' from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.basic import env_fallback -from ansible.module_utils.f5_utils import bigip_api, bigsuds_found - -HAS_DEVEL_IMPORTS = False try: + from library.module_utils.network.f5.bigip import HAS_F5SDK + from library.module_utils.network.f5.bigip import F5Client + from library.module_utils.network.f5.common import F5ModuleError + from library.module_utils.network.f5.common import AnsibleF5Parameters + from library.module_utils.network.f5.common import cleanup_tokens + from library.module_utils.network.f5.common import fq_name + from library.module_utils.network.f5.common import is_valid_hostname from library.module_utils.network.f5.common import f5_argument_spec - from library.module_utils.network.f5.common import fqdn_name - HAS_DEVEL_IMPORTS = True + try: + from library.module_utils.network.f5.common import iControlUnexpectedHTTPError + except ImportError: + HAS_F5SDK = False except ImportError: - from ansible.module_utils.network.f5.common import fqdn_name + from ansible.module_utils.network.f5.bigip import HAS_F5SDK + from ansible.module_utils.network.f5.bigip import F5Client + from ansible.module_utils.network.f5.common import F5ModuleError + from ansible.module_utils.network.f5.common import AnsibleF5Parameters + from ansible.module_utils.network.f5.common import cleanup_tokens + from ansible.module_utils.network.f5.common import fq_name + from ansible.module_utils.network.f5.common import is_valid_hostname from ansible.module_utils.network.f5.common import f5_argument_spec - - -def pool_exists(api, pool): - # hack to determine if pool exists - result = False try: - api.LocalLB.Pool.get_object_status(pool_names=[pool]) - result = True - except bigsuds.OperationFailed as e: - if "was not found" in str(e): - result = False + from ansible.module_utils.network.f5.common import iControlUnexpectedHTTPError + except ImportError: + HAS_F5SDK = False + +try: + import netaddr + HAS_NETADDR = True +except ImportError: + HAS_NETADDR = False + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'rateLimit': 'rate_limit', + 'connectionLimit': 'connection_limit', + 'priorityGroup': 'priority_group', + } + + api_attributes = [ + 'rateLimit', 'connectionLimit', 'description', 'ratio', 'priorityGroup', + 'address', 'fqdn', 'session', 'state' + ] + + returnables = [ + 'rate_limit', 'connection_limit', 'description', 'ratio', 'priority_group', + 'fqdn_auto_populate', 'session', 'state', 'fqdn', 'address' + ] + + updatables = [ + 'rate_limit', 'connection_limit', 'description', 'ratio', 'priority_group', + 'fqdn_auto_populate', 'state' + ] + + +class ModuleParameters(Parameters): + @property + def full_name(self): + if self._values['name'] is None: + name = self._values['address'] if self._values['address'] else self._values['fqdn'] else: - # genuine exception - raise - return result + name = self._values['name'] + return '{0}:{1}'.format(name, self.port) + @property + def node_name(self): + return self.full_name.split(':')[0] -def member_exists(api, pool, address, port): - # hack to determine if member exists - result = False - try: - members = [{'address': address, 'port': port}] - api.LocalLB.Pool.get_member_object_status(pool_names=[pool], - members=[members]) - result = True - except bigsuds.OperationFailed as e: - if "was not found" in str(e): - result = False + @property + def fqdn_name(self): + return self._values['fqdn'] + + @property + def fqdn(self): + result = {} + if self.fqdn_auto_populate: + result['autopopulate'] = 'enabled' else: - # genuine exception - raise - return result + result['autopopulate'] = 'disabled' + if self._values['fqdn'] is None: + return result + if not is_valid_hostname(self._values['fqdn']): + raise F5ModuleError( + "The specified 'fqdn' is not a valid hostname." + ) + 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 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 state(self): + # TODO(Remove all of this state craziness in 2.7) + if self.session_state is not None or self.monitor_state is not None: + if self._values['state'] in ['enabled', 'disabled', 'forced_offline']: + self._values['__warnings'].append([{ + 'msg': "'session_state' is deprecated and will be ignored in favor of 'state'.", + 'version': '2.7' + }]) + return self._values['state'] + else: + if self.session_state is not None: + self._values['__warnings'].append([{ + 'msg': "'session_state' is deprecated and will be removed in the future. Use 'state'.", + 'version': '2.7' + }]) + elif self.monitor_state is not None: + self._values['__warnings'].append([{ + 'msg': "'monitor_state' is deprecated and will be removed in the future. Use 'state'.", + 'version': '2.7' + }]) + + if self.session_state == 'enabled' and self.monitor_state == 'enabled': + return 'enabled' + elif self.session_state == 'disabled' and self.monitor_state == 'enabled': + return 'disabled' + else: + return 'forced_offline' + return self._values['state'] + + @property + def address(self): + if self._values['address'] is None: + return None + elif self._values['address'] == 'any6': + return 'any6' + try: + addr = netaddr.IPAddress(self._values['address']) + return str(addr) + except netaddr.AddrFormatError: + raise F5ModuleError( + "The specified 'address' value is not a valid IP address." + ) -def delete_node_address(api, address): - result = False - try: - api.LocalLB.NodeAddressV2.delete_node_address(nodes=[address]) - result = True - except bigsuds.OperationFailed as e: - if "is referenced by a member of pool" in str(e): - result = False +class ApiParameters(Parameters): + @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'] and self._values['session'] in ['user-enabled']: + return 'present' + elif self._values['state'] in ['user-down'] and self._values['session'] in ['user-disabled']: + return 'forced_offline' else: - # genuine exception - raise - return result + return 'disabled' -def remove_pool_member(api, pool, address, port): - members = [{'address': address, 'port': port}] - api.LocalLB.Pool.remove_member_v2( - pool_names=[pool], - members=[members] - ) +class NodeApiParameters(Parameters): + pass -def add_pool_member(api, pool, address, port): - members = [{'address': address, 'port': port}] - api.LocalLB.Pool.add_member_v2( - pool_names=[pool], - members=[members] - ) +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 -def get_connection_limit(api, pool, address, port): - members = [{'address': address, 'port': port}] - result = api.LocalLB.Pool.get_member_connection_limit( - pool_names=[pool], - members=[members] - )[0][0] - return result +class UsableChanges(Changes): + pass -def set_connection_limit(api, pool, address, port, limit): - members = [{'address': address, 'port': port}] - api.LocalLB.Pool.set_member_connection_limit( - pool_names=[pool], - members=[members], - limits=[[limit]] - ) +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'] and self._values['session'] in ['user-enabled']: + return 'present' + elif self._values['state'] in ['user-down'] and self._values['session'] in ['user-disabled']: + return 'forced_offline' + else: + return 'disabled' -def get_description(api, pool, address, port): - members = [{'address': address, 'port': port}] - result = api.LocalLB.Pool.get_member_description( - pool_names=[pool], - members=[members] - )[0][0] - 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' + } -def set_description(api, pool, address, port, description): - members = [{'address': address, 'port': port}] - api.LocalLB.Pool.set_member_description( - pool_names=[pool], - members=[members], - descriptions=[[description]] - ) +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = kwargs.get('client', None) + self.want = ModuleParameters(params=self.module.params) + self.have = ApiParameters() + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def exec_module(self): + changed = False + result = dict() + state = self.want.state + + try: + if state in ['present', 'present', 'enabled', 'disabled', 'forced_offline']: + changed = self.present() + elif state == "absent": + changed = self.absent() + except iControlUnexpectedHTTPError as e: + raise F5ModuleError(str(e)) + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def exists(self): + try: + pool = self.client.api.tm.ltm.pools.pool.load( + name=self.want.pool, + partition=self.want.partition + ) + except Exception: + raise F5ModuleError('The specified pool does not exist') + result = pool.members_s.members.exists( + name=self.want.full_name, + partition=self.want.partition + ) + return result + + def node_exists(self): + resource = self.client.api.tm.ltm.nodes.node.exists( + name=self.want.node_name, + partition=self.want.partition + ) + return resource + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if not self.want.preserve_node: + self.remove_node_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def _set_host_by_name(self): + try: + netaddr.IPAddress(self.want.name) + self.want.update({ + 'fqdn': None, + 'address': self.want.name + }) + except netaddr.AddrFormatError: + 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', + + # TODO(Remove in 2.7) + 'session_state': None, + 'monitor_state': None + }) + elif self.want.state == 'disabled': + self.want.update({ + 'state': 'user-up', + 'session': 'user-disabled', + + # TODO(Remove in 2.7) + 'session_state': None, + 'monitor_state': None + }) + elif self.want.state in ['present', 'enabled']: + self.want.update({ + 'state': 'user-up', + 'session': 'user-enabled', + + # TODO(Remove in 2.7) + 'session_state': None, + 'monitor_state': None + }) + + 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 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() + + self._update_api_state_attributes() + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() + return True + + def create_on_device(self): + params = self.changes.api_params() + pool = self.client.api.tm.ltm.pools.pool.load( + name=self.want.pool, + partition=self.want.partition + ) + pool.members_s.members.create( + name=self.want.full_name, + partition=self.want.partition, + **params + ) + + def update_on_device(self): + params = self.changes.api_params() + pool = self.client.api.tm.ltm.pools.pool.load( + name=self.want.pool, + partition=self.want.partition + ) + resource = pool.members_s.members.load( + name=self.want.full_name, + partition=self.want.partition + ) + resource.modify(**params) + + 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 remove_from_device(self): + pool = self.client.api.tm.ltm.pools.pool.load( + name=self.want.pool, + partition=self.want.partition + ) + resource = pool.members_s.members.load( + name=self.want.full_name, + partition=self.want.partition + ) + if resource: + resource.delete() + + def remove_node_from_device(self): + resource = self.client.api.tm.ltm.nodes.node.load( + name=self.want.node_name, + partition=self.want.partition + ) + if resource: + resource.delete() + + def read_current_from_device(self): + pool = self.client.api.tm.ltm.pools.pool.load( + name=self.want.pool, + partition=self.want.partition + ) + resource = pool.members_s.members.load( + name=self.want.full_name, + partition=self.want.partition + ) + return ApiParameters(params=resource.attrs) + + def read_current_node_from_device(self, node): + resource = self.client.api.tm.ltm.nodes.node.load( + name=node, + partition=self.want.partition + ) + return NodeApiParameters(params=resource.attrs) -def get_rate_limit(api, pool, address, port): - members = [{'address': address, 'port': port}] - result = api.LocalLB.Pool.get_member_rate_limit( - pool_names=[pool], - members=[members] - )[0][0] - return result +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + pool=dict(required=True), + address=dict(aliases=['host', 'ip']), + fqdn=dict( + aliases=['hostname'] + ), + name=dict(), + port=dict(type='int', required=True), + 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'] + ), + partition=dict( + default='Common', + fallback=(env_fallback, ['F5_PARTITION']) + ), + fqdn_auto_populate=dict(type='bool'), + reuse_nodes=dict(type='bool', default=True), - -def set_rate_limit(api, pool, address, port, limit): - members = [{'address': address, 'port': port}] - api.LocalLB.Pool.set_member_rate_limit( - pool_names=[pool], - members=[members], - limits=[[limit]] - ) - - -def get_ratio(api, pool, address, port): - members = [{'address': address, 'port': port}] - result = api.LocalLB.Pool.get_member_ratio( - pool_names=[pool], - members=[members] - )[0][0] - return result - - -def set_ratio(api, pool, address, port, ratio): - members = [{'address': address, 'port': port}] - api.LocalLB.Pool.set_member_ratio( - pool_names=[pool], - members=[members], - ratios=[[ratio]] - ) - - -def get_priority_group(api, pool, address, port): - members = [{'address': address, 'port': port}] - result = api.LocalLB.Pool.get_member_priority( - pool_names=[pool], - members=[members] - )[0][0] - return result - - -def set_priority_group(api, pool, address, port, priority_group): - members = [{'address': address, 'port': port}] - api.LocalLB.Pool.set_member_priority( - pool_names=[pool], - members=[members], - priorities=[[priority_group]] - ) - - -def set_member_session_enabled_state(api, pool, address, port, session_state): - members = [{'address': address, 'port': port}] - session_state = ["STATE_%s" % session_state.strip().upper()] - api.LocalLB.Pool.set_member_session_enabled_state( - pool_names=[pool], - members=[members], - session_states=[session_state] - ) - - -def get_member_session_status(api, pool, address, port): - members = [{'address': address, 'port': port}] - result = api.LocalLB.Pool.get_member_session_status( - pool_names=[pool], - members=[members] - )[0][0] - result = result.split("SESSION_STATUS_")[-1].lower() - return result - - -def set_member_monitor_state(api, pool, address, port, monitor_state): - members = [{'address': address, 'port': port}] - monitor_state = ["STATE_%s" % monitor_state.strip().upper()] - api.LocalLB.Pool.set_member_monitor_state( - pool_names=[pool], - members=[members], - monitor_states=[monitor_state] - ) - - -def get_member_monitor_status(api, pool, address, port): - members = [{'address': address, 'port': port}] - result = api.LocalLB.Pool.get_member_monitor_status( - pool_names=[pool], - members=[members] - )[0][0] - result = result.split("MONITOR_STATUS_")[-1].lower() - return result + # Deprecated params + # TODO(Remove in 2.7) + session_state=dict(choices=['enabled', 'disabled']), + monitor_state=dict(choices=['enabled', 'disabled']), + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + self.mutually_exclusive = [ + ['address', 'fqdn'] + ] + self.required_one_of = [ + ['name', 'address', 'fqdn'], + ] def main(): - result = {} - argument_spec = f5_argument_spec - - meta_args = dict( - session_state=dict(type='str', choices=['enabled', 'disabled']), - monitor_state=dict(type='str', choices=['enabled', 'disabled']), - pool=dict(type='str', required=True), - host=dict(type='str', required=True, aliases=['address', 'name']), - port=dict(type='int', required=True), - connection_limit=dict(type='int'), - description=dict(type='str'), - rate_limit=dict(type='int'), - ratio=dict(type='int'), - preserve_node=dict(type='bool', default=False), - priority_group=dict(type='int'), - state=dict(default='present', choices=['absent', 'present']), - partition=dict( - default='Common', - fallback=(env_fallback, ['F5_PARTITION']) - ) - ) - argument_spec.update(meta_args) + spec = ArgumentSpec() module = AnsibleModule( - argument_spec=argument_spec, - supports_check_mode=True + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode ) - - if not bigsuds_found: - module.fail_json(msg="the python bigsuds module is required") - - if module.params['validate_certs']: - import ssl - if not hasattr(ssl, 'SSLContext'): - module.fail_json( - msg='bigsuds does not support verifying certificates with python < 2.7.9. ' - 'Either update python or set validate_certs=False on the task') - - server = module.params['server'] - server_port = module.params['server_port'] - user = module.params['user'] - password = module.params['password'] - state = module.params['state'] - partition = module.params['partition'] - validate_certs = module.params['validate_certs'] - - session_state = module.params['session_state'] - monitor_state = module.params['monitor_state'] - pool = fqdn_name(partition, module.params['pool']) - connection_limit = module.params['connection_limit'] - description = module.params['description'] - rate_limit = module.params['rate_limit'] - ratio = module.params['ratio'] - priority_group = module.params['priority_group'] - host = module.params['host'] - address = fqdn_name(partition, host) - port = module.params['port'] - preserve_node = module.params['preserve_node'] - - if (host and port is None) or (port is not None and not host): - module.fail_json(msg="both host and port must be supplied") - - if 0 > port or port > 65535: - module.fail_json(msg="valid ports must be in range 0 - 65535") + if not HAS_F5SDK: + module.fail_json(msg="The python f5-sdk module is required") + if not HAS_NETADDR: + module.fail_json(msg="The python netaddr module is required") try: - api = bigip_api(server, user, password, validate_certs, port=server_port) - if not pool_exists(api, pool): - module.fail_json(msg="pool %s does not exist" % pool) - result = {'changed': False} # default - - if state == 'absent': - if member_exists(api, pool, address, port): - if not module.check_mode: - remove_pool_member(api, pool, address, port) - if preserve_node: - result = {'changed': True} - else: - deleted = delete_node_address(api, address) - result = {'changed': True, 'deleted': deleted} - else: - result = {'changed': True} - - elif state == 'present': - if not member_exists(api, pool, address, port): - if not module.check_mode: - add_pool_member(api, pool, address, port) - if connection_limit is not None: - set_connection_limit(api, pool, address, port, connection_limit) - if description is not None: - set_description(api, pool, address, port, description) - if rate_limit is not None: - set_rate_limit(api, pool, address, port, rate_limit) - if ratio is not None: - set_ratio(api, pool, address, port, ratio) - if session_state is not None: - set_member_session_enabled_state(api, pool, address, port, session_state) - if monitor_state is not None: - set_member_monitor_state(api, pool, address, port, monitor_state) - if priority_group is not None: - set_priority_group(api, pool, address, port, priority_group) - result = {'changed': True} - else: - # pool member exists -- potentially modify attributes - if connection_limit is not None and connection_limit != get_connection_limit(api, pool, address, port): - if not module.check_mode: - set_connection_limit(api, pool, address, port, connection_limit) - result = {'changed': True} - if description is not None and description != get_description(api, pool, address, port): - if not module.check_mode: - set_description(api, pool, address, port, description) - result = {'changed': True} - if rate_limit is not None and rate_limit != get_rate_limit(api, pool, address, port): - if not module.check_mode: - set_rate_limit(api, pool, address, port, rate_limit) - result = {'changed': True} - if ratio is not None and ratio != get_ratio(api, pool, address, port): - if not module.check_mode: - set_ratio(api, pool, address, port, ratio) - result = {'changed': True} - if session_state is not None: - session_status = get_member_session_status(api, pool, address, port) - if session_state == 'enabled' and session_status == 'forced_disabled': - if not module.check_mode: - set_member_session_enabled_state(api, pool, address, port, session_state) - result = {'changed': True} - elif session_state == 'disabled' and session_status != 'forced_disabled': - if not module.check_mode: - set_member_session_enabled_state(api, pool, address, port, session_state) - result = {'changed': True} - if monitor_state is not None: - monitor_status = get_member_monitor_status(api, pool, address, port) - if monitor_state == 'enabled' and monitor_status == 'forced_down': - if not module.check_mode: - set_member_monitor_state(api, pool, address, port, monitor_state) - result = {'changed': True} - elif monitor_state == 'disabled' and monitor_status != 'forced_down': - if not module.check_mode: - set_member_monitor_state(api, pool, address, port, monitor_state) - result = {'changed': True} - if priority_group is not None and priority_group != get_priority_group(api, pool, address, port): - if not module.check_mode: - set_priority_group(api, pool, address, port, priority_group) - result = {'changed': True} - - except Exception as e: - module.fail_json(msg="received exception: %s" % e) - - module.exit_json(**result) + client = F5Client(**module.params) + mm = ModuleManager(module=module, client=client) + results = mm.exec_module() + cleanup_tokens(client) + module.exit_json(**results) + except F5ModuleError as ex: + cleanup_tokens(client) + module.fail_json(msg=str(ex)) if __name__ == '__main__': diff --git a/lib/ansible/modules/network/f5/bigip_profile_client_ssl.py b/lib/ansible/modules/network/f5/bigip_profile_client_ssl.py index 3da33084fd..debee4af56 100644 --- a/lib/ansible/modules/network/f5/bigip_profile_client_ssl.py +++ b/lib/ansible/modules/network/f5/bigip_profile_client_ssl.py @@ -16,8 +16,9 @@ DOCUMENTATION = r''' --- module: bigip_profile_client_ssl short_description: Manages client SSL profiles on a BIG-IP -description: Manages client SSL profiles on a BIG-IP. -version_added: "2.5" +description: + - Manages client SSL profiles on a BIG-IP. +version_added: 2.5 options: name: description: @@ -41,7 +42,8 @@ options: type of each certificate/key type. This means that you can only have one RSA, one DSA, and one ECDSA per profile. If you attempt to assign two RSA, DSA, or ECDSA certificate/key combo, the device will reject this. - - This list is a complex list that specifies a number of keys. There are several supported keys. + - This list is a complex list that specifies a number of keys. There are + several supported keys. suboptions: cert: description: @@ -131,30 +133,25 @@ from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.basic import env_fallback from ansible.module_utils.six import iteritems -HAS_DEVEL_IMPORTS = False - try: - # Sideband repository used for dev from library.module_utils.network.f5.bigip import HAS_F5SDK from library.module_utils.network.f5.bigip import F5Client from library.module_utils.network.f5.common import F5ModuleError from library.module_utils.network.f5.common import AnsibleF5Parameters from library.module_utils.network.f5.common import cleanup_tokens - from library.module_utils.network.f5.common import fqdn_name + from library.module_utils.network.f5.common import fq_name from library.module_utils.network.f5.common import f5_argument_spec try: from library.module_utils.network.f5.common import iControlUnexpectedHTTPError except ImportError: HAS_F5SDK = False - HAS_DEVEL_IMPORTS = True except ImportError: - # Upstream Ansible from ansible.module_utils.network.f5.bigip import HAS_F5SDK from ansible.module_utils.network.f5.bigip import F5Client from ansible.module_utils.network.f5.common import F5ModuleError from ansible.module_utils.network.f5.common import AnsibleF5Parameters from ansible.module_utils.network.f5.common import cleanup_tokens - from ansible.module_utils.network.f5.common import fqdn_name + from ansible.module_utils.network.f5.common import fq_name from ansible.module_utils.network.f5.common import f5_argument_spec try: from ansible.module_utils.network.f5.common import iControlUnexpectedHTTPError @@ -181,11 +178,6 @@ class Parameters(AnsibleF5Parameters): class ModuleParameters(Parameters): - def _fqdn_name(self, value): - if value is not None and not value.startswith('/'): - return '/{0}/{1}'.format(self.partition, value) - return value - def _key_filename(self, name): if name.endswith('.key'): return name @@ -202,14 +194,14 @@ class ModuleParameters(Parameters): if 'chain' not in item or item['chain'] == 'none': result = 'none' else: - result = self._cert_filename(self._fqdn_name(item['chain'])) + result = self._cert_filename(fq_name(self.partition, item['chain'])) return result @property def parent(self): if self._values['parent'] is None: return None - result = self._fqdn_name(self._values['parent']) + result = fq_name(self.partition, self._values['parent']) return result @property @@ -233,8 +225,8 @@ class ModuleParameters(Parameters): filename, ex = os.path.splitext(name) tmp = { 'name': filename, - 'cert': self._fqdn_name(cert), - 'key': self._fqdn_name(key), + 'cert': fq_name(self.partition, cert), + 'key': fq_name(self.partition, key), 'chain': chain } if 'passphrase' in item: diff --git a/test/sanity/validate-modules/ignore.txt b/test/sanity/validate-modules/ignore.txt index d72cac92bc..cb7f5c5b9e 100644 --- a/test/sanity/validate-modules/ignore.txt +++ b/test/sanity/validate-modules/ignore.txt @@ -1079,7 +1079,6 @@ lib/ansible/modules/network/f5/bigip_iapp_service.py E324 lib/ansible/modules/network/f5/bigip_iapp_service.py E325 lib/ansible/modules/network/f5/bigip_monitor_snmp_dca.py E326 lib/ansible/modules/network/f5/bigip_policy.py E324 -lib/ansible/modules/network/f5/bigip_pool.py E326 lib/ansible/modules/network/f5/bigip_profile_client_ssl.py E324 lib/ansible/modules/network/f5/bigip_selfip.py E324 lib/ansible/modules/network/f5/bigip_sys_global.py E326 diff --git a/test/units/modules/network/f5/fixtures/load_net_node_with_fqdn.json b/test/units/modules/network/f5/fixtures/load_net_node_with_fqdn.json new file mode 100644 index 0000000000..67c02568d5 --- /dev/null +++ b/test/units/modules/network/f5/fixtures/load_net_node_with_fqdn.json @@ -0,0 +1,25 @@ +{ + "kind": "tm:ltm:node:nodestate", + "name": "foo.bar.com", + "partition": "Common", + "fullPath": "/Common/foo.bar.com", + "generation": 157, + "selfLink": "https://localhost/mgmt/tm/ltm/node/~Common~foo.bar.com?ver=12.0.0", + "address": "any6", + "connectionLimit": 0, + "dynamicRatio": 1, + "ephemeral": "false", + "fqdn": { + "addressFamily": "ipv4", + "autopopulate": "enabled", + "downInterval": 5, + "interval": "3600", + "tmName": "foo.bar.com" + }, + "logging": "disabled", + "monitor": "default", + "rateLimit": "disabled", + "ratio": 1, + "session": "user-enabled", + "state": "fqdn-up-no-addr" +} diff --git a/test/units/modules/network/f5/fixtures/load_net_node_with_ipv4_address.json b/test/units/modules/network/f5/fixtures/load_net_node_with_ipv4_address.json new file mode 100644 index 0000000000..9e3be88929 --- /dev/null +++ b/test/units/modules/network/f5/fixtures/load_net_node_with_ipv4_address.json @@ -0,0 +1,24 @@ +{ + "kind": "tm:ltm:node:nodestate", + "name": "7.3.67.8", + "partition": "Common", + "fullPath": "/Common/7.3.67.8", + "generation": 162, + "selfLink": "https://localhost/mgmt/tm/ltm/node/~Common~7.3.67.8?ver=12.0.0", + "address": "7.3.67.8", + "connectionLimit": 0, + "dynamicRatio": 1, + "ephemeral": "false", + "fqdn": { + "addressFamily": "ipv4", + "autopopulate": "disabled", + "downInterval": 5, + "interval": "3600" + }, + "logging": "disabled", + "monitor": "default", + "rateLimit": "disabled", + "ratio": 1, + "session": "user-enabled", + "state": "unchecked" +} diff --git a/test/units/modules/network/f5/test_bigip_monitor_udp.py b/test/units/modules/network/f5/test_bigip_monitor_udp.py index e22cc57517..c9431d4e3d 100644 --- a/test/units/modules/network/f5/test_bigip_monitor_udp.py +++ b/test/units/modules/network/f5/test_bigip_monitor_udp.py @@ -21,9 +21,9 @@ from ansible.compat.tests.mock import patch from ansible.module_utils.basic import AnsibleModule try: - from library.bigip_monitor_udp import Parameters - from library.bigip_monitor_udp import ModuleManager - from library.bigip_monitor_udp import ArgumentSpec + from library.modules.bigip_monitor_udp import Parameters + from library.modules.bigip_monitor_udp import ModuleManager + from library.modules.bigip_monitor_udp import ArgumentSpec from library.module_utils.network.f5.common import F5ModuleError from library.module_utils.network.f5.common import iControlUnexpectedHTTPError from test.unit.modules.utils import set_module_args diff --git a/test/units/modules/network/f5/test_bigip_node.py b/test/units/modules/network/f5/test_bigip_node.py index 384270d307..0729e31526 100644 --- a/test/units/modules/network/f5/test_bigip_node.py +++ b/test/units/modules/network/f5/test_bigip_node.py @@ -20,9 +20,9 @@ from ansible.compat.tests.mock import patch from ansible.module_utils.basic import AnsibleModule try: - from library.bigip_node import Parameters - from library.bigip_node import ModuleManager - from library.bigip_node import ArgumentSpec + from library.modules.bigip_node import Parameters + from library.modules.bigip_node import ModuleManager + from library.modules.bigip_node import ArgumentSpec from library.module_utils.network.f5.common import F5ModuleError from library.module_utils.network.f5.common import iControlUnexpectedHTTPError from test.unit.modules.utils import set_module_args diff --git a/test/units/modules/network/f5/test_bigip_partition.py b/test/units/modules/network/f5/test_bigip_partition.py index 795427ee5f..c4aa79f7b2 100644 --- a/test/units/modules/network/f5/test_bigip_partition.py +++ b/test/units/modules/network/f5/test_bigip_partition.py @@ -20,9 +20,9 @@ from ansible.compat.tests.mock import patch from ansible.module_utils.basic import AnsibleModule try: - from library.bigip_partition import Parameters - from library.bigip_partition import ModuleManager - from library.bigip_partition import ArgumentSpec + from library.modules.bigip_partition import Parameters + from library.modules.bigip_partition import ModuleManager + from library.modules.bigip_partition import ArgumentSpec from library.module_utils.network.f5.common import F5ModuleError from library.module_utils.network.f5.common import iControlUnexpectedHTTPError from test.unit.modules.utils import set_module_args diff --git a/test/units/modules/network/f5/test_bigip_policy.py b/test/units/modules/network/f5/test_bigip_policy.py index d9e29b0b8a..0441025076 100644 --- a/test/units/modules/network/f5/test_bigip_policy.py +++ b/test/units/modules/network/f5/test_bigip_policy.py @@ -20,11 +20,11 @@ from ansible.compat.tests.mock import patch from ansible.module_utils.basic import AnsibleModule try: - from library.bigip_policy import Parameters - from library.bigip_policy import ModuleManager - from library.bigip_policy import SimpleManager - from library.bigip_policy import ComplexManager - from library.bigip_policy import ArgumentSpec + from library.modules.bigip_policy import Parameters + from library.modules.bigip_policy import ModuleManager + from library.modules.bigip_policy import SimpleManager + from library.modules.bigip_policy import ComplexManager + from library.modules.bigip_policy import ArgumentSpec from library.module_utils.network.f5.common import F5ModuleError from library.module_utils.network.f5.common import iControlUnexpectedHTTPError from test.unit.modules.utils import set_module_args diff --git a/test/units/modules/network/f5/test_bigip_policy_rule.py b/test/units/modules/network/f5/test_bigip_policy_rule.py index adde3d72ef..33f82e8a1f 100644 --- a/test/units/modules/network/f5/test_bigip_policy_rule.py +++ b/test/units/modules/network/f5/test_bigip_policy_rule.py @@ -20,11 +20,11 @@ from ansible.compat.tests.mock import patch from ansible.module_utils.basic import AnsibleModule try: - from library.bigip_policy_rule import Parameters - from library.bigip_policy_rule import ModuleParameters - from library.bigip_policy_rule import ApiParameters - from library.bigip_policy_rule import ModuleManager - from library.bigip_policy_rule import ArgumentSpec + from library.modules.bigip_policy_rule import Parameters + from library.modules.bigip_policy_rule import ModuleParameters + from library.modules.bigip_policy_rule import ApiParameters + from library.modules.bigip_policy_rule import ModuleManager + from library.modules.bigip_policy_rule import ArgumentSpec from library.module_utils.network.f5.common import F5ModuleError from library.module_utils.network.f5.common import iControlUnexpectedHTTPError from test.unit.modules.utils import set_module_args diff --git a/test/units/modules/network/f5/test_bigip_pool.py b/test/units/modules/network/f5/test_bigip_pool.py index 5c69c09b50..e2b2d788b5 100644 --- a/test/units/modules/network/f5/test_bigip_pool.py +++ b/test/units/modules/network/f5/test_bigip_pool.py @@ -21,10 +21,10 @@ from ansible.compat.tests.mock import patch from ansible.module_utils.basic import AnsibleModule try: - from library.bigip_pool import ApiParameters - from library.bigip_pool import ModuleParameters - from library.bigip_pool import ModuleManager - from library.bigip_pool import ArgumentSpec + from library.modules.bigip_pool import ApiParameters + from library.modules.bigip_pool import ModuleParameters + from library.modules.bigip_pool import ModuleManager + from library.modules.bigip_pool import ArgumentSpec from library.module_utils.network.f5.common import F5ModuleError from library.module_utils.network.f5.common import iControlUnexpectedHTTPError from test.unit.modules.utils import set_module_args diff --git a/test/units/modules/network/f5/test_bigip_pool_member.py b/test/units/modules/network/f5/test_bigip_pool_member.py new file mode 100644 index 0000000000..b1f15a6dc8 --- /dev/null +++ b/test/units/modules/network/f5/test_bigip_pool_member.py @@ -0,0 +1,338 @@ +# -*- 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 pytest +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 Mock +from ansible.compat.tests.mock import patch +from ansible.module_utils.basic import AnsibleModule + +try: + from library.modules.bigip_pool_member import ModuleParameters + from library.modules.bigip_pool_member import ApiParameters + from library.modules.bigip_pool_member import NodeApiParameters + from library.modules.bigip_pool_member import ModuleManager + from library.modules.bigip_pool_member import ArgumentSpec + from library.module_utils.network.f5.common import F5ModuleError + from library.module_utils.network.f5.common import iControlUnexpectedHTTPError + from test.unit.modules.utils import set_module_args +except ImportError: + try: + from ansible.modules.network.f5.bigip_pool_member import ModuleParameters + from ansible.modules.network.f5.bigip_pool_member import ApiParameters + from ansible.modules.network.f5.bigip_pool_member import NodeApiParameters + from ansible.modules.network.f5.bigip_pool_member import ModuleManager + from ansible.modules.network.f5.bigip_pool_member import ArgumentSpec + from ansible.module_utils.network.f5.common import F5ModuleError + from ansible.module_utils.network.f5.common import iControlUnexpectedHTTPError + from units.modules.utils import set_module_args + 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 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 TestParameters(unittest.TestCase): + def test_module_parameters(self): + args = dict( + pool='my-pool', + address='1.2.3.4', + fqdn='fqdn.foo.bar', + name='my-name', + port=2345, + connection_limit=100, + description='this is a description', + rate_limit=70, + ratio=20, + preserve_node=False, + priority_group=10, + state='present', + partition='Common', + fqdn_auto_populate=False, + reuse_nodes=False, + + # Deprecated params + # TODO(Remove in 2.7) + session_state='disabled', + monitor_state='disabled', + ) + + p = ModuleParameters(params=args) + assert p.name == 'my-name' + + def test_api_parameters(self): + args = load_fixture('load_net_node_with_fqdn.json') + p = ApiParameters(params=args) + assert p.state == 'present' + + +class TestManager(unittest.TestCase): + + def setUp(self): + self.spec = ArgumentSpec() + + def test_create_reuse_node_with_name(self, *args): + # Configure the arguments that would be sent to the Ansible module + set_module_args(dict( + pool='my-pool', + name='my-name', + port=2345, + state='present', + partition='Common', + reuse_nodes=True, + password='password', + server='localhost', + user='admin' + )) + + current_node = NodeApiParameters(params=load_fixture('load_net_node_with_fqdn.json')) + module = AnsibleModule( + argument_spec=self.spec.argument_spec, + supports_check_mode=self.spec.supports_check_mode + ) + mm = ModuleManager(module=module) + + # Override methods to force specific logic in the module to happen + mm.exists = Mock(return_value=False) + mm.create_on_device = Mock(return_value=True) + mm.read_current_node_from_device = Mock(return_value=current_node) + + results = mm.exec_module() + + assert results['changed'] is True + assert results['fqdn_auto_populate'] is True + assert results['fqdn'] == 'foo.bar.com' + assert results['state'] == 'present' + + def test_create_reuse_node_with_ipv4_address(self, *args): + # Configure the arguments that would be sent to the Ansible module + set_module_args(dict( + pool='my-pool', + name='7.3.67.8', + port=2345, + state='present', + partition='Common', + reuse_nodes=True, + password='password', + server='localhost', + user='admin' + )) + + current_node = NodeApiParameters(params=load_fixture('load_net_node_with_ipv4_address.json')) + module = AnsibleModule( + argument_spec=self.spec.argument_spec, + supports_check_mode=self.spec.supports_check_mode + ) + mm = ModuleManager(module=module) + + # Override methods to force specific logic in the module to happen + mm.exists = Mock(return_value=False) + mm.create_on_device = Mock(return_value=True) + mm.read_current_node_from_device = Mock(return_value=current_node) + + results = mm.exec_module() + + assert results['changed'] is True + assert results['fqdn_auto_populate'] is False + assert results['address'] == '7.3.67.8' + assert results['state'] == 'present' + + def test_create_reuse_node_with_fqdn_auto_populate(self, *args): + # Configure the arguments that would be sent to the Ansible module + set_module_args(dict( + pool='my-pool', + name='my-name', + port=2345, + state='present', + partition='Common', + reuse_nodes=True, + fqdn_auto_populate=False, + password='password', + server='localhost', + user='admin' + )) + + current_node = NodeApiParameters(params=load_fixture('load_net_node_with_fqdn.json')) + module = AnsibleModule( + argument_spec=self.spec.argument_spec, + supports_check_mode=self.spec.supports_check_mode + ) + mm = ModuleManager(module=module) + + # Override methods to force specific logic in the module to happen + mm.exists = Mock(return_value=False) + mm.create_on_device = Mock(return_value=True) + mm.read_current_node_from_device = Mock(return_value=current_node) + + results = mm.exec_module() + + assert results['changed'] is True + assert results['fqdn_auto_populate'] is True + assert results['fqdn'] == 'foo.bar.com' + assert results['state'] == 'present' + + +class TestLegacyManager(unittest.TestCase): + + def setUp(self): + self.spec = ArgumentSpec() + + def test_create_name_is_hostname_with_session_and_monitor_enabled(self, *args): + # Configure the arguments that would be sent to the Ansible module + set_module_args(dict( + pool='my-pool', + name='my-name', + port=2345, + state='present', + session_state='enabled', + monitor_state='enabled', + partition='Common', + password='password', + server='localhost', + user='admin' + )) + + module = AnsibleModule( + argument_spec=self.spec.argument_spec, + supports_check_mode=self.spec.supports_check_mode + ) + mm = ModuleManager(module=module) + + # Override methods to force specific logic in the module to happen + mm.exists = Mock(return_value=False) + mm.create_on_device = Mock(return_value=True) + + results = mm.exec_module() + + assert results['changed'] is True + assert results['fqdn_auto_populate'] is False + assert results['fqdn'] == 'my-name' + assert results['state'] == 'present' + + def test_create_name_is_address_with_session_and_monitor_enabled(self, *args): + # Configure the arguments that would be sent to the Ansible module + set_module_args(dict( + pool='my-pool', + name='10.10.10.10', + port=2345, + state='present', + session_state='enabled', + monitor_state='enabled', + partition='Common', + password='password', + server='localhost', + user='admin' + )) + + module = AnsibleModule( + argument_spec=self.spec.argument_spec, + supports_check_mode=self.spec.supports_check_mode + ) + mm = ModuleManager(module=module) + + # Override methods to force specific logic in the module to happen + mm.exists = Mock(return_value=False) + mm.create_on_device = Mock(return_value=True) + + results = mm.exec_module() + + assert results['changed'] is True + assert results['fqdn_auto_populate'] is False + assert results['address'] == '10.10.10.10' + assert results['state'] == 'present' + + def test_create_name_is_address_with_session_disabled_and_monitor_enabled(self, *args): + # Configure the arguments that would be sent to the Ansible module + set_module_args(dict( + pool='my-pool', + name='10.10.10.10', + port=2345, + state='present', + monitor_state='enabled', + session_state='disabled', + partition='Common', + password='password', + server='localhost', + user='admin' + )) + + module = AnsibleModule( + argument_spec=self.spec.argument_spec, + supports_check_mode=self.spec.supports_check_mode + ) + mm = ModuleManager(module=module) + + # Override methods to force specific logic in the module to happen + mm.exists = Mock(return_value=False) + mm.create_on_device = Mock(return_value=True) + + results = mm.exec_module() + + assert results['changed'] is True + assert results['fqdn_auto_populate'] is False + assert results['address'] == '10.10.10.10' + assert results['state'] == 'disabled' + + def test_create_name_is_address_with_session_and_monitor_disabled(self, *args): + # Configure the arguments that would be sent to the Ansible module + set_module_args(dict( + pool='my-pool', + name='10.10.10.10', + port=2345, + state='present', + monitor_state='disabled', + session_state='disabled', + partition='Common', + password='password', + server='localhost', + user='admin' + )) + + module = AnsibleModule( + argument_spec=self.spec.argument_spec, + supports_check_mode=self.spec.supports_check_mode + ) + mm = ModuleManager(module=module) + + # Override methods to force specific logic in the module to happen + mm.exists = Mock(return_value=False) + mm.create_on_device = Mock(return_value=True) + + results = mm.exec_module() + + assert results['changed'] is True + assert results['fqdn_auto_populate'] is False + assert results['address'] == '10.10.10.10' + assert results['state'] == 'forced_offline' diff --git a/test/units/modules/network/f5/test_bigip_profile_client_ssl.py b/test/units/modules/network/f5/test_bigip_profile_client_ssl.py index 29b30b45a3..e2a68b3bb0 100644 --- a/test/units/modules/network/f5/test_bigip_profile_client_ssl.py +++ b/test/units/modules/network/f5/test_bigip_profile_client_ssl.py @@ -21,10 +21,10 @@ from ansible.compat.tests.mock import patch from ansible.module_utils.basic import AnsibleModule try: - from library.bigip_profile_client_ssl import ModuleParameters - from library.bigip_profile_client_ssl import ApiParameters - from library.bigip_profile_client_ssl import ModuleManager - from library.bigip_profile_client_ssl import ArgumentSpec + from library.modules.bigip_profile_client_ssl import ModuleParameters + from library.modules.bigip_profile_client_ssl import ApiParameters + from library.modules.bigip_profile_client_ssl import ModuleManager + from library.modules.bigip_profile_client_ssl import ArgumentSpec from library.module_utils.network.f5.common import F5ModuleError from library.module_utils.network.f5.common import iControlUnexpectedHTTPError from test.unit.modules.utils import set_module_args