diff --git a/lib/ansible/module_utils/network/f5/icontrol.py b/lib/ansible/module_utils/network/f5/icontrol.py index 3248cb0ebe..43f8db7e86 100644 --- a/lib/ansible/module_utils/network/f5/icontrol.py +++ b/lib/ansible/module_utils/network/f5/icontrol.py @@ -14,12 +14,10 @@ try: except ImportError: from io import StringIO -from ansible.module_utils.urls import open_url -from ansible.module_utils.six import iteritems -from ansible.module_utils.urls import urllib_error from ansible.module_utils.urls import urlparse +from ansible.module_utils.urls import generic_urlparse from ansible.module_utils._text import to_native -from ansible.module_utils.six import PY3 +from ansible.module_utils.urls import Request try: import json as _json @@ -87,71 +85,7 @@ print(resp.json()) ``` """ - -class Request(object): - def __init__(self, method=None, url=None, headers=None, data=None, params=None, - auth=None, json=None): - self.method = method - self.url = url - self.headers = headers or {} - self.data = data or [] - self.json = json - self.params = params or {} - self.auth = auth - - def prepare(self): - p = PreparedRequest() - p.prepare( - method=self.method, - url=self.url, - headers=self.headers, - data=self.data, - json=self.json, - params=self.params, - ) - return p - - -class PreparedRequest(object): - def __init__(self): - self.method = None - self.url = None - self.headers = None - self.body = None - - def prepare(self, method=None, url=None, headers=None, data=None, params=None, json=None): - self.prepare_method(method) - self.prepare_url(url, params) - self.prepare_headers(headers) - self.prepare_body(data, json) - - def prepare_url(self, url, params): - self.url = url - - def prepare_method(self, method): - self.method = method - if self.method: - self.method = self.method.upper() - - def prepare_headers(self, headers): - self.headers = {} - if headers: - for k, v in iteritems(headers): - self.headers[k] = v - - def prepare_body(self, data, json=None): - body = None - - if not data and json is not None: - self.headers['Content-Type'] = 'application/json' - body = _json.dumps(json) - if not isinstance(body, bytes): - body = body.encode('utf-8') - - if data: - body = data - - self.body = body +from ansible.module_utils.six.moves.urllib.error import HTTPError class Response(object): @@ -162,17 +96,18 @@ class Response(object): self.url = None self.reason = None self.request = None + self.msg = None @property def content(self): - return self._content.decode('utf-8') + return self._content @property def raw_content(self): return self._content def json(self): - return _json.loads(self._content) + return _json.loads(self._content or 'null') @property def ok(self): @@ -190,145 +125,171 @@ class Response(object): class iControlRestSession(object): """Represents a session that communicates with a BigIP. - Instantiate one of these when you want to communicate with an F5 REST - Server, it will handle F5-specific authentication. - - Pass an existing authentication token to the ``token`` argument to re-use - that token for authentication. Otherwise, token authentication is handled - automatically for you. - - On BIG-IQ, it may be necessary to pass the ``auth_provider`` argument if the - user has a different authentication handler configured. Otherwise, the system - defaults for the different products will be used. + This acts as a loose wrapper around Ansible's ``Request`` class. We're doing + this as interim work until we move to the httpapi connector. """ - def __init__(self): - self.headers = self.default_headers() - self.verify = True - self.params = {} - self.timeout = 30 - - self.server = None - self.user = None - self.password = None - self.server_port = None - self.auth_provider = None - - def _normalize_headers(self, headers): - result = {} - result.update(dict((k.lower(), v) for k, v in headers)) - - # Don't be lossy, append header values for duplicate headers - # In Py2 there is nothing that needs done, py2 does this for us - if PY3: - temp_headers = {} - for name, value in headers: - # The same as above, lower case keys to match py2 behavior, and create more consistent results - name = name.lower() - if name in temp_headers: - temp_headers[name] = ', '.join((temp_headers[name], value)) - else: - temp_headers[name] = value - result.update(temp_headers) - return result - - def default_headers(self): - return { - 'connection': 'keep-alive', - 'accept': '*/*', - } - - def prepare_request(self, request): - headers = self.headers.copy() - params = self.params.copy() - - if request.headers is not None: - headers.update(request.headers) - if request.params is not None: - params.update(request.params) - - prepared = PreparedRequest() - prepared.prepare( - method=request.method, - url=request.url, - data=request.data, - json=request.json, + def __init__(self, headers=None, use_proxy=True, force=False, timeout=120, + validate_certs=True, url_username=None, url_password=None, + http_agent=None, force_basic_auth=False, follow_redirects='urllib2', + client_cert=None, client_key=None, cookies=None): + self.request = Request( headers=headers, - params=params, - ) - return prepared - - def request(self, method, url, params=None, data=None, headers=None, auth=None, - timeout=None, verify=None, json=None): - request = Request( - method=method.upper(), - url=url, - headers=headers, - json=json, - data=data or {}, - params=params or {}, - auth=auth - ) - kwargs = dict( + use_proxy=use_proxy, + force=force, timeout=timeout, - verify=verify + validate_certs=validate_certs, + url_username=url_username, + url_password=url_password, + http_agent=http_agent, + force_basic_auth=force_basic_auth, + follow_redirects=follow_redirects, + client_cert=client_cert, + client_key=client_key, + cookies=cookies ) - prepared = self.prepare_request(request) - return self.send(prepared, **kwargs) + self.last_url = None - def send(self, request, **kwargs): + def get_headers(self, result): + try: + return dict(result.getheaders()) + except AttributeError: + return result.headers + + def update_response(self, response, result): + response.headers = self.get_headers(result) + response._content = result.read() + response.status = result.getcode() + response.url = result.geturl() + response.msg = "OK (%s bytes)" % response.headers.get('Content-Length', 'unknown') + + def send(self, method, url, **kwargs): response = Response() - params = dict( - method=request.method, - data=request.body, - timeout=kwargs.get('timeout', None) or self.timeout, - validate_certs=kwargs.get('verify', None) or self.verify, - headers=request.headers - ) + # Set the last_url called + # + # This is used by the object destructor to erase the token when the + # ModuleManager exits and destroys the iControlRestSession object + self.last_url = url + + body = None + data = kwargs.pop('data', None) + json = kwargs.pop('json', None) + + if not data and json is not None: + self.request.headers['Content-Type'] = 'application/json' + body = _json.dumps(json) + if not isinstance(body, bytes): + body = body.encode('utf-8') + if data: + body = data + if body: + kwargs['data'] = body try: - result = open_url(request.url, **params) - response._content = result.read() - response.status = result.getcode() - response.url = result.geturl() - response.msg = "OK (%s bytes)" % result.headers.get('Content-Length', 'unknown') - response.headers = self._normalize_headers(result.headers.items()) - response.request = request - except urllib_error.HTTPError as e: - try: - response._content = e.read() - except AttributeError: - response._content = '' + result = self.request.open(method, url, **kwargs) + except HTTPError as e: + # Catch HTTPError delivered from Ansible + # + # The structure of this object, in Ansible 2.8 is + # + # HttpError { + # args + # characters_written + # close + # code + # delete + # errno + # file + # filename + # filename2 + # fp + # getcode + # geturl + # hdrs + # headers + # info + # msg + # name + # reason + # strerror + # url + # with_traceback + # } + self.update_response(response, e) + return response - response.reason = to_native(e) - response.status_code = e.code + self.update_response(response, result) return response - def delete(self, url, json=None, **kwargs): - return self.request('DELETE', url, json=json, **kwargs) + def delete(self, url, **kwargs): + return self.send('DELETE', url, **kwargs) def get(self, url, **kwargs): - return self.request('GET', url, **kwargs) + return self.send('GET', url, **kwargs) def patch(self, url, data=None, **kwargs): - return self.request('PATCH', url, data=data, **kwargs) + return self.send('PATCH', url, data=data, **kwargs) - def post(self, url, data=None, json=None, **kwargs): - return self.request('POST', url, data=data, json=json, **kwargs) + def post(self, url, data=None, **kwargs): + return self.send('POST', url, data=data, **kwargs) def put(self, url, data=None, **kwargs): - return self.request('PUT', url, data=data, **kwargs) + return self.send('PUT', url, data=data, **kwargs) + + def __del__(self): + if self.last_url is None: + return + token = self.request.headers.get('X-F5-Auth-Token', None) + if not token: + return + try: + p = generic_urlparse(urlparse(self.last_url)) + uri = "https://{0}:{1}/mgmt/shared/authz/tokens/{2}".format( + p['hostname'], p['port'], token + ) + self.delete(uri) + except ValueError: + pass -def debug_prepared_request(url, method, headers, data=None): - result = "curl -k -X {0} {1}".format(method.upper(), url) - for k, v in iteritems(headers): - result = result + " -H '{0}: {1}'".format(k, v) - if any(v == 'application/json' for k, v in iteritems(headers)): - if data: - kwargs = _json.loads(data.decode('utf-8')) - result = result + " -d '" + _json.dumps(kwargs, sort_keys=True) + "'" - return result +class TransactionContextManager(object): + def __init__(self, client, validate_only=False): + self.client = client + self.validate_only = validate_only + self.transid = None + + def __enter__(self): + uri = "https://{0}:{1}/mgmt/tm/transaction/".format( + self.client.provider['server'], + self.client.provider['server_port'] + ) + resp = self.client.api.post(uri, json={}) + if resp.status not in [200]: + raise Exception + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + self.transid = response['transId'] + self.client.api.request.headers['X-F5-REST-Coordination-Id'] = self.transid + return self.client + + def __exit__(self, exc_type, exc_value, exc_tb): + self.client.request.headers.pop('X-F5-REST-Coordination-Id') + if exc_tb is None: + uri = "https://{0}:{1}/mgmt/tm/transaction/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.transid + ) + params = dict( + state="VALIDATING", + validateOnly=self.validate_only + ) + resp = self.client.api.patch(uri, json=params) + if resp.status not in [200]: + raise Exception def download_file(client, url, dest): @@ -375,7 +336,7 @@ def download_file(client, url, dest): # the loop if end == size: break - crange = response.headers['content-range'] + crange = response.headers['Content-Range'] # Determine the total number of bytes to read. if size == 0: size = int(crange.split('/')[-1]) - 1 @@ -542,15 +503,25 @@ def tmos_version(client): def module_provisioned(client, module_name): - modules = dict( - afm='provisioned.cpu.afm', avr='provisioned.cpu.avr', asm='provisioned.cpu.asm', - apm='provisioned.cpu.apm', gtm='provisioned.cpu.gtm', ilx='provisioned.cpu.ilx', - pem='provisioned.cpu.pem', vcmp='provisioned.cpu.vcmp' - ) - uri = "https://{0}:{1}/mgmt/tm/sys/db/{2}".format( + provisioned = modules_provisioned(client) + if module_name in provisioned: + return True + return False + + +def modules_provisioned(client): + """Returns a list of all provisioned modules + + Args: + client: Client connection to the BIG-IP + + Returns: + A list of provisioned modules in their short name for. + For example, ['afm', 'asm', 'ltm'] + """ + uri = "https://{0}:{1}/mgmt/tm/sys/provision".format( client.provider['server'], - client.provider['server_port'], - modules[module_name] + client.provider['server_port'] ) resp = client.api.get(uri) @@ -564,6 +535,6 @@ def module_provisioned(client, module_name): raise F5ModuleError(response['message']) else: raise F5ModuleError(resp.content) - if int(response['value']) == 0: - return False - return True + if 'items' not in response: + return [] + return [x['name'] for x in response['items'] if x['level'] != 'none'] diff --git a/lib/ansible/modules/network/f5/bigip_provision.py b/lib/ansible/modules/network/f5/bigip_provision.py index 93712ba8c9..6c2ca16151 100644 --- a/lib/ansible/modules/network/f5/bigip_provision.py +++ b/lib/ansible/modules/network/f5/bigip_provision.py @@ -1,7 +1,7 @@ #!/usr/bin/python # -*- coding: utf-8 -*- # -# Copyright (c) 2017 F5 Networks Inc. +# 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 @@ -108,29 +108,23 @@ import time from ansible.module_utils.basic import AnsibleModule 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 - from f5.bigip.contexts import TransactionContextManager - 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.icontrol import TransactionContextManager 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 - from f5.bigip.contexts import TransactionContextManager - 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.icontrol import TransactionContextManager class Parameters(AnsibleF5Parameters): @@ -140,16 +134,6 @@ class Parameters(AnsibleF5Parameters): updatables = ['level', 'cgnat'] - def to_return(self): - result = {} - try: - for returnable in self.returnables: - result[returnable] = getattr(self, returnable) - result = self._filter_params(result) - return result - except Exception: - return result - @property def level(self): if self._values['level'] is None: @@ -168,14 +152,22 @@ class ModuleParameters(Parameters): class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + return result + except Exception: + return result + + +class UsableChanges(Changes): pass -class UsableChanges(Parameters): - pass - - -class ReportableChanges(Parameters): +class ReportableChanges(Changes): pass @@ -237,20 +229,14 @@ class ModuleManager(object): return False def exec_module(self): - if not HAS_F5SDK: - raise F5ModuleError("The python f5-sdk module is required") - changed = False 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() changes = self.changes.to_return() result.update(**changes) @@ -264,23 +250,49 @@ class ModuleManager(object): def exists(self): if self.want.module == 'cgnat': - resource = self.client.api.tm.sys.feature_module.cgnat.load() - if resource.disabled is True: - return False - elif resource.enabled is True: - return True + uri = "https://{0}:{1}/mgmt/tm/sys/feature-module/cgnat/".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) + if 'disabled' in response and response['disabled'] is True: + return False + elif 'enabled' in response and response['enabled'] is True: + return True try: for x in range(0, 5): - provision = self.client.api.tm.sys.provision - resource = getattr(provision, self.want.module) - resource = resource.load() - result = resource.attrs - if str(result['level']) != 'none' and self.want.level == 'none': + uri = "https://{0}:{1}/mgmt/tm/sys/provision/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.module + ) + 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 str(response['level']) != 'none' and self.want.level == 'none': return True - if str(result['level']) == 'none' and self.want.level == 'none': + if str(response['level']) == 'none' and self.want.level == 'none': return False - if str(result['level']) == self.want.level: + if str(response['level']) == self.want.level: return True return False except Exception as ex: @@ -314,10 +326,26 @@ class ModuleManager(object): def should_reboot(self): for x in range(0, 24): try: - resource = self.client.api.tm.sys.dbs.db.load(name='provision.action') - if resource.value == 'reboot': + uri = "https://{0}:{1}/mgmt/tm/sys/db/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + 'provision.action' + ) + 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 response['value'] == 'reboot': return True - elif resource.value == 'none': + elif response['value'] == 'none': time.sleep(5) except Exception: time.sleep(5) @@ -328,12 +356,28 @@ class ModuleManager(object): last_reboot = self._get_last_reboot() try: - output = self.client.api.tm.util.bash.exec_cmd( - 'run', + params = dict( + command="run", utilCmdArgs='-c "/sbin/reboot"' ) - if hasattr(output, 'commandResult'): - return str(output.commandResult) + 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() + 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 'commandResult' in response: + return str(response['commandResult']) except Exception: pass @@ -373,44 +417,127 @@ class ModuleManager(object): self.provision_non_dedicated_on_device() def provision_cgnat_on_device(self): - resource = self.client.api.tm.sys.feature_module.cgnat.load() - resource.modify( - enabled=True + uri = "https://{0}:{1}/mgmt/tm/sys/feature-module/cgnat/".format( + self.client.provider['server'], + self.client.provider['server_port'], ) + params = dict(enabled=True) + 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) return True def provision_dedicated_on_device(self): params = self.want.api_params() - tx = self.client.api.tm.transactions.transaction - collection = self.client.api.tm.sys.provision.get_collection() - resources = [x['name'] for x in collection if x['name'] != self.want.module] - with TransactionContextManager(tx) as api: - provision = api.tm.sys.provision + uri = "https://{0}:{1}/mgmt/tm/sys/provision/".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) + + resources = [x['name'] for x in response['items'] if x['name'] != self.want.module] + + with TransactionContextManager(self.client) as transact: for resource in resources: - resource = getattr(provision, resource) - resource = resource.load() - resource.update(level='none') - resource = getattr(provision, self.want.module) - resource = resource.load() - resource.update(**params) + target = uri + resource + resp = transact.api.patch(target, json=dict(level='none')) + 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) + + target = uri + self.want.module + resp = transact.api.patch(target, 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 provision_non_dedicated_on_device(self): params = self.want.api_params() - provision = self.client.api.tm.sys.provision - resource = getattr(provision, self.want.module) - resource = resource.load() - resource.update(**params) + uri = "https://{0}:{1}/mgmt/tm/sys/provision/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.module + ) + 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): if self.want.module == 'cgnat': - resource = self.client.api.tm.sys.feature_module.cgnat.load() - result = resource.attrs + uri = "https://{0}:{1}/mgmt/tm/sys/feature-module/cgnat/".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) + else: - provision = self.client.api.tm.sys.provision - resource = getattr(provision, str(self.want.module)) - resource = resource.load() - result = resource.attrs - return ApiParameters(params=result) + uri = "https://{0}:{1}/mgmt/tm/sys/provision/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.module + ) + 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 absent(self): if self.exists(): @@ -420,11 +547,11 @@ class ModuleManager(object): def remove(self): if self.module.check_mode: return True - result = self.remove_from_device() if self.want.module == 'cgnat': - return result - self._wait_for_module_provisioning() + return self.deprovision_cgnat_on_device() + self.remove_from_device() + self._wait_for_module_provisioning() # For vCMP, because it has to reboot, we also wait for mcpd to become available # before "moving on", or else the REST API would not be available and subsequent # Tasks would fail. @@ -443,27 +570,60 @@ class ModuleManager(object): def save_on_device(self): command = 'tmsh save sys config' - self.client.api.tm.util.bash.exec_cmd( - 'run', + params = dict( + 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() + 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 remove_from_device(self): - if self.want.module == 'cgnat': - if self.changes.cgnat: - return self.deprovision_cgnat_on_device() - return False - - provision = self.client.api.tm.sys.provision - resource = getattr(provision, self.want.module) - resource = resource.load() - resource.update(level='none') + uri = "https://{0}:{1}/mgmt/tm/sys/provision/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + self.want.module + ) + resp = self.client.api.patch(uri, json=dict(level='none')) + 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 deprovision_cgnat_on_device(self): - resource = self.client.api.tm.sys.feature_module.cgnat.load() - resource.modify( - disabled=True + uri = "https://{0}:{1}/mgmt/tm/sys/feature-module/cgnat/".format( + self.client.provider['server'], + self.client.provider['server_port'], ) + params = dict(disabled=True) + 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) return True def _wait_for_module_provisioning(self): @@ -500,11 +660,29 @@ class ModuleManager(object): # /usr/libexec/qemu-kvm -rt-usecs 880 ... -mem-path /dev/mprov/vcmp -f5-tracing ... # try: - output = self.client.api.tm.util.bash.exec_cmd( - 'run', - utilCmdArgs='-c "ps aux | grep \'[m]prov\' | grep -v /usr/libexec/qemu-kvm"' + command = "ps aux | grep \'[m]prov\' | grep -v /usr/libexec/qemu-kvm" + params = dict( + command="run", + utilCmdArgs='-c "{0}"'.format(command) ) - if hasattr(output, 'commandResult'): + 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() + 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 'commandResult' in response: return True except Exception: pass @@ -522,8 +700,24 @@ class ModuleManager(object): restarted_asm = False while nops < 3: try: - policies = self.client.api.tm.asm.policies_s.get_collection() - if len(policies) >= 0: + uri = "https://{0}:{1}/mgmt/tm/asm/policies/".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'] in [400, 403]: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + if len(response['items']) >= 0: nops += 1 else: nops = 0 @@ -544,8 +738,24 @@ class ModuleManager(object): nops = 0 while nops < 3: try: - security = self.client.api.tm.security.get_collection() - if len(security) >= 0: + uri = "https://{0}:{1}/mgmt/tm/security/".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'] in [400, 403]: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + if len(response['items']) >= 0: nops += 1 else: nops = 0 @@ -555,10 +765,26 @@ class ModuleManager(object): def _restart_asm(self): try: - self.client.api.tm.util.bash.exec_cmd( - 'run', + params = dict( + command="run", utilCmdArgs='-c "bigstart restart asm"' ) + 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() + 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) time.sleep(60) return True except Exception: @@ -567,12 +793,29 @@ class ModuleManager(object): def _get_last_reboot(self): try: - output = self.client.api.tm.util.bash.exec_cmd( - 'run', + params = dict( + command="run", utilCmdArgs='-c "/usr/bin/last reboot | head -1"' ) - if hasattr(output, 'commandResult'): - return str(output.commandResult) + 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() + 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 'commandResult' in response: + return str(response['commandResult']) except Exception: pass return None @@ -639,18 +882,15 @@ 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") + + client = F5RestClient(**module.params) try: - client = F5Client(**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_provision.py b/test/units/modules/network/f5/test_bigip_provision.py index df7f37c7b4..6a22b774d0 100644 --- a/test/units/modules/network/f5/test_bigip_provision.py +++ b/test/units/modules/network/f5/test_bigip_provision.py @@ -14,25 +14,30 @@ from nose.plugins.skip import SkipTest if sys.version_info < (2, 7): raise SkipTest("F5 Ansible modules require Python >= 2.7") -from units.compat import unittest -from units.compat.mock import Mock -from units.compat.mock import patch from ansible.module_utils.basic import AnsibleModule try: from library.modules.bigip_provision import Parameters from library.modules.bigip_provision import ModuleManager from library.modules.bigip_provision import ArgumentSpec - from library.module_utils.network.f5.common import F5ModuleError - from library.module_utils.network.f5.common import iControlUnexpectedHTTPError - from test.unit.modules.utils import set_module_args + + # In Ansible 2.8, Ansible changed import paths. + from test.units.compat import unittest + from test.units.compat.mock import Mock + from test.units.compat.mock import patch + + from test.units.modules.utils import set_module_args except ImportError: try: from ansible.modules.network.f5.bigip_provision import Parameters from ansible.modules.network.f5.bigip_provision import ModuleManager from ansible.modules.network.f5.bigip_provision import ArgumentSpec - from ansible.module_utils.network.f5.common import F5ModuleError - from ansible.module_utils.network.f5.common import iControlUnexpectedHTTPError + + # Ansible 2.8 imports + from units.compat import unittest + from units.compat.mock import Mock + from units.compat.mock import patch + from units.modules.utils import set_module_args except ImportError: raise SkipTest("F5 Ansible modules require the f5-sdk Python library")