Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,96 +19,14 @@
#
# SPDX-License-Identifier: MIT

from ansible_collections.cisco.nac_dc_vxlan.plugins.plugin_utils.helper_functions import restructure_leaf_tor_data


class PreparePlugin:
def __init__(self, **kwargs):
self.kwargs = kwargs
self.keys = []

def _restructure_flat_to_nested(self, switches_list, topology_switches, tor_peers):
"""Transform a flat switches list into the nested structure with TOR
entries under their parent leaf switches.

Each TOR is placed under ALL of its parent leaves that are present in the
attach group. If a parent leaf is not explicitly listed in the attach group,
a synthetic leaf entry is created automatically from topology data.

Args:
switches_list: List of switch dicts from network_attach_group
topology_switches: List of switch dicts from vxlan.topology.switches
tor_peers: List of tor_peers dicts from vxlan.topology.tor_peers

Returns:
Restructured switches list with TOR entries nested under parent leaves
"""
# Build set of TOR hostnames from topology
tor_hostnames = {
sw['name'] for sw in topology_switches if sw.get("role") == "tor"
}

# Build TOR -> parent leaves mapping from tor_peers
# Each TOR maps to a list of its parent leaf hostnames
tor_to_parents = {}
for peer in tor_peers:
parent_leaves = []
if peer.get("parent_leaf1"):
parent_leaves.append(peer['parent_leaf1'])
if peer.get("parent_leaf2"):
parent_leaves.append(peer['parent_leaf2'])

for key in ("tor1", "tor2"):
tor_name = peer.get(key)
if tor_name:
tor_to_parents[tor_name] = parent_leaves

# Build topology hostname -> switch data mapping for creating synthetic entries
topology_by_name = {sw['name']: sw for sw in topology_switches}

# Separate leaf entries and TOR entries
leaf_entries = []
tor_entries = []

for sw in switches_list:
hostname = sw.get("hostname", "")
if hostname in tor_hostnames:
tor_entries.append(sw)
else:
leaf_entries.append(sw)

# Initialize empty 'tors' list for each leaf entry
for leaf in leaf_entries:
if "tors" not in leaf:
leaf['tors'] = []

# Build hostname -> leaf entry mapping for quick lookup
leaf_by_hostname = {leaf['hostname']: leaf for leaf in leaf_entries}

# Collect all required parent leaf hostnames from TOR entries
# and create synthetic leaf entries for any that are not in the attach group
for tor_entry in tor_entries:
tor_hostname = tor_entry['hostname']
parent_leaves = tor_to_parents.get(tor_hostname, [])
for parent_hostname in parent_leaves:
if parent_hostname not in leaf_by_hostname:
# Create a synthetic leaf entry from topology data
synthetic_leaf = {'hostname': parent_hostname, 'tors': []}
leaf_entries.append(synthetic_leaf)
leaf_by_hostname[parent_hostname] = synthetic_leaf

# Assign each TOR to ALL of its parent leaves present in this attach group
for tor_entry in tor_entries:
tor_hostname = tor_entry['hostname']
parent_leaves = tor_to_parents.get(tor_hostname, [])
valid_parents = [p for p in parent_leaves if p in leaf_by_hostname]

if valid_parents:
for parent_hostname in valid_parents:
leaf_by_hostname[parent_hostname]['tors'].append(tor_entry)
else:
# No parent leaf defined in tor_peers - keep TOR as top-level entry
leaf_entries.append(tor_entry)

return leaf_entries

def prepare(self):
data_model = self.kwargs['results']['model_extended']

Expand Down Expand Up @@ -154,7 +72,7 @@ def prepare(self):
net_grp_name_list.append(grp['name'])

