Adds parameters and removes netaddr dependency (#44654)

several new parameters added to bigip_node and the netaddr dependency
has been removed.
pull/44665/head
Tim Rupp 6 years ago committed by GitHub
parent b5d45bdd1a
commit 73c97cb779
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -135,16 +135,33 @@ options:
description:
description:
- Specifies descriptive text that identifies the node.
- You can remove a description by either specifying an empty string, or by
specifying the special value C(none).
connection_limit:
description:
- Node connection limit. Setting this to 0 disables the limit.
version_added: 2.7
rate_limit:
description:
- Node rate limit (connections-per-second). Setting this to 0 disables the limit.
version_added: 2.7
ratio:
description:
- Node ratio weight. Valid values range from 1 through 100.
- When creating a new node, if this parameter is not specified, the default of
C(1) will be used.
version_added: 2.7
dynamic_ratio:
description:
- The dynamic ratio number for the node. Used for dynamic ratio load balancing.
- When creating a new node, if this parameter is not specified, the default of
C(1) will be used.
version_added: 2.7
partition:
description:
- Device partition to manage resources on.
default: Common
version_added: 2.5
notes:
- Requires the netaddr Python package on the host. This is as easy as
C(pip install netaddr).
requirements:
- netaddr
extends_documentation_fragment: f5
author:
- Tim Rupp (@caphrim007)
@ -266,44 +283,58 @@ from ansible.module_utils.parsing.convert_bool import BOOLEANS_FALSE
from ansible.module_utils.parsing.convert_bool import BOOLEANS_TRUE
try:
from library.module_utils.network.f5.bigip import HAS_F5SDK
from library.module_utils.network.f5.bigip import F5Client
from library.module_utils.network.f5.bigip import F5RestClient
from library.module_utils.network.f5.common import F5ModuleError
from library.module_utils.network.f5.common import AnsibleF5Parameters
from library.module_utils.network.f5.common import cleanup_tokens
from library.module_utils.network.f5.common import fq_name
from library.module_utils.network.f5.common import f5_argument_spec
try:
from library.module_utils.network.f5.common import iControlUnexpectedHTTPError
except ImportError:
HAS_F5SDK = False
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
except ImportError:
from ansible.module_utils.network.f5.bigip import HAS_F5SDK
from ansible.module_utils.network.f5.bigip import F5Client
from ansible.module_utils.network.f5.bigip import F5RestClient
from ansible.module_utils.network.f5.common import F5ModuleError
from ansible.module_utils.network.f5.common import AnsibleF5Parameters
from ansible.module_utils.network.f5.common import cleanup_tokens
from ansible.module_utils.network.f5.common import fq_name
from ansible.module_utils.network.f5.common import f5_argument_spec
try:
from ansible.module_utils.network.f5.common import iControlUnexpectedHTTPError
except ImportError:
HAS_F5SDK = False
try:
import netaddr
HAS_NETADDR = True
except ImportError:
HAS_NETADDR = False
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
class Parameters(AnsibleF5Parameters):
api_map = {
'monitor': 'monitors'
'monitor': 'monitors',
'connectionLimit': 'connection_limit',
'rateLimit': 'rate_limit'
}
api_attributes = [
'monitor', 'description', 'address', 'fqdn',
# Leave the ``monitor`` attribute commented out
#
# This attribute is commented out to prevent it from trying to be
# sent to the API during a create or update request. This is because
# the field is **broken** and **will not work** if you send some
# formats of the monitor to the API.
#
# Specifically, the m_of_n types will not work because they include
# the brace ( ``{`` ) character and the API considers this character
# to be invalid.
#
# Monitors are handled in a special case within the ``update_one_device``
# and ``create_one_device`` methods. Refer to them if you need to know
# what that special case is.
#
# 'monitor',
'description',
'address',
'fqdn',
'ratio',
'connectionLimit',
'rateLimit',
# Used for changing state
#
@ -318,15 +349,37 @@ class Parameters(AnsibleF5Parameters):
]
returnables = [
'monitor_type', 'quorum', 'monitors', 'description', 'fqdn', 'session', 'state',
'fqdn_auto_populate', 'fqdn_address_type', 'fqdn_up_interval',
'fqdn_down_interval', 'fqdn_name'
'monitor_type',
'quorum',
'monitors',
'description',
'fqdn',
'session',
'state',
'fqdn_auto_populate',
'fqdn_address_type',
'fqdn_up_interval',
'fqdn_down_interval',
'fqdn_name',
'connection_limit',
'ratio',
'rate_limit'
]
updatables = [
'monitor_type', 'quorum', 'monitors', 'description', 'state',
'fqdn_up_interval', 'fqdn_down_interval', 'tmName', 'fqdn_auto_populate',
'fqdn_address_type'
'monitor_type',
'quorum',
'monitors',
'description',
'state',
'fqdn_up_interval',
'fqdn_down_interval',
'tmName',
'fqdn_auto_populate',
'fqdn_address_type',
'connection_limit',
'ratio',
'rate_limit'
]
def to_return(self):
@ -362,42 +415,12 @@ class Parameters(AnsibleF5Parameters):
return result
@property
def quorum(self):
if self.kind == 'tm:ltm:pool:poolstate':
if self._values['monitors'] is None:
return None
pattern = r'min\s+(?P<quorum>\d+)\s+of'
matches = re.search(pattern, self._values['monitors'])
if matches:
quorum = matches.group('quorum')
else:
quorum = None
else:
quorum = self._values['quorum']
try:
if quorum is None:
return None
return int(quorum)
except ValueError:
raise F5ModuleError(
"The specified 'quorum' must be an integer."
)
@property
def monitor_type(self):
if self.kind == 'tm:ltm:node:nodestate':
if self._values['monitors'] is None:
return None
pattern = r'min\s+\d+\s+of'
matches = re.search(pattern, self._values['monitors'])
if matches:
return 'm_of_n'
else:
return 'and_list'
else:
if self._values['monitor_type'] is None:
return None
return self._values['monitor_type']
def rate_limit(self):
if self._values['rate_limit'] is None:
return None
if self._values['rate_limit'] == 'disabled':
return 0
return int(self._values['rate_limit'])
class Changes(Parameters):
@ -416,6 +439,8 @@ class UsableChanges(Changes):
result['autopopulate'] = self._values['fqdn_auto_populate']
if self._values['fqdn_name'] is not None:
result['tmName'] = self._values['fqdn_name']
if not result:
return None
return result
@ -424,6 +449,26 @@ class ReportableChanges(Changes):
class ModuleParameters(Parameters):
@property
def quorum(self):
if self._values['quorum'] is None:
return None
quorum = self._values['quorum']
try:
if quorum is None:
return None
return int(quorum)
except ValueError:
raise F5ModuleError(
"The specified 'quorum' must be an integer."
)
@property
def monitor_type(self):
if self._values['monitor_type'] is None:
return None
return self._values['monitor_type']
@property
def fqdn_up_interval(self):
if self._values['fqdn_up_interval'] is None:
@ -466,8 +511,46 @@ class ModuleParameters(Parameters):
result['autopopulate'] = 'disabled'
return result
@property
def description(self):
if self._values['description'] is None:
return None
elif self._values['description'] in ['none', '']:
return ''
return self._values['description']
class ApiParameters(Parameters):
@property
def quorum(self):
if self._values['monitors'] is None:
return None
pattern = r'min\s+(?P<quorum>\d+)\s+of'
matches = re.search(pattern, self._values['monitors'])
if matches:
quorum = matches.group('quorum')
else:
quorum = None
try:
if quorum is None:
return None
return int(quorum)
except ValueError:
raise F5ModuleError(
"The specified 'quorum' must be an integer."
)
@property
def monitor_type(self):
if self._values['monitors'] is None:
return None
pattern = r'min\s+\d+\s+of'
matches = re.search(pattern, self._values['monitors'])
if matches:
return 'm_of_n'
else:
return 'and_list'
@property
def fqdn_up_interval(self):
if self._values['fqdn'] is None:
@ -496,6 +579,12 @@ class ApiParameters(Parameters):
if 'autopopulate' in self._values['fqdn']:
return str(self._values['fqdn']['autopopulate'])
@property
def description(self):
if self._values['description'] in [None, 'none']:
return None
return self._values['description']
class Difference(object):
def __init__(self, want, have=None):
@ -522,9 +611,13 @@ class Difference(object):
def monitor_type(self):
if self.want.monitor_type is None:
self.want.update(dict(monitor_type=self.have.monitor_type))
if self.want.quorum is None:
self.want.update(dict(quorum=self.have.quorum))
if self.want.monitor_type == 'm_of_n' and self.want.quorum is None:
if self.want.quorum is None and self.have.quorum is None:
return None
raise F5ModuleError(
"Quorum value must be specified with monitor_type 'm_of_n'."
)
@ -608,6 +701,15 @@ class Difference(object):
def fqdn(self):
return None
@property
def description(self):
if self.want.description is None:
return None
if self.have.description is None and self.want.description == '':
return None
if self.want.description != self.have.description:
return self.want.description
class ModuleManager(object):
def __init__(self, *args, **kwargs):
@ -736,6 +838,10 @@ class ModuleManager(object):
self.want.update({'fqdn_up_interval': 3600})
if self.want.fqdn_down_interval is None:
self.want.update({'fqdn_down_interval': 5})
if self.want.ratio is None:
self.want.update({'ratio': 1})
if self.want.dynamic_ratio is None:
self.want.update({'dynamic_ratio': 1})
self._set_changed_options()
if self.module.check_mode:
@ -782,63 +888,164 @@ class ModuleManager(object):
return True
def read_current_from_device(self):
resource = self.client.api.tm.ltm.nodes.node.load(
name=self.want.name,
partition=self.want.partition
uri = "https://{0}:{1}/mgmt/tm/ltm/node/{2}".format(
self.client.provider['server'],
self.client.provider['server_port'],
transform_name(self.want.partition, self.want.name)
)
result = resource.attrs
return ApiParameters(params=result)
resp = self.client.api.get(uri)
try:
response = resp.json()
except ValueError as ex:
raise F5ModuleError(str(ex))
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 exists(self):
result = self.client.api.tm.ltm.nodes.node.exists(
name=self.want.name,
partition=self.want.partition
uri = "https://{0}:{1}/mgmt/tm/ltm/node/{2}".format(
self.client.provider['server'],
self.client.provider['server_port'],
transform_name(self.want.partition, self.want.name)
)
return result
resp = self.client.api.get(uri)
try:
response = resp.json()
except ValueError:
return False
if resp.status == 404 or 'code' in response and response['code'] == 404:
return False
return True
def update_node_offline_on_device(self):
params = dict(
session="user-disabled",
state="user-down"
)
result = self.client.api.tm.ltm.nodes.node.load(
name=self.want.name,
partition=self.want.partition
uri = "https://{0}:{1}/mgmt/tm/ltm/node/{2}".format(
self.client.provider['server'],
self.client.provider['server_port'],
transform_name(self.want.partition, self.want.name)
)
result.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 update_on_device(self):
params = self.changes.api_params()
result = self.client.api.tm.ltm.nodes.node.load(
name=self.want.name,
partition=self.want.partition
uri = "https://{0}:{1}/mgmt/tm/ltm/node/{2}".format(
self.client.provider['server'],
self.client.provider['server_port'],
transform_name(self.want.partition, self.want.name)
)
result.modify(**params)
if 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)
if self.want.monitors:
self.update_monitors_on_device()
def create_on_device(self):
params = self.want.api_params()
resource = self.client.api.tm.ltm.nodes.node.create(
name=self.want.name,
partition=self.want.partition,
**params
params['name'] = self.want.name
params['partition'] = self.want.partition
uri = "https://{0}:{1}/mgmt/tm/ltm/node/".format(
self.client.provider['server'],
self.client.provider['server_port']
)
self._wait_for_fqdn_checks(resource)
resp = self.client.api.post(uri, json=params)
try:
response = resp.json()
except ValueError as ex:
raise F5ModuleError(str(ex))
if 'code' in response and response['code'] in [400, 403]:
if 'message' in response:
raise F5ModuleError(response['message'])
else:
raise F5ModuleError(resp.content)
if self.want.monitors:
self.update_monitors_on_device()
self._wait_for_fqdn_checks()
def _wait_for_fqdn_checks(self, resource):
def _wait_for_fqdn_checks(self):
while True:
if resource.state == 'fqdn-checking':
resource.refresh()
have = self.read_current_from_device()
if have.state == 'fqdn-checking':
time.sleep(1)
else:
break
def remove_from_device(self):
result = self.client.api.tm.ltm.nodes.node.load(
name=self.want.name,
partition=self.want.partition
uri = "https://{0}:{1}/mgmt/tm/ltm/node/{2}".format(
self.client.provider['server'],
self.client.provider['server_port'],
transform_name(self.want.partition, self.want.name)
)
if result:
result.delete()
resp = self.client.api.delete(uri)
if resp.status == 200:
return True
def update_monitors_on_device(self):
"""Updates the monitors string
There is a long-standing bug in 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 ltm node /{0}/{1} monitor {2}'.format(
self.want.partition, self.want.name, self.want.monitors
)
params = {
"command": "run",
"utilCmdArgs": '-c "{0}"'.format(command)
}
uri = "https://{0}:{1}/mgmt/tm/util/bash".format(
self.client.provider['server'],
self.client.provider['server_port']
)
resp = self.client.api.post(uri, json=params)
try:
response = resp.json()
if 'commandResult' in response and len(response['commandResult'].strip()) > 0:
raise F5ModuleError(response['commandResult'])
except ValueError as ex:
raise F5ModuleError(str(ex))
if 'code' in response and response['code'] in [400, 403]:
if 'message' in response:
raise F5ModuleError(response['message'])
else:
raise F5ModuleError(resp.content)
return True
class ArgumentSpec(object):
@ -873,7 +1080,11 @@ class ArgumentSpec(object):
),
fqdn_auto_populate=dict(type='bool'),
fqdn_up_interval=dict(),
fqdn_down_interval=dict(type='int')
fqdn_down_interval=dict(type='int'),
connection_limit=dict(type='int'),
rate_limit=dict(type='int'),
ratio=dict(type='int'),
dynamic_ratio=dict(type='int')
)
self.argument_spec = {}
self.argument_spec.update(f5_argument_spec)
@ -885,22 +1096,16 @@ def main():
module = AnsibleModule(
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")
if not HAS_NETADDR:
module.fail_json(msg="The python netaddr module is required")
try:
client = F5Client(**module.params)
client = F5RestClient(**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__':

Loading…
Cancel
Save