Various f5 modules fixes (#40037)

* Add members to bigip_gtm_pool
* Add monitors to bigip_gtm_pool
* Add availability_requirements to bigip_gtm_pool
* Refactor bigip_gtm_pool
* Normalize the product value returned by gtm facts
* Corrected various documentation
* Updated various F5 coding conventions
* Add partition to bigip_static_route
* Added more unit tests
pull/40038/head
Tim Rupp 6 years ago committed by GitHub
parent 61e7c77dec
commit 383a4f026e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -582,6 +582,14 @@ class ServerParameters(BaseParameters):
'virtual_server_discovery', 'addresses', 'devices', 'virtual_servers'
]
@property
def product(self):
if self._values['product'] is None:
return None
if self._values['product'] in ['single-bigip', 'redundant-bigip']:
return 'bigip'
return self._values['product']
@property
def devices(self):
result = []

@ -18,15 +18,14 @@ module: bigip_gtm_pool
short_description: Manages F5 BIG-IP GTM pools
description:
- Manages F5 BIG-IP GTM pools.
version_added: "2.4"
version_added: 2.4
options:
state:
description:
- Pool member state. When C(present), ensures that the pool is
created and enabled. When C(absent), ensures that the pool is
removed from the system. When C(enabled) or C(disabled), ensures
that the pool is enabled or disabled (respectively) on the remote
device.
- Pool state. When C(present), ensures that the pool is created and enabled.
When C(absent), ensures that the pool is removed from the system. When
C(enabled) or C(disabled), ensures that the pool is enabled or disabled
(respectively) on the remote device.
choices:
- present
- absent
@ -122,6 +121,60 @@ options:
- Device partition to manage resources on.
default: Common
version_added: 2.5
members:
description:
- Members to assign to the pool.
- The order of the members in this list is the order that they will be listed in the pool.
suboptions:
server:
description:
- Name of the server which the pool member is a part of.
required: True
virtual_server:
description:
- Name of the virtual server, associated with the server, that the pool member is a part of.
required: True
version_added: 2.6
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.6
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.6
notes:
- Requires the netaddr Python package on the host. This is as easy as
pip install netaddr.
@ -153,6 +206,24 @@ fallback_ip:
returned: changed
type: string
sample: 10.10.10.10
monitors:
description: The new list of monitors for the resource.
returned: changed
type: list
sample: ['/Common/monitor1', '/Common/monitor2']
members:
description: List of members in the pool.
returned: changed
type: complex
contains:
server:
description: The name of the server portion of the member.
returned: changed
type: string
virtual_server:
description: The name of the virtual server portion of the member.
returned: changed
type: string
'''
EXAMPLES = r'''
@ -174,37 +245,37 @@ EXAMPLES = r'''
delegate_to: localhost
'''
from distutils.version import LooseVersion
import copy
import re
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.basic import env_fallback
HAS_DEVEL_IMPORTS = False
from distutils.version import LooseVersion
try:
# Sideband repository used for dev
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.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 fqdn_name
from library.module_utils.network.f5.common import fq_name
from library.module_utils.network.f5.common import f5_argument_spec
try:
from library.module_utils.network.f5.common import iControlUnexpectedHTTPError
from f5.sdk_exception import LazyAttributesRequired
except ImportError:
HAS_F5SDK = False
HAS_DEVEL_IMPORTS = True
except ImportError:
# Upstream Ansible
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.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 fqdn_name
from ansible.module_utils.network.f5.common import fq_name
from ansible.module_utils.network.f5.common import f5_argument_spec
try:
from ansible.module_utils.network.f5.common import iControlUnexpectedHTTPError
from f5.sdk_exception import LazyAttributesRequired
except ImportError:
HAS_F5SDK = False
@ -214,8 +285,6 @@ try:
except ImportError:
HAS_NETADDR = False
import copy
class Parameters(AnsibleF5Parameters):
api_map = {
@ -225,19 +294,47 @@ class Parameters(AnsibleF5Parameters):
'verifyMemberAvailability': 'verify_member_availability',
'fallbackIpv4': 'fallback_ip',
'fallbackIpv6': 'fallback_ip',
'fallbackIp': 'fallback_ip'
'fallbackIp': 'fallback_ip',
'membersReference': 'members',
'monitor': 'monitors'
}
updatables = [
'preferred_lb_method', 'alternate_lb_method', 'fallback_lb_method',
'fallback_ip', 'state'
'alternate_lb_method',
'fallback_ip',
'fallback_lb_method',
'members',
'monitors',
'preferred_lb_method',
'state',
]
returnables = [
'preferred_lb_method', 'alternate_lb_method', 'fallback_lb_method',
'fallback_ip'
'alternate_lb_method',
'fallback_ip',
'fallback_lb_method',
'members',
'monitors',
'preferred_lb_method',
'enabled',
'disabled'
]
api_attributes = [
'loadBalancingMode', 'alternateMode', 'fallbackMode', 'verifyMemberAvailability',
'fallbackIpv4', 'fallbackIpv6', 'fallbackIp', 'enabled', 'disabled'
'alternateMode',
'disabled',
'enabled',
'fallbackIp',
'fallbackIpv4',
'fallbackIpv6',
'fallbackMode',
'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
]
def to_return(self):
@ -316,10 +413,252 @@ class Parameters(AnsibleF5Parameters):
return True
class ApiParameters(Parameters):
@property
def members(self):
result = []
if self._values['members'] is None or 'items' not in self._values['members']:
return []
for item in self._values['members']['items']:
result.append(dict(item=item['fullPath'], order=item['memberOrder']))
result = [x['item'] for x in sorted(result, key=lambda k: k['order'])]
return result
@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'] == 'default':
return 'default'
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):
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 members(self):
if self._values['members'] is None:
return None
if len(self._values['members']) == 1 and self._values['members'][0] == '':
return []
result = []
for member in self._values['members']:
if 'server' not in member:
raise F5ModuleError(
"One of the provided members is missing a 'server' sub-option."
)
if 'virtual_server' not in member:
raise F5ModuleError(
"One of the provided members is missing a 'virtual_server' sub-option."
)
name = '{0}:{1}'.format(member['server'], member['virtual_server'])
name = fq_name(self.partition, name)
if name in result:
continue
result.append(name)
result = list(result)
return result
@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 len(self._values['monitors']) == 1 and self._values['monitors'][0] == '':
return 'default'
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
@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')
class Changes(Parameters):
pass
class UsableChanges(Changes):
@property
def monitors(self):
if self._values['monitors'] is None:
return None
return self._values['monitors']
@property
def members(self):
results = []
if self._values['members'] is None:
return None
for idx, member in enumerate(self._values['members']):
result = dict(
name=member,
memberOrder=idx
)
results.append(result)
return results
class ReportableChanges(Changes):
@property
def members(self):
results = []
if self._values['members'] is None:
return None
for member in self._values['members']:
parts = member.split(':')
results.append(dict(
server=fq_name(self.partition, parts[0]),
virtual_server=fq_name(self.partition, parts[1])
))
return results
class Difference(object):
def __init__(self, want, have=None):
self.want = want
@ -352,6 +691,21 @@ class Difference(object):
enabled=True
)
@property
def monitors(self):
if self.want.monitors is None:
return None
if self.want.monitors == 'default' and self.have.monitors == 'default':
return None
if self.want.monitors == 'default' and self.have.monitors is None:
return None
if self.want.monitors == 'default' and len(self.have.monitors) > 0:
return 'default'
if self.have.monitors is None:
return self.want.monitors
if self.have.monitors != self.want.monitors:
return self.want.monitors
class ModuleManager(object):
def __init__(self, *args, **kwargs):
@ -396,8 +750,8 @@ class BaseManager(object):
self.module = kwargs.get('module', None)
self.client = kwargs.get('client', None)
self.have = None
self.want = Parameters(params=self.module.params)
self.changes = Changes()
self.want = ModuleParameters(params=self.module.params)
self.changes = UsableChanges()
def _set_changed_options(self):
changed = {}
@ -405,7 +759,7 @@ class BaseManager(object):
if getattr(self.want, key) is not None:
changed[key] = getattr(self.want, key)
if changed:
self.changes = Changes(params=changed)
self.changes = UsableChanges(params=changed)
def _update_changed_options(self):
diff = Difference(self.want, self.have)
@ -421,7 +775,7 @@ class BaseManager(object):
else:
changed[k] = change
if changed:
self.changes = Changes(params=changed)
self.changes = UsableChanges(params=changed)
return True
return False
@ -438,11 +792,21 @@ class BaseManager(object):
except iControlUnexpectedHTTPError as e:
raise F5ModuleError(str(e))
changes = self.changes.to_return()
reportable = ReportableChanges(params=self.changes.to_return())
changes = reportable.to_return()
result.update(**changes)
result.update(dict(changed=changed))
self._announce_deprecations(result)
return result
def _announce_deprecations(self, result):
warnings = result.pop('__warnings', [])
for warning in warnings:
self.module.deprecate(
msg=warning['msg'],
version=warning['version']
)
def present(self):
if self.exists():
return self.update()
@ -474,7 +838,14 @@ class BaseManager(object):
self.want.update({'disabled': True})
elif self.want.state in ['present', 'enabled']:
self.want.update({'enabled': True})
self._set_changed_options()
if self.want.availability_requirement_type == 'require' and len(self.want.monitors_list) > 1:
raise F5ModuleError(
"Only one monitor may be specified when using an availability_requirement type of 'require'"
)
if self.module.check_mode:
return True
self.create_on_device()
@ -491,6 +862,36 @@ 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):
@ -535,7 +936,10 @@ class TypedManager(BaseManager):
name=self.want.name,
partition=self.want.partition
)
result.modify(**params)
if params:
result.modify(**params)
if self.want.monitors:
self.update_monitors_on_device()
def read_current_from_device(self):
pools = self.client.api.tm.gtm.pools
@ -543,13 +947,18 @@ class TypedManager(BaseManager):
resource = getattr(collection, self.want.type)
result = resource.load(
name=self.want.name,
partition=self.want.partition
partition=self.want.partition,
requests_params=dict(
params=dict(
expandSubcollections='true'
)
)
)
result = result.attrs
return Parameters(params=result)
return ApiParameters(params=result)
def create_on_device(self):
params = self.want.api_params()
params = self.changes.api_params()
pools = self.client.api.tm.gtm.pools
collection = getattr(pools, self.want.collection)
resource = getattr(collection, self.want.type)
@ -558,6 +967,8 @@ class TypedManager(BaseManager):
partition=self.want.partition,
**params
)
if self.want.monitors:
self.update_monitors_on_device()
def remove_from_device(self):
pools = self.client.api.tm.gtm.pools
@ -586,6 +997,8 @@ class UntypedManager(BaseManager):
partition=self.want.partition
)
resource.modify(**params)
if self.want.monitors:
self.update_monitors_on_device()
def read_current_from_device(self):
resource = self.client.api.tm.gtm.pools.pool.load(
@ -593,15 +1006,17 @@ class UntypedManager(BaseManager):
partition=self.want.partition
)
result = resource.attrs
return Parameters(params=result)
return ApiParameters(params=result)
def create_on_device(self):
params = self.want.api_params()
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()
def remove_from_device(self):
resource = self.client.api.tm.gtm.pools.pool.load(
@ -656,7 +1071,35 @@ class ArgumentSpec(object):
partition=dict(
default='Common',
fallback=(env_fallback, ['F5_PARTITION'])
)
),
members=dict(
type='list',
options=dict(
server=dict(required=True),
virtual_server=dict(required=True)
)
),
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'),
)
self.argument_spec = {}
self.argument_spec.update(f5_argument_spec)

