Remove f5-sdk from bigip_gtm_virtual_server (#48508)

This commit is contained in:
Tim Rupp 2018-11-10 18:49:46 -08:00 committed by GitHub
parent ce06cfe8ef
commit 8dabd0cde3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 223 additions and 114 deletions

View file

@ -170,6 +170,7 @@ options:
extends_documentation_fragment: f5 extends_documentation_fragment: f5
author: author:
- Tim Rupp (@caphrim007) - Tim Rupp (@caphrim007)
- Wojciech Wypior (@wojtek0806)
''' '''
EXAMPLES = r''' EXAMPLES = r'''
@ -245,38 +246,34 @@ from ansible.module_utils.basic import env_fallback
try: try:
from library.module_utils.compat.ipaddress import ip_address from library.module_utils.compat.ipaddress import ip_address
from library.module_utils.network.f5.bigip import HAS_F5SDK from library.module_utils.network.f5.bigip import F5RestClient
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 F5ModuleError
from library.module_utils.network.f5.common import AnsibleF5Parameters 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 cleanup_tokens
from library.module_utils.network.f5.common import fq_name from library.module_utils.network.f5.common import fq_name
from library.module_utils.network.f5.common import compare_complex_list
from library.module_utils.network.f5.common import f5_argument_spec 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.common import compare_complex_list
from library.module_utils.network.f5.icontrol import module_provisioned
from library.module_utils.network.f5.ipaddress import is_valid_ip from library.module_utils.network.f5.ipaddress import is_valid_ip
from library.module_utils.network.f5.ipaddress import validate_ip_v6_address from library.module_utils.network.f5.ipaddress import validate_ip_v6_address
try:
from library.module_utils.network.f5.common import iControlUnexpectedHTTPError
from f5.sdk_exception import LazyAttributesRequired
except ImportError:
HAS_F5SDK = False
except ImportError: except ImportError:
from ansible.module_utils.compat.ipaddress import ip_address from ansible.module_utils.compat.ipaddress import ip_address
from ansible.module_utils.network.f5.bigip import HAS_F5SDK from ansible.module_utils.network.f5.bigip import F5RestClient
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 F5ModuleError
from ansible.module_utils.network.f5.common import AnsibleF5Parameters 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 cleanup_tokens
from ansible.module_utils.network.f5.common import fq_name from ansible.module_utils.network.f5.common import fq_name
from ansible.module_utils.network.f5.common import compare_complex_list
from ansible.module_utils.network.f5.common import f5_argument_spec 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.common import compare_complex_list
from ansible.module_utils.network.f5.icontrol import module_provisioned
from ansible.module_utils.network.f5.ipaddress import is_valid_ip from ansible.module_utils.network.f5.ipaddress import is_valid_ip
from ansible.module_utils.network.f5.ipaddress import validate_ip_v6_address from ansible.module_utils.network.f5.ipaddress import validate_ip_v6_address
try:
from ansible.module_utils.network.f5.common import iControlUnexpectedHTTPError
from f5.sdk_exception import LazyAttributesRequired
except ImportError:
HAS_F5SDK = False
class Parameters(AnsibleF5Parameters): class Parameters(AnsibleF5Parameters):
@ -308,10 +305,7 @@ class Parameters(AnsibleF5Parameters):
'limitMaxPpsStatus', 'limitMaxPpsStatus',
'translationAddress', 'translationAddress',
'translationPort', 'translationPort',
# The monitor attribute is not included here, because it can break the 'monitor',
# API calls to the device. If this bug is ever fixed, uncomment this code.
#
# monitor
] ]
returnables = [ returnables = [
@ -329,6 +323,7 @@ class Parameters(AnsibleF5Parameters):
'translation_address', 'translation_address',
'translation_port', 'translation_port',
'virtual_server_dependencies', 'virtual_server_dependencies',
'availability_requirements',
] ]
updatables = [ updatables = [
@ -718,9 +713,108 @@ class UsableChanges(Changes):
results.append(dict(name=name)) results.append(dict(name=name))
return results return results
@property
def monitors(self):
monitor_string = self._values['monitors']
if monitor_string is None:
return None
if '{' in monitor_string and '}':
tmp = monitor_string.strip('}').split('{')
monitor = ''.join(tmp).rstrip()
return monitor
return monitor_string
class ReportableChanges(Changes): class ReportableChanges(Changes):
pass
@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): class Difference(object):
@ -828,17 +922,18 @@ class ModuleManager(object):
return False return False
def exec_module(self): def exec_module(self):
if not module_provisioned(self.client, 'gtm'):
raise F5ModuleError(
"GTM must be provisioned to use this module."
)
changed = False changed = False
result = dict() result = dict()
state = self.want.state state = self.want.state
try: if state in ['present', 'enabled', 'disabled']:
if state in ['present', 'enabled', 'disabled']: changed = self.present()
changed = self.present() elif state == 'absent':
elif state == 'absent': changed = self.absent()
changed = self.absent()
except iControlUnexpectedHTTPError as e:
raise F5ModuleError(str(e))
reportable = ReportableChanges(params=self.changes.to_return()) reportable = ReportableChanges(params=self.changes.to_return())
changes = reportable.to_return() changes = reportable.to_return()
@ -862,14 +957,20 @@ class ModuleManager(object):
return self.create() return self.create()
def exists(self): def exists(self):
resource = self.client.api.tm.gtm.servers.server.load( uri = "https://{0}:{1}/mgmt/tm/gtm/server/{2}/virtual-servers/{3}".format(
name=self.want.server_name, self.client.provider['server'],
partition=self.want.partition self.client.provider['server_port'],
transform_name(self.want.partition, self.want.server_name),
self.want.name
) )
result = resource.virtual_servers_s.virtual_server.exists( resp = self.client.api.get(uri)
name=self.want.name try:
) response = resp.json()
return result except ValueError:
return False
if resp.status == 404 or 'code' in response and response['code'] == 404:
return False
return True
def update(self): def update(self):
self.have = self.read_current_from_device() self.have = self.read_current_from_device()
@ -913,29 +1014,44 @@ class ModuleManager(object):
def create_on_device(self): def create_on_device(self):
params = self.changes.api_params() params = self.changes.api_params()
resource = self.client.api.tm.gtm.servers.server.load( params['name'] = self.want.name
name=self.want.server_name, params['partition'] = self.want.partition
partition=self.want.partition uri = "https://{0}:{1}/mgmt/tm/gtm/server/{2}/virtual-servers/".format(
self.client.provider['server'],
self.client.provider['server_port'],
transform_name(self.want.partition, self.want.server_name)
) )
resource.virtual_servers_s.virtual_server.create( resp = self.client.api.post(uri, json=params)
name=self.want.name, try:
**params response = resp.json()
) except ValueError as ex:
if self.want.monitors: raise F5ModuleError(str(ex))
self.update_monitors_on_device()
if 'code' in response and response['code'] in [400, 403, 404]:
if 'message' in response:
raise F5ModuleError(response['message'])
else:
raise F5ModuleError(resp.content)
def update_on_device(self): def update_on_device(self):
params = self.changes.api_params() params = self.changes.api_params()
resource = self.client.api.tm.gtm.servers.server.load( uri = "https://{0}:{1}/mgmt/tm/gtm/server/{2}/virtual-servers/{3}".format(
name=self.want.server_name, self.client.provider['server'],
partition=self.want.partition self.client.provider['server_port'],
transform_name(self.want.partition, self.want.server_name),
self.want.name
) )
resource = resource.virtual_servers_s.virtual_server.load( resp = self.client.api.patch(uri, json=params)
name=self.want.name try:
) response = resp.json()
resource.modify(**params) except ValueError as ex:
if self.want.monitors: raise F5ModuleError(str(ex))
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)
def absent(self): def absent(self):
if self.exists(): if self.exists():
@ -943,56 +1059,36 @@ class ModuleManager(object):
return False return False
def remove_from_device(self): def remove_from_device(self):
resource = self.client.api.tm.gtm.servers.server.load( uri = "https://{0}:{1}/mgmt/tm/gtm/server/{2}/virtual-servers/{3}".format(
name=self.want.server_name, self.client.provider['server'],
partition=self.want.partition self.client.provider['server_port'],
transform_name(self.want.partition, self.want.server_name),
self.want.name
) )
resource = resource.virtual_servers_s.virtual_server.load( response = self.client.api.delete(uri)
name=self.want.name if response.status == 200:
) return True
if resource: raise F5ModuleError(response.content)
resource.delete()
def read_current_from_device(self): def read_current_from_device(self):
resource = self.client.api.tm.gtm.servers.server.load( uri = "https://{0}:{1}/mgmt/tm/gtm/server/{2}/virtual-servers/{3}".format(
name=self.want.server_name, self.client.provider['server'],
partition=self.want.partition self.client.provider['server_port'],
) transform_name(self.want.partition, self.want.server_name),
resource = resource.virtual_servers_s.virtual_server.load( self.want.name
name=self.want.name
)
result = resource.attrs
return ApiParameters(params=result)
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 server /{0}/{1} virtual-servers modify {{ {2} {{ monitor {3} }} }}'.format(
self.want.partition, self.want.server_name, self.want.name, self.want.monitors
)
output = self.client.api.tm.util.bash.exec_cmd(
'run',
utilCmdArgs='-c "{0}"'.format(command)
) )
resp = self.client.api.get(uri)
try: try:
if hasattr(output, 'commandResult'): response = resp.json()
if len(output.commandResult.strip()) > 0: except ValueError as ex:
raise F5ModuleError(output.commandResult) raise F5ModuleError(str(ex))
except (AttributeError, NameError, LazyAttributesRequired):
pass if 'code' in response and response['code'] == 400:
return True if 'message' in response:
raise F5ModuleError(response['message'])
else:
raise F5ModuleError(resp.content)
return ApiParameters(params=response)
class ArgumentSpec(object): class ArgumentSpec(object):
@ -1066,18 +1162,17 @@ def main():
argument_spec=spec.argument_spec, argument_spec=spec.argument_spec,
supports_check_mode=spec.supports_check_mode, supports_check_mode=spec.supports_check_mode,
) )
if not HAS_F5SDK:
module.fail_json(msg="The python f5-sdk module is required") client = F5RestClient(**module.params)
try: try:
client = F5Client(**module.params)
mm = ModuleManager(module=module, client=client) mm = ModuleManager(module=module, client=client)
results = mm.exec_module() results = mm.exec_module()
cleanup_tokens(client) cleanup_tokens(client)
module.exit_json(**results) exit_json(module, results, client)
except F5ModuleError as ex: except F5ModuleError as ex:
cleanup_tokens(client) cleanup_tokens(client)
module.fail_json(msg=str(ex)) fail_json(module, ex, client)
if __name__ == '__main__': if __name__ == '__main__':

