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
author:
- Tim Rupp (@caphrim007)
- Wojciech Wypior (@wojtek0806)
'''
EXAMPLES = r'''
@ -245,38 +246,34 @@ from ansible.module_utils.basic import env_fallback
try:
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 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 compare_complex_list
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 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:
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 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 compare_complex_list
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 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):
@ -308,10 +305,7 @@ class Parameters(AnsibleF5Parameters):
'limitMaxPpsStatus',
'translationAddress',
'translationPort',
# 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',
]
returnables = [
@ -329,6 +323,7 @@ class Parameters(AnsibleF5Parameters):
'translation_address',
'translation_port',
'virtual_server_dependencies',
'availability_requirements',
]
updatables = [
@ -718,9 +713,108 @@ class UsableChanges(Changes):
results.append(dict(name=name))
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):
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):
@ -828,17 +922,18 @@ class ModuleManager(object):
return False
def exec_module(self):
if not module_provisioned(self.client, 'gtm'):
raise F5ModuleError(
"GTM must be provisioned to use this module."
)
changed = False
result = dict()
state = self.want.state
try:
if state in ['present', 'enabled', 'disabled']:
changed = self.present()
elif state == 'absent':
changed = self.absent()
except iControlUnexpectedHTTPError as e:
raise F5ModuleError(str(e))
if state in ['present', 'enabled', 'disabled']:
changed = self.present()
elif state == 'absent':
changed = self.absent()
reportable = ReportableChanges(params=self.changes.to_return())
changes = reportable.to_return()
@ -862,14 +957,20 @@ class ModuleManager(object):
return self.create()
def exists(self):
resource = self.client.api.tm.gtm.servers.server.load(
name=self.want.server_name,
partition=self.want.partition
uri = "https://{0}:{1}/mgmt/tm/gtm/server/{2}/virtual-servers/{3}".format(
self.client.provider['server'],
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(
name=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(self):
self.have = self.read_current_from_device()
@ -913,29 +1014,44 @@ class ModuleManager(object):
def create_on_device(self):
params = self.changes.api_params()
resource = self.client.api.tm.gtm.servers.server.load(
name=self.want.server_name,
partition=self.want.partition
params['name'] = self.want.name
params['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(
name=self.want.name,
**params
)
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, 404]:
if 'message' in response:
raise F5ModuleError(response['message'])
else:
raise F5ModuleError(resp.content)
def update_on_device(self):
params = self.changes.api_params()
resource = self.client.api.tm.gtm.servers.server.load(
name=self.want.server_name,
partition=self.want.partition
uri = "https://{0}:{1}/mgmt/tm/gtm/server/{2}/virtual-servers/{3}".format(
self.client.provider['server'],
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(
name=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 absent(self):
if self.exists():
@ -943,56 +1059,36 @@ class ModuleManager(object):
return False
def remove_from_device(self):
resource = self.client.api.tm.gtm.servers.server.load(
name=self.want.server_name,
partition=self.want.partition
uri = "https://{0}:{1}/mgmt/tm/gtm/server/{2}/virtual-servers/{3}".format(
self.client.provider['server'],
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(
name=self.want.name
)
if resource:
resource.delete()
response = self.client.api.delete(uri)
if response.status == 200:
return True
raise F5ModuleError(response.content)
def read_current_from_device(self):
resource = self.client.api.tm.gtm.servers.server.load(
name=self.want.server_name,
partition=self.want.partition
)
resource = resource.virtual_servers_s.virtual_server.load(
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)
uri = "https://{0}:{1}/mgmt/tm/gtm/server/{2}/virtual-servers/{3}".format(
self.client.provider['server'],
self.client.provider['server_port'],
transform_name(self.want.partition, self.want.server_name),
self.want.name
)
resp = self.client.api.get(uri)
try:
if hasattr(output, 'commandResult'):
if len(output.commandResult.strip()) > 0:
raise F5ModuleError(output.commandResult)
except (AttributeError, NameError, LazyAttributesRequired):
pass
return True
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)
class ArgumentSpec(object):
@ -1066,18 +1162,17 @@ def main():
argument_spec=spec.argument_spec,
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:
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__':

View file

@ -8,16 +8,12 @@ __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 units.compat import unittest
from units.compat.mock import Mock
from units.compat.mock import patch
from ansible.module_utils.basic import AnsibleModule
try:
@ -25,17 +21,25 @@ try:
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 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
# 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_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 ModuleManager
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
except ImportError:
raise SkipTest("F5 Ansible modules require the f5-sdk Python library")
@ -132,13 +136,23 @@ class TestParameters(unittest.TestCase):
assert p.connections_limit == 300
@patch('ansible.module_utils.f5_utils.AnsibleF5Client._get_mgmt_root',
return_value=True)
class TestManager(unittest.TestCase):
def setUp(self):
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):
set_module_args(dict(
server_name='foo',