# Restructure flat TOR entries under parent leaves
grp['switches'] = self._restructure_flat_to_nested(
grp['switches'] = restructure_leaf_tor_data(
grp['switches'], switches, tor_peers
)

Expand Down
82 changes: 81 additions & 1 deletion plugins/action/dtc/prepare_msite_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from ansible.plugins.action import ActionBase
from ansible_collections.cisco.nac_dc_vxlan.plugins.plugin_utils.helper_functions import ndfc_get_fabric_attributes
from ansible_collections.cisco.nac_dc_vxlan.plugins.plugin_utils.helper_functions import ndfc_get_fabric_switches
from ansible_collections.cisco.nac_dc_vxlan.plugins.plugin_utils.helper_functions import restructure_leaf_tor_data
from ansible_collections.cisco.nac_dc_vxlan.plugins.filter.version_compare import version_compare
import re

Expand Down Expand Up @@ -142,9 +143,12 @@ def run(self, tmp=None, task_vars=None):
fabric_switches.append(
{
'hostname': fabric_switch['logicalName'],
'name': fabric_switch['logicalName'],
'mgmt_ip_address': fabric_switch['ipAddress'],
'fabric_name': fabric_switch['fabricName'],
'fabric_cluster': fabric_cluster,
'serial_number': fabric_switch['serialNumber'],
'role': fabric_switch['switchRole'],
}
)

Expand All @@ -165,6 +169,69 @@ def run(self, tmp=None, task_vars=None):

results['switches'] = all_child_fabric_switches

tor_fabrics = {}
for switch in all_child_fabric_switches:
if switch['role'] == 'tor' and switch['fabric_name'] not in tor_fabrics:
if parent_fabric_type == 'MCFG':
tor_fabrics[switch['fabric_name']] = switch['serial_number'], switch['fabric_cluster']
else:
tor_fabrics[switch['fabric_name']] = (switch['serial_number'])

tor_ndfc_responses = {}
if tor_fabrics != {}:
for fabric in tor_fabrics.keys():
if parent_fabric_type == 'MCFG':
proxy = ''
if version_compare(nd_major_minor_patch, '3.2.2', '<='):
proxy = f'/onepath/{tor_fabrics[fabric][1]}'
elif version_compare(nd_major_minor_patch, '4.1.1', '>='):
proxy = f'/fedproxy/{tor_fabrics[fabric][1]}'
tor_response = self._execute_module(
module_name="cisco.dcnm.dcnm_rest",
module_args={
"method": "GET",
"path": f"{proxy}/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/tor/fabrics/{fabric}/switches/{tor_fabrics[fabric][0]}",
},
task_vars=task_vars,
tmp=tmp
)
else:
tor_response = self._execute_module(
module_name="cisco.dcnm.dcnm_rest",
module_args={
"method": "GET",
"path": f"/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/tor/fabrics/{fabric}/switches/{tor_fabrics[fabric]}",
},
task_vars=task_vars,
tmp=tmp
)
if 'response' in tor_response and 'DATA' in tor_response['response']:
tor_ndfc_responses = []
for pair in tor_response['response']['DATA']['torPairs']:
entry = {}
tor_name = pair['torName']
if '~' in tor_name:
entry['tor1'] = tor_name.split('~')[0]
entry['tor2'] = tor_name.split('~')[1]
else:
entry['tor1'] = tor_name

remarks_match = re.search(r'\(([^)]+)\)', pair.get('remarks', ''))
if remarks_match:
leaf_value = remarks_match.group(1)
if ',' in leaf_value:
entry['parent_leaf1'] = leaf_value.split(',')[0].strip()
entry['parent_leaf2'] = leaf_value.split(',')[1].strip()
else:
entry['parent_leaf1'] = leaf_value
else:
entry['parent_leaf1'] = None

tor_ndfc_responses.append(entry)
else:
display.warning(f"Failed to get TOR data for fabric {fabric}: {tor_response}")
tor_ndfc_responses[fabric] = []

# Rebuild sm_data['vxlan']['multisite']['overlay']['vrf_attach_groups'] into
# a structure that is easier to use just like data_model_extended.
vrf_grp_name_list = []
Expand Down Expand Up @@ -205,6 +272,13 @@ def run(self, tmp=None, task_vars=None):
for grp in data_model['vxlan']['multisite']['overlay']['network_attach_groups']:
data_model['vxlan']['multisite']['overlay']['network_attach_groups_dict'][grp['name']] = []
net_grp_name_list.append(grp['name'])

# Restructure flat TOR entries under parent leaves if TORs exist in the fabric
if tor_ndfc_responses != {}:
grp['switches'] = restructure_leaf_tor_data(
grp['switches'], all_child_fabric_switches, tor_ndfc_responses
)

for switch in grp['switches']:
data_model['vxlan']['multisite']['overlay']['network_attach_groups_dict'][grp['name']].append(switch)
# If the switch is in the switch list and a hostname is used, replace the hostname with the management IP
Expand All @@ -215,6 +289,13 @@ def run(self, tmp=None, task_vars=None):
regex_pattern = f"^{switch['hostname']}$|^{switch['hostname']}\\..*$"
if re.search(regex_pattern, sw['hostname']):
switch['mgmt_ip_address'] = sw['mgmt_ip_address']
# Process nested TOR entries and resolve their management IPs
if 'tors' in switch and switch['tors']:
for tor in switch['tors']:
tor_hostname = tor.get('hostname')
if tor_hostname and any(sw['name'] == tor_hostname for sw in child_fabrics_data[child_fabric]['switches']):
found_tor = next((item for item in child_fabrics_data[child_fabric]['switches'] if item["name"] == tor_hostname))
tor['mgmt_ip_address'] = found_tor['mgmt_ip_address']
# Append switch to a flat list of switches for cross comparison later when we query the
# MSD fabric information. We need to stop execution if the list returned by the MSD query
# does not include one of these switches.
Expand All @@ -227,5 +308,4 @@ def run(self, tmp=None, task_vars=None):
del net['network_attach_group']

results['overlay_attach_groups'] = data_model['vxlan']['multisite']['overlay']

return results
88 changes: 88 additions & 0 deletions plugins/plugin_utils/helper_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,10 +231,98 @@ def ndfc_get_fabric_switches(self, task_vars, tmp, fabric):
fabric_switches.append(
{
'hostname': fabric_switch['logicalName'],
'name': fabric_switch['logicalName'],
'mgmt_ip_address': fabric_switch['ipAddress'],
'fabric_name': fabric_switch['fabricName'],
'serial_number': fabric_switch['serialNumber'],
'role': fabric_switch['switchRole'],
}
)

return fabric_switches


def restructure_leaf_tor_data(switches_list, topology_switches, tor_peers):
"""Transform a flat switches list into the nested structure with TOR
entries under their parent leaf switches.

Each TOR is placed under ALL of its parent leaves that are present in the
attach group. If a parent leaf is not explicitly listed in the attach group,
a synthetic leaf entry is created automatically from topology data.

Args:
switches_list: List of switch dicts from network_attach_group
topology_switches: List of switch dicts from vxlan.topology.switches
tor_peers: List of tor_peers dicts from vxlan.topology.tor_peers

Returns:
Restructured switches list with TOR entries nested under parent leaves
"""
# Build set of TOR hostnames from topology
tor_hostnames = {
sw['name'] for sw in topology_switches if sw.get("role") == "tor"
}

# Build TOR -> parent leaves mapping from tor_peers
# Each TOR maps to a list of its parent leaf hostnames
tor_to_parents = {}
for peer in tor_peers:
parent_leaves = []
if peer.get("parent_leaf1"):
parent_leaves.append(peer['parent_leaf1'])
if peer.get("parent_leaf2"):
parent_leaves.append(peer['parent_leaf2'])

for key in ("tor1", "tor2"):
tor_name = peer.get(key)
if tor_name:
tor_to_parents[tor_name] = parent_leaves

# # Build topology hostname -> switch data mapping for creating synthetic entries
# topology_by_name = {sw['name']: sw for sw in topology_switches}

# Separate leaf entries and TOR entries
leaf_entries = []
tor_entries = []

for sw in switches_list:
hostname = sw.get("hostname", "")
if hostname in tor_hostnames:
tor_entries.append(sw)
else:
leaf_entries.append(sw)

# Initialize empty 'tors' list for each leaf entry
for leaf in leaf_entries:
if "tors" not in leaf:
leaf['tors'] = []

# Build hostname -> leaf entry mapping for quick lookup
leaf_by_hostname = {leaf['hostname']: leaf for leaf in leaf_entries}

# Collect all required parent leaf hostnames from TOR entries
# and create synthetic leaf entries for any that are not in the attach group
for tor_entry in tor_entries:
tor_hostname = tor_entry['hostname']
parent_leaves = tor_to_parents.get(tor_hostname, [])
for parent_hostname in parent_leaves:
if parent_hostname not in leaf_by_hostname:
# Create a synthetic leaf entry from topology data
synthetic_leaf = {'hostname': parent_hostname, 'tors': []}
leaf_entries.append(synthetic_leaf)
leaf_by_hostname[parent_hostname] = synthetic_leaf

# Assign each TOR to ALL of its parent leaves present in this attach group
for tor_entry in tor_entries:
tor_hostname = tor_entry['hostname']
parent_leaves = tor_to_parents.get(tor_hostname, [])
valid_parents = [p for p in parent_leaves if p in leaf_by_hostname]

if valid_parents:
for parent_hostname in valid_parents:
leaf_by_hostname[parent_hostname]['tors'].append(tor_entry)
else:
# No parent leaf defined in tor_peers - keep TOR as top-level entry
leaf_entries.append(tor_entry)

return leaf_entries
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,15 @@
{% if attach['ports'] is defined %}
ports: {{ attach['ports'] }}
{% endif %}
{% if 'tors' in attach and attach['tors'] %}
tor_ports:
{% for tor in attach['tors'] %}
- ip_address: {{ tor['mgmt_ip_address'] | default(tor['hostname']) }}
{% if tor['ports'] is defined %}
ports: {{ tor['ports'] }}
{% endif %}
{% endfor %}
{% endif %}
{% endfor %}
deploy: false
{% endif %}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,15 @@
{% if attach['ports'] is defined %}
ports: {{ attach['ports'] }}
{% endif %}
{% if 'tors' in attach and attach['tors'] %}
tor_ports:
{% for tor in attach['tors'] %}
- ip_address: {{ tor['mgmt_ip_address'] | default(tor['hostname']) }}
{% if tor['ports'] is defined %}
ports: {{ tor['ports'] }}
{% endif %}
{% endfor %}
{% endif %}
{% endfor %}
deploy: false
{% endif %}
Expand Down
Loading