diff --git a/lib/ansible/modules/network/f5/bigip_data_group.py b/lib/ansible/modules/network/f5/bigip_data_group.py index c11ef50ba1..1eab26d287 100644 --- a/lib/ansible/modules/network/f5/bigip_data_group.py +++ b/lib/ansible/modules/network/f5/bigip_data_group.py @@ -134,31 +134,28 @@ options: extends_documentation_fragment: f5 author: - Tim Rupp (@caphrim007) + - Wojciech Wypior (@wojtek0806) ''' EXAMPLES = r''' - name: Create a data group of addresses bigip_data_group: name: foo - password: secret - server: lb.mydomain.com - state: present - user: admin records: - key: 0.0.0.0/32 value: External_NAT - key: 10.10.10.10 value: No_NAT type: address + provider: + password: secret + server: lb.mydomain.com + user: admin delegate_to: localhost - name: Create a data group of strings bigip_data_group: name: foo - password: secret - server: lb.mydomain.com - state: present - user: admin records: - key: caddy value: "" @@ -167,27 +164,27 @@ EXAMPLES = r''' - key: cactus value: "" type: string + provider: + password: secret + server: lb.mydomain.com + user: admin delegate_to: localhost - name: Create a data group of IP addresses from a file bigip_data_group: name: foo - password: secret - server: lb.mydomain.com - state: present - user: admin records_src: /path/to/dg-file type: address + provider: + password: secret + server: lb.mydomain.com + user: admin delegate_to: localhost - name: Update an existing internal data group of strings bigip_data_group: name: foo - password: secret - server: lb.mydomain.com - state: present internal: yes - user: admin records: - key: caddy value: "" @@ -195,6 +192,10 @@ EXAMPLES = r''' value: "" - key: cactus value: "" + provider: + password: secret + server: lb.mydomain.com + user: admin delegate_to: localhost - name: Show the data format expected for records_content - address 1 @@ -260,35 +261,33 @@ from ansible.module_utils.basic import env_fallback from io import StringIO 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 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.network.f5.common import compare_complex_list from library.module_utils.network.f5.common import f5_argument_spec from library.module_utils.network.f5.ipaddress import is_valid_ip_interface from library.module_utils.compat.ipaddress import ip_network from library.module_utils.compat.ipaddress import ip_interface - try: - from library.module_utils.network.f5.common import iControlUnexpectedHTTPError - except ImportError: - HAS_F5SDK = False + 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 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.network.f5.common import compare_complex_list from ansible.module_utils.network.f5.common import f5_argument_spec from ansible.module_utils.network.f5.ipaddress import is_valid_ip_interface from ansible.module_utils.compat.ipaddress import ip_network from ansible.module_utils.compat.ipaddress import ip_interface - try: - from ansible.module_utils.network.f5.common import iControlUnexpectedHTTPError - except ImportError: - HAS_F5SDK = False + from ansible.module_utils.network.f5.icontrol import upload_file LINE_LIMIT = 65000 @@ -510,7 +509,9 @@ class Parameters(AnsibleF5Parameters): 'records', 'type' ] - returnables = [] + returnables = [ + 'type', 'records' + ] updatables = [ 'records', 'checksum' @@ -715,13 +716,10 @@ class BaseManager(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() @@ -806,43 +804,87 @@ class InternalManager(BaseManager): return True def exists(self): - result = self.client.api.tm.ltm.data_group.internals.internal.exists( - name=self.want.name, - partition=self.want.partition + uri = "https://{0}:{1}/mgmt/tm/ltm/data-group/internal/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) ) - return result + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError: + return False + if resp.status == 404 or 'code' in response and response['code'] == 404: + return False + return True def create_on_device(self): - params = self.want.api_params() - self.client.api.tm.ltm.data_group.internals.internal.create( - name=self.want.name, - partition=self.want.partition, - **params + params = self.changes.api_params() + params['name'] = self.want.name + params['partition'] = self.want.partition + uri = "https://{0}:{1}/mgmt/tm/ltm/data-group/internal/".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, 409]: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) def update_on_device(self): params = self.changes.api_params() - resource = self.client.api.tm.ltm.data_group.internals.internal.load( - name=self.want.name, - partition=self.want.partition + uri = "https://{0}:{1}/mgmt/tm/ltm/data-group/internal/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) ) - resource.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 remove_from_device(self): - resource = self.client.api.tm.ltm.data_group.internals.internal.load( - name=self.want.name, - partition=self.want.partition + uri = "https://{0}:{1}/mgmt/tm/ltm/data-group/internal/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) ) - if resource: - resource.delete() + resp = self.client.api.delete(uri) + if resp.status == 200: + return True def read_current_from_device(self): - resource = self.client.api.tm.ltm.data_group.internals.internal.load( - name=self.want.name, - partition=self.want.partition + uri = "https://{0}:{1}/mgmt/tm/ltm/data-group/internal/{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) class ExternalManager(BaseManager): @@ -879,84 +921,192 @@ class ExternalManager(BaseManager): return True def exists(self): - result = self.client.api.tm.ltm.data_group.externals.external.exists( - name=self.want.name, - partition=self.want.partition + uri = "https://{0}:{1}/mgmt/tm/ltm/data-group/external/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) ) - return result + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError: + return False + if resp.status == 404 or 'code' in response and response['code'] == 404: + return False + return True def external_file_exists(self): - result = self.client.api.tm.sys.file.data_groups.data_group.exists( - name=self.want.external_file_name, - partition=self.want.partition + uri = "https://{0}:{1}/mgmt/tm/sys/file/data-group/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.external_file_name) ) - return result + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError: + return False + if resp.status == 404 or 'code' in response and response['code'] == 404: + return False + return True + + def 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 _upload_to_file(self, name, type, remote_path, update=False): - self.client.api.shared.file_transfer.uploads.upload_stringio(self.want.records_src, name) - resource = self.client.api.tm.sys.file.data_groups + self.upload_file_to_device(self.want.records_src, name) if update: - resource = resource.data_group.load( - name=name, - partition=self.want.partition + uri = "https://{0}:{1}/mgmt/tm/sys/file/data-group/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, name) ) - resource.modify( - sourcePath='file:{0}'.format(remote_path) - ) - resource.refresh() - result = resource + params = {'sourcePath': 'file:{0}'.format(remote_path)} + 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) else: - result = resource.data_group.create( + uri = "https://{0}:{1}/mgmt/tm/sys/file/data-group/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + params = dict( name=name, type=type, sourcePath='file:{0}'.format(remote_path) ) - return result.name + 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) + return response['name'] + + def remove_file_on_device(self, remote_path): + uri = "https://{0}:{1}/mgmt/tm/util/unix-rm/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + args = dict( + command='run', + utilCmdArgs=remote_path + ) + 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) def create_on_device(self): name = self.want.external_file_name remote_path = '/var/config/rest/downloads/{0}'.format(name) external_file = self._upload_to_file(name, self.want.type, remote_path, update=False) - self.client.api.tm.ltm.data_group.externals.external.create( + + params = dict( name=self.want.name, partition=self.want.partition, externalFileName=external_file ) - self.client.api.tm.util.unix_rm.exec_cmd('run', utilCmdArgs=remote_path) + uri = "https://{0}:{1}/mgmt/tm/ltm/data-group/external/".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) + + self.remove_file_on_device(remote_path) def update_on_device(self): name = self.want.external_file_name remote_path = '/var/config/rest/downloads/{0}'.format(name) external_file = self._upload_to_file(name, self.have.type, remote_path, update=True) - resource = self.client.api.tm.ltm.data_group.externals.external.load( - name=self.want.name, - partition=self.want.partition - ) - resource.modify( - externalFileName=external_file + + uri = "https://{0}:{1}/mgmt/tm/ltm/data-group/external/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) ) + params = {'externalFileName': external_file} + 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 remove_from_device(self): - resource = self.client.api.tm.ltm.data_group.externals.external.load( - name=self.want.name, - partition=self.want.partition + uri = "https://{0}:{1}/mgmt/tm/ltm/data-group/external/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) ) - if resource: - resource.delete() + resp = self.client.api.delete(uri) # Remove the remote data group file if asked to if self.want.delete_data_group_file: self.remove_data_group_file_from_device() - def remove_data_group_file_from_device(self): - resource = self.client.api.tm.sys.file.data_groups.data_group.load( - name=self.want.external_file_name, - partition=self.want.partition - ) - if resource: - resource.delete() + if resp.status == 200: return True - return False + + def remove_data_group_file_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/sys/file/data-group/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.external_file_name) + ) + resp = self.client.api.delete(uri) + + if resp.status == 200: + return True + else: + return False def read_current_from_device(self): """Reads the current configuration from the device @@ -976,18 +1126,46 @@ class ExternalManager(BaseManager): Returns: ExternalApiParameters: Attributes of the remote resource. """ - resource = self.client.api.tm.ltm.data_group.externals.external.load( - name=self.want.name, - partition=self.want.partition + + uri = "https://{0}:{1}/mgmt/tm/ltm/data-group/external/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) ) - external_file = os.path.basename(resource.externalFileName) - external_file_partition = os.path.dirname(resource.externalFileName).strip('/') - resource = self.client.api.tm.sys.file.data_groups.data_group.load( - name=external_file, - partition=external_file_partition + resp_dg = self.client.api.get(uri) + + try: + response_dg = resp_dg.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if 'code' in response_dg and response_dg['code'] == 400: + if 'message' in response_dg: + raise F5ModuleError(response_dg['message']) + else: + raise F5ModuleError(resp_dg.content) + + external_file = os.path.basename(response_dg['externalFileName']) + external_file_partition = os.path.dirname(response_dg['externalFileName']).strip('/') + + uri = "https://{0}:{1}/mgmt/tm/sys/file/data-group/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(external_file_partition, external_file) ) - 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) class ModuleManager(object): @@ -1052,18 +1230,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_data_group.py b/test/units/modules/network/f5/test_bigip_data_group.py index 4aed433ee4..1352c15b65 100644 --- a/test/units/modules/network/f5/test_bigip_data_group.py +++ b/test/units/modules/network/f5/test_bigip_data_group.py @@ -15,9 +15,6 @@ 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: @@ -26,9 +23,15 @@ try: from library.modules.bigip_data_group import ExternalManager from library.modules.bigip_data_group import InternalManager from library.modules.bigip_data_group 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_data_group import ModuleParameters @@ -36,8 +39,14 @@ except ImportError: from ansible.modules.network.f5.bigip_data_group import ExternalManager from ansible.modules.network.f5.bigip_data_group import InternalManager from ansible.modules.network.f5.bigip_data_group 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")