From 4648a441d2b0a07428461ae128f49358692df4cf Mon Sep 17 00:00:00 2001 From: Wojciech Wypior Date: Fri, 4 Jan 2019 20:50:50 +0100 Subject: [PATCH] adds bigip apm policy import (#50559) --- .../network/f5/bigip_apm_policy_import.py | 413 ++++++++++++++++++ .../network/f5/fixtures/fake_policy.tar.gz | Bin 0 -> 1450 bytes .../f5/test_bigip_apm_policy_import.py | 127 ++++++ 3 files changed, 540 insertions(+) create mode 100644 lib/ansible/modules/network/f5/bigip_apm_policy_import.py create mode 100644 test/units/modules/network/f5/fixtures/fake_policy.tar.gz create mode 100644 test/units/modules/network/f5/test_bigip_apm_policy_import.py diff --git a/lib/ansible/modules/network/f5/bigip_apm_policy_import.py b/lib/ansible/modules/network/f5/bigip_apm_policy_import.py new file mode 100644 index 00000000000..722971b0022 --- /dev/null +++ b/lib/ansible/modules/network/f5/bigip_apm_policy_import.py @@ -0,0 +1,413 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2018, 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 + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'certified'} + +DOCUMENTATION = r''' +--- +module: bigip_apm_policy_import +short_description: Manage BIG-IP APM policy or APM access profile imports +description: + - Manage BIG-IP APM policy or APM access profile imports. +version_added: 2.8 +options: + name: + description: + - The name of the APM policy or APM access profile to create or override. + required: True + type: + description: + - Specifies the type of item to export from device. + choices: + - profile_access + - access_policy + default: profile_access + source: + description: + - Full path to a file to be imported into the BIG-IP APM. + type: path + force: + description: + - When set to C(yes) any existing policy with the same name will be overwritten by the new import. + - If policy does not exist this setting is ignored. + default: no + type: bool + partition: + description: + - Device partition to manage resources on. + default: Common +notes: + - Due to ID685681 it is not possible to execute ng_* tools via REST api on v12.x and 13.x, once this is fixed + this restriction will be removed. + - Requires BIG-IP >= 14.0.0 +extends_documentation_fragment: f5 +author: + - Wojciech Wypior (@wojtek0806) +''' + +EXAMPLES = r''' +- name: Import APM profile + bigip_apm_policy_import: + name: new_apm_profile + source: /root/apm_profile.tar.gz + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Import APM policy + bigip_apm_policy_import: + name: new_apm_policy + source: /root/apm_policy.tar.gz + type: access_policy + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost + +- name: Override existing APM policy + bigip_asm_policy: + name: new_apm_policy + source: /root/apm_policy.tar.gz + force: yes + provider: + server: lb.mydomain.com + user: admin + password: secret + delegate_to: localhost +''' + +RETURN = r''' +source: + description: Local path to APM policy file. + returned: changed + type: str + sample: /root/some_policy.tar.gz +name: + description: Name of the APM policy or APM access profile to be created/overwritten. + returned: changed + type: str + sample: APM_policy_global +type: + description: Set to specify type of item to export. + returned: changed + type: str + sample: access_policy +force: + description: Set when overwriting an existing policy or profile. + returned: changed + type: bool + sample: yes +''' + +import os +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.basic import env_fallback +from distutils.version import LooseVersion + +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 fq_name + from library.module_utils.network.f5.common import transform_name + from library.module_utils.network.f5.common import f5_argument_spec + 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 upload_file + from library.module_utils.network.f5.icontrol import tmos_version + from library.module_utils.network.f5.icontrol import module_provisioned +except ImportError: + 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 transform_name + from ansible.module_utils.network.f5.common import f5_argument_spec + 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 upload_file + from ansible.module_utils.network.f5.icontrol import tmos_version + from ansible.module_utils.network.f5.icontrol import module_provisioned + + +class Parameters(AnsibleF5Parameters): + api_map = { + + } + + api_attributes = [ + + ] + + returnables = [ + 'name', + 'source', + 'type', + + ] + + updatables = [ + + ] + + +class ApiParameters(Parameters): + pass + + +class ModuleParameters(Parameters): + pass + + +class Changes(Parameters): + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + pass + return result + + +class UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + pass + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = kwargs.get('client', None) + self.want = ModuleParameters(params=self.module.params) + self.changes = UsableChanges() + + def _set_changed_options(self): + changed = {} + for key in Parameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = UsableChanges(params=changed) + + 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): + if not module_provisioned(self.client, 'apm'): + raise F5ModuleError( + "APM must be provisioned to use this module." + ) + + if self.version_less_than_14(): + raise F5ModuleError('Due to bug ID685681 it is not possible to use this module on TMOS version below 14.x') + + result = dict() + + changed = self.policy_import() + + 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 version_less_than_14(self): + version = tmos_version(self.client) + if LooseVersion(version) < LooseVersion('14.0.0'): + return True + return False + + def policy_import(self): + self._set_changed_options() + if self.module.check_mode: + return True + if self.exists(): + if self.want.force is False: + return False + + self.import_file_to_device() + self.remove_temp_file_from_device() + return True + + def exists(self): + if self.want.type == 'access_policy': + uri = "https://{0}:{1}/mgmt/tm/apm/policy/access-policy/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + else: + uri = "https://{0}:{1}/mgmt/tm/apm/profile/access/{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 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 import_file_to_device(self): + name = os.path.split(self.want.source)[1] + self.upload_file_to_device(self.want.source, name) + + cmd = 'ng_import -s /var/config/rest/downloads/{0} {1} -p {2}'.format(name, self.want.name, self.want.partition) + + uri = "https://{0}:{1}/mgmt/tm/util/bash/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + args = dict( + command='run', + utilCmdArgs='-c "{0}"'.format(cmd) + ) + resp = self.client.api.post(uri, json=args) + + try: + response = resp.json() + if 'commandResult' in response: + raise F5ModuleError(response['commandResult']) + 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 remove_temp_file_from_device(self): + name = os.path.split(self.want.source)[1] + tpath_name = '/var/config/rest/downloads/{0}'.format(name) + uri = "https://{0}:{1}/mgmt/tm/util/unix-rm/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + args = dict( + command='run', + utilCmdArgs=tpath_name + ) + 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) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict( + required=True, + ), + source=dict(type='path'), + force=dict( + type='bool', + default='no' + ), + type=dict( + default='profile_access', + choices=['profile_access', 'access_policy'] + ), + partition=dict( + default='Common', + fallback=(env_fallback, ['F5_PARTITION']) + ) + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + ) + + client = F5RestClient(**module.params) + + try: + mm = ModuleManager(module=module, client=client) + results = mm.exec_module() + cleanup_tokens(client) + exit_json(module, results, client) + except F5ModuleError as ex: + cleanup_tokens(client) + fail_json(module, ex, client) + + +if __name__ == '__main__': + main() diff --git a/test/units/modules/network/f5/fixtures/fake_policy.tar.gz b/test/units/modules/network/f5/fixtures/fake_policy.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..cdc6202779d04c77e2c7f26b04649df8381498e2 GIT binary patch literal 1450 zcmV;b1y%YViwFQ|#4KC@1MOSeZsRr(&9lFPNcXXfMcI~PV{d7;X$!R707?5&1Oh|R z4jV}nC@P6}vHxCPWLu8qYb6&=G!KeJkweazIpmO11WntHf6k+b$9_y7eI8Sv#(^Ku zc;e&nQ3c{C@jTD`bp~khua>@dggRZ+L0uR1R5|iIw{rxXDmvdJSt_vr;7Ia_V7a>9 zrsw}tdHdlh3c&*vDdX|GV|3yi0~!+^uz39L_>b!gczXQded{ezIPr*m!BW-p$AV`` z`+e)b9(^PKR)pd|dd~kbjj6z)pRr2%*n|H)$8Gq32x<8Lx<&Kfea`=pz!81q;-1Bd z9bsskzJDJ(^WQ}davJ_0LK^OZ*pt ziwGwb^|43(@48L?e-LT-|GGu(<1{qH-yhX03>#-2R|@i~CMsTMLRMg_1=SxDFaX2SLXjI^NJ+ z3v~i`I{`R|SZoRv&Jt!xu(5T-0}47Fbb4xT1$|)J;{ci4lzpMVbIot5zydaVF!xq_ zq2F&SJasmvu>?QHdf5SIVaOl1brjZ>cl8uD-BHsWG#XYoYN{Q41zQdMlX7gGA+|7Z ztzf$V+chv(`^gqu+R^+XX|$7{cCyv}flPI>&O5n{2AMR!(Xz>*vx>98Eyly_>9^MW zN^oLklCxP2!b2`{BItp_f}$Eg)b6PmLlZe3lC%ZBsY0(3FTFo?YR`r-vrmN63&&s%!M$w z4`yt)#c)qwmAexVM^ ztviU9>=vX3A6dmO(?fgh+lr~!7M|H{vtyLQ{BFq^xfc|$I144TN#mwmN#|2r?h?$N zZ+$(RJa4EbXEp3qA2_W^?Bq(YFYTE~4tjNH=V~q7D{SIfe>wb0hdXQqu4v#tF^ z?o{6tPGOt%Rh}&DmPQ#x^Sp73)Va_?&dEJ5fS8!_m~(5uos= 2.7") + +from ansible.module_utils.basic import AnsibleModule + +try: + from library.modules.bigip_apm_policy_import import ApiParameters + from library.modules.bigip_apm_policy_import import ModuleParameters + from library.modules.bigip_apm_policy_import import ModuleManager + from library.modules.bigip_apm_policy_import import ArgumentSpec + + # 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: + from ansible.modules.network.f5.bigip_apm_policy_import import ApiParameters + from ansible.modules.network.f5.bigip_apm_policy_import import ModuleParameters + from ansible.modules.network.f5.bigip_apm_policy_import import ModuleManager + from ansible.modules.network.f5.bigip_apm_policy_import import ArgumentSpec + + # 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 + + +fixture_path = os.path.join(os.path.dirname(__file__), 'fixtures') +fixture_data = {} + + +def load_fixture(name): + path = os.path.join(fixture_path, name) + + if path in fixture_data: + return fixture_data[path] + + with open(path) as f: + data = f.read() + + try: + data = json.loads(data) + except Exception: + pass + + fixture_data[path] = data + return data + + +class TestParameters(unittest.TestCase): + def test_module_parameters(self): + args = dict( + name='fake_policy', + type='access_policy', + source='/var/fake/fake.tar.gz' + ) + + p = ModuleParameters(params=args) + assert p.name == 'fake_policy' + assert p.source == '/var/fake/fake.tar.gz' + assert p.type == 'access_policy' + + +class TestManager(unittest.TestCase): + def setUp(self): + self.spec = ArgumentSpec() + self.policy = os.path.join(fixture_path, 'fake_policy.tar.gz') + self.patcher1 = patch('time.sleep') + self.patcher1.start() + + try: + self.p1 = patch('library.modules.bigip_apm_policy_import.module_provisioned') + self.m1 = self.p1.start() + self.m1.return_value = True + except Exception: + self.p1 = patch('ansible.modules.network.f5.bigip_apm_policy_import.module_provisioned') + self.m1 = self.p1.start() + self.m1.return_value = True + + def tearDown(self): + self.patcher1.stop() + self.p1.stop() + + def test_import_from_file(self, *args): + set_module_args(dict( + name='fake_policy', + source=self.policy, + type='access_policy', + server='localhost', + password='password', + user='admin', + )) + + module = AnsibleModule( + argument_spec=self.spec.argument_spec, + supports_check_mode=self.spec.supports_check_mode + ) + + # Override methods to force specific logic in the module to happen + mm = ModuleManager(module=module) + mm.version_less_than_14 = Mock(return_value=False) + mm.exists = Mock(return_value=False) + mm.import_file_to_device = Mock(return_value=True) + mm.remove_temp_file_from_device = Mock(return_value=True) + + results = mm.exec_module() + + assert results['changed'] is True + assert results['name'] == 'fake_policy' + assert results['source'] == self.policy