1
0
Fork 0
mirror of https://github.com/ansible-collections/community.general.git synced 2024-09-14 20:13:21 +02:00

Add new features to gtm server module (#48507)

Remove the f5-sdk. Fix unit tests. Add new prober parameters
This commit is contained in:
Tim Rupp 2018-11-10 18:34:48 -08:00 committed by GitHub
parent 364ed4e660
commit 4a74c1fec6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 722 additions and 87 deletions

View file

@ -1,7 +1,7 @@
#!/usr/bin/python #!/usr/bin/python
# -*- coding: utf-8 -*- # -*- 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) # 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 from __future__ import absolute_import, division, print_function
@ -127,10 +127,88 @@ options:
agent. agent.
type: bool type: bool
version_added: 2.7 version_added: 2.7
monitors:
description:
- Specifies the health monitors that the system currently uses to monitor this resource.
- When C(availability_requirements.type) is C(require), you may only have a single monitor in the
C(monitors) list.
version_added: 2.8
availability_requirements:
description:
- Specifies, if you activate more than one health monitor, the number of health
monitors that must receive successful responses in order for the link to be
considered available.
suboptions:
type:
description:
- Monitor rule type when C(monitors) is specified.
- When creating a new pool, if this value is not specified, the default of 'all' will be used.
choices: ['all', 'at_least', 'require']
at_least:
description:
- Specifies the minimum number of active health monitors that must be successful
before the link is considered up.
- This parameter is only relevant when a C(type) of C(at_least) is used.
- This parameter will be ignored if a type of either C(all) or C(require) is used.
number_of_probes:
description:
- Specifies the minimum number of probes that must succeed for this server to be declared up.
- When creating a new virtual server, if this parameter is specified, then the C(number_of_probers)
parameter must also be specified.
- The value of this parameter should always be B(lower) than, or B(equal to), the value of C(number_of_probers).
- 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.
number_of_probers:
description:
- Specifies the number of probers that should be used when running probes.
- When creating a new virtual server, if this parameter is specified, then the C(number_of_probes)
parameter must also be specified.
- The value of this parameter should always be B(higher) than, or B(equal to), the value of C(number_of_probers).
- 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.8
prober_preference:
description:
- Specifies the type of prober to use to monitor this server's resources.
- This option is ignored in C(TMOS) version C(12.x).
- From C(TMOS) version C(13.x) and up, when prober_preference is set to C(pool)
a C(prober_pool) parameter must be specified.
choices:
- inside-datacenter
- outside-datacenter
- inherit
- pool
version_added: 2.8
prober_fallback:
description:
- Specifies the type of prober to use to monitor this server's resources
when the preferred prober is not available.
- This option is ignored in C(TMOS) version C(12.x).
- From C(TMOS) version C(13.x) and up, when prober_preference is set to C(pool)
a C(prober_pool) parameter must be specified.
- The choices are mutually exclusive with prober_preference parameter,
with the exception of C(any-available) or C(none) option.
choices:
- any
- inside-datacenter
- outside-datacenter
- inherit
- pool
- none
version_added: 2.8
prober_pool:
description:
- Specifies the name of the prober pool to use to monitor this server's resources.
- From C(TMOS) version C(13.x) and up, this parameter is mandatory when C(prober_preference) is set to C(pool).
- Format of the name can be either be prepended by partition (C(/Common/foo)), or specified
just as an object name (C(foo)).
- In C(TMOS) version C(12.x) prober_pool can be set to empty string to revert to default setting of inherit.
version_added: 2.8
extends_documentation_fragment: f5 extends_documentation_fragment: f5
author: author:
- Robert Teller - Robert Teller
- Tim Rupp (@caphrim007) - Tim Rupp (@caphrim007)
- Wojciech Wypior (@wojtek0806)
''' '''
EXAMPLES = r''' EXAMPLES = r'''
@ -183,6 +261,11 @@ EXAMPLES = r'''
''' '''
RETURN = r''' RETURN = r'''
monitors:
description: The new list of monitors for the resource.
returned: changed
type: list
sample: ['/Common/monitor1', '/Common/monitor2']
link_discovery: link_discovery:
description: The new C(link_discovery) configured on the remote device. description: The new C(link_discovery) configured on the remote device.
returned: changed returned: changed
@ -205,34 +288,38 @@ datacenter:
sample: datacenter01 sample: datacenter01
''' '''
import re
from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.basic import env_fallback from ansible.module_utils.basic import env_fallback
from distutils.version import LooseVersion from distutils.version import LooseVersion
try: try:
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 f5_argument_spec
from library.module_utils.network.f5.common import fq_name from library.module_utils.network.f5.common import fq_name
try: from library.module_utils.network.f5.common import f5_argument_spec
from library.module_utils.network.f5.common import iControlUnexpectedHTTPError from library.module_utils.network.f5.common import transform_name
except ImportError: from library.module_utils.network.f5.common import exit_json
HAS_F5SDK = False from library.module_utils.network.f5.common import fail_json
from library.module_utils.network.f5.common import is_empty_list
from library.module_utils.network.f5.icontrol import tmos_version
from library.module_utils.network.f5.icontrol import module_provisioned
except ImportError: except ImportError:
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 f5_argument_spec
from ansible.module_utils.network.f5.common import fq_name from ansible.module_utils.network.f5.common import fq_name
try: from ansible.module_utils.network.f5.common import f5_argument_spec
from ansible.module_utils.network.f5.common import iControlUnexpectedHTTPError from ansible.module_utils.network.f5.common import transform_name
except ImportError: from ansible.module_utils.network.f5.common import exit_json
HAS_F5SDK = False from ansible.module_utils.network.f5.common import fail_json
from ansible.module_utils.network.f5.common import is_empty_list
from ansible.module_utils.network.f5.icontrol import tmos_version
from ansible.module_utils.network.f5.icontrol import module_provisioned
try: try:
from collections import OrderedDict from collections import OrderedDict
@ -252,6 +339,10 @@ class Parameters(AnsibleF5Parameters):
'iqAllowPath': 'iquery_allow_path', 'iqAllowPath': 'iquery_allow_path',
'iqAllowServiceCheck': 'iquery_allow_service_check', 'iqAllowServiceCheck': 'iquery_allow_service_check',
'iqAllowSnmp': 'iquery_allow_snmp', 'iqAllowSnmp': 'iquery_allow_snmp',
'monitor': 'monitors',
'proberPreference': 'prober_preference',
'proberPool': 'prober_pool',
'proberFallback': 'prober_fallback',
} }
api_attributes = [ api_attributes = [
@ -265,6 +356,10 @@ class Parameters(AnsibleF5Parameters):
'iqAllowPath', 'iqAllowPath',
'iqAllowServiceCheck', 'iqAllowServiceCheck',
'iqAllowSnmp', 'iqAllowSnmp',
'monitor',
'proberPreference',
'proberPool',
'proberFallback',
] ]
updatables = [ updatables = [
@ -276,6 +371,10 @@ class Parameters(AnsibleF5Parameters):
'iquery_allow_path', 'iquery_allow_path',
'iquery_allow_service_check', 'iquery_allow_service_check',
'iquery_allow_snmp', 'iquery_allow_snmp',
'monitors',
'prober_preference',
'prober_pool',
'prober_fallback',
] ]
returnables = [ returnables = [
@ -287,6 +386,12 @@ class Parameters(AnsibleF5Parameters):
'iquery_allow_path', 'iquery_allow_path',
'iquery_allow_service_check', 'iquery_allow_service_check',
'iquery_allow_snmp', 'iquery_allow_snmp',
'devices',
'monitors',
'availability_requirements',
'prober_preference',
'prober_pool',
'prober_fallback',
] ]
@ -348,6 +453,111 @@ class ApiParameters(Parameters):
return True return True
return False return False
@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 monitors_list(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 monitors(self):
if self._values['monitors'] is None:
return None
if self._values['monitors'] == '/Common/bigip':
return '/Common/bigip'
monitors = [fq_name(self.partition, x) for x in self.monitors_list]
if self.availability_requirement_type == 'at_least':
monitors = ' '.join(monitors)
result = 'min {0} of {{ {1} }}'.format(self.at_least, monitors)
elif self.availability_requirement_type == 'require':
monitors = ' '.join(monitors)
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
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 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 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 matches.group('least')
class ModuleParameters(Parameters): class ModuleParameters(Parameters):
@property @property
@ -383,11 +593,6 @@ class ModuleParameters(Parameters):
}) })
return result return result
def devices_list(self):
if self._values['devices'] is None:
return None
return self._values['devices']
@property @property
def enabled(self): def enabled(self):
if self._values['state'] in ['present', 'enabled']: if self._values['state'] in ['present', 'enabled']:
@ -435,6 +640,83 @@ class ModuleParameters(Parameters):
return None return None
return self._values['iquery_options']['allow_snmp'] return self._values['iquery_options']['allow_snmp']
@property
def monitors_list(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 monitors(self):
if self._values['monitors'] is None:
return None
if is_empty_list(self._values['monitors']):
return '/Common/bigip'
monitors = [fq_name(self.partition, x) for x in self.monitors_list]
if self.availability_requirement_type == 'at_least':
if self.at_least > len(self.monitors_list):
raise F5ModuleError(
"The 'at_least' value must not exceed the number of 'monitors'."
)
monitors = ' '.join(monitors)
result = 'min {0} of {{ {1} }}'.format(self.at_least, monitors)
elif self.availability_requirement_type == 'require':
monitors = ' '.join(monitors)
if self.number_of_probes > self.number_of_probers:
raise F5ModuleError(
"The 'number_of_probes' must not exceed the 'number_of_probers'."
)
result = 'require {0} from {1} {{ {2} }}'.format(self.number_of_probes, self.number_of_probers, monitors)
else:
result = ' and '.join(monitors).strip()
return result
def _get_availability_value(self, type):
if self._values['availability_requirements'] is None:
return None
if self._values['availability_requirements'][type] is None:
return None
return int(self._values['availability_requirements'][type])
@property
def availability_requirement_type(self):
if self._values['availability_requirements'] is None:
return None
return self._values['availability_requirements']['type']
@property
def number_of_probes(self):
return self._get_availability_value('number_of_probes')
@property
def number_of_probers(self):
return self._get_availability_value('number_of_probers')
@property
def at_least(self):
return self._get_availability_value('at_least')
@property
def prober_pool(self):
if self._values['prober_pool'] is None:
return None
if self._values['prober_pool'] == '':
return self._values['prober_pool']
result = fq_name(self.partition, self._values['prober_pool'])
return result
@property
def prober_fallback(self):
if self._values['prober_fallback'] == 'any':
return 'any-available'
return self._values['prober_fallback']
class Changes(Parameters): class Changes(Parameters):
def to_return(self): def to_return(self):
@ -446,6 +728,19 @@ class Changes(Parameters):
class UsableChanges(Changes): class UsableChanges(Changes):
@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
@property @property
def iquery_allow_path(self): def iquery_allow_path(self):
if self._values['iquery_allow_path'] is None: if self._values['iquery_allow_path'] is None:
@ -478,6 +773,111 @@ class ReportableChanges(Changes):
return 'bigip' return 'bigip'
return self._values['server_type'] return self._values['server_type']
@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
@property
def prober_fallback(self):
if self._values['prober_fallback'] == 'any-available':
return 'any'
return self._values['prober_fallback']
class Difference(object): class Difference(object):
def __init__(self, want, have=None): def __init__(self, want, have=None):
@ -612,8 +1012,8 @@ class Difference(object):
server_change = self._server_type_changed() server_change = self._server_type_changed()
if not devices_change and not server_change: if not devices_change and not server_change:
return None return None
tmos_version = self.client.api.tmos_version tmos = tmos_version(self.client)
if LooseVersion(tmos_version) >= LooseVersion('13.0.0'): if LooseVersion(tmos) >= LooseVersion('13.0.0'):
result = self._handle_current_server_type_and_devices( result = self._handle_current_server_type_and_devices(
devices_change, server_change devices_change, server_change
) )
@ -631,6 +1031,95 @@ class Difference(object):
elif self.want.state in ['present', 'enabled'] and self.have.disabled: elif self.want.state in ['present', 'enabled'] and self.have.disabled:
return dict(enabled=True) return dict(enabled=True)
@property
def monitors(self):
if self.want.monitors is None:
return None
if self.want.monitors == '/Common/bigip' and self.have.monitors == '/Common/bigip':
return None
if self.want.monitors == '/Common/bigip' and self.have.monitors is None:
return None
if self.want.monitors == '/Common/bigip' and len(self.have.monitors) > 0:
return '/Common/bigip'
if self.have.monitors is None:
return self.want.monitors
if self.have.monitors != self.want.monitors:
return self.want.monitors
@property
def prober_pool(self):
if self.want.prober_pool is None:
return None
if self.have.prober_pool is None:
if self.want.prober_pool == '':
return None
if self.want.prober_pool != self.have.prober_pool:
return self.want.prober_pool
@property
def prober_preference(self):
if self.want.prober_preference is None:
return None
if self.want.prober_preference == self.have.prober_preference:
return None
if self.want.prober_preference == 'pool' and self.want.prober_pool is None:
raise F5ModuleError(
"A prober_pool needs to be set if prober_preference is set to 'pool'"
)
if self.want.prober_preference != 'pool' and self.have.prober_preference == 'pool':
if self.want.prober_fallback != 'pool' and self.want.prober_pool != '':
raise F5ModuleError(
"To change prober_preference from {0} to {1}, set prober_pool to an empty string".format(
self.have.prober_preference,
self.want.prober_preference
)
)
if self.want.prober_preference == self.want.prober_fallback:
raise F5ModuleError(
"Prober_preference and prober_fallback must not be equal."
)
if self.want.prober_preference == self.have.prober_fallback:
raise F5ModuleError(
"Cannot set prober_preference to {0} if prober_fallback on device is set to {1}.".format(
self.want.prober_preference,
self.have.prober_fallback
)
)
if self.want.prober_preference != self.have.prober_preference:
return self.want.prober_preference
@property
def prober_fallback(self):
if self.want.prober_fallback is None:
return None
if self.want.prober_fallback == self.have.prober_fallback:
return None
if self.want.prober_fallback == 'pool' and self.want.prober_pool is None:
raise F5ModuleError(
"A prober_pool needs to be set if prober_fallback is set to 'pool'"
)
if self.want.prober_fallback != 'pool' and self.have.prober_fallback == 'pool':
if self.want.prober_preference != 'pool' and self.want.prober_pool != '':
raise F5ModuleError(
"To change prober_fallback from {0} to {1}, set prober_pool to an empty string".format(
self.have.prober_fallback,
self.want.prober_fallback
)
)
if self.want.prober_preference == self.want.prober_fallback:
raise F5ModuleError(
"Prober_preference and prober_fallback must not be equal."
)
if self.want.prober_fallback == self.have.prober_preference:
raise F5ModuleError(
"Cannot set prober_fallback to {0} if prober_preference on device is set to {1}.".format(
self.want.prober_fallback,
self.have.prober_preference
)
)
if self.want.prober_fallback != self.have.prober_fallback:
return self.want.prober_fallback
class ModuleManager(object): class ModuleManager(object):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -639,7 +1128,7 @@ class ModuleManager(object):
self.kwargs = kwargs self.kwargs = kwargs
def exec_module(self): def exec_module(self):
if not self.gtm_provisioned(): if not module_provisioned(self.client, 'gtm'):
raise F5ModuleError( raise F5ModuleError(
"GTM must be provisioned to use this module." "GTM must be provisioned to use this module."
) )
@ -656,20 +1145,12 @@ class ModuleManager(object):
return V2Manager(**self.kwargs) return V2Manager(**self.kwargs)
def version_is_less_than(self, version): def version_is_less_than(self, version):
tmos_version = self.client.api.tmos_version tmos = tmos_version(self.client)
if LooseVersion(tmos_version) < LooseVersion(version): if LooseVersion(tmos) < LooseVersion(version):
return True return True
else: else:
return False 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): class BaseManager(object):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -712,14 +1193,10 @@ class BaseManager(object):
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()
result.update(**changes) result.update(**changes)
@ -761,7 +1238,8 @@ class BaseManager(object):
"You must provide an initial device." "You must provide an initial device."
) )
self._assign_creation_defaults() self._assign_creation_defaults()
self.handle_prober_settings()
self._set_changed_options()
if self.module.check_mode: if self.module.check_mode:
return True return True
self.create_on_device() self.create_on_device()
@ -772,28 +1250,43 @@ class BaseManager(object):
def create_on_device(self): def create_on_device(self):
params = self.changes.api_params() params = self.changes.api_params()
self.client.api.tm.gtm.servers.server.create( params['name'] = self.want.name
name=self.want.name, params['partition'] = self.want.partition
partition=self.want.partition, uri = "https://{0}:{1}/mgmt/tm/gtm/server/".format(
**params 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))
def update(self): if 'code' in response and response['code'] in [400, 403]:
self.have = self.read_current_from_device() if 'message' in response:
if not self.should_update(): raise F5ModuleError(response['message'])
return False else:
if self.module.check_mode: raise F5ModuleError(resp.content)
return True return response['selfLink']
self.update_on_device()
return True
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}".format(
name=self.want.name, self.client.provider['server'],
partition=self.want.partition self.client.provider['server_port'],
transform_name(self.want.partition, self.want.name)
) )
result = resource.attrs resp = self.client.api.get(uri)
return ApiParameters(params=result) 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 should_update(self): def should_update(self):
result = self._update_changed_options() result = self._update_changed_options()
@ -803,11 +1296,22 @@ class BaseManager(object):
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}".format(
name=self.want.name, self.client.provider['server'],
partition=self.want.partition self.client.provider['server_port'],
transform_name(self.want.partition, self.want.name)
) )
resource.modify(**params) 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): def absent(self):
changed = False changed = False
@ -824,18 +1328,30 @@ class BaseManager(object):
return True return True
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}".format(
name=self.want.name, self.client.provider['server'],
partition=self.want.partition 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)
def exists(self): def exists(self):
result = self.client.api.tm.gtm.servers.server.exists( uri = "https://{0}:{1}/mgmt/tm/gtm/server/{2}".format(
name=self.want.name, self.client.provider['server'],
partition=self.want.partition 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
class V1Manager(BaseManager): class V1Manager(BaseManager):
@ -861,8 +1377,33 @@ class V1Manager(BaseManager):
if len(self.want.devices) > 1 and self.want.server_type == 'bigip': if len(self.want.devices) > 1 and self.want.server_type == 'bigip':
self.want.update({'server_type': 'redundant-bigip'}) self.want.update({'server_type': 'redundant-bigip'})
def update(self):
self.have = self.read_current_from_device()
self.handle_prober_settings()
if not self.should_update():
return False
if self.module.check_mode:
return True
self.update_on_device()
return True
def handle_prober_settings(self):
if self.want.prober_preference is not None:
self.want._values.pop('prober_preference')
if self.want.prober_fallback is not None:
self.want._values.pop('prober_fallback')
class V2Manager(BaseManager): class V2Manager(BaseManager):
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 _assign_creation_defaults(self): def _assign_creation_defaults(self):
if self.want.server_type is None: if self.want.server_type is None:
self.want.update({'server_type': 'bigip'}) self.want.update({'server_type': 'bigip'})
@ -875,6 +1416,21 @@ class V2Manager(BaseManager):
def adjust_server_type_by_version(self): def adjust_server_type_by_version(self):
pass pass
def handle_prober_settings(self):
if self.want.prober_preference == 'pool' and self.want.prober_pool is None:
raise F5ModuleError(
"A prober_pool needs to be set if prober_preference is set to 'pool'"
)
if self.want.prober_preference is not None and self.want.prober_fallback is not None:
if self.want.prober_preference == self.want.prober_fallback:
raise F5ModuleError(
"The parameters for prober_preference and prober_fallback must not be the same."
)
if self.want.prober_fallback == 'pool' and self.want.prober_pool is None:
raise F5ModuleError(
"A prober_pool needs to be set if prober_fallback is set to 'pool'"
)
class ArgumentSpec(object): class ArgumentSpec(object):
def __init__(self): def __init__(self):
@ -929,7 +1485,37 @@ class ArgumentSpec(object):
allow_service_check=dict(type='bool'), allow_service_check=dict(type='bool'),
allow_snmp=dict(type='bool') allow_snmp=dict(type='bool')
) )
) ),
availability_requirements=dict(
type='dict',
options=dict(
type=dict(
choices=['all', 'at_least', 'require'],
required=True
),
at_least=dict(type='int'),
number_of_probes=dict(type='int'),
number_of_probers=dict(type='int')
),
mutually_exclusive=[
['at_least', 'number_of_probes'],
['at_least', 'number_of_probers'],
],
required_if=[
['type', 'at_least', ['at_least']],
['type', 'require', ['number_of_probes', 'number_of_probers']]
]
),
monitors=dict(type='list'),
prober_preference=dict(
choices=['inside-datacenter', 'outside-datacenter', 'inherit', 'pool']
),
prober_fallback=dict(
choices=['inside-datacenter', 'outside-datacenter',
'inherit', 'pool', 'any', 'none']
),
prober_pool=dict()
) )
self.argument_spec = {} self.argument_spec = {}
self.argument_spec.update(f5_argument_spec) self.argument_spec.update(f5_argument_spec)
@ -941,20 +1527,19 @@ def main():
module = AnsibleModule( module = AnsibleModule(
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:
@ -27,9 +23,13 @@ try:
from library.modules.bigip_gtm_server import V1Manager from library.modules.bigip_gtm_server import V1Manager
from library.modules.bigip_gtm_server import V2Manager from library.modules.bigip_gtm_server import V2Manager
from library.modules.bigip_gtm_server import ArgumentSpec from library.modules.bigip_gtm_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_server import ApiParameters from ansible.modules.network.f5.bigip_gtm_server import ApiParameters
@ -38,8 +38,12 @@ except ImportError:
from ansible.modules.network.f5.bigip_gtm_server import V1Manager from ansible.modules.network.f5.bigip_gtm_server import V1Manager
from ansible.modules.network.f5.bigip_gtm_server import V2Manager from ansible.modules.network.f5.bigip_gtm_server import V2Manager
from ansible.modules.network.f5.bigip_gtm_server import ArgumentSpec from ansible.modules.network.f5.bigip_gtm_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")
@ -138,6 +142,28 @@ class TestV1Manager(unittest.TestCase):
def setUp(self): def setUp(self):
self.spec = ArgumentSpec() self.spec = ArgumentSpec()
try:
self.p1 = patch('library.modules.bigip_gtm_server.module_provisioned')
self.m1 = self.p1.start()
self.m1.return_value = True
except Exception:
self.p1 = patch('ansible.modules.network.f5.bigip_gtm_server.module_provisioned')
self.m1 = self.p1.start()
self.m1.return_value = True
try:
self.p2 = patch('library.modules.bigip_gtm_server.tmos_version')
self.m2 = self.p2.start()
self.m2.return_value = '13.0.0'
except Exception:
self.p2 = patch('ansible.modules.network.f5.bigip_gtm_server.tmos_version')
self.m2 = self.p2.start()
self.m2.return_value = '13.0.0'
def tearDown(self):
self.p1.stop()
self.p2.stop()
def test_create(self, *args): def test_create(self, *args):
set_module_args(dict( set_module_args(dict(
server='lb.mydomain.com', server='lb.mydomain.com',
@ -204,6 +230,7 @@ class TestV1Manager(unittest.TestCase):
mm.get_manager = Mock(return_value=m1) mm.get_manager = Mock(return_value=m1)
mm.version_is_less_than = Mock(return_value=True) mm.version_is_less_than = Mock(return_value=True)
mm.gtm_provisioned = Mock(return_value=True) mm.gtm_provisioned = Mock(return_value=True)
mm.module_provisioned = Mock(return_value=True)
results = mm.exec_module() results = mm.exec_module()
@ -216,6 +243,28 @@ class TestV2Manager(unittest.TestCase):
def setUp(self): def setUp(self):
self.spec = ArgumentSpec() self.spec = ArgumentSpec()
try:
self.p1 = patch('library.modules.bigip_gtm_server.module_provisioned')
self.m1 = self.p1.start()
self.m1.return_value = True
except Exception:
self.p1 = patch('ansible.modules.network.f5.bigip_gtm_server.module_provisioned')
self.m1 = self.p1.start()
self.m1.return_value = True
try:
self.p2 = patch('library.modules.bigip_gtm_server.tmos_version')
self.m2 = self.p2.start()
self.m2.return_value = '13.0.0'
except Exception:
self.p2 = patch('ansible.modules.network.f5.bigip_gtm_server.tmos_version')
self.m2 = self.p2.start()
self.m2.return_value = '13.0.0'
def tearDown(self):
self.p1.stop()
self.p2.stop()
def test_create(self, *args): def test_create(self, *args):
set_module_args(dict( set_module_args(dict(
server='lb.mydomain.com', server='lb.mydomain.com',
@ -282,6 +331,7 @@ class TestV2Manager(unittest.TestCase):
mm.get_manager = Mock(return_value=m1) mm.get_manager = Mock(return_value=m1)
mm.version_is_less_than = Mock(return_value=False) mm.version_is_less_than = Mock(return_value=False)
mm.gtm_provisioned = Mock(return_value=True) mm.gtm_provisioned = Mock(return_value=True)
mm.module_provisioned = Mock(return_value=True)
results = mm.exec_module() results = mm.exec_module()