Skip to content

Commit be99ab4

Browse files
authored
Merge branch 'CiscoDevNet:develop' into dcnm_vrf_raise_attached_nets_error
2 parents bb88644 + 1ee3763 commit be99ab4

Some content is hidden

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

44 files changed

+3074
-1184
lines changed

.github/workflows/main.yml

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -102,12 +102,6 @@ jobs:
102102

103103
- name: Install ansible-base (v${{ matrix.ansible }})
104104
run: pip install https://github.com/ansible/ansible/archive/v${{ matrix.ansible }}.tar.gz --disable-pip-version-check
105-
106-
- name: Install Pydantic (v2)
107-
run: pip install pydantic==2.11.4
108-
109-
- name: Install DeepDiff (v8.5.0)
110-
run: pip install deepdiff==8.5.0
111105

112106
- name: Install coverage (v7.3.4)
113107
run: pip install coverage==7.3.4
@@ -132,4 +126,4 @@ jobs:
132126

133127
- name: Generate coverage report
134128
run: coverage report
135-
working-directory: /home/runner/.ansible/collections/ansible_collections/cisco/dcnm
129+
working-directory: /home/runner/.ansible/collections/ansible_collections/cisco/dcnm

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,4 +80,5 @@ venv.bak/
8080

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

CHANGELOG.rst

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,29 @@ This project adheres to `Semantic Versioning <http://semver.org/>`_.
88

99
.. contents:: ``Release Versions``
1010

11+
`3.8.1`_
12+
=====================
13+
14+
**Release Date:** ``2025-07-02``
15+
16+
Added
17+
-----
18+
19+
- Enhanced integration tests for the `dcnm_network` and `dcnm_interface` modules
20+
- Refactored support for breakout interfaces to fix workflow issues in the dcnm_interface module
21+
22+
Fixed
23+
-----
24+
25+
- https://github.com/CiscoDevNet/ansible-dcnm/issues/80
26+
- https://github.com/CiscoDevNet/ansible-dcnm/issues/149
27+
- https://github.com/CiscoDevNet/ansible-dcnm/issues/216
28+
- https://github.com/CiscoDevNet/ansible-dcnm/issues/239
29+
- https://github.com/CiscoDevNet/ansible-dcnm/issues/279
30+
- https://github.com/CiscoDevNet/ansible-dcnm/issues/407
31+
- https://github.com/CiscoDevNet/ansible-dcnm/issues/428
32+
- https://github.com/CiscoDevNet/ansible-dcnm/issues/436
33+
1134
`3.8.0`_
1235
=====================
1336

@@ -563,6 +586,7 @@ The Ansible Cisco Data Center Network Manager (DCNM) collection includes modules
563586
- cisco.dcnm.dcnm_network - Add and remove Networks from a DCNM managed VXLAN fabric.
564587
- cisco.dcnm.dcnm_interface - DCNM Ansible Module for managing interfaces.
565588

589+
.. _3.8.1: https://github.com/CiscoDevNet/ansible-dcnm/compare/3.8.0...3.8.1
566590
.. _3.8.0: https://github.com/CiscoDevNet/ansible-dcnm/compare/3.7.0...3.8.0
567591
.. _3.7.0: https://github.com/CiscoDevNet/ansible-dcnm/compare/3.6.0...3.7.0
568592
.. _3.6.0: https://github.com/CiscoDevNet/ansible-dcnm/compare/3.5.1...3.6.0

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ You can also include it in a `requirements.yml` file and install it with `ansibl
7171
---
7272
collections:
7373
- name: cisco.dcnm
74-
version: 3.8.0
74+
version: 3.8.1
7575
```
7676
## Using this collection
7777

