Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
971407c
Fix for #631
skaszlik Oct 13, 2025
d2c22be
TOR_Pairing - First_try
skaszlik Oct 13, 2025
57ef3ce
Tor pairing, fix 1
skaszlik Oct 14, 2025
103fcd4
fix for pairing removing
skaszlik Oct 14, 2025
aac57f7
fix for eBGP fabric type
skaszlik Oct 15, 2025
3924bab
TOR support
skaszlik Oct 21, 2025
210de83
Enhance ToR pairing validation and configuration handling with scenar…
skaszlik Oct 27, 2025
8bb6b18
Fix for po mismatch
skaszlik Oct 28, 2025
511aace
Support for tor pair removing without *.old files
skaszlik Oct 28, 2025
4f615d8
simplifying plugins
skaszlik Oct 29, 2025
aa18c31
Fix for network attach for TORs
skaszlik Nov 4, 2025
4092b79
Merge branch 'TOR_Pairing' into develop
skaszlik Nov 5, 2025
e30ece8
Merge pull request #5 from skaszlik/develop
skaszlik Nov 5, 2025
5547d84
Add change detection flags for ToR pairing validation
skaszlik Nov 5, 2025
94770ac
Add ToR pairing change detection flag updates
skaszlik Nov 6, 2025
322ef0e
Removing ToR pairing tasks for improved clarity and efficiency
skaszlik Nov 7, 2025
b66d5ad
Remove commented-out code
skaszlik Nov 7, 2025
3be0866
Refactor ToR pairing discovery failure conditions for improved error …
skaszlik Nov 14, 2025
9e92a1e
Add ToR pairing management tasks for eBGP VXLAN configuration
skaszlik Nov 17, 2025
6ad5027
Dev comments
skaszlik Nov 25, 2025
1a46ff8
Refactor ToR pairing management:
skaszlik Nov 26, 2025
9cc0556
Refactor TOR pairing operations into a unified action plugin
skaszlik Dec 1, 2025
0e36551
Refactor TOR pairing operations to streamline API interactions and en…
skaszlik Dec 2, 2025
5275e23
Refactor TOR pairing tasks to improve error handling and remove unnec…
skaszlik Dec 2, 2025
81b7789
Add validation rule for TOR switches in network_attach_groups to ensu…
skaszlik Dec 2, 2025
e22929f
Implement ToR pairing diff processor and update YAML tasks to utilize…
skaszlik Dec 2, 2025
93b4dd5
Refactor ToR pairing tasks to remove diff detection logic and streaml…
skaszlik Dec 2, 2025
950fd76
Merge branch 'develop' into TOR_Pairing
skaszlik Dec 2, 2025
9191b1e
Refactor ToR pairing tasks to replace MD_Extended with data_model_ext…
skaszlik Dec 2, 2025
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
8 changes: 7 additions & 1 deletion plugins/action/common/change_flag_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ def initialize_flags(self):
'changes_detected_interface_breakout_preprov': False,
'changes_detected_inventory': False,
'changes_detected_link_vpc_peering': False,
'changes_detected_tor_pairing': False,
'changes_detected_networks': False,
'changes_detected_policy': False,
'changes_detected_sub_interface_routed': False,
Expand Down Expand Up @@ -95,6 +96,7 @@ def initialize_flags(self):
'changes_detected_interface_breakout_preprov': False,
'changes_detected_inventory': False,
'changes_detected_link_vpc_peering': False,
'changes_detected_tor_pairing': False,
'changes_detected_networks': False,
'changes_detected_policy': False,
'changes_detected_sub_interface_routed': False,
Expand Down Expand Up @@ -155,9 +157,13 @@ def initialize_flags(self):
'changes_detected_interface_vpc': False,
'changes_detected_interface_breakout': False,
'changes_detected_interface_breakout_preprov': False,
'changes_detected_inventory': False,
'changes_detected_link_vpc_peering': False,
'changes_detected_tor_pairing': False,
'changes_detected_networks': False,
'changes_detected_policy': False,
'changes_detected_sub_interface_routed': False,
'changes_detected_vpc_peering': False,
'changes_detected_policy': False,
'changes_detected_any': False
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,17 @@ def prepare(self):
switch['mgmt_ip_address'] = found_switch['management']['management_ipv4_address']
elif found_switch.get('management').get('management_ipv6_address'):
switch['mgmt_ip_address'] = found_switch['management']['management_ipv6_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 switches):
found_tor = next((item for item in switches if item["name"] == tor_hostname))
if found_tor.get('management').get('management_ipv4_address'):
tor['mgmt_ip_address'] = found_tor['management']['management_ipv4_address']
elif found_tor.get('management').get('management_ipv6_address'):
tor['mgmt_ip_address'] = found_tor['management']['management_ipv6_address']

# Remove network_attach_group from net if the group_name is not defined
for net in data_model['vxlan']['overlay']['networks']:
Expand Down Expand Up @@ -131,6 +142,18 @@ def prepare(self):
switch['mgmt_ip_address'] = found_switch['management']['management_ipv4_address']
elif found_switch.get('management').get('management_ipv6_address'):
switch['mgmt_ip_address'] = found_switch['management']['management_ipv6_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 switches):
found_tor = next((item for item in switches if item["name"] == tor_hostname))
if found_tor.get('management').get('management_ipv4_address'):
tor['mgmt_ip_address'] = found_tor['management']['management_ipv4_address']
elif found_tor.get('management').get('management_ipv6_address'):
tor['mgmt_ip_address'] = found_tor['management']['management_ipv6_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 Down
213 changes: 213 additions & 0 deletions plugins/action/common/prepare_plugins/prep_110_tor_pairing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
# Copyright (c) 2025 Cisco Systems, Inc. and its affiliates
#
# Permission is hereby granted, free of charge, to any person obtaining a copy of
# this software and associated documentation files (the "Software"), to deal in
# the Software without restriction, including without limitation the rights to
# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
# the Software, and to permit persons to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
# SPDX-License-Identifier: MIT


class PreparePlugin:
"""
ToR Pairing Prepare Plugin.

Transforms user YAML configuration (tor_peers) into NDFC API payloads.

This plugin runs during the validation/prepare phase to:
- Resolve switch names to serial numbers
- Auto-detect VPC scenarios based on configuration
- Build standardized payload format for NDFC API calls

Note: Diff detection for removals is handled separately by the
process_tor_pairing action plugin's 'diff' operation.
"""

def __init__(self, **kwargs):
self.kwargs = kwargs
self.keys = ['vxlan', 'topology', 'tor_peers']

def _get_switch(self, name, expected_role, switches, errors):
"""
Get switch from switches map.
This method focuses on data retrieval for payload generation.
"""
switch = switches.get(name)
if not switch:
# Validation rule #311 should have caught this
errors.append(f"Switch '{name}' referenced in tor_peers is not defined in vxlan.topology.switches")
return None
return switch

def _resolve_vpc_domain(self, peer, key, name_a, name_b, topology):
if peer.get(key) is not None:
return peer.get(key)
if not (name_a and name_b):
return None
vpc_peers = topology.get('vpc_peers') or []
for candidate in vpc_peers:
peers = {candidate.get('peer1'), candidate.get('peer2')}
if {name_a, name_b} == peers:
return candidate.get('domain_id')
return None

def prepare(self):
"""
Main prepare method - transforms user config to API payloads and performs diff detection.

Returns:
dict: results with model_extended updated and optional metadata
- model_extended['vxlan']['topology']['tor_pairing']: current pairings
- model_extended['vxlan']['topology']['tor_pairing_removed']: removals
- results['tor_pairing_diff_stats']: debug statistics (when previous state supplied)
or results['failed'] = True with aggregated error messages
Examples:
[
{
'pairing_id': 'LEAF_11-TOR_24',
'scenario': 'vpc_to_vpc',
'payload': {
'leafSN1': '9BUXESV382R',
'leafSN2': '99FYP2OV1NS',
'torSN1': '9Q2X9XATUNL',
'torSN2': '9M81OZOOCWM'
}
}
]

"""
results = self.kwargs['results']
model_data = results['model_extended']
topology = model_data.get('vxlan', {}).get('topology', {})
tor_peers = topology.get('tor_peers')

if not tor_peers:
return results

switches = {sw.get('name'): sw for sw in topology.get('switches', []) if sw.get('name')}
processed_pairs = []
errors = []
pairing_ids = set()

for peer in tor_peers:
error_count_start = len(errors)
parent_leaf1 = peer.get('parent_leaf1')
tor1 = peer.get('tor1')
if not parent_leaf1 or not tor1:
errors.append("Each tor_peers entry requires parent_leaf1 and tor1 definitions")
continue

# Handle both dict and string formats for switch references
leaf1_name = parent_leaf1.get('name') if isinstance(parent_leaf1, dict) else parent_leaf1
tor1_name = tor1.get('name') if isinstance(tor1, dict) else tor1
leaf1_switch = self._get_switch(leaf1_name, 'leaf', switches, errors)
tor1_switch = self._get_switch(tor1_name, 'tor', switches, errors)

parent_leaf2 = peer.get('parent_leaf2')
tor2 = peer.get('tor2')

# Handle both dict and string formats for optional switches
leaf2_name = parent_leaf2.get('name') if isinstance(parent_leaf2, dict) else parent_leaf2 if parent_leaf2 else None
tor2_name = tor2.get('name') if isinstance(tor2, dict) else tor2 if tor2 else None

leaf2_switch = None
tor2_switch = None

if parent_leaf2:
leaf2_switch = self._get_switch(leaf2_name, 'leaf', switches, errors)
if tor2:
tor2_switch = self._get_switch(tor2_name, 'tor', switches, errors)

# Auto-detect VPC scenarios based on presence of tor2/leaf2
# No need for explicit tor_vpc_peer flag with new simplified model

# Auto-resolve VPC domain IDs from vpc_peers configuration
leaf_vpc_domain = self._resolve_vpc_domain(peer, 'leaf_vpc_id', leaf1_name, leaf2_name, topology)
tor_vpc_domain = self._resolve_vpc_domain(peer, 'tor_vpc_id', tor1_name, tor2_name, topology)

# Determine if this is a VPC scenario based on switch definitions
leaf_is_vpc = bool(leaf2_switch and leaf_vpc_domain)
tor_is_vpc = bool(tor2_switch and tor_vpc_domain)

# Determine scenario based on configuration
scenario = 'standalone_to_standalone'
if leaf_is_vpc and tor_is_vpc:
scenario = 'vpc_to_vpc'
elif leaf_is_vpc and not tor_is_vpc:
scenario = 'vpc_to_standalone'
elif not leaf_is_vpc and tor_is_vpc:
errors.append(
f"Unsupported ToR pairing scenario: ToR vPC with standalone leaf for '{tor1_name}'. "
f"ToR vPC requires both parent_leaf1 and parent_leaf2 to be defined."
)

pairing_id = peer.get('pairing_id') or f"{leaf1_name}-{tor1_name}"
if pairing_id in pairing_ids:
errors.append(f"Duplicate tor pairing identifier '{pairing_id}' detected")
pairing_ids.add(pairing_id)

# Collect serial numbers
if leaf1_switch:
leaf1_serial = leaf1_switch.get('serial_number')
else:
leaf1_serial = None

if tor1_switch:
tor1_serial = tor1_switch.get('serial_number')
else:
tor1_serial = None

leaf2_serial = ''
if leaf_is_vpc and leaf2_switch:
leaf2_serial = leaf2_switch.get('serial_number') or ''

tor2_serial = ''
if tor_is_vpc and tor2_switch:
tor2_serial = tor2_switch.get('serial_number') or ''

required_serials = [leaf1_serial, tor1_serial]
if any(serial is None for serial in required_serials):
errors.append(
f"Serial numbers must be defined for all ToR pairing members. Pairing '{pairing_id}' is missing values."
)

# Skip if scenario validation already failed
if scenario != 'standalone_to_standalone' and not leaf_is_vpc:
continue

if len(errors) > error_count_start:
continue

processed_pairs.append({
'pairing_id': pairing_id,
'scenario': scenario,
'payload': {
'leafSN1': leaf1_serial or '',
'leafSN2': leaf2_serial or '',
'torSN1': tor1_serial or '',
'torSN2': tor2_serial or ''
}
# 'po_map': po_map
})

if errors:
results['failed'] = True
results['msg'] = '\n'.join(errors)
return results

# Store processed pairings in model_extended for downstream tasks
model_data['vxlan']['topology']['tor_pairing'] = processed_pairs

results['model_extended'] = model_data
return results
Loading
Loading