From 9aba711519fb1982db0019e76e3104d050772d81 Mon Sep 17 00:00:00 2001 From: Tim Rupp Date: Sun, 14 Jan 2018 17:40:59 -0800 Subject: [PATCH] Adds bigip_static_route module (#34859) This module can be used to manage static routes on a BIG-IP --- .../modules/network/f5/bigip_static_route.py | 612 ++++++++++++++++++ .../fixtures/load_net_route_description.json | 15 + .../network/f5/test_bigip_static_route.py | 358 ++++++++++ 3 files changed, 985 insertions(+) create mode 100644 lib/ansible/modules/network/f5/bigip_static_route.py create mode 100644 test/units/modules/network/f5/fixtures/load_net_route_description.json create mode 100644 test/units/modules/network/f5/test_bigip_static_route.py diff --git a/lib/ansible/modules/network/f5/bigip_static_route.py b/lib/ansible/modules/network/f5/bigip_static_route.py new file mode 100644 index 00000000000..9506fb7b039 --- /dev/null +++ b/lib/ansible/modules/network/f5/bigip_static_route.py @@ -0,0 +1,612 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# 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 +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = r''' +module: bigip_static_route +short_description: Manipulate static routes on a BIG-IP +description: + - Manipulate static routes on a BIG-IP. +version_added: 2.5 +options: + name: + description: + - Name of the static route. + required: True + description: + description: + - Descriptive text that identifies the route. + destination: + description: + - Specifies an IP address for the static entry in the routing table. + When creating a new static route, this value is required. + - This value cannot be changed once it is set. + netmask: + description: + - The netmask for the static route. When creating a new static route, this value + is required. + - This value can be in either IP or CIDR format. + - This value cannot be changed once it is set. + gateway_address: + description: + - Specifies the router for the system to use when forwarding packets + to the destination host or network. Also known as the next-hop router + address. This can be either an IPv4 or IPv6 address. When it is an + IPv6 address that starts with C(FE80:), the address will be treated + as a link-local address. This requires that the C(vlan) parameter + also be supplied. + vlan: + description: + - Specifies the VLAN or Tunnel through which the system forwards packets + to the destination. When C(gateway_address) is a link-local IPv6 + address, this value is required + pool: + description: + - Specifies the pool through which the system forwards packets to the + destination. + reject: + description: + - Specifies that the system drops packets sent to the destination. + mtu: + description: + - Specifies a specific maximum transmission unit (MTU). + route_domain: + description: + - The route domain id of the system. When creating a new static route, if + this value is not specified, a default value of C(0) will be used. + - This value cannot be changed once it is set. + state: + description: + - When C(present), ensures that the cloud connector exists. When + C(absent), ensures that the cloud connector does not exist. + required: False + default: present + choices: + - present + - absent +notes: + - Requires the netaddr Python package on the host. This is as easy as pip + install netaddr. +extends_documentation_fragment: f5 +requirements: + - netaddr +author: + - Tim Rupp (@caphrim007) +''' + +EXAMPLES = r''' +- name: Create static route with gateway address + bigip_static_route: + destination: 10.10.10.10 + netmask: 255.255.255.255 + gateway_address: 10.2.2.3 + name: test-route + password: secret + server: lb.mydomain.come + user: admin + validate_certs: no + delegate_to: localhost +''' + +RETURN = r''' +vlan: + description: Whether the banner is enabled or not. + returned: changed + type: string + sample: true +gateway_address: + description: Whether the banner is enabled or not. + returned: changed + type: string + sample: true +destination: + description: Whether the banner is enabled or not. + returned: changed + type: string + sample: true +route_domain: + description: Route domain of the static route. + returned: changed + type: int + sample: 1 +netmask: + description: Netmask of the destination. + returned: changed + type: string + sample: 255.255.255.255 +pool: + description: Whether the banner is enabled or not. + returned: changed + type: string + sample: true +description: + description: Whether the banner is enabled or not. + returned: changed + type: string + sample: true +reject: + description: Whether the banner is enabled or not. + returned: changed + type: string + sample: true +''' + +import re + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.parsing.convert_bool import BOOLEANS_TRUE + +HAS_DEVEL_IMPORTS = False + +try: + # Sideband repository used for dev + 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.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 fqdn_name + 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 + HAS_DEVEL_IMPORTS = True +except ImportError: + # Upstream Ansible + 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.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 fqdn_name + 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 + +try: + import netaddr + HAS_NETADDR = True +except ImportError: + HAS_NETADDR = False + + +class Parameters(AnsibleF5Parameters): + api_map = { + 'tmInterface': 'vlan', + 'gw': 'gateway_address', + 'network': 'destination', + 'blackhole': 'reject' + } + + updatables = [ + 'description', 'gateway_address', 'vlan', + 'pool', 'mtu', 'reject', 'destination', 'route_domain', + 'netmask' + ] + + returnables = [ + 'vlan', 'gateway_address', 'destination', 'pool', 'description', + 'reject', 'mtu', 'netmask', 'route_domain' + ] + + api_attributes = [ + 'tmInterface', 'gw', 'network', 'blackhole', 'description', 'pool', 'mtu' + ] + + def to_return(self): + result = {} + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + return result + + @property + def reject(self): + if self._values['reject'] in BOOLEANS_TRUE: + return True + + +class ModuleParameters(Parameters): + @property + def vlan(self): + if self._values['vlan'] is None: + return None + return fqdn_name(self.partition, self._values['vlan']) + + @property + def gateway_address(self): + if self._values['gateway_address'] is None: + return None + try: + ip = netaddr.IPNetwork(self._values['gateway_address']) + return str(ip.ip) + except netaddr.core.AddrFormatError: + raise F5ModuleError( + "The provided gateway_address is not an IP address" + ) + + @property + def route_domain(self): + if self._values['route_domain'] is None: + return None + result = int(self._values['route_domain']) + return result + + @property + def destination(self): + if self._values['destination'] is None: + return None + if self._values['destination'] == 'default': + self._values['destination'] = '0.0.0.0/0' + try: + ip = netaddr.IPNetwork(self.destination_ip) + if self.route_domain: + return '{0}%{2}/{1}'.format(ip.ip, ip.prefixlen, self.route_domain) + else: + return '{0}/{1}'.format(ip.ip, ip.prefixlen) + except netaddr.core.AddrFormatError: + raise F5ModuleError( + "The provided destination is not an IP address" + ) + + @property + def destination_ip(self): + if self._values['destination']: + ip = netaddr.IPNetwork('{0}/{1}'.format(self._values['destination'], self.netmask)) + return '{0}/{1}'.format(ip.ip, ip.prefixlen) + + @property + def netmask(self): + if self._values['netmask'] is None: + return None + # Check if numeric + if isinstance(self._values['netmask'], int): + result = int(self._values['netmask']) + if 0 < result < 256: + return result + raise F5ModuleError( + 'The provided netmask {0} is neither in IP or CIDR format'.format(result) + ) + else: + try: + # IPv4 netmask + address = '0.0.0.0/' + self._values['netmask'] + ip = netaddr.IPNetwork(address) + except netaddr.AddrFormatError as ex: + try: + # IPv6 netmask + address = '::/' + self._values['netmask'] + ip = netaddr.IPNetwork(address) + except netaddr.AddrFormatError as ex: + raise F5ModuleError( + 'The provided netmask {0} is neither in IP or CIDR format'.format(self._values['netmask']) + ) + result = int(ip.prefixlen) + return result + + +class ApiParameters(Parameters): + @property + def route_domain(self): + if self._values['destination'] is None: + return None + pattern = r'([0-9:]%(?P[0-9]+))' + matches = re.search(pattern, self._values['destination']) + if matches: + return int(matches.group('rd')) + return 0 + + @property + def destination_ip(self): + if self._values['destination'] is None: + return None + if self._values['destination'] == 'default': + self._values['destination'] = '0.0.0.0/0' + try: + pattern = r'(?P%[0-9]+)' + addr = re.sub(pattern, '', self._values['destination']) + ip = netaddr.IPNetwork(addr) + return '{0}/{1}'.format(ip.ip, ip.prefixlen) + except netaddr.core.AddrFormatError: + raise F5ModuleError( + "The provided destination is not an IP address" + ) + + @property + def netmask(self): + ip = netaddr.IPNetwork(self.destination_ip) + return int(ip.prefixlen) + + +class Changes(Parameters): + pass + + +class UsableChanges(Parameters): + pass + + +class ReportableChanges(Parameters): + 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 + + @property + def destination(self): + if self.want.destination_ip is None: + return None + if self.want.destination_ip != self.have.destination_ip: + raise F5ModuleError( + "The destination cannot be changed. Delete and recreate " + "the static route if you need to do this." + ) + + @property + def route_domain(self): + if self.want.route_domain is None: + return None + if self.want.route_domain is None and self.have.route_domain == 0: + return None + if self.want.route_domain != self.have.route_domain: + raise F5ModuleError("You cannot change the route domain.") + + @property + def netmask(self): + if self.want.netmask is None: + return None + # It's easiest to just check the netmask by comparing dest IPs. + if self.want.destination_ip != self.have.destination_ip: + raise F5ModuleError( + "The netmask cannot be changed. Delete and recreate " + "the static route if you need to do this." + ) + + +class ModuleManager(object): + def __init__(self, *args, **kwargs): + self.module = kwargs.get('module', None) + self.client = kwargs.get('client', None) + self.have = 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 _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if k in ['netmask', 'route_domain']: + changed['address'] = change + else: + changed[k] = change + if changed: + self.changes = UsableChanges(params=changed) + return True + return False + + def exec_module(self): + changed = False + 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)) + + 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 _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + + def exists(self): + collection = self.client.api.tm.net.routes.get_collection() + for resource in collection: + if resource.name == self.want.name: + if resource.partition == self.want.partition: + return True + return False + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def create(self): + required_resources = ['pool', 'vlan', 'reject', 'gateway_address'] + self._set_changed_options() + if self.want.destination is None: + raise F5ModuleError( + 'destination must be specified when creating a static route' + ) + if self.want.netmask is None: + raise F5ModuleError( + 'netmask must be specified when creating a static route' + ) + if all(getattr(self.want, v) is None for v in required_resources): + raise F5ModuleError( + "You must specify at least one of " + ', '.join(required_resources) + ) + if self.module.check_mode: + return True + self.create_on_device() + return True + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.module.check_mode: + return True + self.update_on_device() + return True + + def update_on_device(self): + params = self.want.api_params() + + # The 'network' attribute is not updatable + params.pop('network', None) + result = self.client.api.tm.net.routes.route.load( + name=self.want.name, + partition=self.want.partition + ) + result.modify(**params) + + def read_current_from_device(self): + resource = self.client.api.tm.net.routes.route.load( + name=self.want.name, + partition=self.want.partition + ) + result = resource.attrs + return ApiParameters(params=result) + + def create_on_device(self): + params = self.want.api_params() + self.client.api.tm.net.routes.route.create( + name=self.want.name, + partition=self.want.partition, + **params + ) + + def absent(self): + if self.exists(): + return self.remove() + return False + + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the static route") + return True + + def remove_from_device(self): + result = self.client.api.tm.net.routes.route.load( + name=self.want.name, + partition=self.want.partition + ) + if result: + result.delete() + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(required=True), + description=dict(), + destination=dict(), + netmask=dict(), + gateway_address=dict(), + vlan=dict(), + pool=dict(), + mtu=dict(), + reject=dict( + type='bool' + ), + state=dict( + default='present', + choices=['absent', 'present'] + ), + route_domain=dict(type='int') + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + self.mutually_exclusive = [ + ['gateway_address', 'vlan', 'pool', 'reject'] + ] + + +def main(): + spec = ArgumentSpec() + + module = AnsibleModule( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + mutually_exclusive=spec.mutually_exclusive, + ) + if not HAS_F5SDK: + module.fail_json(msg="The python f5-sdk module is required") + if not HAS_NETADDR: + module.fail_json(msg="The python netaddr module is required") + + try: + client = F5Client(**module.params) + mm = ModuleManager(module=module, client=client) + results = mm.exec_module() + cleanup_tokens(client) + module.exit_json(**results) + except F5ModuleError as ex: + cleanup_tokens(client) + module.fail_json(msg=str(ex)) + + +if __name__ == '__main__': + main() diff --git a/test/units/modules/network/f5/fixtures/load_net_route_description.json b/test/units/modules/network/f5/fixtures/load_net_route_description.json new file mode 100644 index 00000000000..a7c47bc8fe6 --- /dev/null +++ b/test/units/modules/network/f5/fixtures/load_net_route_description.json @@ -0,0 +1,15 @@ +{ + "kind": "tm:net:route:routestate", + "name": "asdasd", + "partition": "Common", + "fullPath": "/Common/asdasd", + "generation": 113, + "selfLink": "https://localhost/mgmt/tm/net/route/~Common~asdasd?ver=12.1.0", + "description": "asdasd", + "mtu": 0, + "network": "2.2.2.2/32", + "pool": "/Common/adsasd", + "poolReference": { + "link": "https://localhost/mgmt/tm/ltm/pool/~Common~adsasd?ver=12.1.0" + } +} \ No newline at end of file diff --git a/test/units/modules/network/f5/test_bigip_static_route.py b/test/units/modules/network/f5/test_bigip_static_route.py new file mode 100644 index 00000000000..1fff3304b7a --- /dev/null +++ b/test/units/modules/network/f5/test_bigip_static_route.py @@ -0,0 +1,358 @@ +# -*- coding: utf-8 -*- +# +# 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) +__metaclass__ = type + +import os +import json +import sys + +from nose.plugins.skip import SkipTest +if sys.version_info < (2, 7): + raise SkipTest("F5 Ansible modules require Python >= 2.7") + +from ansible.compat.tests import unittest +from ansible.compat.tests.mock import Mock +from ansible.compat.tests.mock import patch +from ansible.module_utils.basic import AnsibleModule + +try: + from library.bigip_static_route import ApiParameters + from library.bigip_static_route import ModuleParameters + from library.bigip_static_route import ModuleManager + from library.bigip_static_route 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 +except ImportError: + try: + from ansible.modules.network.f5.bigip_static_route import ApiParameters + from ansible.modules.network.f5.bigip_static_route import ModuleParameters + from ansible.modules.network.f5.bigip_static_route import ModuleManager + from ansible.modules.network.f5.bigip_static_route import ArgumentSpec + from ansible.module_utils.network.f5.common import F5ModuleError + from ansible.module_utils.network.f5.common import iControlUnexpectedHTTPError + from units.modules.utils import set_module_args + except ImportError: + raise SkipTest("F5 Ansible modules require the f5-sdk Python library") + +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( + vlan="foo", + gateway_address="10.10.10.10" + ) + p = ModuleParameters(params=args) + assert p.vlan == '/Common/foo' + assert p.gateway_address == '10.10.10.10' + + def test_api_parameters(self): + args = dict( + tmInterface="foo", + gw="10.10.10.10" + ) + p = ApiParameters(params=args) + assert p.vlan == 'foo' + assert p.gateway_address == '10.10.10.10' + + def test_reject_parameter_types(self): + # boolean true + args = dict(reject=True) + p = ModuleParameters(params=args) + assert p.reject is True + + # boolean false + args = dict(reject=False) + p = ModuleParameters(params=args) + assert p.reject is None + + # string + args = dict(reject="yes") + p = ModuleParameters(params=args) + assert p.reject is True + + # integer + args = dict(reject=1) + p = ModuleParameters(params=args) + assert p.reject is True + + # none + args = dict(reject=None) + p = ModuleParameters(params=args) + assert p.reject is None + + def test_destination_parameter_types(self): + # cidr address + args = dict( + destination="10.10.10.10", + netmask='32' + ) + p = ModuleParameters(params=args) + assert p.destination == '10.10.10.10/32' + + # netmask + args = dict( + destination="10.10.10.10", + netmask="255.255.255.255" + ) + p = ModuleParameters(params=args) + assert p.destination == '10.10.10.10/32' + + def test_vlan_with_partition(self): + args = dict( + vlan="/Common/foo", + gateway_address="10.10.10.10" + ) + p = ModuleParameters(params=args) + assert p.vlan == '/Common/foo' + assert p.gateway_address == '10.10.10.10' + + def test_api_route_domain(self): + args = dict( + destination="1.1.1.1/32%2" + ) + p = ApiParameters(params=args) + assert p.route_domain == 2 + + args = dict( + destination="2700:bc00:1f10:101::6/64%2" + ) + p = ApiParameters(params=args) + assert p.route_domain == 2 + + +class TestManager(unittest.TestCase): + + def setUp(self): + self.spec = ArgumentSpec() + + def test_create_blackhole(self, *args): + set_module_args(dict( + name='test-route', + password='admin', + server='localhost', + user='admin', + state='present', + destination='10.10.10.10', + netmask='255.255.255.255', + reject='yes' + )) + + module = AnsibleModule( + argument_spec=self.spec.argument_spec, + mutually_exclusive=self.spec.mutually_exclusive, + supports_check_mode=self.spec.supports_check_mode + ) + mm = ModuleManager(module=module) + + # Override methods to force specific logic in the module to happen + mm.exists = Mock(return_value=False) + mm.create_on_device = Mock(return_value=True) + + results = mm.exec_module() + assert results['changed'] is True + + def test_create_route_to_pool(self, *args): + set_module_args(dict( + name='test-route', + password='admin', + server='localhost', + user='admin', + state='present', + destination='10.10.10.10', + netmask='255.255.255.255', + pool="test-pool" + )) + + module = AnsibleModule( + argument_spec=self.spec.argument_spec, + mutually_exclusive=self.spec.mutually_exclusive, + supports_check_mode=self.spec.supports_check_mode + ) + mm = ModuleManager(module=module) + + # Override methods to force specific logic in the module to happen + mm.exists = Mock(return_value=False) + mm.create_on_device = Mock(return_value=True) + results = mm.exec_module() + + assert results['changed'] is True + assert results['pool'] == 'test-pool' + + def test_create_route_to_vlan(self, *args): + set_module_args(dict( + name='test-route', + password='admin', + server='localhost', + user='admin', + state='present', + destination='10.10.10.10', + netmask='255.255.255.255', + vlan="test-vlan" + )) + + module = AnsibleModule( + argument_spec=self.spec.argument_spec, + mutually_exclusive=self.spec.mutually_exclusive, + supports_check_mode=self.spec.supports_check_mode + ) + mm = ModuleManager(module=module) + + # Override methods to force specific logic in the module to happen + mm.exists = Mock(return_value=False) + mm.create_on_device = Mock(return_value=True) + results = mm.exec_module() + + assert results['changed'] is True + assert results['vlan'] == '/Common/test-vlan' + + def test_update_description(self, *args): + set_module_args(dict( + name='test-route', + password='admin', + server='localhost', + user='admin', + state='present', + description='foo description' + )) + + module = AnsibleModule( + argument_spec=self.spec.argument_spec, + mutually_exclusive=self.spec.mutually_exclusive, + supports_check_mode=self.spec.supports_check_mode + ) + mm = ModuleManager(module=module) + + # Override methods to force specific logic in the module to happen + current = ApiParameters(params=load_fixture('load_net_route_description.json')) + mm.exists = Mock(return_value=True) + mm.update_on_device = Mock(return_value=True) + mm.read_current_from_device = Mock(return_value=current) + results = mm.exec_module() + + assert results['changed'] is True + assert results['description'] == 'foo description' + + def test_update_description_idempotent(self, *args): + set_module_args(dict( + name='test-route', + password='admin', + server='localhost', + user='admin', + state='present', + description='asdasd' + )) + + module = AnsibleModule( + argument_spec=self.spec.argument_spec, + mutually_exclusive=self.spec.mutually_exclusive, + supports_check_mode=self.spec.supports_check_mode + ) + mm = ModuleManager(module=module) + + # Override methods to force specific logic in the module to happen + current = ApiParameters(params=load_fixture('load_net_route_description.json')) + mm.exists = Mock(return_value=True) + mm.update_on_device = Mock(return_value=True) + mm.read_current_from_device = Mock(return_value=current) + results = mm.exec_module() + + # There is no assert for the description, because it should + # not have changed + assert results['changed'] is False + + def test_delete(self, *args): + set_module_args(dict( + name='test-route', + password='admin', + server='localhost', + user='admin', + state='absent' + )) + + module = AnsibleModule( + argument_spec=self.spec.argument_spec, + mutually_exclusive=self.spec.mutually_exclusive, + supports_check_mode=self.spec.supports_check_mode + ) + mm = ModuleManager(module=module) + + # 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 + assert 'description' not in results + + def test_invalid_unknown_params(self, *args): + set_module_args(dict( + name='test-route', + password='admin', + server='localhost', + user='admin', + state='present', + foo="bar" + )) + with patch('ansible.module_utils.f5_utils.AnsibleModule.fail_json') as mo: + mo.return_value = True + AnsibleModule( + argument_spec=self.spec.argument_spec, + mutually_exclusive=self.spec.mutually_exclusive, + supports_check_mode=self.spec.supports_check_mode + ) + assert mo.call_count == 1 + + def test_create_with_route_domain(self, *args): + set_module_args(dict( + name='test-route', + password='admin', + server='localhost', + user='admin', + state='present', + destination='10.10.10.10', + netmask='255.255.255.255', + route_domain=1, + reject='yes' + )) + + module = AnsibleModule( + argument_spec=self.spec.argument_spec, + mutually_exclusive=self.spec.mutually_exclusive, + supports_check_mode=self.spec.supports_check_mode + ) + mm = ModuleManager(module=module) + + # Override methods to force specific logic in the module to happen + mm.exists = Mock(return_value=False) + mm.create_on_device = Mock(return_value=True) + + results = mm.exec_module() + assert results['changed'] is True + assert results['route_domain'] == 1 + assert results['destination'] == '10.10.10.10%1/32'