From d05da83495c7e6d34d9e100622ab1476a03ab237 Mon Sep 17 00:00:00 2001 From: Tim Rupp Date: Tue, 21 Aug 2018 18:01:52 -0400 Subject: [PATCH] Removes dependencies and cleans up code (#44484) Portions of the f5-sdk were removed as well as the netaddr library and were replaced with libraries that are part of ansible. Additionally, deprecated code has been removed. --- .../modules/network/f5/bigip_device_httpd.py | 95 ++--- .../modules/network/f5/bigip_pool_member.py | 120 ++----- .../modules/network/f5/bigip_qkview.py | 330 ++++++++++++------ .../modules/network/f5/bigip_static_route.py | 261 ++++++++------ .../network/f5/test_bigip_pool_member.py | 134 ------- 5 files changed, 457 insertions(+), 483 deletions(-) diff --git a/lib/ansible/modules/network/f5/bigip_device_httpd.py b/lib/ansible/modules/network/f5/bigip_device_httpd.py index 07479beebb0..bf333118ca3 100644 --- a/lib/ansible/modules/network/f5/bigip_device_httpd.py +++ b/lib/ansible/modules/network/f5/bigip_device_httpd.py @@ -233,33 +233,21 @@ from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.six import string_types 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 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 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 f5_argument_spec - try: - from ansible.module_utils.network.f5.common import iControlUnexpectedHTTPError - except ImportError: - HAS_F5SDK = False - -try: - from requests.exceptions import ConnectionError - HAS_REQUESTS = True -except ImportError: - HAS_REQUESTS = False + from ansible.module_utils.network.f5.common import exit_json + from ansible.module_utils.network.f5.common import fail_json class Parameters(AnsibleF5Parameters): @@ -561,10 +549,7 @@ class ModuleManager(object): def exec_module(self): result = dict() - try: - changed = self.present() - except iControlUnexpectedHTTPError as e: - raise F5ModuleError(str(e)) + changed = self.present() reportable = ReportableChanges(params=self.changes.to_return()) changes = reportable.to_return() @@ -595,22 +580,54 @@ class ModuleManager(object): def update_on_device(self): params = self.changes.api_params() - resource = self.client.api.tm.sys.httpd.load() + uri = "https://{0}:{1}/mgmt/tm/sys/httpd".format( + self.client.provider['server'], + self.client.provider['server_port'] + ) + try: - resource.modify(**params) - return True - except ConnectionError as ex: + 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) + except Exception as ex: + valid = [ + 'Remote end closed connection', + 'Connection aborted', + ] # BIG-IP will kill your management connection when you change the HTTP # redirect setting. So this catches that and handles it gracefully. - if 'Connection aborted' in str(ex) and 'redirectHttpToHttps' in params: - # Wait for BIG-IP web server to settle after changing this - time.sleep(2) - return True + if 'redirectHttpToHttps' in params: + if any(i for i in valid if i in str(ex)): + # Wait for BIG-IP web server to settle after changing this + time.sleep(2) + return True raise F5ModuleError(str(ex)) def read_current_from_device(self): - resource = self.client.api.tm.sys.httpd.load() - return ApiParameters(params=resource.attrs) + uri = "https://{0}:{1}/mgmt/tm/sys/httpd".format( + self.client.provider['server'], + self.client.provider['server_port'] + ) + 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) class ArgumentSpec(object): @@ -664,22 +681,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_REQUESTS: - module.fail_json(msg="The python requests 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__': diff --git a/lib/ansible/modules/network/f5/bigip_pool_member.py b/lib/ansible/modules/network/f5/bigip_pool_member.py index eb98adfa642..d5bf6d33f66 100644 --- a/lib/ansible/modules/network/f5/bigip_pool_member.py +++ b/lib/ansible/modules/network/f5/bigip_pool_member.py @@ -26,6 +26,7 @@ options: - Name of the node to create, or re-use, when creating a new pool member. - This parameter is optional and, if not specified, a node name will be created automatically from either the specified C(address) or C(fqdn). + - The C(enabled) state is an alias of C(present). version_added: 2.6 state: description: @@ -113,11 +114,11 @@ options: - Specifies whether the system automatically creates ephemeral nodes using the IP addresses returned by the resolution of a DNS query for a node defined by an FQDN. - - When C(enabled), the system generates an ephemeral node for each IP address + - When C(yes), the system generates an ephemeral node for each IP address returned in response to a DNS query for the FQDN of the node. Additionally, when a DNS response indicates the IP address of an ephemeral node no longer exists, the system deletes the ephemeral node. - - When C(disabled), the system resolves a DNS query for the FQDN of the node + - When C(no), the system resolves a DNS query for the FQDN of the node with the single IP address associated with the FQDN. - When creating a new pool member, the default for this parameter is C(yes). - This parameter is ignored when C(reuse_nodes) is C(yes). @@ -129,24 +130,6 @@ options: default: yes type: bool version_added: 2.6 - session_state: - description: - - Set new session availability status for pool member. - - This parameter is deprecated and will be removed in Ansible 2.7. Use C(state) - C(enabled) or C(disabled). - version_added: 2.0 - choices: - - enabled - - disabled - monitor_state: - description: - - Set monitor availability status for pool member. - - This parameter is deprecated and will be removed in Ansible 2.7. Use C(state) - C(enabled) or C(disabled). - version_added: 2.0 - choices: - - enabled - - disabled extends_documentation_fragment: f5 author: - Tim Rupp (@caphrim007) @@ -289,6 +272,9 @@ try: from library.module_utils.network.f5.common import fq_name from library.module_utils.network.f5.common import is_valid_hostname from library.module_utils.network.f5.common import f5_argument_spec + from library.module_utils.network.f5.ipaddress import is_valid_ip + from library.module_utils.network.f5.ipaddress import validate_ip_address + from library.module_utils.network.f5.ipaddress import validate_ip_v6_address try: from library.module_utils.network.f5.common import iControlUnexpectedHTTPError except ImportError: @@ -302,17 +288,14 @@ except ImportError: from ansible.module_utils.network.f5.common import fq_name from ansible.module_utils.network.f5.common import is_valid_hostname from ansible.module_utils.network.f5.common import f5_argument_spec + from ansible.module_utils.network.f5.ipaddress import is_valid_ip + from ansible.module_utils.network.f5.ipaddress import validate_ip_address + from ansible.module_utils.network.f5.ipaddress import validate_ip_v6_address 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 - class Parameters(AnsibleF5Parameters): api_map = { @@ -342,10 +325,9 @@ class ModuleParameters(Parameters): def full_name(self): delimiter = ':' try: - addr = netaddr.IPAddress(self.full_name_dict['name']) - if addr.version == 6: + if validate_ip_v6_address(self.full_name_dict['name']): delimiter = '.' - except netaddr.AddrFormatError: + except TypeError: pass return '{0}{1}{2}'.format(self.full_name_dict['name'], delimiter, self.port) @@ -396,49 +378,23 @@ class ModuleParameters(Parameters): ) return int(self._values['port']) - @property - def state(self): - # TODO(Remove all of this state craziness in 2.7) - if self.session_state is not None or self.monitor_state is not None: - if self._values['state'] in ['enabled', 'disabled', 'forced_offline']: - self._values['__warnings'].append([{ - 'msg': "'session_state' is deprecated and will be ignored in favor of 'state'.", - 'version': '2.7' - }]) - return self._values['state'] - else: - if self.session_state is not None: - self._values['__warnings'].append([{ - 'msg': "'session_state' is deprecated and will be removed in the future. Use 'state'.", - 'version': '2.7' - }]) - elif self.monitor_state is not None: - self._values['__warnings'].append([{ - 'msg': "'monitor_state' is deprecated and will be removed in the future. Use 'state'.", - 'version': '2.7' - }]) - - if self.session_state == 'enabled' and self.monitor_state == 'enabled': - return 'enabled' - elif self.session_state == 'disabled' and self.monitor_state == 'enabled': - return 'disabled' - else: - return 'forced_offline' - return self._values['state'] - @property def address(self): if self._values['address'] is None: return None elif self._values['address'] == 'any6': return 'any6' - try: - addr = netaddr.IPAddress(self._values['address']) - return str(addr) - except netaddr.AddrFormatError: - raise F5ModuleError( - "The specified 'address' value is not a valid IP address." - ) + if is_valid_ip(self._values['address']): + return self._values['address'] + raise F5ModuleError( + "The specified 'address' value is not a valid IP address." + ) + + @property + def state(self): + if self._values['state'] == 'enabled': + return 'present' + return self._values['state'] class ApiParameters(Parameters): @@ -652,7 +608,7 @@ class ModuleManager(object): name=self.want.pool, partition=self.want.partition ) - except Exception: + except Exception as ex: raise F5ModuleError('The specified pool does not exist') result = pool.members_s.members.exists( name=self.want.full_name, @@ -687,13 +643,12 @@ class ModuleManager(object): return True def _set_host_by_name(self): - try: - netaddr.IPAddress(self.want.name) + if is_valid_ip(self.want.name): self.want.update({ 'fqdn': None, 'address': self.want.name }) - except netaddr.AddrFormatError: + else: if not is_valid_hostname(self.want.name): raise F5ModuleError( "'name' is neither a valid IP address or FQDN name." @@ -708,28 +663,16 @@ class ModuleManager(object): self.want.update({ 'state': 'user-down', 'session': 'user-disabled', - - # TODO(Remove in 2.7) - 'session_state': None, - 'monitor_state': None }) elif self.want.state == 'disabled': self.want.update({ 'state': 'user-up', 'session': 'user-disabled', - - # TODO(Remove in 2.7) - 'session_state': None, - 'monitor_state': None }) elif self.want.state in ['present', 'enabled']: self.want.update({ 'state': 'user-up', 'session': 'user-enabled', - - # TODO(Remove in 2.7) - 'session_state': None, - 'monitor_state': None }) def _update_address_with_existing_nodes(self): @@ -863,17 +806,6 @@ class ArgumentSpec(object): ), fqdn_auto_populate=dict(type='bool'), reuse_nodes=dict(type='bool', default=True), - - # Deprecated params - # TODO(Remove in 2.7) - session_state=dict( - choices=['enabled', 'disabled'], - removed_in_version=2.7, - ), - monitor_state=dict( - choices=['enabled', 'disabled'], - removed_in_version=2.7, - ), ) self.argument_spec = {} self.argument_spec.update(f5_argument_spec) @@ -895,8 +827,6 @@ def main(): ) 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) diff --git a/lib/ansible/modules/network/f5/bigip_qkview.py b/lib/ansible/modules/network/f5/bigip_qkview.py index bfcd35640a0..55e4dc4e775 100644 --- a/lib/ansible/modules/network/f5/bigip_qkview.py +++ b/lib/ansible/modules/network/f5/bigip_qkview.py @@ -86,53 +86,53 @@ EXAMPLES = r''' ''' RETURN = r''' -stdout: - description: The set of responses from the commands - returned: always - type: list - sample: ['...', '...'] -stdout_lines: - description: The value of stdout split into a list - returned: always - type: list - sample: [['...', '...'], ['...'], ['...']] +# only common fields returned ''' import os import re +import socket +import time from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.six import string_types from distutils.version import LooseVersion try: - from library.module_utils.network.f5.bigip import HAS_F5SDK - from library.module_utils.network.f5.bigip import F5Client + import urlparse +except ImportError: + import urllib.parse as urlparse + +try: + 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 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 exit_json + from library.module_utils.network.f5.common import fail_json + from library.module_utils.network.f5.common import transform_name + from library.module_utils.network.f5.icontrol import download_file 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 f5_argument_spec - try: - from ansible.module_utils.network.f5.common import iControlUnexpectedHTTPError - except ImportError: - HAS_F5SDK = False + from ansible.module_utils.network.f5.common import exit_json + from ansible.module_utils.network.f5.common import fail_json + from ansible.module_utils.network.f5.common import transform_name + from ansible.module_utils.network.f5.icontrol import download_file class Parameters(AnsibleF5Parameters): api_attributes = [ - 'exclude', 'exclude_core', 'complete_information', 'max_file_size', - 'asm_request_log', 'filename_cmd' + 'asm_request_log', + 'complete_information', + 'exclude', + 'exclude_core', + 'filename_cmd', + 'max_file_size', ] returnables = ['stdout', 'stdout_lines', 'warnings'] @@ -163,8 +163,8 @@ class Parameters(AnsibleF5Parameters): @property def max_file_size(self): - if self._values['max_file_size'] in [None, 0]: - return '-s0' + if self._values['max_file_size'] in [None]: + return None return '-s {0}'.format(self._values['max_file_size']) @property @@ -229,14 +229,16 @@ class ModuleManager(object): return BulkLocationManager(**self.kwargs) def is_version_less_than_14(self): - """Checks to see if the TMOS version is less than 14 - - Anything less than BIG-IP 13.x does not support users - on different partitions. - - :return: Bool - """ - version = self.client.api.tmos_version + uri = "https://{0}:{1}/mgmt/tm/sys".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + version = urlparse.parse_qs(urlparse.urlparse(response['selfLink']).query)['ver'][0] if LooseVersion(version) < LooseVersion('14.0.0'): return True else: @@ -259,19 +261,10 @@ class BaseManager(object): if changed: self.changes = Parameters(params=changed) - def _to_lines(self, stdout): - lines = [] - if isinstance(stdout, string_types): - lines = str(stdout).split('\n') - return lines - def exec_module(self): result = dict() - try: - self.present() - except iControlUnexpectedHTTPError as e: - raise F5ModuleError(str(e)) + self.present() result.update(**self.changes.to_return()) result.update(dict(changed=False)) @@ -292,21 +285,35 @@ class BaseManager(object): self.execute() def exists(self): - ls = self.client.api.tm.util.unix_ls.exec_cmd( - 'run', utilCmdArgs=self.remote_dir + params = dict( + command='run', + utilCmdArgs=self.remote_dir ) - - # Empty directories return nothing to the commandResult - if not hasattr(ls, 'commandResult'): + uri = "https://{0}:{1}/mgmt/tm/util/unix-ls".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.post(uri, json=params) + try: + response = resp.json() + except ValueError: + return False + if resp.status == 404 or 'code' in response and response['code'] == 404: return False - if self.want.filename in ls.commandResult: - return True - else: + try: + if self.want.filename in response['commandResult']: + return True + except KeyError: return False def execute(self): response = self.execute_on_device() + if not response: + raise F5ModuleError( + "Failed to create qkview on device." + ) + result = self._move_qkview_to_download() if not result: raise F5ModuleError( @@ -326,49 +333,179 @@ class BaseManager(object): "Failed to remove the remote qkview" ) - self.changes = Parameters({ - 'stdout': response, - 'stdout_lines': self._to_lines(response) - }) - def _delete_qkview(self): tpath_name = '{0}/{1}'.format(self.remote_dir, self.want.filename) - self.client.api.tm.util.unix_rm.exec_cmd( - 'run', utilCmdArgs=tpath_name + params = dict( + command='run', + utilCmdArgs=tpath_name ) + uri = "https://{0}:{1}/mgmt/tm/util/unix-rm".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.post(uri, json=params) + try: + response = resp.json() + except ValueError: + return False + if resp.status == 404 or 'code' in response and response['code'] == 404: + return False def execute_on_device(self): - params = self.want.api_params().values() - output = self.client.api.tm.util.qkview.exec_cmd( - 'run', - utilCmdArgs='{0}'.format(' '.join(params)) + self._upsert_temporary_cli_script_on_device() + task_id = self._create_async_task_on_device() + self._exec_async_task_on_device(task_id) + self._wait_for_async_task_to_finish_on_device(task_id) + self._remove_temporary_cli_script_from_device() + return True + + def _upsert_temporary_cli_script_on_device(self): + args = { + "name": "__ansible_mkqkview", + "apiAnonymous": """ + proc script::run {} { + set cmd [lreplace $tmsh::argv 0 0]; eval "exec $cmd 2> /dev/null" + } + """ + } + result = self._create_temporary_cli_script_on_device(args) + if result: + return True + return self._update_temporary_cli_script_on_device(args) + + def _create_temporary_cli_script_on_device(self, args): + uri = "https://{0}:{1}/mgmt/tm/cli/script".format( + self.client.provider['server'], + self.client.provider['server_port'], ) - if hasattr(output, 'commandResult'): - return str(output.commandResult) - return None + resp = self.client.api.post(uri, json=args) + try: + response = resp.json() + if 'code' in response and response['code'] in [404, 409]: + return False + except ValueError: + pass + if resp.status in [404, 409]: + return False + return True + def _update_temporary_cli_script_on_device(self, args): + uri = "https://{0}:{1}/mgmt/tm/cli/script/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name('Common', '__ansible_mkqkview') + ) + resp = self.client.api.put(uri, json=args) + try: + resp.json() + return True + except ValueError: + raise F5ModuleError( + "Failed to update temporary cli script on device." + ) -class BulkLocationManager(BaseManager): - def __init__(self, *args, **kwargs): - super(BulkLocationManager, self).__init__(**kwargs) - self.remote_dir = '/var/config/rest/bulk' + def _create_async_task_on_device(self): + """Creates an async cli script task in the REST API - def _move_qkview_to_download(self): + Returns: + int: The ID of the task staged for running. + + :return: + """ + command = ' '.join(self.want.api_params().values()) + args = { + "command": "run", + "name": "__ansible_mkqkview", + "utilCmdArgs": "/usr/bin/qkview {0}".format(command) + } + uri = "https://{0}:{1}/mgmt/tm/task/cli/script".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.post(uri, json=args) try: - move_path = '/var/tmp/{0} {1}/{0}'.format( - self.want.filename, self.remote_dir + response = resp.json() + return response['_taskId'] + except ValueError: + raise F5ModuleError( + "Failed to create the async task on the device." ) - self.client.api.tm.util.unix_mv.exec_cmd( - 'run', - utilCmdArgs=move_path + + def _exec_async_task_on_device(self, task_id): + args = {"_taskState": "VALIDATING"} + uri = "https://{0}:{1}/mgmt/tm/task/cli/script/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + task_id + ) + resp = self.client.api.put(uri, json=args) + try: + resp.json() + return True + except ValueError: + raise F5ModuleError( + "Failed to execute the async task on the device" ) + + def _wait_for_async_task_to_finish_on_device(self, task_id): + uri = "https://{0}:{1}/mgmt/tm/task/cli/script/{2}/result".format( + self.client.provider['server'], + self.client.provider['server_port'], + task_id + ) + while True: + try: + resp = self.client.api.get(uri, timeout=10) + except socket.timeout: + continue + response = resp.json() + if response['_taskState'] == 'FAILED': + raise F5ModuleError( + "qkview creation task failed unexpectedly." + ) + if response['_taskState'] == 'COMPLETED': + return True + time.sleep(3) + + def _remove_temporary_cli_script_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/task/cli/script/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name('Common', '__ansible_mkqkview') + ) + try: + self.client.api.delete(uri) return True - except Exception: - return False + except ValueError: + raise F5ModuleError( + "Failed to remove the temporary cli script from the device." + ) + + def _move_qkview_to_download(self): + uri = "https://{0}:{1}/mgmt/tm/util/unix-mv/".format( + self.client.provider['server'], + self.client.provider['server_port'] + ) + args = dict( + command='run', + utilCmdArgs='/var/tmp/{0} {1}/{0}'.format(self.want.filename, self.remote_dir) + ) + self.client.api.post(uri, json=args) + return True + + +class BulkLocationManager(BaseManager): + def __init__(self, *args, **kwargs): + super(BulkLocationManager, self).__init__(**kwargs) + self.remote_dir = '/var/config/rest/bulk' def _download_file(self): - bulk = self.client.api.shared.file_transfer.bulk - bulk.download_file(self.want.filename, self.want.dest) + uri = "https://{0}:{1}/mgmt/shared/file-transfer/bulk/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.filename + ) + download_file(self.client, uri, self.want.dest) if os.path.exists(self.want.dest): return True return False @@ -379,22 +516,13 @@ class MadmLocationManager(BaseManager): super(MadmLocationManager, self).__init__(**kwargs) self.remote_dir = '/var/config/rest/madm' - def _move_qkview_to_download(self): - try: - move_path = '/var/tmp/{0} {1}/{0}'.format( - self.want.filename, self.remote_dir - ) - self.client.api.tm.util.unix_mv.exec_cmd( - 'run', - utilCmdArgs=move_path - ) - return True - except Exception: - return False - def _download_file(self): - madm = self.client.api.shared.file_transfer.madm - madm.download_file(self.want.filename, self.want.dest) + uri = "https://{0}:{1}/mgmt/shared/file-transfer/madm/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.filename + ) + download_file(self.client, uri, self.want.dest) if os.path.exists(self.want.dest): return True return False @@ -447,20 +575,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") 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__': diff --git a/lib/ansible/modules/network/f5/bigip_static_route.py b/lib/ansible/modules/network/f5/bigip_static_route.py index fc00b1dbd10..196cb31a55d 100644 --- a/lib/ansible/modules/network/f5/bigip_static_route.py +++ b/lib/ansible/modules/network/f5/bigip_static_route.py @@ -79,12 +79,7 @@ options: choices: - present - absent -notes: - - Requires the netaddr Python package on the host. This is as easy as pip - install netaddr. extends_documentation_fragment: f5 -requirements: - - netaddr author: - Tim Rupp (@caphrim007) ''' @@ -158,37 +153,27 @@ from ansible.module_utils.basic import env_fallback 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 + from library.module_utils.compat.ipaddress import ip_network 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 + from ansible.module_utils.compat.ipaddress import ip_network class Parameters(AnsibleF5Parameters): @@ -200,18 +185,37 @@ class Parameters(AnsibleF5Parameters): } updatables = [ - 'description', 'gateway_address', 'vlan', - 'pool', 'mtu', 'reject', 'destination', 'route_domain', - 'netmask' + 'description', + 'gateway_address', + 'vlan', + 'pool', + 'mtu', + 'reject', + 'destination', + 'route_domain', + 'netmask', ] returnables = [ - 'vlan', 'gateway_address', 'destination', 'pool', 'description', - 'reject', 'mtu', 'netmask', 'route_domain' + 'vlan', + 'gateway_address', + 'destination', + 'pool', + 'description', + 'reject', + 'mtu', + 'netmask', + 'route_domain', ] api_attributes = [ - 'tmInterface', 'gw', 'network', 'blackhole', 'description', 'pool', 'mtu' + 'tmInterface', + 'gw', + 'network', + 'blackhole', + 'description', + 'pool', + 'mtu', ] def to_return(self): @@ -239,9 +243,9 @@ class ModuleParameters(Parameters): if self._values['gateway_address'] is None: return None try: - ip = netaddr.IPNetwork(self._values['gateway_address']) - return str(ip.ip) - except netaddr.core.AddrFormatError: + ip = ip_network(u'%s' % str(self._values['gateway_address'])) + return str(ip.network_address) + except ValueError: raise F5ModuleError( "The provided gateway_address is not an IP address" ) @@ -257,17 +261,17 @@ class ModuleParameters(Parameters): def destination(self): if self._values['destination'] is None: return None - if self._values['destination'] == 'default': + if self._values['destination'].startswith('default'): self._values['destination'] = '0.0.0.0/0' - if self._values['destination'] == 'default-inet6': + if self._values['destination'].startswith('default-inet6'): self._values['destination'] = '::/::' try: - ip = netaddr.IPNetwork(self.destination_ip) + ip = ip_network(u'%s' % str(self.destination_ip)) if self.route_domain: - return '{0}%{2}/{1}'.format(ip.ip, ip.prefixlen, self.route_domain) + return '{0}%{2}/{1}'.format(str(ip.network_address), ip.prefixlen, self.route_domain) else: - return '{0}/{1}'.format(ip.ip, ip.prefixlen) - except netaddr.core.AddrFormatError: + return '{0}/{1}'.format(str(ip.network_address), ip.prefixlen) + except ValueError: raise F5ModuleError( "The provided destination is not an IP address" ) @@ -275,35 +279,28 @@ class ModuleParameters(Parameters): @property def destination_ip(self): if self._values['destination']: - ip = netaddr.IPNetwork('{0}/{1}'.format(self._values['destination'], self.netmask)) - return '{0}/{1}'.format(ip.ip, ip.prefixlen) + ip = ip_network(u'{0}/{1}'.format(self._values['destination'], self.netmask)) + return '{0}/{1}'.format(str(ip.network_address), ip.prefixlen) @property def netmask(self): if self._values['netmask'] is None: return None # Check if numeric - if isinstance(self._values['netmask'], int): + try: result = int(self._values['netmask']) - if 0 < result < 256: + if 0 <= result < 256: return result raise F5ModuleError( 'The provided netmask {0} is neither in IP or CIDR format'.format(result) ) - else: + except ValueError: try: - # IPv4 netmask - address = '0.0.0.0/' + self._values['netmask'] - ip = netaddr.IPNetwork(address) - except netaddr.AddrFormatError as ex: - try: - # IPv6 netmask - address = '::/' + self._values['netmask'] - ip = netaddr.IPNetwork(address) - except netaddr.AddrFormatError as ex: - raise F5ModuleError( - 'The provided netmask {0} is neither in IP or CIDR format'.format(self._values['netmask']) - ) + ip = ip_network(u'%s' % str(self._values['netmask'])) + except ValueError: + raise F5ModuleError( + 'The provided netmask {0} is neither in IP or CIDR format'.format(self._values['netmask']) + ) result = int(ip.prefixlen) return result @@ -313,7 +310,7 @@ class ApiParameters(Parameters): def route_domain(self): if self._values['destination'] is None: return None - pattern = r'([0-9:]%(?P[0-9]+))' + pattern = r'([0-9a-zA-Z\:\-\.]+%(?P[0-9]+))' matches = re.search(pattern, self._values['destination']) if matches: return int(matches.group('rd')) @@ -323,25 +320,36 @@ class ApiParameters(Parameters): def destination_ip(self): if self._values['destination'] is None: return None - if self._values['destination'] == 'default': - self._values['destination'] = '0.0.0.0/0' - if self._values['destination'] == 'default-inet6': - self._values['destination'] = '::/::' + destination = self.destination_to_network() + try: pattern = r'(?P%[0-9]+)' - addr = re.sub(pattern, '', self._values['destination']) - ip = netaddr.IPNetwork(addr) - return '{0}/{1}'.format(ip.ip, ip.prefixlen) - except netaddr.core.AddrFormatError: + addr = re.sub(pattern, '', destination) + ip = ip_network(u'%s' % str(addr)) + return '{0}/{1}'.format(str(ip.network_address), ip.prefixlen) + except ValueError: raise F5ModuleError( - "The provided destination is not an IP address" + "The provided destination is not an IP address." ) @property def netmask(self): - ip = netaddr.IPNetwork(self.destination_ip) + destination = self.destination_to_network() + ip = ip_network(u'%s' % str(destination)) return int(ip.prefixlen) + def destination_to_network(self): + destination = self._values['destination'] + if destination.startswith('default%'): + destination = '0.0.0.0%{0}/0'.format(destination.split('%')[1]) + elif destination.startswith('default-inet6%'): + destination = '::%{0}/::'.format(destination.split('%')[1]) + elif destination.startswith('default-inet6'): + destination = '::/::' + elif destination.startswith('default'): + destination = '0.0.0.0/0' + return destination + class Changes(Parameters): pass @@ -446,13 +454,10 @@ class ModuleManager(object): result = dict() state = self.want.state - try: - if state == "present": - changed = self.present() - elif state == "absent": - changed = self.absent() - except iControlUnexpectedHTTPError as e: - raise F5ModuleError(str(e)) + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() reportable = ReportableChanges(params=self.changes.to_return()) changes = reportable.to_return() @@ -470,12 +475,19 @@ class ModuleManager(object): ) def exists(self): - collection = self.client.api.tm.net.routes.get_collection() - for resource in collection: - if resource.name == self.want.name: - if resource.partition == self.want.partition: - return True - return False + uri = "https://{0}:{1}/mgmt/tm/net/route/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + 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 present(self): if self.exists(): @@ -523,27 +535,62 @@ class ModuleManager(object): # The 'network' attribute is not updatable params.pop('network', None) - result = self.client.api.tm.net.routes.route.load( - name=self.want.name, - partition=self.want.partition + + uri = "https://{0}:{1}/mgmt/tm/net/route/{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 read_current_from_device(self): - resource = self.client.api.tm.net.routes.route.load( - name=self.want.name, - partition=self.want.partition + uri = "https://{0}:{1}/mgmt/tm/net/route/{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 create_on_device(self): params = self.want.api_params() - self.client.api.tm.net.routes.route.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/net/route/".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) def absent(self): if self.exists(): @@ -559,12 +606,14 @@ class ModuleManager(object): return True def remove_from_device(self): - result = self.client.api.tm.net.routes.route.load( - name=self.want.name, - partition=self.want.partition + uri = "https://{0}:{1}/mgmt/tm/net/route/{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 class ArgumentSpec(object): @@ -596,7 +645,7 @@ class ArgumentSpec(object): self.argument_spec.update(f5_argument_spec) self.argument_spec.update(argument_spec) self.mutually_exclusive = [ - ['gateway_address', 'vlan', 'pool', 'reject'] + ['gateway_address', 'pool', 'reject'] ] @@ -608,20 +657,14 @@ def main(): supports_check_mode=spec.supports_check_mode, mutually_exclusive=spec.mutually_exclusive, ) - 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__': diff --git a/test/units/modules/network/f5/test_bigip_pool_member.py b/test/units/modules/network/f5/test_bigip_pool_member.py index b1f15a6dc82..381f0241f14 100644 --- a/test/units/modules/network/f5/test_bigip_pool_member.py +++ b/test/units/modules/network/f5/test_bigip_pool_member.py @@ -202,137 +202,3 @@ class TestManager(unittest.TestCase): assert results['fqdn_auto_populate'] is True assert results['fqdn'] == 'foo.bar.com' assert results['state'] == 'present' - - -class TestLegacyManager(unittest.TestCase): - - def setUp(self): - self.spec = ArgumentSpec() - - def test_create_name_is_hostname_with_session_and_monitor_enabled(self, *args): - # Configure the arguments that would be sent to the Ansible module - set_module_args(dict( - pool='my-pool', - name='my-name', - port=2345, - state='present', - session_state='enabled', - monitor_state='enabled', - partition='Common', - password='password', - server='localhost', - user='admin' - )) - - module = AnsibleModule( - argument_spec=self.spec.argument_spec, - supports_check_mode=self.spec.supports_check_mode - ) - mm = ModuleManager(module=module) - - # Override methods to force specific logic in the module to happen - mm.exists = Mock(return_value=False) - mm.create_on_device = Mock(return_value=True) - - results = mm.exec_module() - - assert results['changed'] is True - assert results['fqdn_auto_populate'] is False - assert results['fqdn'] == 'my-name' - assert results['state'] == 'present' - - def test_create_name_is_address_with_session_and_monitor_enabled(self, *args): - # Configure the arguments that would be sent to the Ansible module - set_module_args(dict( - pool='my-pool', - name='10.10.10.10', - port=2345, - state='present', - session_state='enabled', - monitor_state='enabled', - partition='Common', - password='password', - server='localhost', - user='admin' - )) - - module = AnsibleModule( - argument_spec=self.spec.argument_spec, - supports_check_mode=self.spec.supports_check_mode - ) - mm = ModuleManager(module=module) - - # Override methods to force specific logic in the module to happen - mm.exists = Mock(return_value=False) - mm.create_on_device = Mock(return_value=True) - - results = mm.exec_module() - - assert results['changed'] is True - assert results['fqdn_auto_populate'] is False - assert results['address'] == '10.10.10.10' - assert results['state'] == 'present' - - def test_create_name_is_address_with_session_disabled_and_monitor_enabled(self, *args): - # Configure the arguments that would be sent to the Ansible module - set_module_args(dict( - pool='my-pool', - name='10.10.10.10', - port=2345, - state='present', - monitor_state='enabled', - session_state='disabled', - partition='Common', - password='password', - server='localhost', - user='admin' - )) - - module = AnsibleModule( - argument_spec=self.spec.argument_spec, - supports_check_mode=self.spec.supports_check_mode - ) - mm = ModuleManager(module=module) - - # Override methods to force specific logic in the module to happen - mm.exists = Mock(return_value=False) - mm.create_on_device = Mock(return_value=True) - - results = mm.exec_module() - - assert results['changed'] is True - assert results['fqdn_auto_populate'] is False - assert results['address'] == '10.10.10.10' - assert results['state'] == 'disabled' - - def test_create_name_is_address_with_session_and_monitor_disabled(self, *args): - # Configure the arguments that would be sent to the Ansible module - set_module_args(dict( - pool='my-pool', - name='10.10.10.10', - port=2345, - state='present', - monitor_state='disabled', - session_state='disabled', - partition='Common', - password='password', - server='localhost', - user='admin' - )) - - module = AnsibleModule( - argument_spec=self.spec.argument_spec, - supports_check_mode=self.spec.supports_check_mode - ) - mm = ModuleManager(module=module) - - # Override methods to force specific logic in the module to happen - mm.exists = Mock(return_value=False) - mm.create_on_device = Mock(return_value=True) - - results = mm.exec_module() - - assert results['changed'] is True - assert results['fqdn_auto_populate'] is False - assert results['address'] == '10.10.10.10' - assert results['state'] == 'forced_offline'