diff --git a/lib/ansible/modules/network/f5/bigip_asm_policy_import.py b/lib/ansible/modules/network/f5/bigip_asm_policy_import.py
new file mode 100644
index 0000000000..7fd078fb4e
--- /dev/null
+++ b/lib/ansible/modules/network/f5/bigip_asm_policy_import.py
@@ -0,0 +1,477 @@
+#!/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_asm_policy_import
+short_description: Manage BIG-IP ASM policy imports
+description:
+ - Manage BIG-IP ASM policies policy imports.
+version_added: 2.8
+options:
+ name:
+ description:
+ - The ASM policy to create or override.
+ required: True
+ inline:
+ description:
+ - When specified the ASM policy is created from a provided string.
+ - Content needs to be provided in a valid XML format otherwise the operation will fail.
+ source:
+ description:
+ - Full path to a policy file to be imported into the BIG-IP ASM.
+ - Policy files exported from newer versions of BIG-IP cannot be imported into older
+ versions of BIG-IP. The opposite, however, is true; you can import older into
+ newer.
+ - The file format can be binary of XML.
+ force:
+ description:
+ - When set to C(yes) any existing policy with the same name will be overwritten by the new import.
+ - Works in both inline and file imports, if policy does not exist this setting is ignored.
+ default: no
+ type: bool
+ partition:
+ description:
+ - Device partition to manage resources on.
+ default: Common
+extends_documentation_fragment: f5
+author:
+ - Wojciech Wypior (@wojtek0806)
+'''
+
+EXAMPLES = r'''
+- name: Import ASM policy
+ bigip_asm_policy_import:
+ name: new_asm_policy
+ file: /root/asm_policy.xml
+ provider:
+ server: lb.mydomain.com
+ user: admin
+ password: secret
+ delegate_to: localhost
+
+- name: Import ASM policy inline
+ bigip_asm_policy_import:
+ name: foo-policy4
+ inline: content
+ provider:
+ server: lb.mydomain.com
+ user: admin
+ password: secret
+ delegate_to: localhost
+
+- name: Override existing ASM policy
+ bigip_asm_policy:
+ name: new_asm_policy
+ file: /root/asm_policy_new.xml
+ force: yes
+ provider:
+ server: lb.mydomain.com
+ user: admin
+ password: secret
+ delegate_to: localhost
+'''
+
+RETURN = r'''
+file:
+ description: Local path to ASM policy file.
+ returned: changed
+ type: string
+ sample: /root/some_policy.xml
+inline:
+ description: Contents of policy as an inline string
+ returned: changed
+ type: string
+ sample: foobar contents
+name:
+ description: Name of the ASM policy to be created/overwritten
+ returned: changed
+ type: string
+ sample: Asm_APP1_Transparent
+force:
+ description: Set when overwriting an existing policy
+ returned: changed
+ type: bool
+ sample: yes
+'''
+
+import os
+import time
+from ansible.module_utils.basic import AnsibleModule
+from ansible.module_utils.basic import env_fallback
+
+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 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 module_provisioned
+
+
+class Parameters(AnsibleF5Parameters):
+ updatables = []
+
+ returnables = [
+ 'name',
+ 'inline',
+ 'source',
+ 'force'
+ ]
+
+ api_attributes = [
+ 'file',
+ 'name',
+ ]
+
+ api_map = {
+ 'file': 'inline',
+ 'filename': 'source',
+ }
+
+
+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, 'asm'):
+ raise F5ModuleError(
+ "ASM must be provisioned to use this module."
+ )
+
+ 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 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
+ if self.want.inline:
+ task = self.inline_import()
+ self.wait_for_task(task)
+ return True
+
+ self.import_file_to_device()
+ self.remove_temp_policy_from_device()
+ return True
+
+ def exists(self):
+ uri = 'https://{0}:{1}/mgmt/tm/asm/policies/'.format(
+ self.client.provider['server'],
+ self.client.provider['server_port'],
+ )
+
+ query = '?$filter=name+eq+{0}+and+partition+eq+{1}&$select=name'.format(self.want.name, self.want.partition)
+ resp = self.client.api.get(uri + query)
+
+ try:
+ response = resp.json()
+ except ValueError as ex:
+ raise F5ModuleError(str(ex))
+ if 'items' in response and response['items'] != []:
+ return True
+ return False
+
+ 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 _get_policy_link(self):
+ uri = 'https://{0}:{1}/mgmt/tm/asm/policies/'.format(
+ self.client.provider['server'],
+ self.client.provider['server_port'],
+ )
+
+ query = '?$filter=name+eq+{0}+and+partition+eq+{1}&$select=name'.format(self.want.name, self.want.partition)
+ resp = self.client.api.get(uri + query)
+
+ try:
+ response = resp.json()
+ except ValueError as ex:
+ raise F5ModuleError(str(ex))
+
+ policy_link = response['items'][0]['selfLink']
+ return policy_link
+
+ def inline_import(self):
+ params = self.changes.api_params()
+ uri = "https://{0}:{1}/mgmt/tm/asm/tasks/import-policy/".format(
+ self.client.provider['server'],
+ self.client.provider['server_port'],
+ )
+
+ if self.want.force:
+ params.update(dict(policyReference={'link': self._get_policy_link()}))
+ params.pop('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['id']
+
+ def wait_for_task(self, task_id):
+ uri = "https://{0}:{1}/mgmt/tm/asm/tasks/import-policy/{2}".format(
+ self.client.provider['server'],
+ self.client.provider['server_port'],
+ task_id
+ )
+ while True:
+ 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['status'] in ['COMPLETED', 'FAILURE']:
+ break
+ time.sleep(1)
+
+ if response['status'] == 'FAILURE':
+ raise F5ModuleError(
+ 'Failed to export ASM policy.'
+ )
+ if response['status'] == 'COMPLETED':
+ return True
+
+ def import_file_to_device(self):
+ name = os.path.split(self.want.source)[1]
+ self.upload_file_to_device(self.want.source, name)
+ time.sleep(2)
+
+ full_name = fq_name(self.want.partition, self.want.name)
+
+ if self.want.force:
+ cmd = 'tmsh load asm policy {0} file /var/config/rest/downloads/{1} overwrite'.format(full_name, name)
+ else:
+ cmd = 'tmsh load asm policy {0} file /var/config/rest/downloads/{1}'.format(full_name, name)
+
+ 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:
+ if 'Unexpected Error' in response['commandResult']:
+ 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_policy_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'),
+ inline=dict(),
+ force=dict(
+ type='bool',
+ default='no'
+ ),
+ 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)
+ self.mutually_exclusive = [
+ ['source', 'inline']
+ ]
+
+
+def main():
+ spec = ArgumentSpec()
+
+ module = AnsibleModule(
+ argument_spec=spec.argument_spec,
+ supports_check_mode=spec.supports_check_mode,
+ mutually_exclusive=spec.mutually_exclusive
+ )
+
+ 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/test_bigip_asm_policy_import.py b/test/units/modules/network/f5/test_bigip_asm_policy_import.py
new file mode 100644
index 0000000000..1cd046d925
--- /dev/null
+++ b/test/units/modules/network/f5/test_bigip_asm_policy_import.py
@@ -0,0 +1,124 @@
+# -*- 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
+
+import os
+import json
+import pytest
+import sys
+
+if sys.version_info < (2, 7):
+ pytestmark = pytest.mark.skip("F5 Ansible modules require Python >= 2.7")
+
+from ansible.module_utils.basic import AnsibleModule
+
+try:
+ from library.modules.bigip_asm_policy_import import ApiParameters
+ from library.modules.bigip_asm_policy_import import ModuleParameters
+ from library.modules.bigip_asm_policy_import import ModuleManager
+ from library.modules.bigip_asm_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_asm_policy_import import ApiParameters
+ from ansible.modules.network.f5.bigip_asm_policy_import import ModuleParameters
+ from ansible.modules.network.f5.bigip_asm_policy_import import ModuleManager
+ from ansible.modules.network.f5.bigip_asm_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',
+ state='present',
+ source='/var/fake/fake.xml'
+ )
+
+ p = ModuleParameters(params=args)
+ assert p.name == 'fake_policy'
+ assert p.source == '/var/fake/fake.xml'
+
+
+class TestManager(unittest.TestCase):
+ def setUp(self):
+ self.spec = ArgumentSpec()
+ self.policy = os.path.join(fixture_path, 'fake_policy.xml')
+ self.patcher1 = patch('time.sleep')
+ self.patcher1.start()
+
+ try:
+ self.p1 = patch('library.modules.bigip_asm_policy_import.module_provisioned')
+ self.m1 = self.p1.start()
+ self.m1.return_value = True
+ except Exception:
+ self.p1 = patch('ansible.modules.network.f5.bigip_asm_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,
+ 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.exists = Mock(return_value=False)
+ mm.import_file_to_device = Mock(return_value=True)
+ mm.remove_temp_policy_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