From a969a529ab79f485db4ebb05097b9b1900f8d5f6 Mon Sep 17 00:00:00 2001 From: Tim Rupp Date: Fri, 13 Oct 2017 21:51:28 -0700 Subject: [PATCH] Fixes various gtm pool issues (#31728) Various formatting related fixes. Also fixed an idempotency problem with the 'disabled' state --- .../modules/network/f5/bigip_gtm_pool.py | 227 ++++++++++++------ .../modules/network/f5/test_bigip_gtm_pool.py | 2 + 2 files changed, 153 insertions(+), 76 deletions(-) diff --git a/lib/ansible/modules/network/f5/bigip_gtm_pool.py b/lib/ansible/modules/network/f5/bigip_gtm_pool.py index 2a320cc9d3..09fa23cd02 100644 --- a/lib/ansible/modules/network/f5/bigip_gtm_pool.py +++ b/lib/ansible/modules/network/f5/bigip_gtm_pool.py @@ -4,26 +4,29 @@ # 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_pool -short_description: Manages F5 BIG-IP GTM pools. +short_description: Manages F5 BIG-IP GTM pools description: - Manages F5 BIG-IP GTM pools. version_added: "2.4" options: state: description: - - Pool member state. When C(present), ensures that the pool is - created and enabled. When C(absent), ensures that the pool is - removed from the system. When C(enabled) or C(disabled), ensures - that the pool is enabled or disabled (respectively) on the remote - device. - required: True + - Pool member state. When C(present), ensures that the pool is + created and enabled. When C(absent), ensures that the pool is + removed from the system. When C(enabled) or C(disabled), ensures + that the pool is enabled or disabled (respectively) on the remote + device. choices: - present - absent @@ -114,6 +117,11 @@ options: description: - Name of the GTM pool. required: True + partition: + description: + - Device partition to manage resources on. + default: Common + version_added: 2.5 notes: - Requires the f5-sdk Python package on the host. This is as easy as pip install f5-sdk. @@ -127,57 +135,62 @@ author: - Tim Rupp (@caphrim007) ''' -RETURN = ''' +RETURN = r''' preferred_lb_method: - description: New preferred load balancing method for the pool. - returned: changed - type: string - sample: "topology" + description: New preferred load balancing method for the pool. + returned: changed + type: string + sample: topology alternate_lb_method: - description: New alternate load balancing method for the pool. - returned: changed - type: string - sample: "drop-packet" + description: New alternate load balancing method for the pool. + returned: changed + type: string + sample: drop-packet fallback_lb_method: - description: New fallback load balancing method for the pool. - returned: changed - type: string - sample: "fewest-hops" + description: New fallback load balancing method for the pool. + returned: changed + type: string + sample: fewest-hops fallback_ip: - description: New fallback IP used when load balacing using the C(fallback_ip) method. - returned: changed - type: string - sample: "10.10.10.10" + description: New fallback IP used when load balacing using the C(fallback_ip) method. + returned: changed + type: string + sample: 10.10.10.10 ''' -EXAMPLES = ''' +EXAMPLES = r''' - name: Create a GTM pool bigip_gtm_pool: - server: "lb.mydomain.com" - user: "admin" - password: "secret" - name: "my_pool" + server: lb.mydomain.com + user: admin + password: secret + name: my_pool delegate_to: localhost - name: Disable pool bigip_gtm_pool: - server: "lb.mydomain.com" - user: "admin" - password: "secret" - state: "disabled" - name: "my_pool" + server: lb.mydomain.com + user: admin + password: secret + state: disabled + name: my_pool delegate_to: localhost ''' from distutils.version import LooseVersion -from ansible.module_utils.f5_utils import ( - AnsibleF5Client, - AnsibleF5Parameters, - HAS_F5SDK, - F5ModuleError, - iControlUnexpectedHTTPError -) +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.six import iteritems +from collections import defaultdict + + +try: + from ansible.module_utils.f5_utils import iControlUnexpectedHTTPError +except ImportError: + HAS_F5SDK = False try: from netaddr import IPAddress, AddrFormatError @@ -200,7 +213,7 @@ class Parameters(AnsibleF5Parameters): } updatables = [ 'preferred_lb_method', 'alternate_lb_method', 'fallback_lb_method', - 'fallback_ip' + 'fallback_ip', 'state' ] returnables = [ 'preferred_lb_method', 'alternate_lb_method', 'fallback_lb_method', @@ -208,9 +221,39 @@ class Parameters(AnsibleF5Parameters): ] api_attributes = [ 'loadBalancingMode', 'alternateMode', 'fallbackMode', 'verifyMemberAvailability', - 'fallbackIpv4', 'fallbackIpv6', 'fallbackIp' + 'fallbackIpv4', 'fallbackIpv6', 'fallbackIp', 'enabled', 'disabled' ] + def __init__(self, params=None): + self._values = defaultdict(lambda: None) + self._values['__warnings'] = [] + if params: + self.update(params=params) + + 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 + def to_return(self): result = {} for returnable in self.returnables: @@ -264,6 +307,8 @@ class Parameters(AnsibleF5Parameters): return None if self._values['fallback_ip'] == 'any': return 'any' + if self._values['fallback_ip'] == 'any6': + return 'any6' try: address = IPAddress(self._values['fallback_ip']) if address.version == 4: @@ -284,25 +329,52 @@ class Parameters(AnsibleF5Parameters): @property def enabled(self): - if self._values['state'] == 'disabled': - return False - elif self._values['state'] in ['present', 'enabled']: - return True - elif self._values['enabled'] is True: - return True - else: + if self._values['enabled'] is None: return None + return True @property def disabled(self): - if self._values['state'] == 'disabled': - return True - elif self._values['state'] in ['present', 'enabled']: - return False - elif self._values['disabled'] is True: - return True - else: + if self._values['disabled'] is None: return None + return True + + +class Changes(Parameters): + pass + + +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 == 'disabled' and self.have.enabled: + return dict( + disabled=True + ) + elif self.want.state in ['present', 'enabled'] and self.have.disabled: + return dict( + enabled=True + ) class ModuleManager(object): @@ -347,7 +419,7 @@ class BaseManager(object): self.client = client self.have = None self.want = Parameters(self.client.module.params) - self.changes = Parameters() + self.changes = Changes() def _set_changed_options(self): changed = {} @@ -355,24 +427,23 @@ class BaseManager(object): if getattr(self.want, key) is not None: changed[key] = getattr(self.want, key) if changed: - self.changes = Parameters(changed) + self.changes = Changes(changed) def _update_changed_options(self): - changed = {} - for key in Parameters.updatables: - if getattr(self.want, key) is not None: - attr1 = getattr(self.want, key) - attr2 = getattr(self.have, key) - if attr1 != attr2: - changed[key] = attr1 - - if self.want.state == 'disabled' and self.have.enabled: - changed['state'] = self.want.state - elif self.want.state in ['present', 'enabled'] and self.have.disabled: - changed['state'] = self.want.state - + 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 = Parameters(changed) + self.changes = Changes(changed) return True return False @@ -421,6 +492,10 @@ class BaseManager(object): return True def create(self): + if self.want.state == 'disabled': + self.want.update({'disabled': True}) + elif self.want.state in ['present', 'enabled']: + self.want.update({'enabled': True}) self._set_changed_options() if self.client.check_mode: return True @@ -474,7 +549,7 @@ class TypedManager(BaseManager): return result def update_on_device(self): - params = self.want.api_params() + params = self.changes.api_params() pools = self.client.api.tm.gtm.pools collection = getattr(pools, self.want.collection) resource = getattr(collection, self.want.type) @@ -527,7 +602,7 @@ class UntypedManager(BaseManager): return result def update_on_device(self): - params = self.want.api_params() + params = self.changes.api_params() resource = self.client.api.tm.gtm.pools.pool.load( name=self.want.name, partition=self.want.partition diff --git a/test/units/modules/network/f5/test_bigip_gtm_pool.py b/test/units/modules/network/f5/test_bigip_gtm_pool.py index b2ce9498b6..ea29bd69ef 100644 --- a/test/units/modules/network/f5/test_bigip_gtm_pool.py +++ b/test/units/modules/network/f5/test_bigip_gtm_pool.py @@ -40,6 +40,7 @@ try: from library.bigip_gtm_pool import ArgumentSpec from library.bigip_gtm_pool import UntypedManager from library.bigip_gtm_pool import TypedManager + from ansible.module_utils.f5_utils import iControlUnexpectedHTTPError except ImportError: try: from ansible.modules.network.f5.bigip_gtm_pool import Parameters @@ -47,6 +48,7 @@ except ImportError: from ansible.modules.network.f5.bigip_gtm_pool import ArgumentSpec from ansible.modules.network.f5.bigip_gtm_pool import UntypedManager from ansible.modules.network.f5.bigip_gtm_pool import TypedManager + from ansible.module_utils.f5_utils import iControlUnexpectedHTTPError except ImportError: raise SkipTest("F5 Ansible modules require the f5-sdk Python library")