diff --git a/lib/ansible/modules/network/f5/bigip_ucs.py b/lib/ansible/modules/network/f5/bigip_ucs.py index 9f99e22e5b..e4ed996444 100644 --- a/lib/ansible/modules/network/f5/bigip_ucs.py +++ b/lib/ansible/modules/network/f5/bigip_ucs.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 @@ -99,6 +99,7 @@ notes: extends_documentation_fragment: f5 author: - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) ''' EXAMPLES = r''' @@ -174,27 +175,25 @@ from ansible.module_utils.six import iteritems 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 + 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.icontrol import tmos_version + from library.module_utils.network.f5.icontrol import upload_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.icontrol import tmos_version + from ansible.module_utils.network.f5.icontrol import upload_file try: from collections import OrderedDict @@ -211,6 +210,12 @@ class Parameters(AnsibleF5Parameters): returnables = [] api_attributes = [] + +class ApiParameters(Parameters): + pass + + +class ModuleParameters(Parameters): def _check_required_if(self, parameter): if self._values[parameter] is not True: return self._values[parameter] @@ -272,22 +277,25 @@ class Parameters(AnsibleF5Parameters): cmd += ' %s' % (k) return cmd + +class Changes(Parameters): def to_return(self): result = {} - for returnable in self.returnables: - result[returnable] = getattr(self, returnable) - result = self._filter_params(result) + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + pass return result - def api_params(self): - result = {} - for api_attribute in self.api_attributes: - if self.api_map is not None and api_attribute in self.api_map: - result[api_attribute] = getattr(self, self.api_map[api_attribute]) - else: - result[api_attribute] = getattr(self, api_attribute) - result = self._filter_params(result) - return result + +class ReportableChanges(Changes): + pass + + +class UsableChanges(Changes): + pass class ModuleManager(object): @@ -313,36 +321,47 @@ class ModuleManager(object): :return: Bool """ - version = self.client.api.tmos_version + version = tmos_version(self.client) if LooseVersion(version) < LooseVersion('12.1.0'): return True else: return False +class Difference(object): + pass + + class BaseManager(object): def __init__(self, *args, **kwargs): self.module = kwargs.get('module', None) self.client = kwargs.get('client', None) - self.want = Parameters(params=self.module.params) - self.changes = Parameters() + self.want = ModuleParameters(params=self.module.params) + self.changes = UsableChanges() + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) def exec_module(self): changed = False result = dict() state = self.want.state - try: - if state in ['present', 'installed']: - changed = self.present() - elif state == "absent": - changed = self.absent() - except iControlUnexpectedHTTPError as e: - raise F5ModuleError(str(e)) + if state in ['present', 'installed']: + changed = self.present() + elif state == "absent": + changed = self.absent() - changes = self.changes.to_return() + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() result.update(**changes) result.update(dict(changed=changed)) + self._announce_deprecations(result) return result def present(self): @@ -407,20 +426,33 @@ class BaseManager(object): while noops < 4: time.sleep(3) try: - output = self.client.api.tm.util.bash.exec_cmd( - 'run', - utilCmdArgs='-c "tmsh show sys mcp-state"' + params = dict(command="run", + utilCmdArgs='-c "tmsh show sys mcp-state"' + ) + 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: + output = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + if 'code' in output and output['code'] in [400, 403]: + if 'message' in output: + raise F5ModuleError(output['message']) + else: + raise F5ModuleError(resp.content) except Exception as ex: # This can be caused by restjavad restarting. continue - if not hasattr(output, 'commandResult'): + if 'commandResult' not in output: continue # Need to re-connect here because the REST framework will be restarting # and thus be clearing its authorization cache - result = output.commandResult + result = output['commandResult'] if self._is_config_reloading_failed_on_device(result): raise F5ModuleError( "Failed to reload the configuration. This may be due " @@ -466,33 +498,67 @@ class V1Manager(BaseManager): * No API to upload UCS files """ + def upload_file_to_device(self, content, name): + url = 'https://{0}:{1}/mgmt/shared/file-transfer/uploads'.format( + self.client.provider['server'], + self.client.provider['server_port'] + ) + try: + upload_file(self.client, url, content, name) + except F5ModuleError: + raise F5ModuleError( + "Failed to upload the file." + ) + def create_on_device(self): remote_path = "/var/local/ucs" tpath_name = '/var/config/rest/downloads' - upload = self.client.api.shared.file_transfer.uploads + self.upload_file_to_device(self.want.ucs, self.want.basename) - try: - upload.upload_file(self.want.ucs) - except IOError as ex: - raise F5ModuleError(str(ex)) - - self.client.api.tm.util.unix_mv.exec_cmd( - 'run', + uri = "https://{0}:{1}/mgmt/tm/util/unix-mv/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + args = dict( + command='run', utilCmdArgs='{0}/{2} {1}/{2}'.format( tpath_name, remote_path, self.want.basename ) ) + resp = self.client.api.post(uri, json=args) + 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 read_current_from_device(self): result = [] - output = self.client.api.tm.util.bash.exec_cmd( - 'run', - utilCmdArgs='-c "tmsh list sys ucs"' + params = dict(command="run", + utilCmdArgs='-c "tmsh list sys ucs"' + ) + uri = "https://{0}:{1}/mgmt/tm/util/bash".format( + self.client.provider['server'], + self.client.provider['server_port'] ) - if hasattr(output, 'commandResult'): - lines = output.commandResult.split("\n") + resp = self.client.api.post(uri, json=params) + try: + output = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + if 'code' in output and output['code'] in [400, 403]: + if 'message' in output: + raise F5ModuleError(output['message']) + else: + raise F5ModuleError(resp.content) + if 'commandResult' in output: + lines = output['commandResult'].split("\n") result = [x.strip() for x in lines] result = list(set(result)) return result @@ -504,21 +570,47 @@ class V1Manager(BaseManager): return False def remove_from_device(self): - output = self.client.api.tm.util.bash.exec_cmd( - 'run', - utilCmdArgs='-c "tmsh delete sys ucs {0}"'.format(self.want.basename) + params = dict(command="run", + utilCmdArgs='-c "tmsh delete sys ucs {0}"'.format(self.want.basename) + ) + uri = "https://{0}:{1}/mgmt/tm/util/bash".format( + self.client.provider['server'], + self.client.provider['server_port'] ) - if hasattr(output, 'commandResult'): - if '{0} is deleted'.format(self.want.basename) in output.commandResult: + resp = self.client.api.post(uri, json=params) + try: + output = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + if 'code' in output and output['code'] in [400, 403]: + if 'message' in output: + raise F5ModuleError(output['message']) + else: + raise F5ModuleError(resp.content) + if 'commandResult' in output: + if '{0} is deleted'.format(self.want.basename) in output['commandResult']: return True return False def install_on_device(self): try: - self.client.api.tm.util.bash.exec_cmd( - 'run', - utilCmdArgs='-c "{0}"'.format(self.want.install_command) + params = dict(command="run", + utilCmdArgs='-c "{0}"'.format(self.want.install_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: + output = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + if 'code' in output and output['code'] in [400, 403]: + if 'message' in output: + raise F5ModuleError(output['message']) + else: + raise F5ModuleError(resp.content) except Exception as ex: # Reloading a UCS configuration will cause restjavad to restart, # aborting the connection. @@ -546,8 +638,22 @@ class V2Manager(V1Manager): def read_current_from_device(self): result = [] - resource = self.client.api.tm.sys.ucs.load() - items = resource.attrs.get('items', []) + uri = "https://{0}:{1}/mgmt/tm/sys/ucs/".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) + items = response.get('items', []) for item in items: result.append(os.path.basename(item['apiRawValues']['filename'])) return result @@ -596,18 +702,17 @@ def main(): argument_spec=spec.argument_spec, supports_check_mode=spec.supports_check_mode ) - 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_ucs.py b/test/units/modules/network/f5/test_bigip_ucs.py index d0268981b5..6c4b5a549c 100644 --- a/test/units/modules/network/f5/test_bigip_ucs.py +++ b/test/units/modules/network/f5/test_bigip_ucs.py @@ -15,29 +15,38 @@ 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_ucs import Parameters + from library.modules.bigip_ucs import ModuleParameters from library.modules.bigip_ucs import ModuleManager from library.modules.bigip_ucs import ArgumentSpec from library.modules.bigip_ucs import V1Manager from library.modules.bigip_ucs import V2Manager + 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_ucs import Parameters + from ansible.modules.network.f5.bigip_ucs import ModuleParameters from ansible.modules.network.f5.bigip_ucs import ModuleManager from ansible.modules.network.f5.bigip_ucs import ArgumentSpec from ansible.modules.network.f5.bigip_ucs import V1Manager from ansible.modules.network.f5.bigip_ucs import V2Manager + 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") @@ -77,7 +86,7 @@ class TestParameters(unittest.TestCase): state='installed' ) - p = Parameters(params=args) + p = ModuleParameters(params=args) assert p.ucs == '/root/bigip.localhost.localdomain.ucs' assert p.force is True assert p.include_chassis_level_config is True @@ -99,7 +108,7 @@ class TestParameters(unittest.TestCase): reset_trust=False ) - p = Parameters(params=args) + p = ModuleParameters(params=args) assert p.ucs == '/root/bigip.localhost.localdomain.ucs' assert p.include_chassis_level_config is False assert p.no_license is False