diff --git a/lib/ansible/modules/network/f5/bigip_iapp_template.py b/lib/ansible/modules/network/f5/bigip_iapp_template.py
new file mode 100644
index 0000000000..7027ff507a
--- /dev/null
+++ b/lib/ansible/modules/network/f5/bigip_iapp_template.py
@@ -0,0 +1,488 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright 2017 F5 Networks Inc.
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see .
+
+ANSIBLE_METADATA = {
+ 'status': ['preview'],
+ 'supported_by': 'community',
+ 'metadata_version': '1.0'
+}
+
+DOCUMENTATION = '''
+---
+module: bigip_iapp_template
+short_description: Manages TCL iApp templates on a BIG-IP.
+description:
+ - Manages TCL iApp templates on a BIG-IP. This module will allow you to
+ deploy iApp templates to the BIG-IP and manage their lifecycle. The
+ conventional way to use this module is to import new iApps as needed
+ or by extracting the contents of the iApp archive that is provided at
+ downloads.f5.com and then importing all the iApps with this module.
+ This module can also update existing iApps provided that the source
+ of the iApp changed while the name stayed the same. Note however that
+ this module will not reconfigure any services that may have been
+ created using the C(bigip_iapp_service) module. iApps are normally
+ not updated in production. Instead, new versions are deployed and then
+ existing services are changed to consume that new template. As such,
+ the ability to update templates in-place requires the C(force) option
+ to be used.
+version_added: "2.4"
+options:
+ force:
+ description:
+ - Specifies whether or not to force the uploading of an iApp. When
+ C(yes), will force update the iApp even if there are iApp services
+ using it. This will not update the running service though. Use
+ C(bigip_iapp_service) to do that. When C(no), will update the iApp
+ only if there are no iApp services using the template.
+ choices:
+ - yes
+ - no
+ name:
+ description:
+ - The name of the iApp template that you want to delete. This option
+ is only available when specifying a C(state) of C(absent) and is
+ provided as a way to delete templates that you may no longer have
+ the source of.
+ content:
+ description:
+ - Sets the contents of an iApp template directly to the specified
+ value. This is for simple values, but can be used with lookup
+ plugins for anything complex or with formatting. C(content) must
+ be provided when creating new templates.
+ state:
+ description:
+ - Whether the iRule should exist or not.
+ default: present
+ choices:
+ - present
+ - absent
+notes:
+ - Requires the f5-sdk Python package on the host. This is as easy as pip
+ install f5-sdk.
+extends_documentation_fragment: f5
+author:
+ - Tim Rupp (@caphrim007)
+'''
+
+EXAMPLES = '''
+- name: Add the iApp contained in template iapp.tmpl
+ bigip_iapp_template:
+ content: "{{ lookup('template', 'iapp.tmpl') }}"
+ password: "secret"
+ server: "lb.mydomain.com"
+ state: "present"
+ user: "admin"
+ delegate_to: localhost
+
+- name: Update a template in place
+ bigip_iapp_template:
+ content: "{{ lookup('template', 'iapp-new.tmpl') }}"
+ password: "secret"
+ server: "lb.mydomain.com"
+ state: "present"
+ user: "admin"
+ delegate_to: localhost
+
+- name: Update a template in place that has existing services created from it.
+ bigip_iapp_template:
+ content: "{{ lookup('template', 'iapp-new.tmpl') }}"
+ force: yes
+ password: "secret"
+ server: "lb.mydomain.com"
+ state: "present"
+ user: "admin"
+ delegate_to: localhost
+'''
+
+RETURN = '''
+# only common fields returned
+'''
+
+import re
+import uuid
+
+from ansible.module_utils.basic import BOOLEANS
+from ansible.module_utils.f5_utils import (
+ AnsibleF5Client,
+ AnsibleF5Parameters,
+ HAS_F5SDK,
+ F5ModuleError,
+ iteritems,
+ defaultdict,
+ iControlUnexpectedHTTPError
+)
+from f5.utils.iapp_parser import (
+ NonextantTemplateNameException
+)
+
+try:
+ from StringIO import StringIO
+except ImportError:
+ from io import StringIO
+
+
+class Parameters(AnsibleF5Parameters):
+ api_attributes = []
+ returnables = []
+
+ def __init__(self, params=None):
+ self._values = defaultdict(lambda: None)
+ if params:
+ self.update(params=params)
+
+ def update(self, params=None):
+ if params:
+ for k, v in iteritems(params):
+ if self.api_map is not None and k in self.api_map:
+ map_key = self.api_map[k]
+ else:
+ map_key = k
+
+ # Handle weird API parameters like `dns.proxy.__iter__` by
+ # using a map provided by the module developer
+ class_attr = getattr(type(self), map_key, None)
+ if isinstance(class_attr, property):
+ # There is a mapped value for the api_map key
+ if class_attr.fset is None:
+ # If the mapped value does not have an associated setter
+ self._values[map_key] = v
+ else:
+ # The mapped value has a setter
+ setattr(self, map_key, v)
+ else:
+ # If the mapped value is not a @property
+ self._values[map_key] = v
+
+ @property
+ def name(self):
+ if self._values['name']:
+ return self._values['name']
+ if self._values['content']:
+ try:
+ name = self._get_template_name()
+ return name
+ except NonextantTemplateNameException:
+ raise F5ModuleError(
+ "No template name was found in the template"
+ )
+ return None
+
+ @property
+ def content(self):
+ if self._values['content'] is None:
+ return None
+ result = self._squash_template_name_prefix()
+ if self._values['name']:
+ result = self._replace_template_name(result)
+ return result
+
+ @property
+ def checksum(self):
+ return self._values['tmplChecksum']
+
+ 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
+
+ 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
+
+ def _squash_template_name_prefix(self):
+ """Removes the template name prefix
+
+ The IappParser in the SDK treats the partition prefix as part of
+ the iApp's name. This method removes that partition from the name
+ in the iApp so that comparisons can be done properly and entries
+ can be created properly when using REST.
+
+ :return string
+ """
+ pattern = r'sys\s+application\s+template\s+/Common/'
+ replace = 'sys application template '
+ return re.sub(pattern, replace, self._values['content'])
+
+ def _replace_template_name(self, template):
+ """Replaces template name at runtime
+
+ To allow us to do the switch-a-roo with temporary templates and
+ checksum comparisons, we need to take the template provided to us
+ and change its name to a temporary value so that BIG-IP will create
+ a clone for us.
+
+ :return string
+ """
+ pattern = r'sys\s+application\s+template\s+[^ ]+'
+ replace = 'sys application template {0}'.format(self._values['name'])
+ return re.sub(pattern, replace, template)
+
+ def _get_template_name(self):
+ # There is a bug in the iApp parser in the F5 SDK that prevents us from
+ # using it in all cases to get the name of an iApp. So we'll use this
+ # pattern for now and file a bug with the F5 SDK
+ pattern = r'sys\s+application\s+template\s+(?P\/\w+\/)?(?P[\w.]+)'
+ matches = re.search(pattern, self.content)
+ try:
+ result = matches.group('name')
+ except IndexError:
+ result = None
+ if result:
+ return result
+ raise NonextantTemplateNameException
+
+
+class ModuleManager(object):
+ def __init__(self, client):
+ self.client = client
+ self.want = Parameters(self.client.module.params)
+ self.changes = Parameters()
+
+ def exec_module(self):
+ result = dict()
+ changed = False
+ 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))
+
+ changes = self.changes.to_return()
+ result.update(**changes)
+ result.update(dict(changed=changed))
+ return result
+
+ def present(self):
+ if self.exists():
+ return self.update()
+ else:
+ return self.create()
+
+ def update(self):
+ self.have = self.read_current_from_device()
+
+ if not self.templates_differ():
+ return False
+
+ if not self.want.force and self.template_in_use():
+ return False
+
+ if self.client.check_mode:
+ return True
+
+ self._remove_iapp_checksum()
+ # The same process used for creating (load) can be used for updating
+ self.create_on_device()
+ self._generate_template_checksum_on_device()
+ return True
+
+ def template_in_use(self):
+ collection = self.client.api.tm.sys.application.services.get_collection()
+ fullname = '/{0}/{1}'.format(self.want.partition, self.want.name)
+ for resource in collection:
+ if resource.template == fullname:
+ return True
+ return False
+
+ def read_current_from_device(self):
+ self._generate_template_checksum_on_device()
+ resource = self.client.api.tm.sys.application.templates.template.load(
+ name=self.want.name,
+ partition=self.want.partition
+ )
+ result = resource.attrs
+ return Parameters(result)
+
+ def absent(self):
+ changed = False
+ if self.exists():
+ changed = self.remove()
+ return changed
+
+ def exists(self):
+ result = self.client.api.tm.sys.application.templates.template.exists(
+ name=self.want.name,
+ partition=self.want.partition
+ )
+ return result
+
+ def _remove_iapp_checksum(self):
+ """Removes the iApp tmplChecksum
+
+ This is required for updating in place or else the load command will
+ fail with a "AppTemplate ... content does not match the checksum"
+ error.
+
+ :return:
+ """
+ resource = self.client.api.tm.sys.application.templates.template.load(
+ name=self.want.name,
+ partition=self.want.partition
+ )
+ resource.modify(tmplChecksum=None)
+
+ def templates_differ(self):
+ # BIG-IP can generate checksums of iApps, but the iApp needs to be
+ # on the box to do this. Additionally, the checksum is MD5, but it
+ # is not an MD5 of the entire content of the template. Instead, it
+ # is a hash of some portion of the template that is unknown to me.
+ #
+ # The code below is responsible for uploading the provided template
+ # under a unique name and creating a checksum for it so that that
+ # checksum can be compared to the one of the existing template.
+ #
+ # Using this method we can compare the checksums of the existing
+ # iApp and the iApp that the user is providing to the module.
+ backup = self.want.name
+
+ # Override whatever name may have been provided so that we can
+ # temporarily create a new template to test checksums with
+ self.want.update({
+ 'name': 'ansible-{0}'.format(str(uuid.uuid4()))
+ })
+
+ # Create and remove temporary template
+ temp = self._get_temporary_template()
+
+ # Set the template name back to what it was originally so that
+ # any future operations only happen on the real template.
+ self.want.update({
+ 'name': backup
+ })
+ if temp.checksum != self.have.checksum:
+ return True
+ return False
+
+ def _get_temporary_template(self):
+ self.create_on_device()
+ temp = self.read_current_from_device()
+ self.remove_from_device()
+ return temp
+
+ def _generate_template_checksum_on_device(self):
+ generate = 'tmsh generate sys application template {0} checksum'.format(
+ self.want.name
+ )
+ self.client.api.tm.util.bash.exec_cmd(
+ 'run',
+ utilCmdArgs='-c "{0}"'.format(generate)
+ )
+
+ def create(self):
+ if self.client.check_mode:
+ return True
+ self.create_on_device()
+ if self.exists():
+ return True
+ else:
+ raise F5ModuleError("Failed to create the iApp template")
+
+ def create_on_device(self):
+ remote_path = "/var/config/rest/downloads/{0}".format(self.want.name)
+ load_command = 'tmsh load sys application template {0}'.format(remote_path)
+
+ template = StringIO(self.want.content)
+
+ upload = self.client.api.shared.file_transfer.uploads
+ upload.upload_stringio(template, self.want.name)
+ output = self.client.api.tm.util.bash.exec_cmd(
+ 'run',
+ utilCmdArgs='-c "{0}"'.format(load_command)
+ )
+
+ if hasattr(output, 'commandResult'):
+ result = output.commandResult
+ if 'Syntax Error' in result:
+ raise F5ModuleError(output.commandResult)
+
+ def remove(self):
+ if self.client.check_mode:
+ return True
+ self.remove_from_device()
+ if self.exists():
+ raise F5ModuleError("Failed to delete the iApp template")
+ return True
+
+ def remove_from_device(self):
+ resource = self.client.api.tm.sys.application.templates.template.load(
+ name=self.want.name,
+ partition=self.want.partition
+ )
+ resource.delete()
+
+
+class ArgumentSpec(object):
+ def __init__(self):
+ self.supports_check_mode = True
+ self.argument_spec = dict(
+ name=dict(),
+ state=dict(
+ default='present',
+ choices=['present', 'absent']
+ ),
+ force=dict(
+ choices=BOOLEANS,
+ type='bool'
+ ),
+ content=dict()
+ )
+ self.f5_product_name = 'bigip'
+ self.mutually_exclusive = [
+ ['sync_device_to_group', 'sync_group_to_device']
+ ]
+
+
+def main():
+ if not HAS_F5SDK:
+ raise F5ModuleError("The python f5-sdk module is required")
+
+ spec = ArgumentSpec()
+
+ client = AnsibleF5Client(
+ argument_spec=spec.argument_spec,
+ supports_check_mode=spec.supports_check_mode,
+ f5_product_name=spec.f5_product_name,
+ mutually_exclusive=spec.mutually_exclusive
+ )
+
+ try:
+ mm = ModuleManager(client)
+ results = mm.exec_module()
+ client.module.exit_json(**results)
+ except F5ModuleError as e:
+ client.module.fail_json(msg=str(e))
+
+if __name__ == '__main__':
+ main()
diff --git a/test/units/modules/network/f5/fixtures/basic-iapp.tmpl b/test/units/modules/network/f5/fixtures/basic-iapp.tmpl
new file mode 100644
index 0000000000..7f9a681b74
--- /dev/null
+++ b/test/units/modules/network/f5/fixtures/basic-iapp.tmpl
@@ -0,0 +1,25 @@
+sys application template good_templ {
+ actions {
+ definition {
+ html-help {
+ # HTML Help for the template
+ }
+ implementation {
+ # TMSH implementation code
+ }
+ macro {
+ # TMSH macro code
+ }
+ presentation {
+ # APL presentation language
+ }
+ role-acl { admin manager resource-admin }
+ run-as none
+ }
+ }
+ description "My basic template"
+ partition Common
+ requires-modules { ltm }
+ ignore-verification true
+ requires-bigip-version-min 11.6.0
+}
diff --git a/test/units/modules/network/f5/fixtures/create_iapp_template.iapp b/test/units/modules/network/f5/fixtures/create_iapp_template.iapp
new file mode 100644
index 0000000000..995bd4d636
--- /dev/null
+++ b/test/units/modules/network/f5/fixtures/create_iapp_template.iapp
@@ -0,0 +1,56 @@
+cli admin-partitions {
+ update-partition Common
+}
+
+sys application template foo.iapp {
+
+ actions {
+ definition {
+
+ implementation {
+ set cfg { ltm virtual forwarding-repro {
+ destination 0.0.0.0:any
+ description "something 1"
+ mask any
+ profiles {
+ fastL4 { }
+ }
+ source 1.1.1.1/32
+ translate-address disabled
+ translate-port disabled
+ vlans { __forwarding_vlans__ }
+ vlans-enabled
+} }
+
+
+ if {![info exists {::var__forwarding_vlans}] || (${::var__forwarding_vlans} == "")} {
+ set {::var__forwarding_vlans} "{}"
+ puts "Info: assigning empty string to variable {::var__forwarding_vlans}"
+ }
+
+
+ set cfg [string map "__forwarding_vlans__ ${::var__forwarding_vlans} __app_service__ $tmsh::app_name.app/$tmsh::app_name " $cfg]
+ set fileId [open /var/tmp/demo.repro.cfg "w"]
+ puts -nonewline $fileId $cfg
+ close $fileId
+
+
+ tmsh::load sys config merge file /var/tmp/demo.repro.cfg
+ }
+
+ presentation {
+
+ include "/Common/f5.apl_common"
+ section var {
+ string forwarding_vlans display "xxlarge"
+ }
+
+ text {
+ var "General variables"
+ var.forwarding_vlans "__var__forwarding_vlans__"
+ }
+ }
+ role-acl { admin manager resource-admin }
+ }
+ }
+}
diff --git a/test/units/modules/network/f5/fixtures/load_sys_application_template_w_new_checksum.json b/test/units/modules/network/f5/fixtures/load_sys_application_template_w_new_checksum.json
new file mode 100644
index 0000000000..57f3c3755a
--- /dev/null
+++ b/test/units/modules/network/f5/fixtures/load_sys_application_template_w_new_checksum.json
@@ -0,0 +1,21 @@
+{
+ "kind": "tm:sys:application:template:templatestate",
+ "name": "good_templ",
+ "partition": "Common",
+ "fullPath": "/Common/good_templ",
+ "generation": 410,
+ "selfLink": "https://localhost/mgmt/tm/sys/application/template/~Common~good_templ?ver=13.0.0",
+ "description": "My basic template",
+ "ignoreVerification": "false",
+ "requiresBigipVersionMin": "11.6.0",
+ "requiresModules": [
+ "ltm"
+ ],
+ "tmplChecksum": "90c46acee5ca08e300da0bcdb9130745",
+ "totalSigningStatus": "checksum",
+ "verificationStatus": "checksum-verified",
+ "actionsReference": {
+ "link": "https://localhost/mgmt/tm/sys/application/template/~Common~good_templ/actions?ver=13.0.0",
+ "isSubcollection": true
+ }
+}
diff --git a/test/units/modules/network/f5/fixtures/load_sys_application_template_w_old_checksum.json b/test/units/modules/network/f5/fixtures/load_sys_application_template_w_old_checksum.json
new file mode 100644
index 0000000000..da477515bf
--- /dev/null
+++ b/test/units/modules/network/f5/fixtures/load_sys_application_template_w_old_checksum.json
@@ -0,0 +1,21 @@
+{
+ "kind": "tm:sys:application:template:templatestate",
+ "name": "good_templ",
+ "partition": "Common",
+ "fullPath": "/Common/good_templ",
+ "generation": 410,
+ "selfLink": "https://localhost/mgmt/tm/sys/application/template/~Common~good_templ?ver=13.0.0",
+ "description": "My basic foo bar",
+ "ignoreVerification": "false",
+ "requiresBigipVersionMin": "12.0.0",
+ "requiresModules": [
+ "ltm"
+ ],
+ "tmplChecksum": "eee01710dbe330d380d1a4fa30eeabdb",
+ "totalSigningStatus": "checksum",
+ "verificationStatus": "checksum-verified",
+ "actionsReference": {
+ "link": "https://localhost/mgmt/tm/sys/application/template/~Common~good_templ/actions?ver=13.0.0",
+ "isSubcollection": true
+ }
+}
diff --git a/test/units/modules/network/f5/test_bigip_iapp_template.py b/test/units/modules/network/f5/test_bigip_iapp_template.py
new file mode 100644
index 0000000000..88c374e2cc
--- /dev/null
+++ b/test/units/modules/network/f5/test_bigip_iapp_template.py
@@ -0,0 +1,191 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright 2017 F5 Networks Inc.
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see .
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import sys
+
+if sys.version_info < (2, 7):
+ from nose.plugins.skip import SkipTest
+ raise SkipTest("F5 Ansible modules require Python >= 2.7")
+
+import os
+import json
+
+from ansible.compat.tests import unittest
+from ansible.module_utils import basic
+from ansible.compat.tests.mock import patch, Mock
+from ansible.module_utils._text import to_bytes
+from ansible.module_utils.f5_utils import AnsibleF5Client
+
+try:
+ from library.bigip_iapp_template import Parameters
+ from library.bigip_iapp_template import ModuleManager
+ from library.bigip_iapp_template import ArgumentSpec
+except ImportError:
+ from ansible.modules.network.f5.bigip_iapp_template import Parameters
+ from ansible.modules.network.f5.bigip_iapp_template import ArgumentSpec
+ from ansible.modules.network.f5.bigip_iapp_template import ModuleManager
+
+fixture_path = os.path.join(os.path.dirname(__file__), 'fixtures')
+fixture_data = {}
+
+
+def set_module_args(args):
+ args = json.dumps({'ANSIBLE_MODULE_ARGS': args})
+ basic._ANSIBLE_ARGS = to_bytes(args)
+
+
+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):
+ iapp = load_fixture('create_iapp_template.iapp')
+ args = dict(
+ content=iapp
+ )
+ p = Parameters(args)
+ assert p.name == 'foo.iapp'
+
+
+@patch('ansible.module_utils.f5_utils.AnsibleF5Client._get_mgmt_root',
+ return_value=True)
+class TestManager(unittest.TestCase):
+
+ def setUp(self):
+ self.spec = ArgumentSpec()
+
+ def test_create_iapp_template(self, *args):
+ # Configure the arguments that would be sent to the Ansible module
+ set_module_args(dict(
+ content=load_fixture('basic-iapp.tmpl'),
+ password='passsword',
+ server='localhost',
+ user='admin'
+ ))
+
+ client = AnsibleF5Client(
+ argument_spec=self.spec.argument_spec,
+ supports_check_mode=self.spec.supports_check_mode,
+ f5_product_name=self.spec.f5_product_name
+ )
+ mm = ModuleManager(client)
+
+ # Override methods to force specific logic in the module to happen
+ mm.exists = Mock(side_effect=[False, True])
+ mm.create_on_device = Mock(return_value=True)
+
+ results = mm.exec_module()
+
+ assert results['changed'] is True
+
+ def test_update_iapp_template(self, *args):
+ # Configure the arguments that would be sent to the Ansible module
+ set_module_args(dict(
+ content=load_fixture('basic-iapp.tmpl'),
+ password='passsword',
+ server='localhost',
+ user='admin'
+ ))
+
+ current1 = Parameters(load_fixture('load_sys_application_template_w_new_checksum.json'))
+ current2 = Parameters(load_fixture('load_sys_application_template_w_old_checksum.json'))
+ client = AnsibleF5Client(
+ argument_spec=self.spec.argument_spec,
+ supports_check_mode=self.spec.supports_check_mode,
+ f5_product_name=self.spec.f5_product_name
+ )
+ mm = ModuleManager(client)
+
+ # Override methods to force specific logic in the module to happen
+ mm.exists = Mock(side_effect=[True, True])
+ mm.create_on_device = Mock(return_value=True)
+ mm.read_current_from_device = Mock(return_value=current1)
+ mm.template_in_use = Mock(return_value=False)
+ mm._get_temporary_template = Mock(return_value=current2)
+ mm._remove_iapp_checksum = Mock(return_value=None)
+ mm._generate_template_checksum_on_device = Mock(return_value=None)
+
+ results = mm.exec_module()
+
+ assert results['changed'] is True
+
+ def test_delete_iapp_template(self, *args):
+ set_module_args(dict(
+ content=load_fixture('basic-iapp.tmpl'),
+ password='passsword',
+ server='localhost',
+ user='admin',
+ state='absent'
+ ))
+
+ client = AnsibleF5Client(
+ argument_spec=self.spec.argument_spec,
+ supports_check_mode=self.spec.supports_check_mode,
+ f5_product_name=self.spec.f5_product_name
+ )
+ mm = ModuleManager(client)
+
+ # Override methods to force specific logic in the module to happen
+ mm.exists = Mock(side_effect=[True, False])
+ mm.remove_from_device = Mock(return_value=True)
+
+ results = mm.exec_module()
+
+ assert results['changed'] is True
+
+ def test_delete_iapp_template_idempotent(self, *args):
+ set_module_args(dict(
+ content=load_fixture('basic-iapp.tmpl'),
+ password='passsword',
+ server='localhost',
+ user='admin',
+ state='absent'
+ ))
+
+ client = AnsibleF5Client(
+ argument_spec=self.spec.argument_spec,
+ supports_check_mode=self.spec.supports_check_mode,
+ f5_product_name=self.spec.f5_product_name
+ )
+ mm = ModuleManager(client)
+
+ # Override methods to force specific logic in the module to happen
+ mm.exists = Mock(side_effect=[False, False])
+
+ results = mm.exec_module()
+
+ assert results['changed'] is False