@ -18,7 +18,7 @@ module: bigip_hostname
short_description: Manage the hostname of a BIG-IP
description:
- Manage the hostname of a BIG-IP.
version_added: "2.3"
version_added: 2.3
options:
hostname:
description:
@ -50,30 +50,23 @@ hostname:
from ansible.module_utils.basic import AnsibleModule
HAS_DEVEL_IMPORTS = False
try:
# Sideband repository used for dev
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.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 fqdn_name
from library.module_utils.network.f5.common import f5_argument_spec
try:
from library.module_utils.network.f5.common import iControlUnexpectedHTTPError
except ImportError:
HAS_F5SDK = False
HAS_DEVEL_IMPORTS = True
except ImportError:
# Upstream Ansible
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.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 fqdn_name
from ansible.module_utils.network.f5.common import f5_argument_spec
try:
from ansible.module_utils.network.f5.common import iControlUnexpectedHTTPError
@ -100,13 +93,55 @@ class Parameters(AnsibleF5Parameters):
return str(self._values['hostname'])
class ApiParameters(Parameters):
pass
class ModuleParameters(Parameters):
pass
class Changes(Parameters):
pass
class UsableChanges(Changes):
pass
class ReportableChanges(Changes):
pass
class Difference(object):
def __init__(self, want, have=None):
self.want = want
self.have = have
def compare(self, param):
try:
result = getattr(self, param)
return result
except AttributeError:
return self.__default(param)
def __default(self, param):
attr1 = getattr(self.want, param)
try:
attr2 = getattr(self.have, param)
if attr1 != attr2:
return attr1
except AttributeError:
return attr1
class ModuleManager(object):
def __init__(self, *args, **kwargs):
self.module = kwargs.get('module', None)
self.client = kwargs.get('client', None)
self.have = None
self.want = Parameters(params=self.module.params)
self.changes = Parameters()
self.have = ApiParameters()
self.want = ModuleParameters(params=self.module.params)
self.changes = UsableChanges()
def _set_changed_options(self):
changed = {}
@ -114,18 +149,20 @@ class ModuleManager(object):
if getattr(self.want, key) is not None:
changed[key] = getattr(self.want, key)
if changed:
self.changes = Parameters(params=changed)
self.changes = UsableChanges(params=changed)
def _update_changed_options(self):
changed = {}
for key in Parameters.updatables:
if getattr(self.want, key) is not None:
attr1 = getattr(self.want, key)
attr2 = getattr(self.have, key)
if attr1 != attr2:
changed[key] = attr1
self.changes = Parameters(params=changed)
diff = Difference(self.want, self.have)
updatables = Parameters.updatables
changed = dict()
for k in updatables:
change = diff.compare(k)
if change is None:
continue
else:
changed[k] = change
if changed:
self.changes = UsableChanges(params=changed)
return True
return False
@ -137,15 +174,29 @@ class ModuleManager(object):
except iControlUnexpectedHTTPError as e:
raise F5ModuleError(str(e))
changes = self.changes.to_return()
reportable = ReportableChanges(params=self.changes.to_return())
changes = reportable.to_return()
result.update(**changes)
result.update(dict(changed=changed))
self._announce_deprecations(result)
return result
def _announce_deprecations(self, result):
warnings = result.pop('__warnings', [])
for warning in warnings:
self.module.deprecate(
msg=warning['msg'],
version=warning['version']
)
def read_current_from_device(self):
resource = self.client.api.tm.sys.global_settings.load()
result = resource.attrs
return Parameters(params=result)
collection = self.client.api.tm.cm.devices.get_collection()
self_device = next((x.name for x in collection if x.selfDevice == "true"), None)
result['self_device'] = self_device
return ApiParameters(params=result)
def update(self):
self.have = self.read_current_from_device()
@ -166,9 +217,10 @@ class ModuleManager(object):
params = self.want.api_params()
resource = self.client.api.tm.sys.global_settings.load()
resource.modify(**params)
self.client.api.tm.cm.devices.exec_cmd(
'mv', name=self.have.hostname, target=self.want.hostname
)
if self.have.self_device:
self.client.api.tm.cm.devices.exec_cmd(
'mv', name=self.have.self_device, target=self.want.hostname
)
class ArgumentSpec(object):

