Adds parameters and removes netaddr dependency (#44654)

several new parameters added to bigip_node and the netaddr dependency
has been removed.
pull/4420/head
Tim Rupp 2018-08-24 14:38:23 -04:00 committed by GitHub
parent b5d45bdd1a
commit 73c97cb779
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 314 additions and 109 deletions

View File

@ -135,16 +135,33 @@ options:
description: description:
description: description:
- Specifies descriptive text that identifies the node. - 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: partition:
description: description:
- Device partition to manage resources on. - Device partition to manage resources on.
default: Common default: Common
version_added: 2.5 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 extends_documentation_fragment: f5
author: author:
- Tim Rupp (@caphrim007) - 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 from ansible.module_utils.parsing.convert_bool import BOOLEANS_TRUE
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 fq_name from library.module_utils.network.f5.common import fq_name
from library.module_utils.network.f5.common import f5_argument_spec from library.module_utils.network.f5.common import f5_argument_spec
try: from library.module_utils.network.f5.common import transform_name
from library.module_utils.network.f5.common import iControlUnexpectedHTTPError from library.module_utils.network.f5.common import exit_json
except ImportError: from library.module_utils.network.f5.common import fail_json
HAS_F5SDK = False
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 fq_name from ansible.module_utils.network.f5.common import fq_name
from ansible.module_utils.network.f5.common import f5_argument_spec from ansible.module_utils.network.f5.common import f5_argument_spec
try: from ansible.module_utils.network.f5.common import transform_name
from ansible.module_utils.network.f5.common import iControlUnexpectedHTTPError from ansible.module_utils.network.f5.common import exit_json
except ImportError: from ansible.module_utils.network.f5.common import fail_json
HAS_F5SDK = False
try:
import netaddr
HAS_NETADDR = True
except ImportError:
HAS_NETADDR = False
class Parameters(AnsibleF5Parameters): class Parameters(AnsibleF5Parameters):
api_map = { api_map = {
'monitor': 'monitors' 'monitor': 'monitors',
'connectionLimit': 'connection_limit',
'rateLimit': 'rate_limit'
} }
api_attributes = [ 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 # Used for changing state
# #
@ -318,15 +349,37 @@ class Parameters(AnsibleF5Parameters):
] ]
returnables = [ returnables = [
'monitor_type', 'quorum', 'monitors', 'description', 'fqdn', 'session', 'state', 'monitor_type',
'fqdn_auto_populate', 'fqdn_address_type', 'fqdn_up_interval', 'quorum',
'fqdn_down_interval', 'fqdn_name' '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 = [ updatables = [
'monitor_type', 'quorum', 'monitors', 'description', 'state', 'monitor_type',
'fqdn_up_interval', 'fqdn_down_interval', 'tmName', 'fqdn_auto_populate', 'quorum',
'fqdn_address_type' '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): def to_return(self):
@ -362,42 +415,12 @@ class Parameters(AnsibleF5Parameters):
return result return result
@property @property
def quorum(self): def rate_limit(self):
if self.kind == 'tm:ltm:pool:poolstate': if self._values['rate_limit'] is None:
if self._values['monitors'] is None:
return None return None
pattern = r'min\s+(?P<quorum>\d+)\s+of' if self._values['rate_limit'] == 'disabled':
matches = re.search(pattern, self._values['monitors']) return 0
if matches: return int(self._values['rate_limit'])
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']
class Changes(Parameters): class Changes(Parameters):
@ -416,6 +439,8 @@ class UsableChanges(Changes):
result['autopopulate'] = self._values['fqdn_auto_populate'] result['autopopulate'] = self._values['fqdn_auto_populate']
if self._values['fqdn_name'] is not None: if self._values['fqdn_name'] is not None:
result['tmName'] = self._values['fqdn_name'] result['tmName'] = self._values['fqdn_name']
if not result:
return None
return result return result
@ -424,6 +449,26 @@ class ReportableChanges(Changes):
class ModuleParameters(Parameters): 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 @property
def fqdn_up_interval(self): def fqdn_up_interval(self):
if self._values['fqdn_up_interval'] is None: if self._values['fqdn_up_interval'] is None:
@ -466,8 +511,46 @@ class ModuleParameters(Parameters):
result['autopopulate'] = 'disabled' result['autopopulate'] = 'disabled'
return result 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): 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 @property
def fqdn_up_interval(self): def fqdn_up_interval(self):
if self._values['fqdn'] is None: if self._values['fqdn'] is None:
@ -496,6 +579,12 @@ class ApiParameters(Parameters):
if 'autopopulate' in self._values['fqdn']: if 'autopopulate' in self._values['fqdn']:
return str(self._values['fqdn']['autopopulate']) 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): class Difference(object):
def __init__(self, want, have=None): def __init__(self, want, have=None):
@ -522,9 +611,13 @@ class Difference(object):
def monitor_type(self): def monitor_type(self):
if self.want.monitor_type is None: if self.want.monitor_type is None:
self.want.update(dict(monitor_type=self.have.monitor_type)) self.want.update(dict(monitor_type=self.have.monitor_type))
if self.want.quorum is None: if self.want.quorum is None:
self.want.update(dict(quorum=self.have.quorum)) 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.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( raise F5ModuleError(
"Quorum value must be specified with monitor_type 'm_of_n'." "Quorum value must be specified with monitor_type 'm_of_n'."
) )
@ -608,6 +701,15 @@ class Difference(object):
def fqdn(self): def fqdn(self):
return None 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): class ModuleManager(object):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -736,6 +838,10 @@ class ModuleManager(object):
self.want.update({'fqdn_up_interval': 3600}) self.want.update({'fqdn_up_interval': 3600})
if self.want.fqdn_down_interval is None: if self.want.fqdn_down_interval is None:
self.want.update({'fqdn_down_interval': 5}) 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() self._set_changed_options()
if self.module.check_mode: if self.module.check_mode:
@ -782,63 +888,164 @@ class ModuleManager(object):
return True return True
def read_current_from_device(self): def read_current_from_device(self):
resource = self.client.api.tm.ltm.nodes.node.load( uri = "https://{0}:{1}/mgmt/tm/ltm/node/{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 exists(self): def exists(self):
result = self.client.api.tm.ltm.nodes.node.exists( uri = "https://{0}:{1}/mgmt/tm/ltm/node/{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
def update_node_offline_on_device(self): def update_node_offline_on_device(self):
params = dict( params = dict(
session="user-disabled", session="user-disabled",
state="user-down" state="user-down"
) )
result = self.client.api.tm.ltm.nodes.node.load( uri = "https://{0}:{1}/mgmt/tm/ltm/node/{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.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): def update_on_device(self):
params = self.changes.api_params() params = self.changes.api_params()
result = self.client.api.tm.ltm.nodes.node.load( uri = "https://{0}:{1}/mgmt/tm/ltm/node/{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.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): def create_on_device(self):
params = self.want.api_params() params = self.want.api_params()
resource = self.client.api.tm.ltm.nodes.node.create(
name=self.want.name,
partition=self.want.partition,
**params
)
self._wait_for_fqdn_checks(resource)
def _wait_for_fqdn_checks(self, resource): 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']
)
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):
while True: while True:
if resource.state == 'fqdn-checking': have = self.read_current_from_device()
resource.refresh() if have.state == 'fqdn-checking':
time.sleep(1) time.sleep(1)
else: else:
break break
def remove_from_device(self): def remove_from_device(self):
result = self.client.api.tm.ltm.nodes.node.load( uri = "https://{0}:{1}/mgmt/tm/ltm/node/{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)
) )
if result: resp = self.client.api.delete(uri)
result.delete() 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): class ArgumentSpec(object):
@ -873,7 +1080,11 @@ class ArgumentSpec(object):
), ),
fqdn_auto_populate=dict(type='bool'), fqdn_auto_populate=dict(type='bool'),
fqdn_up_interval=dict(), 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 = {}
self.argument_spec.update(f5_argument_spec) self.argument_spec.update(f5_argument_spec)
@ -885,22 +1096,16 @@ 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")
if not HAS_NETADDR:
module.fail_json(msg="The python netaddr module is required")
try: try:
client = F5Client(**module.params) client = F5RestClient(**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) exit_json(module, results, client)
module.exit_json(**results)
except F5ModuleError as ex: except F5ModuleError as ex:
cleanup_tokens(client) fail_json(module, ex, client)
module.fail_json(msg=str(ex))
if __name__ == '__main__': if __name__ == '__main__':