View file

@ -8,16 +8,12 @@ __metaclass__ = type
import os import os
import json import json
import pytest
import sys import sys
from nose.plugins.skip import SkipTest from nose.plugins.skip import SkipTest
if sys.version_info < (2, 7): if sys.version_info < (2, 7):
raise SkipTest("F5 Ansible modules require Python >= 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 from ansible.module_utils.basic import AnsibleModule
try: try:
@ -25,17 +21,25 @@ try:
from library.modules.bigip_gtm_virtual_server import ModuleParameters from library.modules.bigip_gtm_virtual_server import ModuleParameters
from library.modules.bigip_gtm_virtual_server import ModuleManager from library.modules.bigip_gtm_virtual_server import ModuleManager
from library.modules.bigip_gtm_virtual_server import ArgumentSpec from library.modules.bigip_gtm_virtual_server import ArgumentSpec
from library.module_utils.network.f5.common import F5ModuleError
from library.module_utils.network.f5.common import iControlUnexpectedHTTPError # In Ansible 2.8, Ansible changed import paths.
from test.unit.modules.utils import set_module_args 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: except ImportError:
try: try:
from ansible.modules.network.f5.bigip_gtm_virtual_server import ApiParameters from ansible.modules.network.f5.bigip_gtm_virtual_server import ApiParameters
from ansible.modules.network.f5.bigip_gtm_virtual_server import ModuleParameters from ansible.modules.network.f5.bigip_gtm_virtual_server import ModuleParameters
from ansible.modules.network.f5.bigip_gtm_virtual_server import ModuleManager from ansible.modules.network.f5.bigip_gtm_virtual_server import ModuleManager
from ansible.modules.network.f5.bigip_gtm_virtual_server import ArgumentSpec from ansible.modules.network.f5.bigip_gtm_virtual_server import ArgumentSpec
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 from units.modules.utils import set_module_args
except ImportError: except ImportError:
raise SkipTest("F5 Ansible modules require the f5-sdk Python library") raise SkipTest("F5 Ansible modules require the f5-sdk Python library")
@ -132,13 +136,23 @@ class TestParameters(unittest.TestCase):
assert p.connections_limit == 300 assert p.connections_limit == 300
@patch('ansible.module_utils.f5_utils.AnsibleF5Client._get_mgmt_root',
return_value=True)
class TestManager(unittest.TestCase): class TestManager(unittest.TestCase):
def setUp(self): def setUp(self):
self.spec = ArgumentSpec() self.spec = ArgumentSpec()
try:
self.p1 = patch('library.modules.bigip_gtm_virtual_server.module_provisioned')
self.m1 = self.p1.start()
self.m1.return_value = True
except Exception:
self.p1 = patch('ansible.modules.network.f5.bigip_gtm_virtual_server.module_provisioned')
self.m1 = self.p1.start()
self.m1.return_value = True
def tearDown(self):
self.p1.stop()
def test_create_datacenter(self, *args): def test_create_datacenter(self, *args):
set_module_args(dict( set_module_args(dict(
server_name='foo', server_name='foo',