@ -18,7 +18,11 @@ module: bigip_iapp_service
short_description: Manages TCL iApp services on a BIG-IP
description:
- Manages TCL iApp services on a BIG-IP.
version_added: "2.4"
- If you are looking for the API that is communicated with on the BIG-IP,
the one the is used is C(/mgmt/tm/sys/application/service/). There are a
couple of APIs in a BIG-IP that might seem like they are relevant to iApp
Services, but the API mentioned here is the one that is used.
version_added: 2.4
options:
name:
description:
@ -46,6 +50,7 @@ options:
This option is equivalent to re-configuring the iApp if that template
has changed.
default: no
type: bool
state:
description:
- When C(present), ensures that the iApp service is created and running.
@ -214,30 +219,25 @@ from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.basic import env_fallback
from ansible.module_utils.six import iteritems
HAS_DEVEL_IMPORTS = False
try:
# Sideband repository used for dev
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.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 fqdn_name
from library.module_utils.network.f5.common import fq_name
from library.module_utils.network.f5.common import f5_argument_spec
try:
from library.module_utils.network.f5.common import iControlUnexpectedHTTPError
except ImportError:
HAS_F5SDK = False
HAS_DEVEL_IMPORTS = True
except ImportError:
# Upstream Ansible
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.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 fqdn_name
from ansible.module_utils.network.f5.common import fq_name
from ansible.module_utils.network.f5.common import f5_argument_spec
try:
from ansible.module_utils.network.f5.common import iControlUnexpectedHTTPError
@ -250,18 +250,16 @@ class Parameters(AnsibleF5Parameters):
'strictUpdates': 'strict_updates',
'trafficGroup': 'traffic_group',
}
returnables = []
api_attributes = [
'tables', 'variables', 'template', 'lists', 'deviceGroup',
'inheritedDevicegroup', 'inheritedTrafficGroup', 'trafficGroup',
'strictUpdates'
]
updatables = ['tables', 'variables', 'lists', 'strict_updates', 'traffic_group']
def _fqdn_name(self, value):
if value is not None and not value.startswith('/'):
return '/{0}/{1}'.format(self.partition, value)
return value
updatables = ['tables', 'variables', 'lists', 'strict_updates', 'traffic_group']
def to_return(self):
result = {}
@ -388,7 +386,7 @@ class Parameters(AnsibleF5Parameters):
def template(self):
if self._values['template'] is None:
return None
return self._fqdn_name(self._values['template'])
return fq_name(self.partition, self._values['template'])
@template.setter
def template(self, value):
@ -419,14 +417,14 @@ class Parameters(AnsibleF5Parameters):
# Specifying the value overrides any associated value in the payload
elif self._values['traffic_group']:
result = self._fqdn_name(self._values['traffic_group'])
result = fq_name(self.partition, self._values['traffic_group'])
# This will be automatically `None` if it was not set by the
# `parameters` setter
elif self.trafficGroup:
result = self._fqdn_name(self.trafficGroup)
result = fq_name(self.partition, self.trafficGroup)
else:
result = self._fqdn_name(self._values['traffic_group'])
result = fq_name(self.partition, self._values['traffic_group'])
if result.startswith('/Common/'):
return result
else:

