From 1a57daf9b0269bae1bcf6b75b07c7c757e05f38a Mon Sep 17 00:00:00 2001 From: Olivier BLIN Date: Wed, 27 Mar 2019 15:52:42 +0100 Subject: [PATCH] Add support for multiple IPv6 addresses in nxos_l3_interface module (#50892) * Add support for multiple IPv6 addresses in nxos_l3_interface module Cisco support multiple IPv6 addresses on each interface but only the first IPv6 is considered by this module. There is no impact on the configuration but the module is not idempotent. * Add internal support for IPv6 list * Fix module idempotency * Initialize tests for nxos_l3_interface * Fix IPv4 removal idempotency * Fix data extraction from nxos config * Fix silently ignored interfaces in nxos_l3_interface * Add warning when interface does not exist in nxos config --- .../modules/network/nxos/nxos_l3_interface.py | 26 +- .../nxos_l3_interface/ethernet_noshut | 3 + .../ethernet_noshut_ipv4_ipv6 | 5 + .../ethernet_noshut_multiple_ipv6 | 7 + .../network/nxos/test_nxos_l3_interface.py | 226 ++++++++++++++++++ 5 files changed, 255 insertions(+), 12 deletions(-) create mode 100644 test/units/modules/network/nxos/fixtures/nxos_l3_interface/ethernet_noshut create mode 100644 test/units/modules/network/nxos/fixtures/nxos_l3_interface/ethernet_noshut_ipv4_ipv6 create mode 100644 test/units/modules/network/nxos/fixtures/nxos_l3_interface/ethernet_noshut_multiple_ipv6 create mode 100644 test/units/modules/network/nxos/test_nxos_l3_interface.py diff --git a/lib/ansible/modules/network/nxos/nxos_l3_interface.py b/lib/ansible/modules/network/nxos/nxos_l3_interface.py index 8d1d1cb1f40..715c96dd69b 100644 --- a/lib/ansible/modules/network/nxos/nxos_l3_interface.py +++ b/lib/ansible/modules/network/nxos/nxos_l3_interface.py @@ -99,7 +99,7 @@ def search_obj_in_list(name, lst): return o -def map_obj_to_commands(updates, module): +def map_obj_to_commands(updates, module, warnings): commands = list() want, have = updates @@ -112,24 +112,26 @@ def map_obj_to_commands(updates, module): obj_in_have = search_obj_in_list(name, have) - if state == 'absent' and obj_in_have: + if not obj_in_have: + warnings.append('Unknown interface {0}'.format(name)) + elif state == 'absent': command = [] if obj_in_have['name'] == name: - if ipv4 and obj_in_have['ipv4']: + if ipv4 and ipv4 == obj_in_have['ipv4']: command.append('no ip address {0}'.format(ipv4)) - if ipv6 and obj_in_have['ipv6']: + if ipv6 and ipv6 in obj_in_have['ipv6']: command.append('no ipv6 address {0}'.format(ipv6)) if command: command.append('exit') command.insert(0, 'interface {0}'.format(name)) commands.extend(command) - elif state == 'present' and obj_in_have: + elif state == 'present': command = [] if obj_in_have['name'] == name: if ipv4 and ipv4 != obj_in_have['ipv4']: command.append('ip address {0}'.format(ipv4)) - if ipv6 and ipv6 != obj_in_have['ipv6']: + if ipv6 and ipv6 not in obj_in_have['ipv6']: command.append('ipv6 address {0}'.format(ipv6)) if command: command.append('exit') @@ -174,7 +176,7 @@ def map_config_to_obj(want, module): for w in want: parents = ['interface {0}'.format(w['name'])] config = netcfg.get_section(parents) - obj = dict(name=None, ipv4=None, ipv6=None) + obj = dict(name=None, ipv4=None, ipv6=[]) if config: match_name = re.findall(r'interface (\S+)', config, re.M) @@ -187,9 +189,9 @@ def map_config_to_obj(want, module): match_ipv6 = re.findall(r'ipv6 address (\S+)', config, re.M) if match_ipv6: - obj['ipv6'] = match_ipv6[0] + obj['ipv6'] = match_ipv6 - objs.append(obj) + objs.append(obj) return objs @@ -224,15 +226,15 @@ def main(): warnings = list() result = {'changed': False} - if warnings: - result['warnings'] = warnings want = map_params_to_obj(module) have = map_config_to_obj(want, module) - commands = map_obj_to_commands((want, have), module) + commands = map_obj_to_commands((want, have), module, warnings) result['commands'] = commands + if warnings: + result['warnings'] = warnings if commands: if not module.check_mode: load_config(module, commands) diff --git a/test/units/modules/network/nxos/fixtures/nxos_l3_interface/ethernet_noshut b/test/units/modules/network/nxos/fixtures/nxos_l3_interface/ethernet_noshut new file mode 100644 index 00000000000..b0407edc491 --- /dev/null +++ b/test/units/modules/network/nxos/fixtures/nxos_l3_interface/ethernet_noshut @@ -0,0 +1,3 @@ +interface Ethernet1/1 + description Configured by Ansible + no shutdown diff --git a/test/units/modules/network/nxos/fixtures/nxos_l3_interface/ethernet_noshut_ipv4_ipv6 b/test/units/modules/network/nxos/fixtures/nxos_l3_interface/ethernet_noshut_ipv4_ipv6 new file mode 100644 index 00000000000..3e4f197876f --- /dev/null +++ b/test/units/modules/network/nxos/fixtures/nxos_l3_interface/ethernet_noshut_ipv4_ipv6 @@ -0,0 +1,5 @@ +interface Ethernet1/1 + description Configured by Ansible + ip address 192.168.0.1/24 + ipv6 address 2001:db8::1/124 + no shutdown diff --git a/test/units/modules/network/nxos/fixtures/nxos_l3_interface/ethernet_noshut_multiple_ipv6 b/test/units/modules/network/nxos/fixtures/nxos_l3_interface/ethernet_noshut_multiple_ipv6 new file mode 100644 index 00000000000..9013b1348e1 --- /dev/null +++ b/test/units/modules/network/nxos/fixtures/nxos_l3_interface/ethernet_noshut_multiple_ipv6 @@ -0,0 +1,7 @@ +interface Ethernet1/1 + description Configured by Ansible + ip address 192.168.0.1/24 + ipv6 address 2001:db8:1::1/124 + ipv6 address 2001:db8:2::1/124 + ipv6 address 2001:db8::1/124 + no shutdown diff --git a/test/units/modules/network/nxos/test_nxos_l3_interface.py b/test/units/modules/network/nxos/test_nxos_l3_interface.py new file mode 100644 index 00000000000..0257793768c --- /dev/null +++ b/test/units/modules/network/nxos/test_nxos_l3_interface.py @@ -0,0 +1,226 @@ +# Copyright: (c) 2019, Olivier Blin +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +from units.compat.mock import patch +from ansible.modules.network.nxos import nxos_l3_interface +from .nxos_module import TestNxosModule, load_fixture, set_module_args + + +class TestNxosL3InterfaceModule(TestNxosModule): + + module = nxos_l3_interface + + def setUp(self): + super(TestNxosL3InterfaceModule, self).setUp() + + self.mock_load_config = patch('ansible.modules.network.nxos.nxos_l3_interface.load_config') + self.load_config = self.mock_load_config.start() + + self.mock_get_config = patch('ansible.modules.network.nxos.nxos_l3_interface.get_config') + self.get_config = self.mock_get_config.start() + + def tearDown(self): + super(TestNxosL3InterfaceModule, self).tearDown() + self.mock_load_config.stop() + self.mock_get_config.stop() + + def load_fixtures(self, commands=None, device=''): + self.load_config.return_value = None + self.get_config.return_value = load_fixture('nxos_l3_interface', self.mode) + + def test_nxos_l3_interface_unknonw_ethernet(self): + self.mode = 'ethernet_noshut' + set_module_args(dict(name='Ethernet1/2', ipv4='192.168.0.1/24')) + result = self.execute_module(changed=False) + + # Add when missing + def test_nxos_l3_interface_add_missing_ipv4(self): + self.mode = 'ethernet_noshut' + set_module_args(dict(name='Ethernet1/1', ipv4='192.168.0.1/24')) + result = self.execute_module(changed=True) + self.assertEqual(result['commands'], ['interface Ethernet1/1', 'ip address 192.168.0.1/24', 'exit']) + + def test_nxos_l3_interface_add_missing_ipv4_on_e11(self): + self.mode = 'ethernet_noshut' + set_module_args(dict(name='et1/1', ipv4='192.168.0.1/24')) + result = self.execute_module(changed=True) + self.assertEqual(result['commands'], ['interface Ethernet1/1', 'ip address 192.168.0.1/24', 'exit']) + + def test_nxos_l3_interface_add_missing_ipv6(self): + self.mode = 'ethernet_noshut' + set_module_args(dict(name='Ethernet1/1', ipv6='2001:db8::1/124')) + result = self.execute_module(changed=True) + self.assertEqual(result['commands'], ['interface Ethernet1/1', 'ipv6 address 2001:db8::1/124', 'exit']) + + def test_nxos_l3_interface_add_missing_ipv4_and_ipv6(self): + self.mode = 'ethernet_noshut' + set_module_args(dict(name='Ethernet1/1', ipv4='192.168.0.1/24', ipv6='2001:db8::1/124')) + result = self.execute_module(changed=True) + self.assertEqual(result['commands'], ['interface Ethernet1/1', 'ip address 192.168.0.1/24', 'ipv6 address 2001:db8::1/124', 'exit']) + + # Add when existing + def test_nxos_l3_interface_add_existing_ipv4(self): + self.mode = 'ethernet_noshut_ipv4_ipv6' + set_module_args(dict(name='Ethernet1/1', ipv4='192.168.0.1/24')) + result = self.execute_module() + + def test_nxos_l3_interface_add_existing_ipv4_on_e11(self): + self.mode = 'ethernet_noshut_ipv4_ipv6' + set_module_args(dict(name='et1/1', ipv4='192.168.0.1/24')) + result = self.execute_module() + + def test_nxos_l3_interface_add_existing_ipv6(self): + self.mode = 'ethernet_noshut_ipv4_ipv6' + set_module_args(dict(name='Ethernet1/1', ipv6='2001:db8::1/124')) + result = self.execute_module() + + def test_nxos_l3_interface_add_existing_ipv4_and_ipv6(self): + self.mode = 'ethernet_noshut_ipv4_ipv6' + set_module_args(dict(name='Ethernet1/1', ipv4='192.168.0.1/24', ipv6='2001:db8::1/124')) + result = self.execute_module() + + def test_nxos_l3_interface_new_ipv4_and_ipv6(self): + self.mode = 'ethernet_noshut_ipv4_ipv6' + set_module_args(dict(name='Ethernet1/1', ipv4='192.168.0.2/24', ipv6='2001:db8::2/124')) + result = self.execute_module(changed=True) + self.assertEqual(result['commands'], ['interface Ethernet1/1', 'ip address 192.168.0.2/24', 'ipv6 address 2001:db8::2/124', 'exit']) + + # Add when existing with multiple IPv6 + def test_nxos_l3_interface_multiple_ipv6_add_first(self): + self.mode = 'ethernet_noshut_multiple_ipv6' + set_module_args(dict(name='Ethernet1/1', ipv6='2001:db8::1/124')) + result = self.execute_module() + + def test_nxos_l3_interface_multiple_ipv6_add_last(self): + self.mode = 'ethernet_noshut_multiple_ipv6' + set_module_args(dict(name='Ethernet1/1', ipv6='2001:db8:2::1/124')) + result = self.execute_module() + + # Add aggregate + def test_nxos_l3_interface_add_missing_with_empty_aggregate(self): + self.mode = 'ethernet_noshut' + set_module_args(dict(aggregate=[])) + result = self.execute_module() + + def test_nxos_l3_interface_add_missing_with_aggregate(self): + self.mode = 'ethernet_noshut' + set_module_args(dict(aggregate=[ + dict(name='Ethernet1/1', ipv4='192.168.0.2/24', ipv6='2001:db8::2/124'), + dict(name='Ethernet1/1', ipv6='2001:db8:1::2/124'), + dict(name='Ethernet1/1', ipv6='2001:db8:2::2/124')])) + result = self.execute_module(changed=True) + self.assertEqual(result['commands'], [ + 'interface Ethernet1/1', 'ip address 192.168.0.2/24', 'ipv6 address 2001:db8::2/124', 'exit', + 'interface Ethernet1/1', 'ipv6 address 2001:db8:1::2/124', 'exit', + 'interface Ethernet1/1', 'ipv6 address 2001:db8:2::2/124', 'exit']) + + # Rem when missing + def test_nxos_l3_interface_rem_missing_ipv4(self): + self.mode = 'ethernet_noshut' + set_module_args(dict(name='Ethernet1/1', ipv4='192.168.0.1/24', state='absent')) + result = self.execute_module() + + def test_nxos_l3_interface_rem_missing_ipv4_on_e11(self): + self.mode = 'ethernet_noshut' + set_module_args(dict(name='et1/1', ipv4='192.168.0.1/24', state='absent')) + result = self.execute_module() + + def test_nxos_l3_interface_rem_missing_ipv6(self): + self.mode = 'ethernet_noshut' + set_module_args(dict(name='Ethernet1/1', ipv6='2001:db8::1/124', state='absent')) + result = self.execute_module() + + def test_nxos_l3_interface_rem_missing_ipv4_and_ipv6(self): + self.mode = 'ethernet_noshut' + set_module_args(dict(name='Ethernet1/1', ipv4='192.168.0.1/24', ipv6='2001:db8::1/124', state='absent')) + result = self.execute_module() + + # Rem when existing + def test_nxos_l3_interface_rem_existing_ipv4(self): + self.mode = 'ethernet_noshut_ipv4_ipv6' + set_module_args(dict(name='Ethernet1/1', ipv4='192.168.0.1/24', state='absent')) + result = self.execute_module(changed=True) + self.assertEqual(result['commands'], ['interface Ethernet1/1', 'no ip address 192.168.0.1/24', 'exit']) + + def test_nxos_l3_interface_rem_existing_ipv4_on_e11(self): + self.mode = 'ethernet_noshut_ipv4_ipv6' + set_module_args(dict(name='et1/1', ipv4='192.168.0.1/24', state='absent')) + result = self.execute_module(changed=True) + self.assertEqual(result['commands'], ['interface Ethernet1/1', 'no ip address 192.168.0.1/24', 'exit']) + + def test_nxos_l3_interface_rem_existing_ipv6(self): + self.mode = 'ethernet_noshut_ipv4_ipv6' + set_module_args(dict(name='Ethernet1/1', ipv6='2001:db8::1/124', state='absent')) + result = self.execute_module(changed=True) + self.assertEqual(result['commands'], ['interface Ethernet1/1', 'no ipv6 address 2001:db8::1/124', 'exit']) + + def test_nxos_l3_interface_rem_existing_ipv4_and_ipv6(self): + self.mode = 'ethernet_noshut_ipv4_ipv6' + set_module_args(dict(name='Ethernet1/1', ipv4='192.168.0.1/24', ipv6='2001:db8::1/124', state='absent')) + result = self.execute_module(changed=True) + self.assertEqual(result['commands'], ['interface Ethernet1/1', 'no ip address 192.168.0.1/24', 'no ipv6 address 2001:db8::1/124', 'exit']) + + # Rem when existing with multiple IPv6 + def test_nxos_l3_interface_multiple_ipv6_rem_first(self): + self.mode = 'ethernet_noshut_multiple_ipv6' + set_module_args(dict(name='Ethernet1/1', ipv6='2001:db8::1/124', state='absent')) + result = self.execute_module(changed=True) + self.assertEqual(result['commands'], ['interface Ethernet1/1', 'no ipv6 address 2001:db8::1/124', 'exit']) + + def test_nxos_l3_interface_multiple_ipv6_rem_last(self): + self.mode = 'ethernet_noshut_multiple_ipv6' + set_module_args(dict(name='Ethernet1/1', ipv6='2001:db8:2::1/124', state='absent')) + result = self.execute_module(changed=True) + self.assertEqual(result['commands'], ['interface Ethernet1/1', 'no ipv6 address 2001:db8:2::1/124', 'exit']) + + # Rem when missing with aggregate + def test_nxos_l3_interface_rem_with_empty_aggregate(self): + self.mode = 'ethernet_noshut_multiple_ipv6' + set_module_args(dict(aggregate=[], state='absent')) + result = self.execute_module() + + def test_nxos_l3_interface_rem_missing_with_aggregate(self): + self.mode = 'ethernet_noshut_multiple_ipv6' + set_module_args(dict(state='absent', aggregate=[ + dict(name='Ethernet1/1', ipv4='192.168.0.2/24', ipv6='2001:db8::2/124'), + dict(name='Ethernet1/1', ipv6='2001:db8:1::2/124'), + dict(name='Ethernet1/1', ipv6='2001:db8:2::2/124')])) + result = self.execute_module() + + # Rem when existing with aggregate + def test_nxos_l3_interface_rem_existing_with_aggregate(self): + self.mode = 'ethernet_noshut_multiple_ipv6' + set_module_args(dict(state='absent', aggregate=[ + dict(name='Ethernet1/1', ipv4='192.168.0.1/24', ipv6='2001:db8::1/124'), + dict(name='Ethernet1/1', ipv6='2001:db8:1::1/124'), + dict(name='Ethernet1/1', ipv6='2001:db8:2::1/124')])) + result = self.execute_module(changed=True) + self.assertEqual(result['commands'], [ + 'interface Ethernet1/1', 'no ip address 192.168.0.1/24', 'no ipv6 address 2001:db8::1/124', 'exit', + 'interface Ethernet1/1', 'no ipv6 address 2001:db8:1::1/124', 'exit', + 'interface Ethernet1/1', 'no ipv6 address 2001:db8:2::1/124', 'exit']) + + # Add itf only + def test_nxos_l3_interface_add_on_itf_only(self): + self.mode = 'ethernet_noshut' + set_module_args(dict(name='Ethernet1/1')) + result = self.execute_module(changed=True) + self.assertEqual(result['commands'], ['interface Ethernet1/1']) + + # Add unknown interface + def test_nxos_l3_interface_add_on_unknown_itf(self): + self.mode = 'ethernet_noshut' + set_module_args(dict(name='Ethernet1/2', ipv4='192.168.0.1/24')) + result = self.execute_module() + self.assertEqual(result['warnings'], ['Unknown interface Ethernet1/2']) + + # Rem unknown interface + def test_nxos_l3_interface_rem_on_unknown_itf(self): + self.mode = 'ethernet_noshut' + set_module_args(dict(name='Ethernet1/2', ipv4='192.168.0.1/24', state='absent')) + result = self.execute_module() + self.assertEqual(result['warnings'], ['Unknown interface Ethernet1/2'])