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
pull/60266/head
Chris Van Heuveln 5 years ago committed by Nilashish Chakraborty
parent 687f57d6ca
commit 3ebc96e5c7

@ -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'
},

@ -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)
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
cmds = []
name = want['name']
obj_in_have = search_obj_in_list(want['name'], have, 'name')
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)
return commands
commands.extend(self.generate_afi_commands(diff['ipv6']))
self.cmd_order_fixup(commands, name)
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 '
return commands
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 <name>' 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)

@ -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:

@ -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

@ -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),
]

@ -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'] }

@ -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

@ -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
- block:
- name: setup2
cli_config:
config: |
interface {{ test_int1 }}
no switchport
- name: setup2 cleanup all L3 states on all interfaces
nxos_l3_interfaces:
state: deleted
- block:
- 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

@ -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

@ -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

@ -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,188 @@ 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)
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_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)
@ -124,13 +305,276 @@ 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)
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)
# 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_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)

Loading…
Cancel
Save