@ -30,7 +30,7 @@ description:
existing services are changed to consume that new template. As such,
the ability to update templates in-place requires the C(force) option
to be used.
version_added: "2.4"
version_added: 2.4
options:
force:
description:
@ -108,41 +108,31 @@ import uuid
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.basic import env_fallback
HAS_DEVEL_IMPORTS = False
try:
# Sideband repository used for dev
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.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 fqdn_name
from library.module_utils.network.f5.common import f5_argument_spec
try:
from library.module_utils.network.f5.common import iControlUnexpectedHTTPError
from f5.utils.iapp_parser import NonextantTemplateNameException
except ImportError:
HAS_F5SDK = False
HAS_DEVEL_IMPORTS = True
except ImportError:
# Upstream Ansible
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.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 fqdn_name
from ansible.module_utils.network.f5.common import f5_argument_spec
try:
from ansible.module_utils.network.f5.common import iControlUnexpectedHTTPError
from f5.utils.iapp_parser import NonextantTemplateNameException
except ImportError:
HAS_F5SDK = False
try:
from f5.utils.iapp_parser import NonextantTemplateNameException
except ImportError:
HAS_F5SDK = False
try:
from StringIO import StringIO
except ImportError:
@ -151,6 +141,7 @@ except ImportError:
class Parameters(AnsibleF5Parameters):
api_attributes = []
returnables = []
@property

