Skip to content

Commit 547192b

Browse files
authored
It dcnm interface (#497)
* templates for IT dcnm_interface * Refactor DCNM SVI and VPC test cases for improved readability and validation - Updated SVI test cases to use `test_data_common` for fabric and switch information. - Enhanced assertions to provide clearer success and failure messages. - Added validity checks after interface creation, merge, override, and replace operations to ensure state consistency in NDFC. - Removed commented-out assertions and replaced them with structured checks. - Improved idempotence checks to allow for empty responses and clarified assertion conditions. - Added entry point debug messages for VPC tests to indicate test execution context. * Integration test changes dcnm_intf * IT: dcnm_intf * added fabric for svi intf in IT dcnm_intf * fixed sanity errors * automated ip to sn mapping
1 parent 6b1d05f commit 547192b

File tree

171 files changed

+9337
-7261
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

171 files changed

+9337
-7261
lines changed

.gitignore

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,4 @@ venv.bak/
7979
.mypy_cache/
8080

8181
# Ignore Integration Tests Files Directories
82-
tests/integration/targets/ndfc_interface/files
83-
tests/integration/targets/dcnm_network/files
84-
tests/integration/targets/dcnm_inventory/files
82+
tests/integration/targets/*?/files

playbooks/roles/dcnm_interface/dcnm_hosts.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ all:
66
ansible_httpapi_validate_certs: False
77
ansible_httpapi_use_ssl: True
88
children:
9-
ndfc:
9+
dcnm:
1010
vars:
1111
ansible_connection: ansible.netcommon.httpapi
1212
ansible_network_os: cisco.dcnm.dcnm
1313
hosts:
1414
nac-ndfc1:
15-
ansible_host: 10.0.55.128
15+
ansible_host: 1.1.1.1

playbooks/roles/dcnm_interface/dcnm_tests.yaml

Lines changed: 40 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -6,41 +6,50 @@
66
#
77
# Modify the vars section with details for your testing setup.
88
#
9-
- hosts: ndfc
9+
- hosts: dcnm
1010
gather_facts: no
1111
connection: ansible.netcommon.httpapi
1212

1313
vars:
1414
# Uncomment testcase to run a specific test
15-
testcase: dcnm_vpc*
16-
ansible_it_fabric: test_fabric
17-
ansible_switch1: 192.168.1.13
18-
ansible_switch2: 192.168.1.14
19-
ansible_eth_intf2: Ethernet1/2
20-
ansible_eth_intf3: Ethernet1/3
21-
ansible_eth_intf4: Ethernet1/4
22-
ansible_eth_intf5: Ethernet1/5
23-
ansible_eth_intf6: Ethernet1/6
24-
ansible_eth_intf7: Ethernet1/7
25-
ansible_eth_intf8: Ethernet1/8
26-
ansible_eth_intf9: Ethernet1/9
27-
ansible_eth_intf10: Ethernet1/10
28-
ansible_eth_intf11: Ethernet1/11
29-
ansible_eth_intf12: Ethernet1/12
30-
ansible_eth_intf13: Ethernet1/13
31-
ansible_eth_intf14: Ethernet1/14
32-
ansible_eth_intf15: Ethernet1/15
33-
ansible_eth_intf16: Ethernet1/16
34-
ansible_eth_intf17: Ethernet1/17
35-
ansible_eth_intf18: Ethernet1/18
36-
ansible_eth_intf19: Ethernet1/19
37-
ansible_eth_intf20: Ethernet1/20
38-
ansible_eth_intf21: Ethernet1/21
39-
ansible_eth_intf22: Ethernet1/22
40-
ansible_eth_intf23: Ethernet1/23
41-
ansible_eth_intf24: Ethernet1/24
42-
ansible_parent_intf1: Ethernet1/25
43-
ansible_sub_intf1: Ethernet1/25.100
44-
15+
# testcase: dcnm_intf_sani*
16+
# IT_CONTEXT: true
17+
test_data_common:
18+
fabric: fabric_name
19+
# svi_fabric: svi_fabric_name # Define this for dcnm_svi_* tests
20+
deploy: false
21+
check_deploy: false
22+
switch1: 192.168.10.203
23+
switch2: 192.168.10.204
24+
# sw_sn mapping will be automatically populated from switch inventory
25+
eth_intf2: Ethernet1/2
26+
eth_intf3: Ethernet1/3
27+
eth_intf4: Ethernet1/4
28+
eth_intf5: Ethernet1/5
29+
eth_intf6: Ethernet1/6
30+
eth_intf7: Ethernet1/7
31+
eth_intf8: Ethernet1/8
32+
eth_intf9: Ethernet1/9
33+
eth_intf10: Ethernet1/10
34+
eth_intf11: Ethernet1/11
35+
eth_intf12: Ethernet1/12
36+
eth_intf13: Ethernet1/13
37+
eth_intf14: Ethernet1/14
38+
eth_intf15: Ethernet1/15
39+
eth_intf16: Ethernet1/16
40+
eth_intf17: Ethernet1/17
41+
eth_intf18: Ethernet1/18
42+
eth_intf19: Ethernet1/19
43+
eth_intf20: Ethernet1/20
44+
eth_intf21: Ethernet1/21
45+
eth_intf22: Ethernet1/22
46+
eth_intf23: Ethernet1/23
47+
eth_intf24: Ethernet1/24
48+
parent_intf1: Ethernet1/25
49+
sub_intf1: Ethernet1/25.100
50+
parent_intf2: Ethernet1/26
51+
sub_intf2: Ethernet1/26.100
52+
parent_intf3: Ethernet1/27
53+
sub_intf3: Ethernet1/27.100
4554
roles:
4655
- dcnm_interface
Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
from __future__ import absolute_import, division, print_function
2+
3+
4+
__metaclass__ = type
5+
6+
from pprint import pprint
7+
import json
8+
from ansible.utils.display import Display
9+
from ansible.plugins.action import ActionBase
10+
from ansible.module_utils.six import raise_from
11+
from ansible.errors import AnsibleError
12+
from ansible.module_utils.common.text.converters import to_native
13+
from ..plugin_utils.tools import load_yaml_file, process_deepdiff
14+
from ..plugin_utils.pydantic_schemas.dcnm_interface.schemas import DcnmInterfaceQuerySchema
15+
16+
try:
17+
from deepdiff import DeepDiff
18+
except ImportError as imp_exc:
19+
DEEPDIFF_IMPORT_ERROR = imp_exc
20+
else:
21+
DEEPDIFF_IMPORT_ERROR = None
22+
23+
if DEEPDIFF_IMPORT_ERROR:
24+
raise_from(
25+
AnsibleError('DeepDiff must be installed to use this plugin. Use pip or install test-requirements.'),
26+
DEEPDIFF_IMPORT_ERROR)
27+
28+
display = Display()
29+
30+
31+
class ActionModule(ActionBase):
32+
33+
def run(self, tmp=None, task_vars=None):
34+
results = super(ActionModule, self).run(tmp, task_vars)
35+
results['failed'] = False
36+
37+
ndfc_data = self._task.args.get('ndfc_data', None)
38+
test_data = self._task.args.get('test_data', None)
39+
config_path = self._task.args.get('config_path', None)
40+
check_deleted = self._task.args.get('check_deleted', False)
41+
ignore_fields = list(self._task.args.get('ignore_fields', []))
42+
43+
for input_item in [ndfc_data, test_data, config_path]:
44+
if input_item is None:
45+
results['failed'] = True
46+
results['msg'] = f"Required input parameter not found: '{input_item}'"
47+
return results
48+
49+
# Get switch mapping from test_data.sw_sn
50+
switch_ip_sn_mapping = test_data.get('sw_sn', {})
51+
52+
# removes ansible embeddings and converts to native python types
53+
native_ndfc_data = json.loads(json.dumps(ndfc_data, default=to_native))
54+
55+
test_fabric = test_data['fabric']
56+
57+
expected_data_parsed = None
58+
if config_path != "":
59+
# only parse if config file exists
60+
expected_config_data = load_yaml_file(config_path)
61+
expected_data = DcnmInterfaceQuerySchema.yaml_config_to_dict(expected_config_data, test_fabric, switch_ip_sn_mapping)
62+
expected_data_parsed = DcnmInterfaceQuerySchema.model_validate(expected_data).model_dump(exclude_none=True)
63+
64+
# Parse NDFC data through schema to normalize it (case conversion, tuple handling, etc.)
65+
ndfc_data_parsed = DcnmInterfaceQuerySchema.model_validate(native_ndfc_data).model_dump(exclude_none=True)
66+
67+
if deleted_results := self.verify_deleted(results, check_deleted, expected_data_parsed, ndfc_data_parsed, config_path):
68+
return deleted_results
69+
70+
validity = DeepDiff(
71+
expected_data_parsed,
72+
ndfc_data_parsed,
73+
ignore_order=True,
74+
cutoff_distance_for_pairs=0,
75+
cutoff_intersection_for_pairs=0,
76+
report_repetition=True
77+
)
78+
79+
# Process the output of deepdiff to make it easier to read
80+
# Effects the iterable_item_added and iterable_item_removed to remove unneeded fields
81+
# ignore_extra_fields=True will ignore dictionary_item_added changes
82+
# This is useful when the actual data has more fields than the expected data
83+
# keys_to_ignore is a list of fields to ignore, useful for auto provisioned fields which are not known
84+
processed_validity = process_deepdiff(validity, keys_to_ignore=ignore_fields, ignore_extra_fields=True)
85+
if processed_validity == {}:
86+
results['failed'] = False
87+
results['msg'] = f'Data is valid. \n\n Expected data: \n\n{expected_data_parsed}\n\nActual data: \n\n{ndfc_data_parsed}'
88+
else:
89+
results['failed'] = True
90+
print("\n\nExpected: ")
91+
pprint(expected_data_parsed)
92+
print("\n\nActual: ")
93+
pprint(ndfc_data_parsed)
94+
print("\n\nDifferences: ")
95+
pprint(processed_validity)
96+
results['msg'] = 'Data is not valid.'
97+
98+
return results
99+
100+
def _normalize_interface_id_fields(self, expected_data, ndfc_data):
101+
"""
102+
Normalize PO_ID and INTF_NAME fields in expected data based on interface type and what's present in NDFC data.
103+
For port-channel interfaces, use PO_ID. For other interfaces, use INTF_NAME.
104+
Also handle special cases like V6IP vs IPv6 for loopback interfaces.
105+
Remove the incorrect field from expected data if it's not present in the actual NDFC data.
106+
"""
107+
for expected_policy in expected_data.get("response", []):
108+
for expected_interface in expected_policy.get("interfaces", []):
109+
expected_nvpairs = expected_interface.get("nvPairs", {})
110+
111+
# Find corresponding interface in NDFC data
112+
ndfc_interface = self._find_matching_interface(expected_interface, ndfc_data)
113+
if not ndfc_interface:
114+
continue
115+
116+
ndfc_nvpairs = ndfc_interface.get("nvPairs", {})
117+
118+
# Determine if this is a port-channel or loopback interface
119+
ifname = expected_interface.get("ifName", "").lower()
120+
is_port_channel = ifname.startswith("port-channel")
121+
is_loopback = ifname.startswith("loopback")
122+
123+
if is_port_channel:
124+
# For port-channel interfaces, use PO_ID
125+
if "PO_ID" not in ndfc_nvpairs and "PO_ID" in expected_nvpairs:
126+
del expected_nvpairs["PO_ID"]
127+
display.vvv(f"Removed PO_ID from expected data for port-channel interface {ifname}")
128+
129+
# Remove INTF_NAME if present (shouldn't be used for port-channel)
130+
if "INTF_NAME" in expected_nvpairs:
131+
del expected_nvpairs["INTF_NAME"]
132+
display.vvv(f"Removed INTF_NAME from expected data for port-channel interface {ifname}")
133+
else:
134+
# For other interfaces, use INTF_NAME
135+
if "INTF_NAME" not in ndfc_nvpairs and "INTF_NAME" in expected_nvpairs:
136+
del expected_nvpairs["INTF_NAME"]
137+
display.vvv(f"Removed INTF_NAME from expected data for interface {ifname}")
138+
139+
# Remove PO_ID if present (shouldn't be used for non-port-channel)
140+
if "PO_ID" in expected_nvpairs:
141+
del expected_nvpairs["PO_ID"]
142+
display.vvv(f"Removed PO_ID from expected data for non-port-channel interface {ifname}")
143+
144+
# Handle special IPv6 field mapping for loopback interfaces
145+
if is_loopback:
146+
# For loopback interfaces, NDFC uses V6IP instead of IPv6
147+
if "IPv6" in expected_nvpairs and "V6IP" in ndfc_nvpairs:
148+
# Move IPv6 value to V6IP in expected data
149+
expected_nvpairs["V6IP"] = expected_nvpairs["IPv6"]
150+
del expected_nvpairs["IPv6"]
151+
display.vvv(f"Moved IPv6 to V6IP for loopback interface {ifname}")
152+
elif "IPv6" in expected_nvpairs and "V6IP" not in ndfc_nvpairs:
153+
# Remove IPv6 if NDFC doesn't have V6IP
154+
del expected_nvpairs["IPv6"]
155+
display.vvv(f"Removed IPv6 from expected data for loopback interface {ifname}")
156+
157+
# For loopback interfaces, NDFC uses ROUTE_MAP_TAG instead of ROUTING_TAG
158+
if "ROUTING_TAG" in expected_nvpairs and "ROUTE_MAP_TAG" in ndfc_nvpairs:
159+
# Move ROUTING_TAG value to ROUTE_MAP_TAG in expected data
160+
expected_nvpairs["ROUTE_MAP_TAG"] = expected_nvpairs["ROUTING_TAG"]
161+
del expected_nvpairs["ROUTING_TAG"]
162+
display.vvv(f"Moved ROUTING_TAG to ROUTE_MAP_TAG for loopback interface {ifname}")
163+
elif "ROUTING_TAG" in expected_nvpairs and "ROUTE_MAP_TAG" not in ndfc_nvpairs:
164+
# Remove ROUTING_TAG if NDFC doesn't have ROUTE_MAP_TAG
165+
del expected_nvpairs["ROUTING_TAG"]
166+
display.vvv(f"Removed ROUTING_TAG from expected data for loopback interface {ifname}")
167+
else:
168+
# For non-loopback interfaces, remove V6IP if present
169+
if "V6IP" in expected_nvpairs:
170+
del expected_nvpairs["V6IP"]
171+
display.vvv(f"Removed V6IP from expected data for non-loopback interface {ifname}")
172+
173+
# For non-loopback interfaces, remove ROUTE_MAP_TAG if present
174+
if "ROUTE_MAP_TAG" in expected_nvpairs:
175+
del expected_nvpairs["ROUTE_MAP_TAG"]
176+
display.vvv(f"Removed ROUTE_MAP_TAG from expected data for non-loopback interface {ifname}")
177+
178+
def _find_matching_interface(self, expected_interface, ndfc_data):
179+
"""
180+
Find the matching interface in NDFC data based on ifName and serialNumber.
181+
"""
182+
expected_ifname = expected_interface.get("ifName", "").lower()
183+
expected_serial = expected_interface.get("serialNumber", "")
184+
185+
for ndfc_policy in ndfc_data.get("response", []):
186+
for ndfc_interface in ndfc_policy.get("interfaces", []):
187+
ndfc_ifname = ndfc_interface.get("ifName", "").lower()
188+
ndfc_serial = ndfc_interface.get("serialNumber", "")
189+
190+
if expected_ifname == ndfc_ifname and expected_serial == ndfc_serial:
191+
return ndfc_interface
192+
193+
return None
194+
195+
def verify_deleted(self, results, check_deleted, expected_data, ndfc_data, config_path):
196+
if not check_deleted:
197+
return None
198+
199+
existing_interfaces = set()
200+
for policy in ndfc_data["response"]:
201+
for interface in policy["interfaces"]:
202+
existing_interfaces.add((interface["serialNumber"], interface["ifName"]))
203+
204+
if config_path == "":
205+
# check for full delete
206+
if not ndfc_data["failed"] and len(existing_interfaces) == 0:
207+
results['msg'] = 'All interfaces are deleted'
208+
else:
209+
print("Interfaces still existing: ")
210+
print(existing_interfaces)
211+
results['failed'] = True
212+
results['msg'] = 'Error: Expected full delete as config_path is empty but interfaces still exist.'
213+
if ndfc_data["failed"]:
214+
results['msg'] += '\n\nError: ' + ndfc_data["error"]
215+
return results
216+
return results
217+
218+
# checks for a partial delete
219+
deleted_interfaces = set()
220+
for policy in expected_data["response"]:
221+
for interface in policy["interfaces"]:
222+
deleted_interfaces.add((interface["serialNumber"], interface["ifName"]))
223+
224+
remaining_interfaces = existing_interfaces.intersection(deleted_interfaces)
225+
if len(remaining_interfaces) > 0:
226+
results['failed'] = True
227+
print("Expected interfaces to be deleted: ")
228+
print(deleted_interfaces)
229+
print("\nInterfaces present in NDFC: ")
230+
print(existing_interfaces)
231+
print("\nInterfaces still not deleted: ")
232+
print(remaining_interfaces)
233+
results['msg'] = 'All interfaces are not deleted'
234+
return results
235+
236+
print("Expected interfaces to be deleted: ")
237+
print(deleted_interfaces)
238+
print("\n\nInterfaces present in NDFC: ")
239+
print(existing_interfaces)
240+
print("Interfaces still not deleted: ")
241+
print(remaining_interfaces)
242+
results['failed'] = False
243+
results['msg'] = 'Provided interfaces are deleted'
244+
return results

plugins/action/tests/plugin_utils/pydantic_schemas/dcnm_interface/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)