galaxy.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
namespace: cisco
33
name: dcnm
4-
version: 3.8.0-dev
4+
version: 3.8.1-dev
55
readme: README.md
66
authors:
77
- Shrishail Kariyappanavar <nkshrishail>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
ndfc:
2+
children:
3+
prod:
4+
hosts:
5+
192.168.1.1:
6+
ansible_connection: ansible.netcommon.httpapi
7+
ansible_httpapi_use_ssl: true
8+
ansible_httpapi_validate_certs: false
9+
ansible_python_interpreter: auto_silent
10+
ansible_network_os: cisco.dcnm.dcnm
11+
ansible_user: admin
12+
ansible_password: password
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
- hosts: ndfc
3+
gather_facts: no
4+
connection: ansible.netcommon.httpapi
5+
6+
vars:
7+
ansible_switch1: 192.168.2.4
8+
ansible_switch2: 192.168.2.5
9+
ansible_switch3: 192.168.2.6
10+
ansible_it_fabric: test-fabric
11+
deploy: false
12+
switch_username: admin
13+
switch_password: password
14+
15+
roles:
16+
- dcnm_inventory
17+
tags: always
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
from __future__ import absolute_import, division, print_function
2+
from ansible.utils.display import Display
3+
from ansible.plugins.action import ActionBase
4+
from typing import List, Dict, Optional, Union
5+
from pydantic import BaseModel, model_validator, validator, ValidationError
6+
import re
7+
import json
8+
__metaclass__ = type
9+
10+
display = Display()
11+
12+
13+
class ConfigData(BaseModel):
14+
role: Optional[str] = None
15+
seed_ip: Optional[str] = None
16+
17+
18+
class NDFCData(BaseModel):
19+
ipAddress: Optional[str] = None
20+
switchRoleEnum: Optional[str] = None
21+
switchRole: Optional[str] = None
22+
23+
24+
class InventoryValidate(BaseModel):
25+
config_data: Optional[List[ConfigData]] = None
26+
ndfc_data: Optional[Union[List[NDFCData], str]] = None
27+
ignore_fields: Optional[Dict[str, int]] = None
28+
response: Union[bool, str] = None
29+
30+
@validator('config_data', pre=True)
31+
@classmethod
32+
def parse_config_data(cls, value):
33+
"""
34+
Validates and transforms the config_data input.
35+
Accepts a dictionary or list of dictionaries and converts them to ConfigData objects.
36+
37+
Args:
38+
value: The input data to validate (dict, list, or None)
39+
Returns:
40+
List of ConfigData objects or None
41+
Raises:
42+
ValueError: If the input format is invalid
43+
"""
44+
if isinstance(value, dict):
45+
return [ConfigData.parse_obj(value)]
46+
if isinstance(value, list):
47+
try:
48+
return [ConfigData.parse_obj(item) for item in value]
49+
except ValidationError as e:
50+
raise ValueError(f"Invalid format in Config Data: {e}")
51+
elif value is None:
52+
return None
53+
else:
54+
raise ValueError("Config Data must be a single/list of dictionary, or None.")
55+
56+
@validator('ndfc_data', pre=True)
57+
@classmethod
58+
def parse_ndfc_data(cls, value):
59+
"""
60+
Validates and transforms the ndfc_data input.
61+
Accepts a string (error message) or list of dictionaries and converts to NDFCData objects.
62+
Args:
63+
value: The NDFC response data (str or list)
64+
Returns:
65+
List of NDFCData objects or the original error string
66+
Raises:
67+
ValueError: If the input format is invalid
68+
"""
69+
if isinstance(value, str):
70+
return value
71+
if isinstance(value, list):
72+
try:
73+
return [NDFCData.parse_obj(item) for item in value]
74+
except ValidationError as e:
75+
raise ValueError(f"Invalid format in NDFC Response: {e}")
76+
else:
77+
raise ValueError("NDFC Response must be a list of dictionaries or an error string")
78+
79+
@model_validator(mode='after')
80+
@classmethod
81+
def validate_lists_equality(cls, values):
82+
"""
83+
Validates that the configuration data matches the NDFC response data.
84+
Performs matching based on seed_ip and role, respecting ignore_fields settings.
85+
Args:
86+
values: The model instance after individual field validation
87+
Returns:
88+
"True" if validation is successful, "False" otherwise
89+
"""
90+
config_data = values.config_data
91+
ndfc_data = values.ndfc_data
92+
ignore_fields = values.ignore_fields
93+
response = values.response
94+
95+
if isinstance(ndfc_data, str):
96+
if config_data is None and ndfc_data == "The queried switch is not part of the fabric configured":
97+
values.response = "True"
98+
return values
99+
else:
100+
print(" NDFC Query returned an Invalid Response\n")
101+
return values
102+
103+
missing_ips = []
104+
role_mismatches = {}
105+
ndfc_data_copy = ndfc_data.copy()
106+
matched_indices_two = set()
107+
108+
for config_data_item in config_data:
109+
found_match = False
110+
config_data_item_dict = config_data_item.dict(exclude_none=True)
111+
for i, ndfc_data_item in enumerate(ndfc_data_copy):
112+
ndfc_data_item = ndfc_data_item.dict(include=set(['ipAddress', 'switchRole'] + list(config_data_item_dict.keys())))
113+
if i in matched_indices_two:
114+
continue
115+
116+
seed_ip_match = False
117+
role_match = False
118+
119+
ip_address_two = ndfc_data_item.get('ipAddress')
120+
switch_role_two = ndfc_data_item.get('switchRole')
121+
role_one = config_data_item_dict.get('role')
122+
if switch_role_two is not None:
123+
switch_role_two = re.sub(r'[^a-zA-Z0-9]', '', switch_role_two.lower())
124+
if role_one is not None:
125+
role_one = re.sub(r'[^a-zA-Z0-9]', '', role_one.lower())
126+
seed_ip_one = config_data_item_dict.get('seed_ip')
127+
128+
if ((seed_ip_one is not None and ip_address_two is not None and ip_address_two == seed_ip_one) or (ignore_fields['seed_ip'])):
129+
seed_ip_match = True
130+
131+
if ((role_one is not None and switch_role_two is not None and switch_role_two == role_one) or (ignore_fields['role'])) :
132+
role_match = True
133+
134+
if seed_ip_match and role_match:
135+
matched_indices_two.add(i)
136+
found_match = True
137+
if ignore_fields['seed_ip']:
138+
break
139+
elif ((seed_ip_match and role_one is not None and switch_role_two is not None and switch_role_two != role_one) or (ignore_fields['role'])):
140+
role_mismatches.setdefault((seed_ip_one or ip_address_two), {"expected_role": role_one, "response_role": switch_role_two})
141+
matched_indices_two.add(i) # Consider it a partial match to avoid further matching
142+
found_match = True
143+
if ignore_fields['seed_ip']:
144+
break
145+
146+
if not found_match and config_data_item_dict is not None and config_data_item_dict.get('seed_ip') is not None:
147+
missing_ips.append(config_data_item_dict.get('seed_ip'))
148+
149+
if not missing_ips and not role_mismatches:
150+
values.response = True
151+
else:
152+
print("Invalid Data:\n ")
153+
if not missing_ips:
154+
print(missing_ips)
155+
if not role_mismatches:
156+
print(json.dumps(role_mismatches, indent=2))
157+
return values
158+
159+
160+
class ActionModule(ActionBase):
161+
"""
162+
Ansible action plugin for validating NDFC inventory data.
163+
Compares test data against NDFC response data and validates according to specified mode.
164+
"""
165+
166+
def run(self, tmp=None, task_vars=None):
167+
"""
168+
Execute the action plugin logic.
169+
Args:
170+
tmp: Temporary directory
171+
task_vars: Variables available to the task
172+
Returns:
173+
dict: Results dictionary with success/failure status and appropriate messages
174+
"""
175+
results = super(ActionModule, self).run(tmp, task_vars)
176+
results['failed'] = False
177+
ndfc_data = self._task.args['ndfc_data']
178+
test_data = self._task.args['test_data']
179+
response = False
180+
181+
if 'changed' in self._task.args:
182+
changed = self._task.args['changed']
183+
if not changed:
184+
results['failed'] = True
185+
results['msg'] = 'Changed is "false"'
186+
return results
187+
188+
if len(ndfc_data['response']) == 0:
189+
results['failed'] = True
190+
results['msg'] = 'No response data found'
191+
return results
192+
193+
ignore_fields = {"seed_ip": 0, "role": 0}
194+
195+
if 'mode' in self._task.args:
196+
mode = self._task.args['mode'].lower()
197+
if mode == 'ip':
198+
# In IP mode, we ignore role matching
199+
ignore_fields['role'] = 1
200+
elif mode == 'role':
201+
# In role mode, we ignore IP matching
202+
ignore_fields['seed_ip'] = 1
203+
204+
validation_result = InventoryValidate(config_data=test_data, ndfc_data=ndfc_data['response'], ignore_fields=ignore_fields, response=response)
205+
validation_output = InventoryValidate.model_validate(validation_result)
206+
207+
if validation_output.response:
208+
results['failed'] = False
209+
results['msg'] = 'Validation Successful!'
210+
else:
211+
results['failed'] = True
212+
results['msg'] = 'Validation Failed! Please check output above.'
213+
214+
return results