@ -19,7 +19,7 @@ short_description: Manages Javascript iApp packages on a BIG-IP
description:
- Manages Javascript iApp packages on a BIG-IP. This module will allow
you to deploy iAppLX packages to the BIG-IP and manage their lifecycle.
version_added: "2.5"
version_added: 2.5
options:
package:
description:
@ -88,33 +88,26 @@ import os
import subprocess
import time
from distutils.version import LooseVersion
from ansible.module_utils.basic import AnsibleModule
HAS_DEVEL_IMPORTS = False
from distutils.version import LooseVersion
try:
# Sideband repository used for dev
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.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 fqdn_name
from library.module_utils.network.f5.common import f5_argument_spec
try:
from library.module_utils.network.f5.common import iControlUnexpectedHTTPError
except ImportError:
HAS_F5SDK = False
HAS_DEVEL_IMPORTS = True
except ImportError:
# Upstream Ansible
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.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 fqdn_name
from ansible.module_utils.network.f5.common import f5_argument_spec
try:
from ansible.module_utils.network.f5.common import iControlUnexpectedHTTPError

@ -179,7 +179,7 @@ conditions:
type: complex
contains:
type:
description: The condition type
description: The condition type.
returned: changed
type: string
sample: http_uri

@ -66,6 +66,11 @@ options:
- The route domain id of the system. When creating a new static route, if
this value is not specified, a default value of C(0) will be used.
- This value cannot be changed once it is set.
partition:
description:
- Device partition to manage resources on.
default: Common
version_added: 2.6
state:
description:
- When C(present), ensures that the static route exists.
@ -129,6 +134,11 @@ pool:
returned: changed
type: string
sample: true
partition:
description: The partition that the static route was created on.
returned: changed
type: string
sample: Common
description:
description: Whether the banner is enabled or not.
returned: changed
@ -144,6 +154,7 @@ reject:
import re
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.basic import env_fallback
from ansible.module_utils.parsing.convert_bool import BOOLEANS_TRUE
try:
@ -575,6 +586,10 @@ class ArgumentSpec(object):
default='present',
choices=['absent', 'present']
),
partition=dict(
default='Common',
fallback=(env_fallback, ['F5_PARTITION'])
),
route_domain=dict(type='int')
)
self.argument_spec = {}

