From 3ebc96e5c7c73bbd1abb9a65a5cbd4066f98900d Mon Sep 17 00:00:00 2001 From: Chris Van Heuveln Date: Fri, 10 Jan 2020 06:03:22 -0500 Subject: [PATCH] nxos_l3_interfaces: fix states, add new minor attributes (#64853) * (WIP) nxos_l3_interfaces: fix states, add new minor attributes * sa cleanup * more regression fixes * test_8 for add'l code coverage * Fix regressions to handle mgmt w/o IP * add 'no system default switchport' to regression setups * add err msg to terminal_stderr_re so that cli_config will catch L2 failures * regression test change: /int4/int3/ * Add default rsvd_intf_len for Zuul CI * Fix replaced-with-no-ipaddr and ip redirect issues --- .../argspec/l3_interfaces/l3_interfaces.py | 11 +- .../config/l3_interfaces/l3_interfaces.py | 360 ++++++++++---- .../nxos/facts/l3_interfaces/l3_interfaces.py | 3 + .../network/nxos/nxos_l3_interfaces.py | 25 +- lib/ansible/plugins/terminal/nxos.py | 1 + .../nxos_l3_interfaces/tasks/main.yaml | 16 + .../nxos_l3_interfaces/tests/cli/deleted.yaml | 36 +- .../nxos_l3_interfaces/tests/cli/merged.yaml | 43 +- .../tests/cli/overridden.yaml | 23 +- .../tests/cli/replaced.yaml | 85 +++- .../network/nxos/test_nxos_l3_interfaces.py | 470 +++++++++++++++++- 11 files changed, 917 insertions(+), 156 deletions(-) diff --git a/lib/ansible/module_utils/network/nxos/argspec/l3_interfaces/l3_interfaces.py b/lib/ansible/module_utils/network/nxos/argspec/l3_interfaces/l3_interfaces.py index d8618c1068b..d6f2098bd60 100644 --- a/lib/ansible/module_utils/network/nxos/argspec/l3_interfaces/l3_interfaces.py +++ b/lib/ansible/module_utils/network/nxos/argspec/l3_interfaces/l3_interfaces.py @@ -39,6 +39,9 @@ class L3_interfacesArgs(object): # pylint: disable=R0903 'config': { 'elements': 'dict', 'options': { + 'dot1q': { + 'type': 'int' + }, 'ipv4': { 'elements': 'dict', 'options': { @@ -69,7 +72,13 @@ class L3_interfacesArgs(object): # pylint: disable=R0903 'name': { 'required': True, 'type': 'str' - } + }, + 'redirects': { + 'type': 'bool', + }, + 'unreachables': { + 'type': 'bool', + }, }, 'type': 'list' }, diff --git a/lib/ansible/module_utils/network/nxos/config/l3_interfaces/l3_interfaces.py b/lib/ansible/module_utils/network/nxos/config/l3_interfaces/l3_interfaces.py index f3df173c075..6febab96d7c 100644 --- a/lib/ansible/module_utils/network/nxos/config/l3_interfaces/l3_interfaces.py +++ b/lib/ansible/module_utils/network/nxos/config/l3_interfaces/l3_interfaces.py @@ -14,6 +14,9 @@ created from __future__ import absolute_import, division, print_function __metaclass__ = type +import re + +from copy import deepcopy from ansible.module_utils.network.common.cfg.base import ConfigBase from ansible.module_utils.network.common.utils import to_list, remove_empties from ansible.module_utils.network.nxos.facts.facts import Facts @@ -49,11 +52,16 @@ class L3_interfaces(ConfigBase): """ facts, _warnings = Facts(self._module).get_facts(self.gather_subset, self.gather_network_resources) l3_interfaces_facts = facts['ansible_network_resources'].get('l3_interfaces') - if not l3_interfaces_facts: return [] + + self.platform = self.get_platform_type() return remove_rsvd_interfaces(l3_interfaces_facts) + def get_platform_type(self): + default, _warnings = Facts(self._module).get_facts(legacy_facts_type=['default']) + return default.get('ansible_net_platform', '') + def edit_config(self, commands): return self._connection.edit_config(commands) @@ -100,7 +108,8 @@ class L3_interfaces(ConfigBase): if get_interface_type(w['name']) == 'management': self._module.fail_json(msg="The 'management' interface is not allowed to be managed by this module") want.append(remove_empties(w)) - have = existing_l3_interfaces_facts + have = deepcopy(existing_l3_interfaces_facts) + self.init_check_existing(have) resp = self.set_state(want, have) return to_list(resp) @@ -130,41 +139,74 @@ class L3_interfaces(ConfigBase): commands.extend(self._state_replaced(w, have)) return commands - def _state_replaced(self, w, have): + def _state_replaced(self, want, have): """ The command generator when state is replaced + Scope is limited to interface objects defined in the playbook. :rtype: A list :returns: the commands necessary to migrate the current configuration to the desired configuration """ - commands = [] - merged_commands = self.set_commands(w, have) - replaced_commands = self.del_delta_attribs(w, have) + cmds = [] + name = want['name'] + obj_in_have = search_obj_in_list(want['name'], have, 'name') - if merged_commands: - cmds = set(replaced_commands).intersection(set(merged_commands)) - for cmd in cmds: - merged_commands.remove(cmd) - commands.extend(replaced_commands) - commands.extend(merged_commands) - return commands + have_v4 = obj_in_have.pop('ipv4', []) if obj_in_have else [] + have_v6 = obj_in_have.pop('ipv6', []) if obj_in_have else [] + + # Process lists of dicts separately + v4_cmds = self._v4_cmds(want.pop('ipv4', []), have_v4, state='replaced') + v6_cmds = self._v6_cmds(want.pop('ipv6', []), have_v6, state='replaced') + + # Process remaining attrs + if obj_in_have: + # Find 'want' changes first + diff = self.diff_of_dicts(want, obj_in_have) + rmv = {'name': name} + haves_not_in_want = set(obj_in_have.keys()) - set(want.keys()) - set(diff.keys()) + for i in haves_not_in_want: + rmv[i] = obj_in_have[i] + cmds.extend(self.generate_delete_commands(rmv)) + else: + diff = want + + cmds.extend(self.add_commands(diff, name=name)) + cmds.extend(v4_cmds) + cmds.extend(v6_cmds) + self.cmd_order_fixup(cmds, name) + return cmds def _state_overridden(self, want, have): """ The command generator when state is overridden + Scope includes all interface objects on the device. :rtype: A list :returns: the commands necessary to migrate the current configuration to the desired configuration """ - commands = [] - for h in have: - obj_in_want = search_obj_in_list(h['name'], want, 'name') - if h == obj_in_want: + # overridden behavior is the same as replaced except for scope. + cmds = [] + existing_vlans = [] + for i in have: + obj_in_want = search_obj_in_list(i['name'], want, 'name') + if obj_in_want: + if i != obj_in_want: + v4_cmds = self._v4_cmds(obj_in_want.pop('ipv4', []), i.pop('ipv4', []), state='overridden') + replaced_cmds = self._state_replaced(obj_in_want, [i]) + replaced_cmds.extend(v4_cmds) + self.cmd_order_fixup(replaced_cmds, obj_in_want['name']) + cmds.extend(replaced_cmds) + else: + deleted_cmds = self.generate_delete_commands(i) + self.cmd_order_fixup(deleted_cmds, i['name']) + cmds.extend(deleted_cmds) + + for i in want: + if [item for item in have if i['name'] == item['name']]: continue - commands.extend(self.del_all_attribs(h)) - for w in want: - commands.extend(self.set_commands(w, have)) - return commands + cmds.extend(self.add_commands(i, name=i['name'])) + + return cmds def _state_merged(self, w, have): """ The command generator when state is merged @@ -175,6 +217,115 @@ class L3_interfaces(ConfigBase): """ return self.set_commands(w, have) + def _v4_cmds(self, want, have, state=None): + """Helper method for processing ipv4 changes. + This is needed to handle primary/secondary address changes, which require a specific sequence when changing. + """ + # The ip address cli does not allow removing primary addresses while + # secondaries are present, but it does allow changing a primary to a + # new address as long as the address is not a current secondary. + # Be aware of scenarios where a secondary is taking over + # the role of the primary, which must be changed in sequence. + # In general, primaries/secondaries should change in this order: + # Step 1. Remove secondaries that are being changed or removed + # Step 2. Change the primary if needed + # Step 3. Merge secondaries + + # Normalize inputs (add tag key if not present) + for i in want: + i['tag'] = i.get('tag') + for i in have: + i['tag'] = i.get('tag') + + merged = True if state == 'merged' else False + replaced = True if state == 'replaced' else False + overridden = True if state == 'overridden' else False + + # Create secondary and primary wants/haves + sec_w = [i for i in want if i.get('secondary')] + sec_h = [i for i in have if i.get('secondary')] + pri_w = [i for i in want if not i.get('secondary')] + pri_h = [i for i in have if not i.get('secondary')] + pri_w = pri_w[0] if pri_w else {} + pri_h = pri_h[0] if pri_h else {} + cmds = [] + + # Remove all addrs when no primary is specified in want (pri_w) + if pri_h and not pri_w and (replaced or overridden): + cmds.append('no ip address') + return cmds + + # 1. Determine which secondaries are changing and remove them. Need a have/want + # diff instead of want/have because a have sec addr may be changing to a pri. + sec_to_rmv = [] + sec_diff = self.diff_list_of_dicts(sec_h, sec_w) + for i in sec_diff: + if overridden or [w for w in sec_w if w['address'] == i['address']]: + sec_to_rmv.append(i['address']) + + # Check if new primary is currently a secondary + if pri_w and [h for h in sec_h if h['address'] == pri_w['address']]: + if not overridden: + sec_to_rmv.append(pri_w['address']) + + # Remove the changing secondaries + cmds.extend(['no ip address %s secondary' % i for i in sec_to_rmv]) + + # 2. change primary + if pri_w: + diff = dict(set(pri_w.items()) - set(pri_h.items())) + if diff: + cmd = 'ip address %s' % diff['address'] + tag = diff.get('tag') + cmd += ' tag %s' % tag if tag else '' + cmds.append(cmd) + + # 3. process remaining secondaries last + sec_w_to_chg = self.diff_list_of_dicts(sec_w, sec_h) + for i in sec_w_to_chg: + cmd = 'ip address %s secondary' % i['address'] + cmd += ' tag %s' % i['tag'] if i['tag'] else '' + cmds.append(cmd) + + return cmds + + def _v6_cmds(self, want, have, state=''): + """Helper method for processing ipv6 changes. + This is needed to avoid unnecessary churn on the device when removing or changing multiple addresses. + """ + # Normalize inputs (add tag key if not present) + for i in want: + i['tag'] = i.get('tag') + for i in have: + i['tag'] = i.get('tag') + + cmds = [] + # items to remove (items in 'have' only) + if state == 'replaced': + for i in self.diff_list_of_dicts(have, want): + want_addr = [w for w in want if w['address'] == i['address']] + if not want_addr: + cmds.append('no ipv6 address %s' % i['address']) + elif i['tag'] and not want_addr[0]['tag']: + # Must remove entire cli when removing tag + cmds.append('no ipv6 address %s' % i['address']) + + # items to merge/add + for i in self.diff_list_of_dicts(want, have): + addr = i['address'] + tag = i['tag'] + if not tag and state == 'merged': + # When want is IP-no-tag and have is IP+tag it will show up in diff, + # but for merged nothing has changed, so ignore it for idempotence. + have_addr = [h for h in have if h['address'] == addr] + if have_addr and have_addr[0].get('tag'): + continue + cmd = 'ipv6 address %s' % i['address'] + cmd += ' tag %s' % tag if tag else '' + cmds.append(cmd) + + return cmds + def _state_deleted(self, want, have): """ The command generator when state is deleted @@ -199,38 +350,57 @@ class L3_interfaces(ConfigBase): if not obj or len(obj.keys()) == 1: return commands commands = self.generate_delete_commands(obj) - if commands: - commands.insert(0, 'interface ' + obj['name']) - return commands - - def del_delta_attribs(self, w, have): - commands = [] - obj_in_have = search_obj_in_list(w['name'], have, 'name') - if obj_in_have: - lst_to_del = [] - ipv4_intersect = self.intersect_list_of_dicts(w.get('ipv4'), obj_in_have.get('ipv4')) - ipv6_intersect = self.intersect_list_of_dicts(w.get('ipv6'), obj_in_have.get('ipv6')) - if ipv4_intersect: - lst_to_del.append({'ipv4': ipv4_intersect}) - if ipv6_intersect: - lst_to_del.append({'ipv6': ipv6_intersect}) - if lst_to_del: - for item in lst_to_del: - commands.extend(self.generate_delete_commands(item)) - else: - commands.extend(self.generate_delete_commands(obj_in_have)) - if commands: - commands.insert(0, 'interface ' + obj_in_have['name']) + self.cmd_order_fixup(commands, obj['name']) return commands def generate_delete_commands(self, obj): + """Generate CLI commands to remove non-default settings. + obj: dict of attrs to remove + """ commands = [] + name = obj.get('name') + if 'dot1q' in obj: + commands.append('no encapsulation dot1q') + if 'redirects' in obj: + if not self.check_existing(name, 'has_secondary') or re.match('N[3567]', self.platform): + # device auto-enables redirects when secondaries are removed; + # auto-enable may fail on legacy platforms so always do explicit enable + commands.append('ip redirects') + if 'unreachables' in obj: + commands.append('no ip unreachables') if 'ipv4' in obj: commands.append('no ip address') if 'ipv6' in obj: commands.append('no ipv6 address') return commands + def init_check_existing(self, have): + """Creates a class var dict for easier access to existing states + """ + self.existing_facts = dict() + have_copy = deepcopy(have) + for intf in have_copy: + name = intf['name'] + self.existing_facts[name] = intf + # Check for presence of secondaries; used for ip redirects logic + if [i for i in intf.get('ipv4', []) if i.get('secondary')]: + self.existing_facts[name]['has_secondary'] = True + + def check_existing(self, name, query): + """Helper method to lookup existing states on an interface. + This is needed for attribute changes that have additional dependencies; + e.g. 'ip redirects' may auto-enable when all secondary ip addrs are removed. + """ + if name: + have = self.existing_facts.get(name, {}) + if 'has_secondary' in query: + return have.get('has_secondary', False) + if 'redirects' in query: + return have.get('redirects', True) + if 'unreachables' in query: + return have.get('unreachables', False) + return None + def diff_of_dicts(self, w, obj): diff = set(w.items()) - set(obj.items()) diff = dict(diff) @@ -247,66 +417,72 @@ class L3_interfaces(ConfigBase): diff.append(dict((x, y) for x, y in element)) return diff - def intersect_list_of_dicts(self, w, h): - intersect = [] - waddr = [] - haddr = [] - set_w = set() - set_h = set() - if w: - for d in w: - waddr.append({'address': d['address']}) - set_w = set(tuple(sorted(d.items())) for d in waddr) if waddr else set() - if h: - for d in h: - haddr.append({'address': d['address']}) - set_h = set(tuple(sorted(d.items())) for d in haddr) if haddr else set() - intersection = set_w.intersection(set_h) - for element in intersection: - intersect.append(dict((x, y) for x, y in element)) - return intersect - - def add_commands(self, diff, name): + def add_commands(self, diff, name=''): commands = [] if not diff: return commands - + if 'dot1q' in diff: + commands.append('encapsulation dot1q ' + str(diff['dot1q'])) + if 'redirects' in diff: + # Note: device will auto-disable redirects when secondaries are present + if diff['redirects'] != self.check_existing(name, 'redirects'): + no_cmd = 'no ' if diff['redirects'] is False else '' + commands.append(no_cmd + 'ip redirects') + self.cmd_order_fixup(commands, name) + if 'unreachables' in diff: + if diff['unreachables'] != self.check_existing(name, 'unreachables'): + no_cmd = 'no ' if diff['unreachables'] is False else '' + commands.append(no_cmd + 'ip unreachables') if 'ipv4' in diff: - commands.extend(self.generate_commands(diff['ipv4'], flag='ipv4')) + commands.extend(self.generate_afi_commands(diff['ipv4'])) if 'ipv6' in diff: - commands.extend(self.generate_commands(diff['ipv6'], flag='ipv6')) - if commands: - commands.insert(0, 'interface ' + name) + commands.extend(self.generate_afi_commands(diff['ipv6'])) + self.cmd_order_fixup(commands, name) + return commands - def generate_commands(self, d, flag=None): - commands = [] - - for i in d: - cmd = '' - if flag == 'ipv4': - cmd = 'ip address ' - elif flag == 'ipv6': - cmd = 'ipv6 address ' - + def generate_afi_commands(self, diff): + cmds = [] + for i in diff: + cmd = 'ipv6 address ' if re.search('::', i['address']) else 'ip address ' cmd += i['address'] - if 'secondary' in i and i['secondary'] is True: - cmd += ' ' + 'secondary' - if 'tag' in i: - cmd += ' ' + 'tag ' + str(i['tag']) - elif 'tag' in i: - cmd += ' ' + 'tag ' + str(i['tag']) - commands.append(cmd) - return commands + if i.get('secondary'): + cmd += ' secondary' + if i.get('tag'): + cmd += ' tag ' + str(i['tag']) + cmds.append(cmd) + return cmds def set_commands(self, w, have): commands = [] - obj_in_have = search_obj_in_list(w['name'], have, 'name') + name = w['name'] + obj_in_have = search_obj_in_list(name, have, 'name') if not obj_in_have: - commands = self.add_commands(w, w['name']) + commands = self.add_commands(w, name=name) else: - diff = {} - diff.update({'ipv4': self.diff_list_of_dicts(w.get('ipv4'), obj_in_have.get('ipv4'))}) - diff.update({'ipv6': self.diff_list_of_dicts(w.get('ipv6'), obj_in_have.get('ipv6'))}) - commands = self.add_commands(diff, w['name']) + # lists of dicts must be processed separately from non-list attrs + v4_cmds = self._v4_cmds(w.pop('ipv4', []), obj_in_have.pop('ipv4', []), state='merged') + v6_cmds = self._v6_cmds(w.pop('ipv6', []), obj_in_have.pop('ipv6', []), state='merged') + + # diff remaining attrs + diff = self.diff_of_dicts(w, obj_in_have) + commands = self.add_commands(diff, name=name) + commands.extend(v4_cmds) + commands.extend(v6_cmds) + + self.cmd_order_fixup(commands, name) return commands + + def cmd_order_fixup(self, cmds, name): + """Inserts 'interface ' config at the beginning of populated command list; reorders dependent commands that must process after others. + """ + if cmds: + if name and not [item for item in cmds if item.startswith('interface')]: + cmds.insert(0, 'interface ' + name) + + redirects = [item for item in cmds if re.match('(no )*ip redirects', item)] + if redirects: + # redirects should occur after ipv4 commands, just move to end of list + redirects = redirects.pop() + cmds.remove(redirects) + cmds.append(redirects) diff --git a/lib/ansible/module_utils/network/nxos/facts/l3_interfaces/l3_interfaces.py b/lib/ansible/module_utils/network/nxos/facts/l3_interfaces/l3_interfaces.py index f318b9f49ca..c5be39a4dcb 100644 --- a/lib/ansible/module_utils/network/nxos/facts/l3_interfaces/l3_interfaces.py +++ b/lib/ansible/module_utils/network/nxos/facts/l3_interfaces/l3_interfaces.py @@ -83,6 +83,9 @@ class L3_interfacesFacts(object): if get_interface_type(intf) == 'unknown': return {} config['name'] = intf + config['dot1q'] = utils.parse_conf_arg(conf, 'encapsulation dot1[qQ]') + config['redirects'] = utils.parse_conf_cmd_arg(conf, 'no ip redirects', False, True) + config['unreachables'] = utils.parse_conf_cmd_arg(conf, 'ip unreachables', True, False) ipv4_match = re.compile(r'\n ip address (.*)') matches = ipv4_match.findall(conf) if matches: diff --git a/lib/ansible/modules/network/nxos/nxos_l3_interfaces.py b/lib/ansible/modules/network/nxos/nxos_l3_interfaces.py index 7d1d3f6af59..0c4ddc35cc1 100644 --- a/lib/ansible/modules/network/nxos/nxos_l3_interfaces.py +++ b/lib/ansible/modules/network/nxos/nxos_l3_interfaces.py @@ -53,6 +53,11 @@ options: - Full name of L3 interface, i.e. Ethernet1/1. type: str required: true + dot1q: + description: + - Configures IEEE 802.1Q VLAN encapsulation on a subinterface. + type: int + version_added: 2.10 ipv4: description: - IPv4 address and attributes of the L3 interface. @@ -86,6 +91,16 @@ options: description: - URIB route tag value for local/direct routes. type: int + redirects: + description: + - Enables/disables ip redirects + type: bool + version_added: 2.10 + unreachables: + description: + - Enables/disables ip redirects + type: bool + version_added: 2.10 state: description: @@ -119,6 +134,10 @@ EXAMPLES = """ ipv6: - address: fd5d:12c9:2201:2::1/64 tag: 6 + - name: Ethernet1/7.42 + dot1q: 42 + redirects: False + unreachables: False state: merged # After state: @@ -127,8 +146,12 @@ EXAMPLES = """ # interface Ethernet1/6 # ip address 192.168.22.1/24 tag 5 # ip address 10.1.1.1/24 secondary tag 10 -# interfaqce Ethernet1/6 +# interface Ethernet1/6 # ipv6 address fd5d:12c9:2201:2::1/64 tag 6 +# interface Ethernet1/7.42 +# encapsulation dot1q 42 +# no ip redirects +# no ip unreachables # Using replaced diff --git a/lib/ansible/plugins/terminal/nxos.py b/lib/ansible/plugins/terminal/nxos.py index dbeab1f3ffc..a832f94264a 100644 --- a/lib/ansible/plugins/terminal/nxos.py +++ b/lib/ansible/plugins/terminal/nxos.py @@ -48,6 +48,7 @@ class TerminalModule(TerminalBase): re.compile(br"unknown command"), re.compile(br"user not present"), re.compile(br"invalid (.+?)at '\^' marker", re.I), + re.compile(br"configuration not allowed .+ at '\^' marker"), re.compile(br"[B|b]aud rate of console should be.* (\d*) to increase [a-z]* level", re.I), ] diff --git a/test/integration/targets/nxos_l3_interfaces/tasks/main.yaml b/test/integration/targets/nxos_l3_interfaces/tasks/main.yaml index 415c99d8b12..44edf825814 100644 --- a/test/integration/targets/nxos_l3_interfaces/tasks/main.yaml +++ b/test/integration/targets/nxos_l3_interfaces/tasks/main.yaml @@ -1,2 +1,18 @@ --- +# The interface-count asserts need to also account for mgmt0 which is a reserved +# interface; i.e. it will be included in L3 facts when it has non-default values +# but excluded from result.before/after because it's not allowed to be managed. +- set_fact: + # Zuul CI skips prepare_nxos but will have dhcp configured on mgmt0 + rsvd_intf_len: 1 + +- block: + - set_fact: + mgmt: + "{{ intdataraw|selectattr('interface', 'equalto', 'mgmt0')|list}}" + - set_fact: + rsvd_intf_len: + "{{ 1 if (mgmt and 'ip_addr' in mgmt[0]) else 0}}" + when: prepare_nxos_tests_task | default(True) | bool + - { include: cli.yaml, tags: ['cli'] } diff --git a/test/integration/targets/nxos_l3_interfaces/tests/cli/deleted.yaml b/test/integration/targets/nxos_l3_interfaces/tests/cli/deleted.yaml index 1f721dd3ec7..51bf75350b9 100644 --- a/test/integration/targets/nxos_l3_interfaces/tests/cli/deleted.yaml +++ b/test/integration/targets/nxos_l3_interfaces/tests/cli/deleted.yaml @@ -2,20 +2,32 @@ - debug: msg: "Start nxos_l3_interfaces deleted integration tests connection={{ ansible_connection }}" -- set_fact: test_int1="{{ nxos_int1 }}" +- set_fact: + test_int3: "{{ nxos_int3 }}" + subint3: "{{ nxos_int3 }}.42" - name: setup1 cli_config: &cleanup config: | - default interface {{ test_int1 }} + no system default switchport + default interface {{ test_int3 }} + interface {{ test_int3 }} + no switchport + ignore_errors: yes + +- name: setup2 cleanup all L3 interfaces on device + nxos_l3_interfaces: + state: deleted - block: - - name: setup2 + - name: setup3 cli_config: config: | - interface {{ test_int1 }} - no switchport + interface {{ subint3 }} + encapsulation dot1q 42 ip address 192.168.10.2/24 + no ip redirects + ip unreachables - name: Gather l3_interfaces facts nxos_facts: &facts @@ -31,12 +43,15 @@ - assert: that: - - "result.before|length == (ansible_facts.network_resources.l3_interfaces|length - 1)" + - "result.before|length == (ansible_facts.network_resources.l3_interfaces|length|int - rsvd_intf_len|int)" - "result.after|length == 0" - "result.changed == true" - - "'interface {{ test_int1 }}' in result.commands" + - "'interface {{ subint3 }}' in result.commands" + - "'no encapsulation dot1q' in result.commands" + - "'ip redirects' in result.commands" + - "'no ip unreachables' in result.commands" - "'no ip address' in result.commands" - - "result.commands|length == 2" + - "result.commands|length == 5" - name: Idempotence - deleted nxos_l3_interfaces: *deleted @@ -49,4 +64,7 @@ always: - name: teardown - cli_config: *cleanup + cli_config: + config: | + no interface {{ subint3 }} + ignore_errors: yes diff --git a/test/integration/targets/nxos_l3_interfaces/tests/cli/merged.yaml b/test/integration/targets/nxos_l3_interfaces/tests/cli/merged.yaml index c54f9a9d947..80404ef18ec 100644 --- a/test/integration/targets/nxos_l3_interfaces/tests/cli/merged.yaml +++ b/test/integration/targets/nxos_l3_interfaces/tests/cli/merged.yaml @@ -2,24 +2,31 @@ - debug: msg: "Start nxos_l3_interfaces merged integration tests connection={{ ansible_connection }}" -- set_fact: test_int1="{{ nxos_int1 }}" +- set_fact: + test_int3: "{{ nxos_int3 }}" + subint3: "{{ nxos_int3 }}.42" - name: setup1 cli_config: &cleanup config: | - default interface {{ test_int1 }} + no system default switchport + default interface {{ test_int3 }} + interface {{ test_int3 }} + no switchport + ignore_errors: yes + +- name: setup2 cleanup all L3 states on all interfaces + nxos_l3_interfaces: + state: deleted - block: - - name: setup2 - cli_config: - config: | - interface {{ test_int1 }} - no switchport - - name: Merged nxos_l3_interfaces: &merged config: - - name: "{{ test_int1 }}" + - name: "{{ subint3 }}" + dot1q: 42 + redirects: false + unreachables: true ipv4: - address: 192.168.10.2/24 state: merged @@ -29,9 +36,12 @@ that: - "result.changed == true" - "result.before|length == 0" - - "'interface {{ test_int1 }}' in result.commands" + - "'interface {{ subint3 }}' in result.commands" + - "'encapsulation dot1q 42' in result.commands" + - "'no ip redirects' in result.commands" + - "'ip unreachables' in result.commands" - "'ip address 192.168.10.2/24' in result.commands" - - "result.commands|length == 2" + - "result.commands|length == 5" - name: Gather l3_interfaces facts nxos_facts: @@ -40,13 +50,9 @@ - '!min' gather_network_resources: l3_interfaces - # The nxos_l3_interfaces module should never attempt to modify the mgmt interface ip. - # The module will still collect facts about the interface however so in this case - # the facts will contain all l3 enabled interfaces including mgmt) but the after state in - # result will only contain the modification - assert: that: - - "result.after|length == (ansible_facts.network_resources.l3_interfaces|length - 1)" + - "result.after|length == (ansible_facts.network_resources.l3_interfaces|length|int - rsvd_intf_len|int)" - name: Idempotence - Merged nxos_l3_interfaces: *merged @@ -59,4 +65,7 @@ always: - name: teardown - cli_config: *cleanup + cli_config: + config: | + no interface {{ subint3 }} + ignore_errors: yes diff --git a/test/integration/targets/nxos_l3_interfaces/tests/cli/overridden.yaml b/test/integration/targets/nxos_l3_interfaces/tests/cli/overridden.yaml index e5f66a27877..f3d6e5cf210 100644 --- a/test/integration/targets/nxos_l3_interfaces/tests/cli/overridden.yaml +++ b/test/integration/targets/nxos_l3_interfaces/tests/cli/overridden.yaml @@ -9,22 +9,30 @@ - name: setup1 cli_config: &cleanup config: | + no system default switchport default interface {{ test_int1 }} default interface {{ test_int2 }} default interface {{ test_int3 }} + interface {{ test_int1 }} + no switchport + interface {{ test_int2 }} + no switchport + interface {{ test_int3 }} + no switchport + ignore_errors: yes + +- name: setup2 cleanup all L3 states on all interfaces + nxos_l3_interfaces: + state: deleted - block: - - name: setup2 + - name: setup3 cli_config: config: | interface {{ test_int1 }} - no switchport ip address 192.168.10.2/24 tag 5 interface {{ test_int2 }} - no switchport ip address 10.1.1.1/24 - interface {{ test_int3 }} - no switchport - name: Gather l3_interfaces facts nxos_facts: &facts @@ -44,7 +52,7 @@ - assert: that: - - "result.before|length == (ansible_facts.network_resources.l3_interfaces|length - 1)" + - "result.before|length == (ansible_facts.network_resources.l3_interfaces|length|int - rsvd_intf_len|int)" - "result.changed == true" - "'interface {{ test_int1 }}' in result.commands" - "'no ip address' in result.commands" @@ -59,7 +67,7 @@ - assert: that: - - "result.after|length == (ansible_facts.network_resources.l3_interfaces|length - 1)" + - "result.after|length == (ansible_facts.network_resources.l3_interfaces|length|int - rsvd_intf_len|int)" - name: Idempotence - Overridden nxos_l3_interfaces: *overridden @@ -73,3 +81,4 @@ always: - name: teardown cli_config: *cleanup + ignore_errors: yes diff --git a/test/integration/targets/nxos_l3_interfaces/tests/cli/replaced.yaml b/test/integration/targets/nxos_l3_interfaces/tests/cli/replaced.yaml index ca3d517f5be..2841a7e35ef 100644 --- a/test/integration/targets/nxos_l3_interfaces/tests/cli/replaced.yaml +++ b/test/integration/targets/nxos_l3_interfaces/tests/cli/replaced.yaml @@ -2,20 +2,32 @@ - debug: msg: "Start nxos_l3_interfaces replaced integration tests connection={{ ansible_connection }}" -- set_fact: test_int1="{{ nxos_int1 }}" +- set_fact: + test_int3: "{{ nxos_int3 }}" + subint3: "{{ nxos_int3 }}.42" - name: setup1 cli_config: &cleanup config: | - default interface {{ test_int1 }} + no system default switchport + default interface {{ test_int3 }} + interface {{ test_int3 }} + no switchport + ignore_errors: yes + +- name: setup2 cleanup all L3 states on all interfaces + nxos_l3_interfaces: + state: deleted - block: - - name: setup2 + - name: setup3 cli_config: config: | - interface {{ test_int1 }} - no switchport + interface {{ subint3 }} + encapsulation dot1q 42 ip address 192.168.10.2/24 + no ip redirects + ip unreachables - name: Gather l3_interfaces facts nxos_facts: &facts @@ -27,28 +39,36 @@ - name: Replaced nxos_l3_interfaces: &replaced config: - - name: "{{ test_int1 }}" - ipv6: - - address: "fd5d:12c9:2201:1::1/64" - tag: 6 + - name: "{{ subint3 }}" + dot1q: 442 + # Note: device auto-disables redirects when secondaries are present + redirects: false + unreachables: false + ipv4: + - address: 192.168.20.2/24 + tag: 5 + - address: 192.168.200.2/24 + secondary: True state: replaced register: result - assert: that: - - "result.before|length == (ansible_facts.network_resources.l3_interfaces|length - 1)" + - "result.before|length == (ansible_facts.network_resources.l3_interfaces|length|int - rsvd_intf_len|int)" - "result.changed == true" - - "'interface {{ test_int1 }}' in result.commands" - - "'no ip address' in result.commands" - - "'ipv6 address fd5d:12c9:2201:1::1/64 tag 6' in result.commands" - - "result.commands|length == 3" + - "'interface {{ subint3 }}' in result.commands" + - "'encapsulation dot1q 442' in result.commands" + - "'no ip unreachables' in result.commands" + - "'ip address 192.168.20.2/24 tag 5' in result.commands" + - "'ip address 192.168.200.2/24 secondary' in result.commands" + - "result.commands|length == 5" - name: Gather l3_interfaces post facts nxos_facts: *facts - assert: that: - - "result.after|length == (ansible_facts.network_resources.l3_interfaces|length - 1)" + - "result.after|length == (ansible_facts.network_resources.l3_interfaces|length|int - rsvd_intf_len|int)" - name: Idempotence - Replaced nxos_l3_interfaces: *replaced @@ -59,6 +79,39 @@ - "result.changed == false" - "result.commands|length == 0" + - name: Replaced with no optional attrs specified + nxos_l3_interfaces: &replaced_no_attrs + config: + - name: "{{ subint3 }}" + state: replaced + register: result + + - assert: + that: + - "result.changed == true" + - "'interface {{ subint3 }}' in result.commands" + - "'no encapsulation dot1q' in result.commands" + - "'no ip address' in result.commands" + + - assert: + that: + # 'ip redirects' normally auto-enables due to rmv'ing the secondaries; + # this behavior is unreliable on legacy platforms thus command is explicit. + - "'ip redirects' in result.commands" + when: platform is match('N[3567]') + + - name: Idempotence - Replaced with no attrs specified + nxos_l3_interfaces: *replaced_no_attrs + register: result + + - assert: + that: + - "result.changed == false" + - "result.commands|length == 0" + always: - name: teardown - cli_config: *cleanup + cli_config: + config: | + no interface {{ subint3 }} + ignore_errors: yes diff --git a/test/units/modules/network/nxos/test_nxos_l3_interfaces.py b/test/units/modules/network/nxos/test_nxos_l3_interfaces.py index 02c3109cc31..79ca244ef4d 100644 --- a/test/units/modules/network/nxos/test_nxos_l3_interfaces.py +++ b/test/units/modules/network/nxos/test_nxos_l3_interfaces.py @@ -48,17 +48,22 @@ class TestNxosL3InterfacesModule(TestNxosModule): self.mock_edit_config = patch('ansible.module_utils.network.nxos.config.l3_interfaces.l3_interfaces.L3_interfaces.edit_config') self.edit_config = self.mock_edit_config.start() + self.mock_get_platform_type = patch('ansible.module_utils.network.nxos.config.l3_interfaces.l3_interfaces.L3_interfaces.get_platform_type') + self.get_platform_type = self.mock_get_platform_type.start() + def tearDown(self): super(TestNxosL3InterfacesModule, self).tearDown() self.mock_FACT_LEGACY_SUBSETS.stop() self.mock_get_resource_connection_config.stop() self.mock_get_resource_connection_facts.stop() self.mock_edit_config.stop() + self.mock_get_platform_type.stop() - def load_fixtures(self, commands=None, device=''): + def load_fixtures(self, commands=None, device='N9K'): self.mock_FACT_LEGACY_SUBSETS.return_value = dict() self.get_resource_connection_config.return_value = None self.edit_config.return_value = None + self.get_platform_type.return_value = device # --------------------------- # L3_interfaces Test Cases @@ -85,7 +90,7 @@ class TestNxosL3InterfacesModule(TestNxosModule): self.execute_module({'failed': True, 'msg': "The 'mgmt0' interface is not allowed to be managed by this module"}) def test_2(self): - # Change existing config states + # basic tests existing = dedent('''\ interface mgmt0 ip address 10.0.0.254/24 @@ -109,12 +114,11 @@ class TestNxosL3InterfacesModule(TestNxosModule): merged = ['interface Ethernet1/1', 'ip address 192.168.1.1/24'] deleted = ['interface Ethernet1/1', 'no ip address', 'interface Ethernet1/2', 'no ip address'] - overridden = ['interface Ethernet1/1', 'no ip address', - 'interface Ethernet1/2', 'no ip address', - 'interface Ethernet1/3', 'no ip address', - 'interface Ethernet1/1', 'ip address 192.168.1.1/24'] - replaced = ['interface Ethernet1/1', 'no ip address', 'ip address 192.168.1.1/24', + replaced = ['interface Ethernet1/1', 'ip address 192.168.1.1/24', 'interface Ethernet1/2', 'no ip address'] + overridden = ['interface Ethernet1/1', 'ip address 192.168.1.1/24', + 'interface Ethernet1/2', 'no ip address', + 'interface Ethernet1/3', 'no ip address'] playbook['state'] = 'merged' set_module_args(playbook, ignore_provider_arg) @@ -124,13 +128,453 @@ class TestNxosL3InterfacesModule(TestNxosModule): set_module_args(playbook, ignore_provider_arg) self.execute_module(changed=True, commands=deleted) + playbook['state'] = 'replaced' + set_module_args(playbook, ignore_provider_arg) + self.execute_module(changed=True, commands=replaced) + playbook['state'] = 'overridden' set_module_args(playbook, ignore_provider_arg) self.execute_module(changed=True, commands=overridden) - # TBD: 'REPLACED' BEHAVIOR IS INCORRECT, - # IT IS WRONGLY IGNORING ETHERNET1/2. - # ****************** SKIP TEST FOR NOW ***************** - # playbook['state'] = 'replaced' - # set_module_args(playbook, ignore_provider_arg) - # self.execute_module(changed=True, commands=replaced) + def test_3(self): + # encap testing + existing = dedent('''\ + interface mgmt0 + ip address 10.0.0.254/24 + interface Ethernet1/1.41 + encapsulation dot1q 4100 + ip address 10.1.1.1/24 + interface Ethernet1/1.42 + encapsulation dot1q 42 + interface Ethernet1/1.44 + encapsulation dot1q 44 + interface Ethernet1/1.45 + encapsulation dot1q 45 + ip address 10.5.5.5/24 + ipv6 address 10::5/128 + ''') + self.get_resource_connection_facts.return_value = {self.SHOW_CMD: existing} + playbook = dict(config=[ + dict(name='Ethernet1/1.41', dot1q=41, ipv4=[{'address': '10.2.2.2/24'}]), + dict(name='Ethernet1/1.42', dot1q=42), + dict(name='Ethernet1/1.43', dot1q=43, ipv6=[{'address': '10::2/128'}]), + dict(name='Ethernet1/1.44') + ]) + # Expected result commands for each 'state' + merged = [ + 'interface Ethernet1/1.41', 'encapsulation dot1q 41', 'ip address 10.2.2.2/24', + 'interface Ethernet1/1.43', 'encapsulation dot1q 43', 'ipv6 address 10::2/128', + ] + deleted = [ + 'interface Ethernet1/1.41', 'no encapsulation dot1q', 'no ip address', + 'interface Ethernet1/1.42', 'no encapsulation dot1q', + 'interface Ethernet1/1.44', 'no encapsulation dot1q' + ] + replaced = [ + 'interface Ethernet1/1.41', 'encapsulation dot1q 41', 'ip address 10.2.2.2/24', + # 42 no chg + 'interface Ethernet1/1.43', 'encapsulation dot1q 43', 'ipv6 address 10::2/128', + 'interface Ethernet1/1.44', 'no encapsulation dot1q' + ] + overridden = [ + 'interface Ethernet1/1.41', 'encapsulation dot1q 41', 'ip address 10.2.2.2/24', + # 42 no chg + 'interface Ethernet1/1.44', 'no encapsulation dot1q', + 'interface Ethernet1/1.45', 'no encapsulation dot1q', 'no ip address', 'no ipv6 address', + 'interface Ethernet1/1.43', 'encapsulation dot1q 43', 'ipv6 address 10::2/128' + ] + + playbook['state'] = 'merged' + set_module_args(playbook, ignore_provider_arg) + self.execute_module(changed=True, commands=merged) + + playbook['state'] = 'deleted' + set_module_args(playbook, ignore_provider_arg) + self.execute_module(changed=True, commands=deleted) + + playbook['state'] = 'replaced' + set_module_args(playbook, ignore_provider_arg) + self.execute_module(changed=True, commands=replaced) + + playbook['state'] = 'overridden' + set_module_args(playbook, ignore_provider_arg) + self.execute_module(changed=True, commands=overridden) + + def test_4(self): + # IPv4-centric testing + existing = dedent('''\ + interface mgmt0 + ip address 10.0.0.254/24 + interface Ethernet1/1 + no ip redirects + ip address 10.1.1.1/24 tag 11 + ip address 10.2.2.2/24 secondary tag 12 + ip address 10.3.3.3/24 secondary + ip address 10.4.4.4/24 secondary tag 14 + ip address 10.5.5.5/24 secondary tag 15 + ip address 10.6.6.6/24 secondary tag 16 + interface Ethernet1/2 + ip address 10.12.12.12/24 + interface Ethernet1/3 + ip address 10.13.13.13/24 + interface Ethernet1/5 + no ip redirects + ip address 10.15.15.15/24 + ip address 10.25.25.25/24 secondary + ''') + self.get_resource_connection_facts.return_value = {self.SHOW_CMD: existing} + playbook = dict(config=[ + dict(name='Ethernet1/1', + ipv4=[{'address': '10.1.1.1/24', 'secondary': True}, # prim->sec + {'address': '10.2.2.2/24', 'secondary': True}, # rmv tag + {'address': '10.3.3.3/24', 'tag': 3}, # become prim + {'address': '10.4.4.4/24', 'secondary': True, 'tag': 14}, # no chg + {'address': '10.5.5.5/24', 'secondary': True, 'tag': 55}, # chg tag + {'address': '10.7.7.7/24', 'secondary': True, 'tag': 77}]), # new ip + dict(name='Ethernet1/2'), + dict(name='Ethernet1/4', + ipv4=[{'address': '10.40.40.40/24'}, + {'address': '10.41.41.41/24', 'secondary': True}]), + dict(name='Ethernet1/5'), + ]) + # Expected result commands for each 'state' + merged = [ + 'interface Ethernet1/1', + 'no ip address 10.5.5.5/24 secondary', + 'no ip address 10.2.2.2/24 secondary', + 'no ip address 10.3.3.3/24 secondary', + 'ip address 10.3.3.3/24 tag 3', # Changes primary + 'ip address 10.1.1.1/24 secondary', + 'ip address 10.2.2.2/24 secondary', + 'ip address 10.7.7.7/24 secondary tag 77', + 'ip address 10.5.5.5/24 secondary tag 55', + 'interface Ethernet1/4', + 'ip address 10.40.40.40/24', + 'ip address 10.41.41.41/24 secondary' + ] + deleted = [ + 'interface Ethernet1/1', 'no ip address', + 'interface Ethernet1/2', 'no ip address', + 'interface Ethernet1/5', 'no ip address' + ] + replaced = [ + 'interface Ethernet1/1', + 'no ip address 10.5.5.5/24 secondary', + 'no ip address 10.2.2.2/24 secondary', + 'no ip address 10.3.3.3/24 secondary', + 'ip address 10.3.3.3/24 tag 3', # Changes primary + 'ip address 10.1.1.1/24 secondary', + 'ip address 10.2.2.2/24 secondary', + 'ip address 10.7.7.7/24 secondary tag 77', + 'ip address 10.5.5.5/24 secondary tag 55', + 'interface Ethernet1/2', + 'no ip address', + 'interface Ethernet1/4', + 'ip address 10.40.40.40/24', + 'ip address 10.41.41.41/24 secondary', + 'interface Ethernet1/5', + 'no ip address' + ] + overridden = [ + 'interface Ethernet1/1', + 'no ip address 10.6.6.6/24 secondary', + 'no ip address 10.5.5.5/24 secondary', + 'no ip address 10.2.2.2/24 secondary', + 'no ip address 10.3.3.3/24 secondary', + 'ip address 10.3.3.3/24 tag 3', # Changes primary + 'ip address 10.1.1.1/24 secondary', + 'ip address 10.2.2.2/24 secondary', + 'ip address 10.7.7.7/24 secondary tag 77', + 'ip address 10.5.5.5/24 secondary tag 55', + 'interface Ethernet1/2', + 'no ip address', + 'interface Ethernet1/3', + 'no ip address', + 'interface Ethernet1/4', + 'ip address 10.40.40.40/24', + 'ip address 10.41.41.41/24 secondary', + 'interface Ethernet1/5', + 'no ip address', + ] + + playbook['state'] = 'merged' + set_module_args(playbook, ignore_provider_arg) + self.execute_module(changed=True, commands=merged) + + playbook['state'] = 'deleted' + set_module_args(playbook, ignore_provider_arg) + self.execute_module(changed=True, commands=deleted) + + playbook['state'] = 'replaced' + set_module_args(playbook, ignore_provider_arg) + self.execute_module(changed=True, commands=replaced) + + playbook['state'] = 'overridden' + set_module_args(playbook, ignore_provider_arg) + self.execute_module(changed=True, commands=overridden) + + def test_5(self): + # IPv6-centric testing + existing = dedent('''\ + interface Ethernet1/1 + ipv6 address 10::1/128 + ipv6 address 10::2/128 tag 12 + ipv6 address 10::3/128 tag 13 + ipv6 address 10::4/128 tag 14 + interface Ethernet1/2 + ipv6 address 10::12/128 + interface Ethernet1/3 + ipv6 address 10::13/128 + ''') + self.get_resource_connection_facts.return_value = {self.SHOW_CMD: existing} + playbook = dict(config=[ + dict(name='Ethernet1/1', + ipv6=[{'address': '10::1/128'}, # no chg + {'address': '10::3/128'}, # tag rmv + {'address': '10::4/128', 'tag': 44}, # tag chg + {'address': '10::5/128'}, # new addr + {'address': '10::6/128', 'tag': 66}]), # new addr+tag + dict(name='Ethernet1/2'), + ]) + # Expected result commands for each 'state' + merged = [ + 'interface Ethernet1/1', + 'ipv6 address 10::4/128 tag 44', + 'ipv6 address 10::5/128', + 'ipv6 address 10::6/128 tag 66', + ] + deleted = [ + 'interface Ethernet1/1', 'no ipv6 address', + 'interface Ethernet1/2', 'no ipv6 address', + ] + replaced = [ + 'interface Ethernet1/1', + 'no ipv6 address 10::3/128', + 'no ipv6 address 10::2/128', + 'ipv6 address 10::4/128 tag 44', + 'ipv6 address 10::3/128', + 'ipv6 address 10::5/128', + 'ipv6 address 10::6/128 tag 66', + 'interface Ethernet1/2', + 'no ipv6 address 10::12/128' + ] + overridden = [ + 'interface Ethernet1/1', + 'no ipv6 address 10::3/128', + 'no ipv6 address 10::2/128', + 'ipv6 address 10::4/128 tag 44', + 'ipv6 address 10::3/128', + 'ipv6 address 10::5/128', + 'ipv6 address 10::6/128 tag 66', + 'interface Ethernet1/2', + 'no ipv6 address 10::12/128', + 'interface Ethernet1/3', + 'no ipv6 address' + ] + + playbook['state'] = 'merged' + set_module_args(playbook, ignore_provider_arg) + self.execute_module(changed=True, commands=merged) + + playbook['state'] = 'deleted' + set_module_args(playbook, ignore_provider_arg) + self.execute_module(changed=True, commands=deleted) + # + playbook['state'] = 'replaced' + set_module_args(playbook, ignore_provider_arg) + self.execute_module(changed=True, commands=replaced) + # + playbook['state'] = 'overridden' + set_module_args(playbook, ignore_provider_arg) + self.execute_module(changed=True, commands=overridden) + + def test_6(self): + # misc tests + existing = dedent('''\ + interface Ethernet1/1 + ip address 10.1.1.1/24 + no ip redirects + ip unreachables + interface Ethernet1/2 + interface Ethernet1/3 + interface Ethernet1/4 + interface Ethernet1/5 + no ip redirects + ''') + self.get_resource_connection_facts.return_value = {self.SHOW_CMD: existing} + playbook = dict(config=[ + dict(name='Ethernet1/1', redirects=True, unreachables=False, + ipv4=[{'address': '192.168.1.1/24'}]), + dict(name='Ethernet1/2'), + dict(name='Ethernet1/3', redirects=True, unreachables=False), # defaults + dict(name='Ethernet1/4', redirects=False, unreachables=True), + ]) + merged = [ + 'interface Ethernet1/1', + 'ip redirects', + 'no ip unreachables', + 'ip address 192.168.1.1/24', + 'interface Ethernet1/4', + 'no ip redirects', + 'ip unreachables' + ] + deleted = [ + 'interface Ethernet1/1', + 'ip redirects', + 'no ip unreachables', + 'no ip address' + ] + replaced = [ + 'interface Ethernet1/1', + 'ip redirects', + 'no ip unreachables', + 'ip address 192.168.1.1/24', + 'interface Ethernet1/4', + 'no ip redirects', + 'ip unreachables' + ] + overridden = [ + 'interface Ethernet1/1', + 'ip redirects', + 'no ip unreachables', + 'ip address 192.168.1.1/24', + 'interface Ethernet1/5', + 'ip redirects', + 'interface Ethernet1/4', + 'no ip redirects', + 'ip unreachables' + ] + + playbook['state'] = 'merged' + set_module_args(playbook, ignore_provider_arg) + self.execute_module(changed=True, commands=merged) + + playbook['state'] = 'deleted' + set_module_args(playbook, ignore_provider_arg) + self.execute_module(changed=True, commands=deleted) + + playbook['state'] = 'replaced' + set_module_args(playbook, ignore_provider_arg) + self.execute_module(changed=True, commands=replaced) + + playbook['state'] = 'overridden' + set_module_args(playbook, ignore_provider_arg) + self.execute_module(changed=True, commands=overridden) + + def test_7(self): + # idempotence + existing = dedent('''\ + interface Ethernet1/1 + ip address 10.1.1.1/24 + ip address 10.2.2.2/24 secondary tag 2 + ip address 10.3.3.3/24 secondary tag 3 + ip address 10.4.4.4/24 secondary + ipv6 address 10::1/128 + ipv6 address 10::2/128 tag 2 + no ip redirects + ip unreachables + interface Ethernet1/2 + ''') + self.get_resource_connection_facts.return_value = {self.SHOW_CMD: existing} + playbook = dict(config=[ + dict(name='Ethernet1/1', redirects=False, unreachables=True, + ipv4=[{'address': '10.1.1.1/24'}, + {'address': '10.2.2.2/24', 'secondary': True, 'tag': 2}, + {'address': '10.3.3.3/24', 'secondary': True, 'tag': 3}, + {'address': '10.4.4.4/24', 'secondary': True}], + ipv6=[{'address': '10::1/128'}, + {'address': '10::2/128', 'tag': 2}]), + dict(name='Ethernet1/2') + ]) + playbook['state'] = 'merged' + set_module_args(playbook, ignore_provider_arg) + self.execute_module(changed=False) + + playbook['state'] = 'replaced' + set_module_args(playbook, ignore_provider_arg) + self.execute_module(changed=False) + + playbook['state'] = 'overridden' + set_module_args(playbook, ignore_provider_arg) + self.execute_module(changed=False) + + # Modify output for deleted idempotence test + existing = dedent('''\ + interface Ethernet1/1 + interface Ethernet1/2 + ''') + self.get_resource_connection_facts.return_value = {self.SHOW_CMD: existing} + playbook['state'] = 'deleted' + set_module_args(playbook, ignore_provider_arg) + self.execute_module(changed=False) + + def test_8(self): + # no 'config' key in playbook + existing = dedent('''\ + interface Ethernet1/1 + ip address 10.1.1.1/24 + ''') + self.get_resource_connection_facts.return_value = {self.SHOW_CMD: existing} + playbook = dict() + + for i in ['merged', 'replaced', 'overridden']: + playbook['state'] = i + set_module_args(playbook, ignore_provider_arg) + self.execute_module(failed=True) + + deleted = [ + 'interface Ethernet1/1', + 'no ip address', + ] + playbook['state'] = 'deleted' + set_module_args(playbook, ignore_provider_arg) + self.execute_module(changed=True, commands=deleted) + + def test_9(self): + # Platform specific checks + # 'ip redirects' has platform-specific behaviors + existing = dedent('''\ + interface mgmt0 + ip address 10.0.0.254/24 + interface Ethernet1/3 + ip address 10.13.13.13/24 + interface Ethernet1/5 + no ip redirects + ip address 10.15.15.15/24 + ip address 10.25.25.25/24 secondary + ''') + self.get_resource_connection_facts.return_value = {self.SHOW_CMD: existing} + playbook = dict(config=[ + dict(name='Ethernet1/3'), + dict(name='Ethernet1/5'), + ]) + # Expected result commands for each 'state' + deleted = [ + 'interface Ethernet1/3', 'no ip address', + 'interface Ethernet1/5', 'no ip address', 'ip redirects' + ] + replaced = [ + 'interface Ethernet1/3', 'no ip address', + 'interface Ethernet1/5', 'no ip address', 'ip redirects' + ] + overridden = [ + 'interface Ethernet1/3', 'no ip address', + 'interface Ethernet1/5', 'no ip address', 'ip redirects' + ] + platform = 'N3K' + + playbook['state'] = 'merged' + set_module_args(playbook, ignore_provider_arg) + self.execute_module(changed=False, device=platform) + + playbook['state'] = 'deleted' + set_module_args(playbook, ignore_provider_arg) + self.execute_module(changed=True, commands=deleted, device=platform) + + playbook['state'] = 'replaced' + set_module_args(playbook, ignore_provider_arg) + self.execute_module(changed=True, commands=replaced, device=platform) + + playbook['state'] = 'overridden' + set_module_args(playbook, ignore_provider_arg) + self.execute_module(changed=True, commands=overridden, device=platform)