From 46fa68ac27dc1dd510e8cd88141c9ab13c58517b Mon Sep 17 00:00:00 2001 From: Tim Rupp Date: Fri, 9 Nov 2018 11:06:18 -0800 Subject: [PATCH] Updates to the f5 module utils (#48428) Updating module utils to align with functionality in modules --- lib/ansible/module_utils/network/f5/bigip.py | 157 ++++++++++++------ lib/ansible/module_utils/network/f5/bigiq.py | 111 +++++-------- .../module_utils/network/f5/ipaddress.py | 12 ++ lib/ansible/module_utils/network/f5/urls.py | 122 ++++++++++++++ 4 files changed, 275 insertions(+), 127 deletions(-) create mode 100644 lib/ansible/module_utils/network/f5/urls.py diff --git a/lib/ansible/module_utils/network/f5/bigip.py b/lib/ansible/module_utils/network/f5/bigip.py index 344523179d..75e4e72a14 100644 --- a/lib/ansible/module_utils/network/f5/bigip.py +++ b/lib/ansible/module_utils/network/f5/bigip.py @@ -7,7 +7,7 @@ from __future__ import absolute_import, division, print_function __metaclass__ = type -import time +import re try: from f5.bigip import ManagementRoot @@ -33,72 +33,123 @@ class F5Client(F5BaseClient): @property def api(self): - exc = None if self._client: return self._client - for x in range(0, 10): - try: - result = ManagementRoot( - self.provider['server'], - self.provider['user'], - self.provider['password'], - port=self.provider['server_port'], - verify=self.provider['validate_certs'], - token='tmos' - ) - self._client = result - return self._client - except Exception as ex: - exc = ex - time.sleep(1) - error = 'Unable to connect to {0} on port {1}.'.format( - self.provider['server'], self.provider['server_port'] - ) - - if exc is not None: - error += ' The reported error was "{0}".'.format(str(exc)) - raise F5ModuleError(error) + try: + result = ManagementRoot( + self.provider['server'], + self.provider['user'], + self.provider['password'], + port=self.provider['server_port'], + verify=self.provider['validate_certs'], + token='tmos' + ) + self._client = result + return self._client + except Exception as ex: + error = 'Unable to connect to {0} on port {1}. The reported error was "{2}".'.format( + self.provider['server'], self.provider['server_port'], str(ex) + ) + raise F5ModuleError(error) class F5RestClient(F5BaseClient): def __init__(self, *args, **kwargs): super(F5RestClient, self).__init__(*args, **kwargs) self.provider = self.merge_provider_params() + self.headers = { + 'Content-Type': 'application/json' + } @property def api(self): - exc = None if self._client: return self._client - for x in range(0, 10): - try: - url = "https://{0}:{1}/mgmt/shared/authn/login".format( - self.provider['server'], self.provider['server_port'] - ) - payload = { - 'username': self.provider['user'], - 'password': self.provider['password'], - 'loginProviderName': self.provider['auth_provider'] or 'tmos' - } - session = iControlRestSession() - session.verify = self.provider['validate_certs'] - response = session.post(url, json=payload) + session, err = self.connect_via_token_auth() + if err or session is None: + session, err = self.connect_via_basic_auth() + if err or session is None: + raise F5ModuleError(err) + self._client = session + return session - if response.status not in [200]: - raise F5ModuleError('Status code: {0}. Unexpected Error: {1} for uri: {2}\nText: {3}'.format( - response.status, response.reason, response.url, response.content - )) - - session.headers['X-F5-Auth-Token'] = response.json()['token']['token'] - self._client = session - return self._client - except Exception as ex: - exc = ex - time.sleep(1) - error = 'Unable to connect to {0} on port {1}.'.format( + def connect_via_token_auth(self): + url = "https://{0}:{1}/mgmt/shared/authn/login".format( self.provider['server'], self.provider['server_port'] ) - if exc is not None: - error += ' The reported error was "{0}".'.format(str(exc)) - raise F5ModuleError(error) + payload = { + 'username': self.provider['user'], + 'password': self.provider['password'], + 'loginProviderName': self.provider['auth_provider'] or 'tmos' + } + session = iControlRestSession( + validate_certs=self.provider['validate_certs'] + ) + + response = session.post( + url, + json=payload, + headers=self.headers + ) + + if response.status not in [200]: + return None, response.content + + session.request.headers['X-F5-Auth-Token'] = response.json()['token']['token'] + return session, None + + def connect_via_basic_auth(self): + url = "https://{0}:{1}/mgmt/tm/sys".format( + self.provider['server'], self.provider['server_port'] + ) + session = iControlRestSession( + url_username=self.provider['user'], + url_password=self.provider['password'], + validate_certs=self.provider['validate_certs'], + ) + + response = session.get( + url, + headers=self.headers + ) + + if response.status not in [200]: + return None, response.content + return session, None + + # TODO(This section of code should be developed to support proxy_to) + # + # def get_identifier(self, proxy_to): + # if re.search(r'([0-9-a-z]+\-){4}[0-9-a-z]+', proxy_to, re.I): + # return proxy_to + # return self.get_device_uuid(proxy_to) + # + # def get_device_uuid(self, proxy_to): + # uri = "https://{0}:{1}/mgmt/shared/resolver/device-groups/cm-cloud-managed-devices/devices/?$filter=hostname+eq+'{2}'&$select=uuid".format( + # self.provider['server'], self.provider['server_port'], proxy_to + # ) + # 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) + # + # if len(collection) > 1: + # raise F5ModuleError( + # "More that one managed device was found with this hostname. " + # "'proxy_to' devices must be unique. Consider specifying the UUID of the device." + # ) + # elif len(collection) == 0: + # raise F5ModuleError( + # "No device was found with that hostname" + # ) + # else: + # resource = collection.pop() + # return resource.pop('uuid', None) diff --git a/lib/ansible/module_utils/network/f5/bigiq.py b/lib/ansible/module_utils/network/f5/bigiq.py index 6ac024987c..f994ed6f43 100644 --- a/lib/ansible/module_utils/network/f5/bigiq.py +++ b/lib/ansible/module_utils/network/f5/bigiq.py @@ -10,12 +10,6 @@ __metaclass__ = type import os import time -try: - from f5.bigiq import ManagementRoot - from icontrol.exceptions import iControlUnexpectedHTTPError - HAS_F5SDK = True -except ImportError: - HAS_F5SDK = False try: from library.module_utils.network.f5.common import F5BaseClient @@ -29,87 +23,56 @@ except ImportError: from ansible.module_utils.network.f5.icontrol import iControlRestSession -class F5Client(F5BaseClient): - def __init__(self, *args, **kwargs): - super(F5Client, self).__init__(*args, **kwargs) - self.provider = self.merge_provider_params() - - @property - def api(self): - exc = None - if self._client: - return self._client - for x in range(0, 10): - try: - result = ManagementRoot( - self.provider['server'], - self.provider['user'], - self.provider['password'], - port=self.provider['server_port'], - verify=self.provider['validate_certs'] - ) - self._client = result - return self._client - except Exception as ex: - exc = ex - time.sleep(1) - error = 'Unable to connect to {0} on port {1}.'.format( - self.provider['server'], self.provider['server_port'] - ) - - if exc is not None: - error += ' The reported error was "{0}".'.format(str(exc)) - raise F5ModuleError(error) - - class F5RestClient(F5BaseClient): def __init__(self, *args, **kwargs): super(F5RestClient, self).__init__(*args, **kwargs) self.provider = self.merge_provider_params() + self.headers = { + 'Content-Type': 'application/json' + } @property def api(self): - exc = None if self._client: return self._client - for x in range(0, 10): - try: - provider = self.provider['auth_provider'] or 'local' - url = "https://{0}:{1}/mgmt/shared/authn/login".format( - self.provider['server'], self.provider['server_port'] - ) - payload = { - 'username': self.provider['user'], - 'password': self.provider['password'], - } + session, err = self.connect_via_token_auth() + if err: + raise F5ModuleError(err) + self._client = session + return session - # - local is a special provider that is baked into the system and - # has no loginReference - if provider != 'local': - login_ref = self.get_login_ref(provider) - payload.update(login_ref) + def connect_via_token_auth(self): + provider = self.provider['auth_provider'] or 'local' - session = iControlRestSession() - session.verify = self.provider['validate_certs'] - response = session.post(url, json=payload) - - if response.status not in [200]: - raise F5ModuleError('Status code: {0}. Unexpected Error: {1} for uri: {2}\nText: {3}'.format( - response.status, response.reason, response.url, response.content - )) - - session.headers['X-F5-Auth-Token'] = response.json()['token']['token'] - self._client = session - return self._client - except Exception as ex: - exc = ex - time.sleep(1) - error = 'Unable to connect to {0} on port {1}.'.format( + url = "https://{0}:{1}/mgmt/shared/authn/login".format( self.provider['server'], self.provider['server_port'] ) - if exc is not None: - error += ' The reported error was "{0}".'.format(str(exc)) - raise F5ModuleError(error) + payload = { + 'username': self.provider['user'], + 'password': self.provider['password'], + } + + # - local is a special provider that is baked into the system and + # has no loginReference + if provider != 'local': + login_ref = self.get_login_ref(provider) + payload.update(login_ref) + + session = iControlRestSession( + validate_certs=self.provider['validate_certs'] + ) + + response = session.post( + url, + json=payload, + headers=self.headers + ) + + if response.status not in [200]: + return None, response.content + + session.request.headers['X-F5-Auth-Token'] = response.json()['token']['token'] + return session, None def get_login_ref(self, provider): info = self.read_provider_info_from_device() diff --git a/lib/ansible/module_utils/network/f5/ipaddress.py b/lib/ansible/module_utils/network/f5/ipaddress.py index 22c8f8d2c3..495b0b90e6 100644 --- a/lib/ansible/module_utils/network/f5/ipaddress.py +++ b/lib/ansible/module_utils/network/f5/ipaddress.py @@ -91,3 +91,15 @@ def is_valid_ip_interface(address): return True except ValueError: return False + + +def get_netmask(address): + addr = ip_network(address) + netmask = addr.netmask.compressed + return netmask + + +def compress_address(address): + addr = ip_network(address) + result = addr.compressed.split('/')[0] + return result diff --git a/lib/ansible/module_utils/network/f5/urls.py b/lib/ansible/module_utils/network/f5/urls.py new file mode 100644 index 0000000000..c3fc857117 --- /dev/null +++ b/lib/ansible/module_utils/network/f5/urls.py @@ -0,0 +1,122 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2017, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +import re + +try: + from library.module_utils.network.f5.common import F5ModuleError +except ImportError: + from ansible.module_utils.network.f5.common import F5ModuleError + +_CLEAN_HEADER_REGEX_BYTE = re.compile(b'^\\S[^\\r\\n]*$|^$') +_CLEAN_HEADER_REGEX_STR = re.compile(r'^\S[^\r\n]*$|^$') + + +def check_header_validity(header): + """Verifies that header value is a string which doesn't contain + leading whitespace or return characters. + + NOTE: This is a slightly modified version of the original function + taken from the requests library: + http://docs.python-requests.org/en/master/_modules/requests/utils/ + + :param header: string containing ':'. + """ + try: + name, value = header.split(':') + except ValueError: + raise F5ModuleError('Invalid header format: {0}'.format(header)) + if name == '': + raise F5ModuleError('Invalid header format: {0}'.format(header)) + + if isinstance(value, bytes): + pat = _CLEAN_HEADER_REGEX_BYTE + else: + pat = _CLEAN_HEADER_REGEX_STR + try: + if not pat.match(value): + raise F5ModuleError("Invalid return character or leading space in header: %s" % name) + except TypeError: + raise F5ModuleError("Value for header {%s: %s} must be of type str or " + "bytes, not %s" % (name, value, type(value))) + + +def build_service_uri(base_uri, partition, name): + """Build the proper uri for a service resource. + This follows the scheme: + /~~<.app>~ + :param base_uri: str -- base uri of the REST endpoint + :param partition: str -- partition for the service + :param name: str -- name of the service + :returns: str -- uri to access the service + """ + name = name.replace('/', '~') + return '%s~%s~%s.app~%s' % (base_uri, partition, name, name) + + +def parseStats(entry): + if 'description' in entry: + return entry['description'] + elif 'value' in entry: + return entry['value'] + elif 'entries' in entry or 'nestedStats' in entry and 'entries' in entry['nestedStats']: + if 'entries' in entry: + entries = entry['entries'] + else: + entries = entry['nestedStats']['entries'] + result = None + + for name in entries: + entry = entries[name] + if 'https://localhost' in name: + name = name.split('/') + name = name[-1] + if result and isinstance(result, list): + result.append(parseStats(entry)) + elif result and isinstance(result, dict): + result[name] = parseStats(entry) + else: + try: + int(name) + result = list() + result.append(parseStats(entry)) + except ValueError: + result = dict() + result[name] = parseStats(entry) + else: + if '.' in name: + names = name.split('.') + key = names[0] + value = names[1] + if result is None: + # result can be None if this branch is reached first + # + # For example, the mgmt/tm/net/trunk/NAME/stats API + # returns counters.bitsIn before anything else. + result = dict() + result[key] = dict() + elif key not in result: + result[key] = dict() + elif result[key] is None: + result[key] = dict() + result[key][value] = parseStats(entry) + else: + if result and isinstance(result, list): + result.append(parseStats(entry)) + elif result and isinstance(result, dict): + result[name] = parseStats(entry) + else: + try: + int(name) + result = list() + result.append(parseStats(entry)) + except ValueError: + result = dict() + result[name] = parseStats(entry) + return result