@ -0,0 +1,94 @@
{
"kind": "tm:gtm:pool:a:astate",
"name": "foo",
"partition": "Common",
"fullPath": "/Common/foo",
"generation": 142,
"selfLink": "https://localhost/mgmt/tm/gtm/pool/a/~Common~foo?expandSubcollections=true&ver=12.0.0",
"alternateMode": "round-robin",
"dynamicRatio": "disabled",
"enabled": true,
"fallbackIp": "any",
"fallbackMode": "return-to-dns",
"limitMaxBps": 0,
"limitMaxBpsStatus": "disabled",
"limitMaxConnections": 0,
"limitMaxConnectionsStatus": "disabled",
"limitMaxPps": 0,
"limitMaxPpsStatus": "disabled",
"loadBalancingMode": "round-robin",
"manualResume": "disabled",
"maxAnswersReturned": 1,
"monitor": "default",
"qosHitRatio": 5,
"qosHops": 0,
"qosKilobytesSecond": 3,
"qosLcs": 30,
"qosPacketRate": 1,
"qosRtt": 50,
"qosTopology": 0,
"qosVsCapacity": 0,
"qosVsScore": 0,
"ttl": 30,
"verifyMemberAvailability": "enabled",
"membersReference": {
"link": "https://localhost/mgmt/tm/gtm/pool/a/~Common~foo/members?ver=12.0.0",
"isSubcollection": true,
"items": [
{
"kind": "tm:gtm:pool:a:members:membersstate",
"name": "server1:vs1",
"partition": "Common",
"fullPath": "/Common/server1:vs1",
"generation": 141,
"selfLink": "https://localhost/mgmt/tm/gtm/pool/a/~Common~foo/members/~Common~server1:vs1?ver=12.0.0",
"enabled": true,
"limitMaxBps": 0,
"limitMaxBpsStatus": "disabled",
"limitMaxConnections": 0,
"limitMaxConnectionsStatus": "disabled",
"limitMaxPps": 0,
"limitMaxPpsStatus": "disabled",
"memberOrder": 0,
"monitor": "default",
"ratio": 1
},
{
"kind": "tm:gtm:pool:a:members:membersstate",
"name": "server1:vs2",
"partition": "Common",
"fullPath": "/Common/server1:vs2",
"generation": 142,
"selfLink": "https://localhost/mgmt/tm/gtm/pool/a/~Common~foo/members/~Common~server1:vs1?ver=12.0.0",
"enabled": true,
"limitMaxBps": 0,
"limitMaxBpsStatus": "disabled",
"limitMaxConnections": 0,
"limitMaxConnectionsStatus": "disabled",
"limitMaxPps": 0,
"limitMaxPpsStatus": "disabled",
"memberOrder": 1,
"monitor": "/Common/tcp ",
"ratio": 1
},
{
"kind": "tm:gtm:pool:a:members:membersstate",
"name": "server1:vs3",
"partition": "Common",
"fullPath": "/Common/server1:vs3",
"generation": 141,
"selfLink": "https://localhost/mgmt/tm/gtm/pool/a/~Common~foo/members/~Common~server1:vs3?ver=12.0.0",
"enabled": true,
"limitMaxBps": 0,
"limitMaxBpsStatus": "disabled",
"limitMaxConnections": 0,
"limitMaxConnectionsStatus": "disabled",
"limitMaxPps": 0,
"limitMaxPpsStatus": "disabled",
"memberOrder": 2,
"monitor": "default",
"ratio": 1
}
]
}
}

