diff --git a/lib/ansible/modules/network/f5/bigip_node.py b/lib/ansible/modules/network/f5/bigip_node.py index 6a333123983..bdc47242cb8 100644 --- a/lib/ansible/modules/network/f5/bigip_node.py +++ b/lib/ansible/modules/network/f5/bigip_node.py @@ -87,6 +87,51 @@ options: aliases: - hostname version_added: 2.5 + fqdn_address_type: + description: + - Specifies whether the FQDN of the node resolves to an IPv4 or IPv6 address. + - When creating a new node, if this parameter is not specified and C(fqdn) is + specified, this parameter will default to C(ipv4). + - This parameter cannot be changed after it has been set. + choices: + - ipv4 + - ipv6 + - all + version_added: 2.6 + fqdn_auto_populate: + description: + - Specifies whether the system automatically creates ephemeral nodes using + the IP addresses returned by the resolution of a DNS query for a node defined + by an FQDN. + - When C(yes), the system generates an ephemeral node for each IP address + returned in response to a DNS query for the FQDN of the node. Additionally, + when a DNS response indicates the IP address of an ephemeral node no longer + exists, the system deletes the ephemeral node. + - When C(no), the system resolves a DNS query for the FQDN of the node with the + single IP address associated with the FQDN. + - When creating a new node, if this parameter is not specified and C(fqdn) is + specified, this parameter will default to C(yes). + - This parameter cannot be changed after it has been set. + type: bool + version_added: 2.6 + fqdn_up_interval: + description: + - Specifies the interval in which a query occurs, when the DNS server is up. + The associated monitor attempts to probe three times, and marks the server + down if it there is no response within the span of three times the interval + value, in seconds. + - This parameter accepts a value of C(ttl) to query based off of the TTL of + the FQDN. The default TTL interval is akin to specifying C(3600). + - When creating a new node, if this parameter is not specified and C(fqdn) is + specified, this parameter will default to C(3600). + version_added: 2.6 + fqdn_down_interval: + description: + - Specifies the interval in which a query occurs, when the DNS server is down. + The associated monitor continues polling as long as the DNS server is down. + - When creating a new node, if this parameter is not specified and C(fqdn) is + specified, this parameter will default to C(5). + version_added: 2.6 description: description: - Specifies descriptive text that identifies the node. @@ -217,6 +262,8 @@ import time from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.basic import env_fallback +from ansible.module_utils.parsing.convert_bool import BOOLEANS_FALSE +from ansible.module_utils.parsing.convert_bool import BOOLEANS_TRUE try: from library.module_utils.network.f5.bigip import HAS_F5SDK @@ -271,11 +318,15 @@ class Parameters(AnsibleF5Parameters): ] returnables = [ - 'monitor_type', 'quorum', 'monitors', 'description', 'fqdn', 'session', 'state' + 'monitor_type', 'quorum', 'monitors', 'description', 'fqdn', 'session', 'state', + 'fqdn_auto_populate', 'fqdn_address_type', 'fqdn_up_interval', + 'fqdn_down_interval', 'fqdn_name' ] updatables = [ - 'monitor_type', 'quorum', 'monitors', 'description', 'state' + 'monitor_type', 'quorum', 'monitors', 'description', 'state', + 'fqdn_up_interval', 'fqdn_down_interval', 'tmName', 'fqdn_auto_populate', + 'fqdn_address_type' ] def to_return(self): @@ -348,21 +399,102 @@ class Parameters(AnsibleF5Parameters): return None return self._values['monitor_type'] + +class Changes(Parameters): + pass + + +class UsableChanges(Changes): + @property + def fqdn(self): + result = dict() + if self._values['fqdn_up_interval'] is not None: + result['interval'] = self._values['fqdn_up_interval'] + if self._values['fqdn_down_interval'] is not None: + result['downInterval'] = self._values['fqdn_down_interval'] + if self._values['fqdn_auto_populate'] is not None: + result['autopopulate'] = self._values['fqdn_auto_populate'] + if self._values['fqdn_name'] is not None: + result['tmName'] = self._values['fqdn_name'] + return result + + +class ReportableChanges(Changes): + pass + + +class ModuleParameters(Parameters): + @property + def fqdn_up_interval(self): + if self._values['fqdn_up_interval'] is None: + return None + return str(self._values['fqdn_up_interval']) + + @property + def fqdn_down_interval(self): + if self._values['fqdn_down_interval'] is None: + return None + return str(self._values['fqdn_down_interval']) + + @property + def fqdn_auto_populate(self): + auto_populate = self._values.get('fqdn_auto_populate', None) + if auto_populate in BOOLEANS_TRUE: + return 'enabled' + elif auto_populate in BOOLEANS_FALSE: + return 'disabled' + + @property + def fqdn_name(self): + return self._values.get('fqdn', None) + @property def fqdn(self): if self._values['fqdn'] is None: return None result = dict( - addressFamily='ipv4', - autopopulate='disabled', - downInterval=3600, - tmName=self._values['fqdn'] + addressFamily=self._values.get('fqdn_address_type', None), + downInterval=self._values.get('fqdn_down_interval', None), + interval=self._values.get('fqdn_up_interval', None), + autopopulate=None, + tmName=self._values.get('fqdn', None) ) + auto_populate = self._values.get('fqdn_auto_populate', None) + if auto_populate in BOOLEANS_TRUE: + result['autopopulate'] = 'enabled' + elif auto_populate in BOOLEANS_FALSE: + result['autopopulate'] = 'disabled' return result -class Changes(Parameters): - pass +class ApiParameters(Parameters): + @property + def fqdn_up_interval(self): + if self._values['fqdn'] is None: + return None + if 'interval' in self._values['fqdn']: + return str(self._values['fqdn']['interval']) + + @property + def fqdn_down_interval(self): + if self._values['fqdn'] is None: + return None + if 'downInterval' in self._values['fqdn']: + return str(self._values['fqdn']['downInterval']) + + @property + def fqdn_address_type(self): + if self._values['fqdn'] is None: + return None + if 'addressFamily' in self._values['fqdn']: + return str(self._values['fqdn']['addressFamily']) + + @property + def fqdn_auto_populate(self): + if self._values['fqdn'] is None: + return None + if 'autopopulate' in self._values['fqdn']: + return str(self._values['fqdn']['autopopulate']) class Difference(object): @@ -454,14 +586,36 @@ class Difference(object): ) return result + @property + def fqdn_auto_populate(self): + if self.want.fqdn_auto_populate is None: + return None + if self.want.fqdn_auto_populate != self.have.fqdn_auto_populate: + raise F5ModuleError( + "The 'fqdn_auto_populate' parameter cannot be changed." + ) + + @property + def fqdn_address_type(self): + if self.want.fqdn_address_type is None: + return None + if self.want.fqdn_address_type != self.have.fqdn_address_type: + raise F5ModuleError( + "The 'fqdn_address_type' parameter cannot be changed." + ) + + @property + def fqdn(self): + return None + 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 = Parameters(params=self.module.params) - self.changes = Changes() + self.want = ModuleParameters(params=self.module.params) + self.changes = UsableChanges() def _set_changed_options(self): changed = {} @@ -469,7 +623,7 @@ class ModuleManager(object): if getattr(self.want, key) is not None: changed[key] = getattr(self.want, key) if changed: - self.changes = Changes(params=changed) + self.changes = UsableChanges(params=changed) def _update_changed_options(self): diff = Difference(self.want, self.have) @@ -485,7 +639,7 @@ class ModuleManager(object): else: changed[k] = change if changed: - self.changes = Changes(params=changed) + self.changes = UsableChanges(params=changed) return True return False @@ -573,9 +727,20 @@ class ModuleManager(object): def create(self): self._check_required_creation_vars() self._munge_creation_state_for_device() + + if self.want.fqdn_auto_populate is None: + self.want.update({'fqdn_auto_populate': True}) + if self.want.fqdn_address_type is None: + self.want.update({'fqdn_address_type': 'ipv4'}) + if self.want.fqdn_up_interval is None: + self.want.update({'fqdn_up_interval': 3600}) + if self.want.fqdn_down_interval is None: + self.want.update({'fqdn_down_interval': 5}) + self._set_changed_options() if self.module.check_mode: return True + self.create_on_device() if not self.exists(): raise F5ModuleError("Failed to create the node") @@ -597,6 +762,7 @@ class ModuleManager(object): return False if self.module.check_mode: return True + self.update_on_device() if self.want.state == 'offline': self.update_node_offline_on_device() @@ -621,7 +787,7 @@ class ModuleManager(object): partition=self.want.partition ) result = resource.attrs - return Parameters(params=result) + return ApiParameters(params=result) def exists(self): result = self.client.api.tm.ltm.nodes.node.exists( @@ -701,7 +867,13 @@ class ArgumentSpec(object): partition=dict( default='Common', fallback=(env_fallback, ['F5_PARTITION']) - ) + ), + fqdn_address_type=dict( + choices=['ipv4', 'ipv6', 'all'] + ), + fqdn_auto_populate=dict(type='bool'), + fqdn_up_interval=dict(), + fqdn_down_interval=dict(type='int') ) self.argument_spec = {} self.argument_spec.update(f5_argument_spec) diff --git a/test/units/modules/network/f5/fixtures/load_ltm_node_2.json b/test/units/modules/network/f5/fixtures/load_ltm_node_2.json new file mode 100644 index 00000000000..f23f6b5f42f --- /dev/null +++ b/test/units/modules/network/f5/fixtures/load_ltm_node_2.json @@ -0,0 +1,26 @@ +{ + "kind": "tm:ltm:node:nodestate", + "name": "fqdn-foo", + "partition": "Common", + "fullPath": "/Common/fqdn-foo", + "generation": 161, + "selfLink": "https://localhost/mgmt/tm/ltm/node/~Common~fqdn-foo?ver=13.0.0", + "address": "any6", + "connectionLimit": 0, + "description": "another node but with fqdn", + "dynamicRatio": 1, + "ephemeral": "false", + "fqdn": { + "addressFamily": "ipv4", + "autopopulate": "disabled", + "downInterval": 5, + "interval": "3600", + "tmName": "google.com" + }, + "logging": "disabled", + "monitor": "/Common/icmp and /Common/tcp_echo ", + "rateLimit": "disabled", + "ratio": 1, + "session": "user-enabled", + "state": "fqdn-up" +} diff --git a/test/units/modules/network/f5/test_bigip_node.py b/test/units/modules/network/f5/test_bigip_node.py index 0729e315261..3793e52825a 100644 --- a/test/units/modules/network/f5/test_bigip_node.py +++ b/test/units/modules/network/f5/test_bigip_node.py @@ -21,6 +21,8 @@ from ansible.module_utils.basic import AnsibleModule try: from library.modules.bigip_node import Parameters + from library.modules.bigip_node import ModuleParameters + from library.modules.bigip_node import ApiParameters from library.modules.bigip_node import ModuleManager from library.modules.bigip_node import ArgumentSpec from library.module_utils.network.f5.common import F5ModuleError @@ -29,6 +31,8 @@ try: except ImportError: try: from ansible.modules.network.f5.bigip_node import Parameters + from ansible.modules.network.f5.bigip_node import ModuleParameters + from ansible.modules.network.f5.bigip_node import ApiParameters from ansible.modules.network.f5.bigip_node import ModuleManager from ansible.modules.network.f5.bigip_node import ArgumentSpec from ansible.module_utils.network.f5.common import F5ModuleError @@ -139,3 +143,94 @@ class TestManager(unittest.TestCase): results = mm.exec_module() assert results['changed'] is False + + def test_create_node_fqdn(self, *args): + set_module_args(dict( + fqdn='foo.bar', + name='mytestserver', + monitors=[ + '/Common/icmp' + ], + partition='Common', + state='present', + password='passsword', + server='localhost', + user='admin' + )) + + module = AnsibleModule( + argument_spec=self.spec.argument_spec, + 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=[False, True]) + mm.create_on_device = Mock(return_value=True) + + results = mm.exec_module() + + assert results['changed'] is True + + def test_update_node_fqdn_up_interval(self, *args): + set_module_args(dict( + fqdn='foo.bar', + fqdn_up_interval=100, + name='mytestserver', + monitors=[ + '/Common/icmp' + ], + partition='Common', + state='present', + password='passsword', + server='localhost', + user='admin' + )) + + current = ApiParameters(params=load_fixture('load_ltm_node_2.json')) + module = AnsibleModule( + argument_spec=self.spec.argument_spec, + 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, 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 + + def test_update_node_fqdn_up_interval_idempotent(self, *args): + set_module_args(dict( + fqdn='google.com', + fqdn_up_interval=3600, + name='fqdn-foo', + monitors=[ + 'icmp', + 'tcp_echo' + ], + partition='Common', + state='present', + password='passsword', + server='localhost', + user='admin' + )) + + current = ApiParameters(params=load_fixture('load_ltm_node_2.json')) + module = AnsibleModule( + argument_spec=self.spec.argument_spec, + 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, 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 not True