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
This commit is contained in:
parent
687f57d6ca
commit
3ebc96e5c7
11 changed files with 917 additions and 156 deletions
|
@ -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)
|
||||
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 <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
|
||||
|
||||
- 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
|
||||
|
|
|
@ -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,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)
|
||||
|
|
Loading…
Reference in a new issue