diff --git a/plugins/action/common/prepare_plugins/prep_105_fabric_overlay.py b/plugins/action/common/prepare_plugins/prep_105_fabric_overlay.py index 8aac13460..5d7546061 100644 --- a/plugins/action/common/prepare_plugins/prep_105_fabric_overlay.py +++ b/plugins/action/common/prepare_plugins/prep_105_fabric_overlay.py @@ -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'] @@ -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 ) diff --git a/plugins/action/dtc/prepare_msite_data.py b/plugins/action/dtc/prepare_msite_data.py index 8d41aa94f..1d79a399c 100644 --- a/plugins/action/dtc/prepare_msite_data.py +++ b/plugins/action/dtc/prepare_msite_data.py @@ -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 @@ -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'], } ) @@ -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 = [] @@ -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 @@ -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. @@ -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 diff --git a/plugins/plugin_utils/helper_functions.py b/plugins/plugin_utils/helper_functions.py index 299cd1723..904549189 100644 --- a/plugins/plugin_utils/helper_functions.py +++ b/plugins/plugin_utils/helper_functions.py @@ -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 diff --git a/roles/dtc/common/templates/ndfc_networks/mcfg_fabric/mcfg_fabric_networks.j2 b/roles/dtc/common/templates/ndfc_networks/mcfg_fabric/mcfg_fabric_networks.j2 index a18c116a3..9c92b186a 100644 --- a/roles/dtc/common/templates/ndfc_networks/mcfg_fabric/mcfg_fabric_networks.j2 +++ b/roles/dtc/common/templates/ndfc_networks/mcfg_fabric/mcfg_fabric_networks.j2 @@ -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 %} diff --git a/roles/dtc/common/templates/ndfc_networks/msd_fabric/msd_fabric_networks.j2 b/roles/dtc/common/templates/ndfc_networks/msd_fabric/msd_fabric_networks.j2 index 696e2d2e2..517fad7b1 100644 --- a/roles/dtc/common/templates/ndfc_networks/msd_fabric/msd_fabric_networks.j2 +++ b/roles/dtc/common/templates/ndfc_networks/msd_fabric/msd_fabric_networks.j2 @@ -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 %}