plugins/module_utils/fabric/fabric_types.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ def _init_fabric_types(self) -> None:
7373
- Value is a list of mandatory parameters for the fabric type
7474
"""
7575
self._fabric_type_to_template_name_map = {}
76+
self._fabric_type_to_template_name_map["BGP"] = "Easy_Fabric_eBGP"
7677
self._fabric_type_to_template_name_map["IPFM"] = "Easy_Fabric_IPFM"
7778
self._fabric_type_to_template_name_map["ISN"] = "External_Fabric"
7879
self._fabric_type_to_template_name_map["LAN_CLASSIC"] = "LAN_Classic"
@@ -82,6 +83,7 @@ def _init_fabric_types(self) -> None:
8283
# Map fabric type to the feature name that must be running
8384
# on the controller to enable the fabric type.
8485
self._fabric_type_to_feature_name_map = {}
86+
self._fabric_type_to_feature_name_map["BGP"] = "vxlan"
8587
self._fabric_type_to_feature_name_map["IPFM"] = "pmn"
8688
self._fabric_type_to_feature_name_map["ISN"] = "vxlan"
8789
self._fabric_type_to_feature_name_map["LAN_CLASSIC"] = "lan"
@@ -115,6 +117,9 @@ def _init_fabric_types(self) -> None:
115117
self._mandatory_parameters_all_fabrics.append("FABRIC_TYPE")
116118

117119
self._mandatory_parameters = {}
120+
self._mandatory_parameters["BGP"] = copy.copy(
121+
self._mandatory_parameters_all_fabrics
122+
)
118123
self._mandatory_parameters["IPFM"] = copy.copy(
119124
self._mandatory_parameters_all_fabrics
120125
)
@@ -127,12 +132,14 @@ def _init_fabric_types(self) -> None:
127132
self._mandatory_parameters["VXLAN_EVPN"] = copy.copy(
128133
self._mandatory_parameters_all_fabrics
129134
)
135+
self._mandatory_parameters["BGP"].append("BGP_AS")
130136
self._mandatory_parameters["ISN"].append("BGP_AS")
131137
self._mandatory_parameters["VXLAN_EVPN"].append("BGP_AS")
132138
self._mandatory_parameters["VXLAN_EVPN_MSD"] = copy.copy(
133139
self._mandatory_parameters_all_fabrics
134140
)
135141

142+
self._mandatory_parameters["BGP"].sort()
136143
self._mandatory_parameters["IPFM"].sort()
137144
self._mandatory_parameters["ISN"].sort()
138145
self._mandatory_parameters["LAN_CLASSIC"].sort()

0 commit comments

Comments
 (0)