Remove f5-sdk from bigip_gtm_pool (#48505)

pull/48515/head
Tim Rupp 6 years ago committed by GitHub
parent 98a15013dc
commit 830f1880b7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,7 +1,7 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright (c) 2017 F5 Networks Inc.
# 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
@ -177,9 +177,38 @@ options:
- This parameter is only relevant when a C(type) of C(require) is used.
- This parameter will be ignored if a type of either C(all) or C(at_least) is used.
version_added: 2.6
max_answers_returned:
description:
- Specifies the maximum number of available virtual servers that the system lists in a response.
- The maximum is 500.
version_added: 2.8
ttl:
description:
- Specifies the number of seconds that the IP address, once found, is valid.
version_added: 2.8
extends_documentation_fragment: f5
author:
- Tim Rupp (@caphrim007)
- Wojciech Wypior (@wojtek0806)
'''
EXAMPLES = r'''
- name: Create a GTM pool
bigip_gtm_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
delegate_to: localhost
'''
RETURN = r'''
@ -221,25 +250,11 @@ members:
description: The name of the virtual server portion of the member.
returned: changed
type: string
'''
EXAMPLES = r'''
- name: Create a GTM pool
bigip_gtm_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
delegate_to: localhost
max_answers_returned:
description: The new Maximum Answers Returned value.
returned: changed
type: int
sample: 25
'''
import copy
@ -250,33 +265,31 @@ from ansible.module_utils.basic import env_fallback
from distutils.version import LooseVersion
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.bigip import F5RestClient
from library.module_utils.network.f5.common import F5ModuleError
from library.module_utils.network.f5.common import AnsibleF5Parameters
from library.module_utils.network.f5.common import cleanup_tokens
from library.module_utils.network.f5.common import fq_name
from library.module_utils.network.f5.common import f5_argument_spec
from library.module_utils.network.f5.common import transform_name
from library.module_utils.network.f5.common import exit_json
from library.module_utils.network.f5.common import fail_json
from library.module_utils.network.f5.icontrol import tmos_version
from library.module_utils.network.f5.icontrol import module_provisioned
from library.module_utils.network.f5.ipaddress import is_valid_ip
try:
from library.module_utils.network.f5.common import iControlUnexpectedHTTPError
from f5.sdk_exception import LazyAttributesRequired
except ImportError:
HAS_F5SDK = False
except ImportError:
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.bigip import F5RestClient
from ansible.module_utils.network.f5.common import F5ModuleError
from ansible.module_utils.network.f5.common import AnsibleF5Parameters
from ansible.module_utils.network.f5.common import cleanup_tokens
from ansible.module_utils.network.f5.common import fq_name
from ansible.module_utils.network.f5.common import f5_argument_spec
from ansible.module_utils.network.f5.common import transform_name
from ansible.module_utils.network.f5.common import exit_json
from ansible.module_utils.network.f5.common import fail_json
from ansible.module_utils.network.f5.icontrol import tmos_version
from ansible.module_utils.network.f5.icontrol import module_provisioned
from ansible.module_utils.network.f5.ipaddress import is_valid_ip
try:
from ansible.module_utils.network.f5.common import iControlUnexpectedHTTPError
from f5.sdk_exception import LazyAttributesRequired
except ImportError:
HAS_F5SDK = False
class Parameters(AnsibleF5Parameters):
@ -289,7 +302,8 @@ class Parameters(AnsibleF5Parameters):
'fallbackIpv6': 'fallback_ip',
'fallbackIp': 'fallback_ip',
'membersReference': 'members',
'monitor': 'monitors'
'monitor': 'monitors',
'maxAnswersReturned': 'max_answers_returned',
}
updatables = [
@ -300,6 +314,8 @@ class Parameters(AnsibleF5Parameters):
'monitors',
'preferred_lb_method',
'state',
'max_answers_returned',
'ttl',
]
returnables = [
@ -310,7 +326,10 @@ class Parameters(AnsibleF5Parameters):
'monitors',
'preferred_lb_method',
'enabled',
'disabled'
'disabled',
'availability_requirements',
'max_answers_returned',
'ttl',
]
api_attributes = [
@ -324,34 +343,11 @@ class Parameters(AnsibleF5Parameters):
'loadBalancingMode',
'members',
'verifyMemberAvailability',
# The monitor attribute is not included here, because it can break the
# API calls to the device. If this bug is ever fixed, uncomment this code.
#
# monitor
'monitor',
'maxAnswersReturned',
'ttl',
]
def to_return(self):
result = {}
for returnable in self.returnables:
result[returnable] = getattr(self, returnable)
result = self._filter_params(result)
return result
@property
def collection(self):
type_map = dict(
a='a_s',
aaaa='aaaas',
cname='cnames',
mx='mxs',
naptr='naptrs',
srv='srvs'
)
if self._values['type'] is None:
return None
wideip_type = self._values['type']
return type_map[wideip_type]
@property
def type(self):
if self._values['type'] is None:
@ -449,7 +445,6 @@ class ApiParameters(Parameters):
result = 'require {0} from {1} {{ {2} }}'.format(self.number_of_probes, self.number_of_probers, monitors)
else:
result = ' and '.join(monitors).strip()
return result
@property
@ -608,15 +603,25 @@ class ModuleParameters(Parameters):
class Changes(Parameters):
pass
def to_return(self):
result = {}
for returnable in self.returnables:
result[returnable] = getattr(self, returnable)
result = self._filter_params(result)
return result
class UsableChanges(Changes):
@property
def monitors(self):
if self._values['monitors'] is None:
monitor_string = self._values['monitors']
if monitor_string is None:
return None
return self._values['monitors']
if '{' in monitor_string and '}':
tmp = monitor_string.strip('}').split('{')
monitor = ''.join(tmp).rstrip()
return monitor
return monitor_string
@property
def members(self):
@ -646,6 +651,93 @@ class ReportableChanges(Changes):
))
return results
@property
def monitors(self):
if self._values['monitors'] is None:
return []
try:
result = re.findall(r'/\w+/[^\s}]+', self._values['monitors'])
result.sort()
return result
except Exception:
return self._values['monitors']
@property
def availability_requirement_type(self):
if self._values['monitors'] is None:
return None
if 'min ' in self._values['monitors']:
return 'at_least'
elif 'require ' in self._values['monitors']:
return 'require'
else:
return 'all'
@property
def number_of_probes(self):
"""Returns the probes value from the monitor string.
The monitor string for a Require monitor looks like this.
require 1 from 2 { /Common/tcp }
This method parses out the first of the numeric values. This values represents
the "probes" value that can be updated in the module.
Returns:
int: The probes value if found. None otherwise.
"""
if self._values['monitors'] is None:
return None
pattern = r'require\s+(?P<probes>\d+)\s+from'
matches = re.search(pattern, self._values['monitors'])
if matches is None:
return None
return int(matches.group('probes'))
@property
def number_of_probers(self):
"""Returns the probers value from the monitor string.
The monitor string for a Require monitor looks like this.
require 1 from 2 { /Common/tcp }
This method parses out the first of the numeric values. This values represents
the "probers" value that can be updated in the module.
Returns:
int: The probers value if found. None otherwise.
"""
if self._values['monitors'] is None:
return None
pattern = r'require\s+\d+\s+from\s+(?P<probers>\d+)\s+'
matches = re.search(pattern, self._values['monitors'])
if matches is None:
return None
return int(matches.group('probers'))
@property
def at_least(self):
"""Returns the 'at least' value from the monitor string.
The monitor string for a Require monitor looks like this.
min 1 of { /Common/gateway_icmp }
This method parses out the first of the numeric values. This values represents
the "at_least" value that can be updated in the module.
Returns:
int: The at_least value if found. None otherwise.
"""
if self._values['monitors'] is None:
return None
pattern = r'min\s+(?P<least>\d+)\s+of\s+'
matches = re.search(pattern, self._values['monitors'])
if matches is None:
return None
return int(matches.group('least'))
@property
def availability_requirements(self):
if self._values['monitors'] is None:
return None
result = dict()
result['type'] = self.availability_requirement_type
result['at_least'] = self.at_least
result['number_of_probers'] = self.number_of_probers
result['number_of_probes'] = self.number_of_probes
return result
class Difference(object):
def __init__(self, want, have=None):
@ -701,7 +793,7 @@ class ModuleManager(object):
self.client = kwargs.get('client', None)
def exec_module(self):
if not self.gtm_provisioned():
if not module_provisioned(self.client, 'gtm'):
raise F5ModuleError(
"GTM must be provisioned to use this module."
)
@ -718,20 +810,12 @@ class ModuleManager(object):
return UntypedManager(**self.kwargs)
def version_is_less_than_12(self):
version = self.client.api.tmos_version
version = tmos_version(self.client)
if LooseVersion(version) < LooseVersion('12.0.0'):
return True
else:
return False
def gtm_provisioned(self):
resource = self.client.api.tm.sys.dbs.db.load(
name='provisioned.cpu.gtm'
)
if int(resource.value) == 0:
return False
return True
class BaseManager(object):
def __init__(self, *args, **kwargs):
@ -772,13 +856,10 @@ class BaseManager(object):
result = dict()
state = self.want.state
try:
if state in ["present", "disabled"]:
changed = self.present()
elif state == "absent":
changed = self.absent()
except iControlUnexpectedHTTPError as e:
raise F5ModuleError(str(e))
if state in ["present", "disabled"]:
changed = self.present()
elif state == "absent":
changed = self.absent()
reportable = ReportableChanges(params=self.changes.to_return())
changes = reportable.to_return()
@ -850,36 +931,6 @@ class BaseManager(object):
raise F5ModuleError("Failed to delete the GTM pool")
return True
def update_monitors_on_device(self):
"""Updates the monitors string on a virtual server
There is a long-standing bug in GTM virtual servers where the monitor value
is a string that includes braces. These braces cause the REST API to panic and
fail to update or create any resources that have an "at_least" or "require"
set of availability_requirements.
This method exists to do a tmsh command to cause the update to take place on
the device.
Preferably, this method can be removed and the bug be fixed. The API should
be working, obviously, but the more concerning issue is if tmsh commands change
over time, breaking this method.
"""
command = 'tmsh modify gtm pool {0} /{1}/{2} monitor {3}'.format(
self.want.type, self.want.partition, self.want.name, self.want.monitors
)
output = self.client.api.tm.util.bash.exec_cmd(
'run',
utilCmdArgs='-c "{0}"'.format(command)
)
try:
if hasattr(output, 'commandResult'):
if len(output.commandResult.strip()) > 0:
raise F5ModuleError(output.commandResult)
except (AttributeError, NameError, LazyAttributesRequired):
pass
return True
class TypedManager(BaseManager):
def __init__(self, *args, **kwargs):
@ -906,112 +957,184 @@ class TypedManager(BaseManager):
return super(TypedManager, self).present()
def exists(self):
pools = self.client.api.tm.gtm.pools
collection = getattr(pools, self.want.collection)
resource = getattr(collection, self.want.type)
result = resource.exists(
name=self.want.name,
partition=self.want.partition
uri = "https://{0}:{1}/mgmt/tm/gtm/pool/{2}/{3}".format(
self.client.provider['server'],
self.client.provider['server_port'],
self.want.type,
transform_name(self.want.partition, self.want.name)
)
return result
resp = self.client.api.get(uri)
try:
response = resp.json()
except ValueError:
return False
if resp.status == 404 or 'code' in response and response['code'] == 404:
return False
return True
def update_on_device(self):
params = self.changes.api_params()
pools = self.client.api.tm.gtm.pools
collection = getattr(pools, self.want.collection)
resource = getattr(collection, self.want.type)
result = resource.load(
name=self.want.name,
partition=self.want.partition
uri = "https://{0}:{1}/mgmt/tm/gtm/pool/{2}/{3}".format(
self.client.provider['server'],
self.client.provider['server_port'],
self.want.type,
transform_name(self.want.partition, self.want.name)
)
if params:
result.modify(**params)
if self.want.monitors:
self.update_monitors_on_device()
resp = self.client.api.patch(uri, json=params)
try:
response = resp.json()
except ValueError as ex:
raise F5ModuleError(str(ex))
if 'code' in response and response['code'] == 400:
if 'message' in response:
raise F5ModuleError(response['message'])
else:
raise F5ModuleError(resp.content)
def read_current_from_device(self):
pools = self.client.api.tm.gtm.pools
collection = getattr(pools, self.want.collection)
resource = getattr(collection, self.want.type)
result = resource.load(
name=self.want.name,
partition=self.want.partition,
requests_params=dict(
params=dict(
expandSubcollections='true'
)
)
uri = "https://{0}:{1}/mgmt/tm/gtm/pool/{2}/{3}".format(
self.client.provider['server'],
self.client.provider['server_port'],
self.want.type,
transform_name(self.want.partition, self.want.name)
)
result = result.attrs
return ApiParameters(params=result)
query = '?expandSubcollections=true'
resp = self.client.api.get(uri + query)
try:
response = resp.json()
except ValueError as ex:
raise F5ModuleError(str(ex))
if 'code' in response and response['code'] == 400:
if 'message' in response:
raise F5ModuleError(response['message'])
else:
raise F5ModuleError(resp.content)
return ApiParameters(params=response)
def create_on_device(self):
params = self.changes.api_params()
pools = self.client.api.tm.gtm.pools
collection = getattr(pools, self.want.collection)
resource = getattr(collection, self.want.type)
resource.create(
name=self.want.name,
partition=self.want.partition,
**params
params['name'] = self.want.name
params['partition'] = self.want.partition
uri = "https://{0}:{1}/mgmt/tm/gtm/pool/{2}".format(
self.client.provider['server'],
self.client.provider['server_port'],
self.want.type
)
if self.want.monitors:
self.update_monitors_on_device()
resp = self.client.api.post(uri, json=params)
try:
response = resp.json()
except ValueError as ex:
raise F5ModuleError(str(ex))
if 'code' in response and response['code'] in [400, 403]:
if 'message' in response:
raise F5ModuleError(response['message'])
else:
raise F5ModuleError(resp.content)
return response['selfLink']
def remove_from_device(self):
pools = self.client.api.tm.gtm.pools
collection = getattr(pools, self.want.collection)
resource = getattr(collection, self.want.type)
resource = resource.load(
name=self.want.name,
partition=self.want.partition
uri = "https://{0}:{1}/mgmt/tm/gtm/pool/{2}/{3}".format(
self.client.provider['server'],
self.client.provider['server_port'],
self.want.type,
transform_name(self.want.partition, self.want.name)
)
if resource:
resource.delete()
response = self.client.api.delete(uri)
if response.status == 200:
return True
raise F5ModuleError(response.content)
class UntypedManager(BaseManager):
def exists(self):
result = self.client.api.tm.gtm.pools.pool.exists(
name=self.want.name,
partition=self.want.partition
uri = "https://{0}:{1}/mgmt/tm/gtm/pool/{2}".format(
self.client.provider['server'],
self.client.provider['server_port'],
transform_name(self.want.partition, self.want.name)
)
return result
resp = self.client.api.get(uri)
try:
response = resp.json()
except ValueError:
return False
if resp.status == 404 or 'code' in response and response['code'] == 404:
return False
return True
def create_on_device(self):
params = self.changes.api_params()
params['name'] = self.want.name
params['partition'] = self.want.partition
uri = "https://{0}:{1}/mgmt/tm/gtm/pool/".format(
self.client.provider['server'],
self.client.provider['server_port']
)
resp = self.client.api.post(uri, json=params)
try:
response = resp.json()
except ValueError as ex:
raise F5ModuleError(str(ex))
if 'code' in response and response['code'] in [400, 403]:
if 'message' in response:
raise F5ModuleError(response['message'])
else:
raise F5ModuleError(resp.content)
return response['selfLink']
def update_on_device(self):
params = self.changes.api_params()
resource = self.client.api.tm.gtm.pools.pool.load(
name=self.want.name,
partition=self.want.partition
uri = "https://{0}:{1}/mgmt/tm/gtm/pool/{2}".format(
self.client.provider['server'],
self.client.provider['server_port'],
transform_name(self.want.partition, self.want.name)
)
resource.modify(**params)
if self.want.monitors:
self.update_monitors_on_device()
resp = self.client.api.patch(uri, json=params)
try:
response = resp.json()
except ValueError as ex:
raise F5ModuleError(str(ex))
if 'code' in response and response['code'] == 400:
if 'message' in response:
raise F5ModuleError(response['message'])
else:
raise F5ModuleError(resp.content)
def read_current_from_device(self):
resource = self.client.api.tm.gtm.pools.pool.load(
name=self.want.name,
partition=self.want.partition
uri = "https://{0}:{1}/mgmt/tm/gtm/pool/{2}".format(
self.client.provider['server'],
self.client.provider['server_port'],
transform_name(self.want.partition, self.want.name)
)
result = resource.attrs
return ApiParameters(params=result)
resp = self.client.api.get(uri)
try:
response = resp.json()
except ValueError as ex:
raise F5ModuleError(str(ex))
def create_on_device(self):
params = self.changes.api_params()
self.client.api.tm.gtm.pools.pool.create(
name=self.want.name,
partition=self.want.partition,
**params
)
if self.want.monitors:
self.update_monitors_on_device()
if 'code' in response and response['code'] == 400:
if 'message' in response:
raise F5ModuleError(response['message'])
else:
raise F5ModuleError(resp.content)
return ApiParameters(params=response)
def remove_from_device(self):
resource = self.client.api.tm.gtm.pools.pool.load(
name=self.want.name,
partition=self.want.partition
uri = "https://{0}:{1}/mgmt/tm/gtm/pool/{2}".format(
self.client.provider['server'],
self.client.provider['server_port'],
transform_name(self.want.partition, self.want.name)
)
resource.delete()
response = self.client.api.delete(uri)
if response.status == 200:
return True
raise F5ModuleError(response.content)
class ArgumentSpec(object):
@ -1088,6 +1211,8 @@ class ArgumentSpec(object):
]
),
monitors=dict(type='list'),
max_answers_returned=dict(type='int'),
ttl=dict(type='int')
)
self.argument_spec = {}
self.argument_spec.update(f5_argument_spec)
@ -1105,20 +1230,18 @@ def main():
module = AnsibleModule(
argument_spec=spec.argument_spec,
supports_check_mode=spec.supports_check_mode,
required_if=spec.required_if
)
if not HAS_F5SDK:
module.fail_json(msg="The python f5-sdk module is required")
client = F5RestClient(**module.params)
try:
client = F5Client(**module.params)
mm = ModuleManager(module=module, client=client)
results = mm.exec_module()
cleanup_tokens(client)
module.exit_json(**results)
exit_json(module, results, client)
except F5ModuleError as ex:
cleanup_tokens(client)
module.fail_json(msg=str(ex))
fail_json(module, ex, client)
if __name__ == '__main__':

@ -14,9 +14,6 @@ from nose.plugins.skip import SkipTest
if sys.version_info < (2, 7):
raise SkipTest("F5 Ansible modules require Python >= 2.7")
from units.compat import unittest
from units.compat.mock import Mock
from units.compat.mock import patch
from ansible.module_utils.basic import AnsibleModule
try:
@ -26,9 +23,13 @@ try:
from library.modules.bigip_gtm_pool import ArgumentSpec
from library.modules.bigip_gtm_pool import UntypedManager
from library.modules.bigip_gtm_pool import TypedManager
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
# In Ansible 2.8, Ansible changed import paths.
from test.units.compat import unittest
from test.units.compat.mock import Mock
from test.units.compat.mock import patch
from test.units.modules.utils import set_module_args
except ImportError:
try:
from ansible.modules.network.f5.bigip_gtm_pool import ApiParameters
@ -37,8 +38,12 @@ 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.network.f5.common import F5ModuleError
from ansible.module_utils.network.f5.common import iControlUnexpectedHTTPError
# Ansible 2.8 imports
from units.compat import unittest
from units.compat.mock import Mock
from units.compat.mock import patch
from units.modules.utils import set_module_args
except ImportError:
raise SkipTest("F5 Ansible modules require the f5-sdk Python library")
@ -126,6 +131,18 @@ class TestUntypedManager(unittest.TestCase):
def setUp(self):
self.spec = ArgumentSpec()
try:
self.p1 = patch('library.modules.bigip_gtm_pool.module_provisioned')
self.m1 = self.p1.start()
self.m1.return_value = True
except Exception:
self.p1 = patch('ansible.modules.network.f5.bigip_gtm_pool.module_provisioned')
self.m1 = self.p1.start()
self.m1.return_value = True
def tearDown(self):
self.p1.stop()
def test_create_pool(self, *args):
set_module_args(dict(
name='foo',
@ -150,6 +167,7 @@ class TestUntypedManager(unittest.TestCase):
mm.version_is_less_than_12 = Mock(return_value=True)
mm.get_manager = Mock(return_value=tm)
mm.gtm_provisioned = Mock(return_value=True)
mm.module_provisioned = Mock(return_value=True)
results = mm.exec_module()
@ -185,6 +203,7 @@ class TestUntypedManager(unittest.TestCase):
mm.version_is_less_than_12 = Mock(return_value=True)
mm.get_manager = Mock(return_value=tm)
mm.gtm_provisioned = Mock(return_value=True)
mm.module_provisioned = Mock(return_value=True)
results = mm.exec_module()
@ -217,6 +236,7 @@ class TestUntypedManager(unittest.TestCase):
mm.version_is_less_than_12 = Mock(return_value=True)
mm.get_manager = Mock(return_value=tm)
mm.gtm_provisioned = Mock(return_value=True)
mm.module_provisioned = Mock(return_value=True)
results = mm.exec_module()
@ -228,6 +248,18 @@ class TestTypedManager(unittest.TestCase):
def setUp(self):
self.spec = ArgumentSpec()
try:
self.p1 = patch('library.modules.bigip_gtm_pool.module_provisioned')
self.m1 = self.p1.start()
self.m1.return_value = True
except Exception:
self.p1 = patch('ansible.modules.network.f5.bigip_gtm_pool.module_provisioned')
self.m1 = self.p1.start()
self.m1.return_value = True
def tearDown(self):
self.p1.stop()
def test_create_pool(self, *args):
set_module_args(dict(
name='foo',
@ -253,6 +285,7 @@ class TestTypedManager(unittest.TestCase):
mm.version_is_less_than_12 = Mock(return_value=False)
mm.get_manager = Mock(return_value=tm)
mm.gtm_provisioned = Mock(return_value=True)
mm.module_provisioned = Mock(return_value=True)
results = mm.exec_module()
@ -289,6 +322,7 @@ class TestTypedManager(unittest.TestCase):
mm.version_is_less_than_12 = Mock(return_value=False)
mm.get_manager = Mock(return_value=tm)
mm.gtm_provisioned = Mock(return_value=True)
mm.module_provisioned = Mock(return_value=True)
results = mm.exec_module()
@ -322,6 +356,7 @@ class TestTypedManager(unittest.TestCase):
mm.version_is_less_than_12 = Mock(return_value=False)
mm.get_manager = Mock(return_value=tm)
mm.gtm_provisioned = Mock(return_value=True)
mm.module_provisioned = Mock(return_value=True)
results = mm.exec_module()

Loading…
Cancel
Save