@ -20,17 +20,19 @@ from ansible.compat.tests.mock import patch
from ansible.module_utils.basic import AnsibleModule
try:
from library.bigip_gtm_pool import Parameters
from library.bigip_gtm_pool import ModuleManager
from library.bigip_gtm_pool import ArgumentSpec
from library.bigip_gtm_pool import UntypedManager
from library.bigip_gtm_pool import TypedManager
from library.modules.bigip_gtm_pool import ApiParameters
from library.modules.bigip_gtm_pool import ModuleParameters
from library.modules.bigip_gtm_pool import ModuleManager
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
except ImportError:
try:
from ansible.modules.network.f5.bigip_gtm_pool import Parameters
from ansible.modules.network.f5.bigip_gtm_pool import ApiParameters
from ansible.modules.network.f5.bigip_gtm_pool import ModuleParameters
from ansible.modules.network.f5.bigip_gtm_pool import ModuleManager
from ansible.modules.network.f5.bigip_gtm_pool import ArgumentSpec
from ansible.modules.network.f5.bigip_gtm_pool import UntypedManager
@ -73,7 +75,7 @@ class TestParameters(unittest.TestCase):
fallback_ip='10.10.10.10',
type='a'
)
p = Parameters(params=args)
p = ModuleParameters(params=args)
assert p.name == 'foo'
assert p.preferred_lb_method == 'topology'
assert p.alternate_lb_method == 'ratio'
@ -81,6 +83,20 @@ class TestParameters(unittest.TestCase):
assert p.fallback_ip == '10.10.10.10'
assert p.type == 'a'
def test_module_parameters_members(self):
args = dict(
partition='Common',
members=[
dict(
server='foo',
virtual_server='bar'
)
]
)
p = ModuleParameters(params=args)
assert len(p.members) == 1
assert p.members[0] == '/Common/foo:bar'
def test_api_parameters(self):
args = dict(
name='foo',
@ -89,13 +105,21 @@ class TestParameters(unittest.TestCase):
fallbackMode='fewest-hops',
fallbackIp='10.10.10.10'
)
p = Parameters(params=args)
p = ApiParameters(params=args)
assert p.name == 'foo'
assert p.preferred_lb_method == 'topology'
assert p.alternate_lb_method == 'ratio'
assert p.fallback_lb_method == 'fewest-hops'
assert p.fallback_ip == '10.10.10.10'
def test_api_parameters_members(self):
args = load_fixture('load_gtm_pool_a_with_members_1.json')
p = ApiParameters(params=args)
assert len(p.members) == 3
assert p.members[0] == '/Common/server1:vs1'
assert p.members[1] == '/Common/server1:vs2'
assert p.members[2] == '/Common/server1:vs3'
class TestUntypedManager(unittest.TestCase):
@ -148,7 +172,7 @@ class TestUntypedManager(unittest.TestCase):
supports_check_mode=self.spec.supports_check_mode
)
current = Parameters(params=load_fixture('load_gtm_pool_untyped_default.json'))
current = ApiParameters(params=load_fixture('load_gtm_pool_untyped_default.json'))
# Override methods in the specific type of manager
tm = UntypedManager(module=module)
@ -252,7 +276,7 @@ class TestTypedManager(unittest.TestCase):
supports_check_mode=self.spec.supports_check_mode
)
current = Parameters(params=load_fixture('load_gtm_pool_a_default.json'))
current = ApiParameters(params=load_fixture('load_gtm_pool_a_default.json'))
# Override methods in the specific type of manager
tm = TypedManager(module=module)

@ -20,15 +20,17 @@ from ansible.compat.tests.mock import patch
from ansible.module_utils.basic import AnsibleModule
try:
from library.bigip_hostname import Parameters
from library.bigip_hostname import ModuleManager
from library.bigip_hostname import ArgumentSpec
from library.modules.bigip_hostname import ApiParameters
from library.modules.bigip_hostname import ModuleParameters
from library.modules.bigip_hostname import ModuleManager
from library.modules.bigip_hostname 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
except ImportError:
try:
from ansible.modules.network.f5.bigip_hostname import Parameters
from ansible.modules.network.f5.bigip_hostname import ApiParameters
from ansible.modules.network.f5.bigip_hostname import ModuleParameters
from ansible.modules.network.f5.bigip_hostname import ModuleManager
from ansible.modules.network.f5.bigip_hostname import ArgumentSpec
from ansible.module_utils.network.f5.common import F5ModuleError
@ -64,7 +66,7 @@ class TestParameters(unittest.TestCase):
args = dict(
hostname='foo.internal.com'
)
p = Parameters(params=args)
p = ModuleParameters(params=args)
assert p.hostname == 'foo.internal.com'
@ -83,8 +85,8 @@ class TestManager(unittest.TestCase):
# Configure the parameters that would be returned by querying the
# remote device
current = Parameters(
dict(
current = ApiParameters(
params=dict(
hostname='foo.internal.com'
)
)

@ -20,9 +20,9 @@ from ansible.compat.tests.mock import patch
from ansible.module_utils.basic import AnsibleModule
try:
from library.bigip_iapp_service import Parameters
from library.bigip_iapp_service import ModuleManager
from library.bigip_iapp_service import ArgumentSpec
from library.modules.bigip_iapp_service import Parameters
from library.modules.bigip_iapp_service import ModuleManager
from library.modules.bigip_iapp_service 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

@ -20,9 +20,9 @@ from ansible.compat.tests.mock import patch
from ansible.module_utils.basic import AnsibleModule
try:
from library.bigip_iapp_template import Parameters
from library.bigip_iapp_template import ModuleManager
from library.bigip_iapp_template import ArgumentSpec
from library.modules.bigip_iapp_template import Parameters
from library.modules.bigip_iapp_template import ModuleManager
from library.modules.bigip_iapp_template 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

@ -20,9 +20,9 @@ from ansible.compat.tests.mock import patch
from ansible.module_utils.basic import AnsibleModule
try:
from library.bigip_iapp_template import Parameters
from library.bigip_iapp_template import ModuleManager
from library.bigip_iapp_template import ArgumentSpec
from library.modules.bigip_iapp_template import Parameters
from library.modules.bigip_iapp_template import ModuleManager
from library.modules.bigip_iapp_template 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

Loading…
Cancel
Save