From 971407cdb1567ea9a0c3e4f46f4ec91e0af92c71 Mon Sep 17 00:00:00 2001 From: "Slawomir Kaszlikowski (skaszlik)" Date: Mon, 13 Oct 2025 10:18:37 +0200 Subject: [PATCH 1/8] Fix for #631 --- .../ndfc_networks/dc_vxlan_fabric/dc_vxlan_fabric_networks.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/dtc/common/templates/ndfc_networks/dc_vxlan_fabric/dc_vxlan_fabric_networks.j2 b/roles/dtc/common/templates/ndfc_networks/dc_vxlan_fabric/dc_vxlan_fabric_networks.j2 index a29e02956..9a2f64d44 100644 --- a/roles/dtc/common/templates/ndfc_networks/dc_vxlan_fabric/dc_vxlan_fabric_networks.j2 +++ b/roles/dtc/common/templates/ndfc_networks/dc_vxlan_fabric/dc_vxlan_fabric_networks.j2 @@ -55,7 +55,7 @@ int_desc: {{ net['int_desc'] | default(defaults.vxlan.overlay.networks.net_description) }} l3gw_on_border: {{ net['l3gw_on_border'] | default(defaults.vxlan.overlay.networks.l3gw_on_border) }} mtu_l3intf: {{ net['mtu_l3intf'] | default(defaults.vxlan.overlay.networks.mtu_l3intf) }} -{% if (MD_Extended.vxlan.underlay.general.replication_mode | lower) == 'multicast' %} +{% if (MD_Extended.vxlan.underlay.general.replication_mode | default(defaults.vxlan.underlay.general.replication_mode) | lower) == 'multicast' %} multicast_group_address: {{ net['multicast_group_address'] | default(defaults.vxlan.overlay.networks.multicast_group_address) }} {% endif %} netflow_enable: {{ net['netflow_enable'] | default(defaults.vxlan.overlay.networks.netflow_enable) }} From 49f082f9c9a8e0fba0e1e10ffff7ae46d458b510 Mon Sep 17 00:00:00 2001 From: "Slawomir Kaszlikowski (skaszlik)" Date: Wed, 15 Oct 2025 15:06:52 +0200 Subject: [PATCH 2/8] Fix for #203 --- ...310_topology_switch_interface_breakouts.py | 71 ++++++++++--------- 1 file changed, 37 insertions(+), 34 deletions(-) diff --git a/roles/validate/files/rules/common/310_topology_switch_interface_breakouts.py b/roles/validate/files/rules/common/310_topology_switch_interface_breakouts.py index f59b076cf..5617f00f4 100644 --- a/roles/validate/files/rules/common/310_topology_switch_interface_breakouts.py +++ b/roles/validate/files/rules/common/310_topology_switch_interface_breakouts.py @@ -1,7 +1,17 @@ +import re + + class Rule: id = "310" description = "Verify if interfaces with sub-levels are part of the defined interface breakouts" severity = "HIGH" + + # Compiled regex patterns for better performance + # FEX pattern: Ethernet[101-199]/1/[1-99] + FEX_PATTERN = re.compile(r'^e(?:th(?:ernet)?)?1(0[1-9]|[1-9][0-9])/1/([1-9]|[1-9][0-9])$', re.IGNORECASE) + + # Breakout pattern: Ethernet[1-99]/[1-99]/[1-4] + BREAKOUT_PATTERN = re.compile(r'^e(?:th(?:ernet)?)?([1-9]|[1-9][0-9])/([1-9]|[1-9][0-9])/([1-4])$', re.IGNORECASE) @classmethod def match(cls, data_model): @@ -31,11 +41,20 @@ def match(cls, data_model): # Check each interface for interface in interfaces: interface_name = interface.get("name", "") - normalized_interface = interface_name.lower().replace("ethernet", "e").replace("eth", "e") - - # Skip interfaces without sub-levels (e.g., E1/x) - if cls.has_sub_level(normalized_interface): - if not cls.is_interface_in_breakouts(normalized_interface, interface_breakouts): + + # Check if it's a FEX interface - skip validation + if cls.FEX_PATTERN.match(interface_name): + continue + + # Check if it's a breakout interface pattern + match = cls.BREAKOUT_PATTERN.match(interface_name) + if match: + # Extract module, port, sub_level from regex groups + module_num = int(match.group(1)) + port_num = int(match.group(2)) + + # Verify if this breakout interface is defined in interface_breakouts + if not cls.is_interface_in_breakouts(module_num, port_num, interface_breakouts): results.append( f"Interface {interface_name} on vxlan.topology.switches." f"{switch['name']} is not part of the interface breakouts" @@ -44,46 +63,30 @@ def match(cls, data_model): return results @staticmethod - def has_sub_level(interface_name): - """ - Check if an interface has a sub-level (e.g., E1/x/y). - - Parameters: - - interface_name (str): The interface name to check. - - Returns: - - bool: True if the interface has a sub-level, False otherwise. - """ - parts = interface_name.split("/") - return len(parts) == 3 # Check if the interface has exactly 3 parts (e.g., E1/x/y) - - @staticmethod - def is_interface_in_breakouts(interface_name, breakouts): + def is_interface_in_breakouts(module_num, port_num, breakouts): """ Check if the given interface is part of the interface_breakouts. Parameters: - - interface_name (str): The interface name to check (e.g., e1/100/3, Eth1/99/3). + - module_num (int): The module number (e.g., 1 from Ethernet1/x/y). + - port_num (int): The port number (e.g., 5 from Ethernet1/5/y). - breakouts (list): List of interface breakout definitions. Returns: - bool: True if the interface is part of the breakouts, False otherwise. """ - # Extract module and port details from the normalized interface name - try: - module, port, sub_level = interface_name.split("/") - port = int(port) # Convert port to an integer - except ValueError: - return False # If the interface name is invalid or port conversion fails, return False - + # If no breakouts defined, interface cannot be valid + if not breakouts: + return False + # Check each breakout definition for breakout in breakouts: - # Ensure the 'from' value is less than or equal to the 'to' value - if breakout["module"] == int(module[1:]): # Compare module numbers - if "to" in breakout: # Range breakout - if breakout["from"] <= port <= breakout["to"]: + if breakout.get("module") == module_num: + # Check if port is in range (for range breakouts) or matches (for single-port) + if "to" in breakout: + if breakout["from"] <= port_num <= breakout["to"]: return True - elif breakout["from"] == port: # Single-port breakout + elif breakout.get("from") == port_num: return True - + return False From 42d5d92c94fa1b68a6e683b086487e2cd1bd9e4c Mon Sep 17 00:00:00 2001 From: "Slawomir Kaszlikowski (skaszlik)" Date: Wed, 22 Oct 2025 12:44:15 +0200 Subject: [PATCH 3/8] Enhanced fix --- ...310_topology_switch_interface_breakouts.py | 61 +++++++++++-------- 1 file changed, 34 insertions(+), 27 deletions(-) diff --git a/roles/validate/files/rules/common/310_topology_switch_interface_breakouts.py b/roles/validate/files/rules/common/310_topology_switch_interface_breakouts.py index 5617f00f4..9dfafe35c 100644 --- a/roles/validate/files/rules/common/310_topology_switch_interface_breakouts.py +++ b/roles/validate/files/rules/common/310_topology_switch_interface_breakouts.py @@ -1,6 +1,5 @@ import re - class Rule: id = "310" description = "Verify if interfaces with sub-levels are part of the defined interface breakouts" @@ -9,9 +8,6 @@ class Rule: # Compiled regex patterns for better performance # FEX pattern: Ethernet[101-199]/1/[1-99] FEX_PATTERN = re.compile(r'^e(?:th(?:ernet)?)?1(0[1-9]|[1-9][0-9])/1/([1-9]|[1-9][0-9])$', re.IGNORECASE) - - # Breakout pattern: Ethernet[1-99]/[1-99]/[1-4] - BREAKOUT_PATTERN = re.compile(r'^e(?:th(?:ernet)?)?([1-9]|[1-9][0-9])/([1-9]|[1-9][0-9])/([1-4])$', re.IGNORECASE) @classmethod def match(cls, data_model): @@ -41,20 +37,15 @@ def match(cls, data_model): # Check each interface for interface in interfaces: interface_name = interface.get("name", "") + normalized_interface = interface_name.lower().replace("ethernet", "e").replace("eth", "e") # Check if it's a FEX interface - skip validation if cls.FEX_PATTERN.match(interface_name): continue - # Check if it's a breakout interface pattern - match = cls.BREAKOUT_PATTERN.match(interface_name) - if match: - # Extract module, port, sub_level from regex groups - module_num = int(match.group(1)) - port_num = int(match.group(2)) - - # Verify if this breakout interface is defined in interface_breakouts - if not cls.is_interface_in_breakouts(module_num, port_num, interface_breakouts): + # Skip interfaces without sub-levels (e.g., E1/x) + if cls.has_sub_level(normalized_interface): + if not cls.is_interface_in_breakouts(normalized_interface, interface_breakouts): results.append( f"Interface {interface_name} on vxlan.topology.switches." f"{switch['name']} is not part of the interface breakouts" @@ -63,30 +54,46 @@ def match(cls, data_model): return results @staticmethod - def is_interface_in_breakouts(module_num, port_num, breakouts): + def has_sub_level(interface_name): + """ + Check if an interface has a sub-level (e.g., E1/x/y). + + Parameters: + - interface_name (str): The interface name to check. + + Returns: + - bool: True if the interface has a sub-level, False otherwise. + """ + parts = interface_name.split("/") + return len(parts) == 3 # Check if the interface has exactly 3 parts (e.g., E1/x/y) + + @staticmethod + def is_interface_in_breakouts(interface_name, breakouts): """ Check if the given interface is part of the interface_breakouts. Parameters: - - module_num (int): The module number (e.g., 1 from Ethernet1/x/y). - - port_num (int): The port number (e.g., 5 from Ethernet1/5/y). + - interface_name (str): The interface name to check (e.g., e1/100/3, Eth1/99/3). - breakouts (list): List of interface breakout definitions. Returns: - bool: True if the interface is part of the breakouts, False otherwise. """ - # If no breakouts defined, interface cannot be valid - if not breakouts: - return False - + # Extract module and port details from the normalized interface name + try: + module, port, sub_level = interface_name.split("/") + port = int(port) # Convert port to an integer + except ValueError: + return False # If the interface name is invalid or port conversion fails, return False + # Check each breakout definition for breakout in breakouts: - if breakout.get("module") == module_num: - # Check if port is in range (for range breakouts) or matches (for single-port) - if "to" in breakout: - if breakout["from"] <= port_num <= breakout["to"]: + # Ensure the 'from' value is less than or equal to the 'to' value + if breakout["module"] == int(module[1:]): # Compare module numbers + if "to" in breakout: # Range breakout + if breakout["from"] <= port <= breakout["to"]: return True - elif breakout.get("from") == port_num: + elif breakout["from"] == port: # Single-port breakout return True - - return False + + return False \ No newline at end of file From 1fd2ce8ed852a29776f6ffd835591bfed052bac6 Mon Sep 17 00:00:00 2001 From: "Slawomir Kaszlikowski (skaszlik)" Date: Wed, 22 Oct 2025 12:45:25 +0200 Subject: [PATCH 4/8] doc updated --- .../rules/common/310_topology_switch_interface_breakouts.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/roles/validate/files/rules/common/310_topology_switch_interface_breakouts.py b/roles/validate/files/rules/common/310_topology_switch_interface_breakouts.py index 9dfafe35c..1de25b8de 100644 --- a/roles/validate/files/rules/common/310_topology_switch_interface_breakouts.py +++ b/roles/validate/files/rules/common/310_topology_switch_interface_breakouts.py @@ -5,8 +5,7 @@ class Rule: description = "Verify if interfaces with sub-levels are part of the defined interface breakouts" severity = "HIGH" - # Compiled regex patterns for better performance - # FEX pattern: Ethernet[101-199]/1/[1-99] + # FEX regex pattern: Ethernet[101-199]/1/[1-99] FEX_PATTERN = re.compile(r'^e(?:th(?:ernet)?)?1(0[1-9]|[1-9][0-9])/1/([1-9]|[1-9][0-9])$', re.IGNORECASE) @classmethod From bb3467f8d19a11884cd88b298e1a2982224f7564 Mon Sep 17 00:00:00 2001 From: "Slawomir Kaszlikowski (skaszlik)" Date: Wed, 22 Oct 2025 12:53:08 +0200 Subject: [PATCH 5/8] Trailing whitespace --- .../common/310_topology_switch_interface_breakouts.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/roles/validate/files/rules/common/310_topology_switch_interface_breakouts.py b/roles/validate/files/rules/common/310_topology_switch_interface_breakouts.py index 1de25b8de..cce3ed70c 100644 --- a/roles/validate/files/rules/common/310_topology_switch_interface_breakouts.py +++ b/roles/validate/files/rules/common/310_topology_switch_interface_breakouts.py @@ -4,7 +4,7 @@ class Rule: id = "310" description = "Verify if interfaces with sub-levels are part of the defined interface breakouts" severity = "HIGH" - + # FEX regex pattern: Ethernet[101-199]/1/[1-99] FEX_PATTERN = re.compile(r'^e(?:th(?:ernet)?)?1(0[1-9]|[1-9][0-9])/1/([1-9]|[1-9][0-9])$', re.IGNORECASE) @@ -37,11 +37,11 @@ def match(cls, data_model): for interface in interfaces: interface_name = interface.get("name", "") normalized_interface = interface_name.lower().replace("ethernet", "e").replace("eth", "e") - + # Check if it's a FEX interface - skip validation if cls.FEX_PATTERN.match(interface_name): continue - + # Skip interfaces without sub-levels (e.g., E1/x) if cls.has_sub_level(normalized_interface): if not cls.is_interface_in_breakouts(normalized_interface, interface_breakouts): @@ -95,4 +95,4 @@ def is_interface_in_breakouts(interface_name, breakouts): elif breakout["from"] == port: # Single-port breakout return True - return False \ No newline at end of file + return False From dedf18767acd7b4d02fb296aa9da9f77b4840f23 Mon Sep 17 00:00:00 2001 From: "Slawomir Kaszlikowski (skaszlik)" Date: Wed, 22 Oct 2025 12:58:30 +0200 Subject: [PATCH 6/8] sanity issues --- .../rules/common/310_topology_switch_interface_breakouts.py | 1 + 1 file changed, 1 insertion(+) diff --git a/roles/validate/files/rules/common/310_topology_switch_interface_breakouts.py b/roles/validate/files/rules/common/310_topology_switch_interface_breakouts.py index cce3ed70c..deccfa976 100644 --- a/roles/validate/files/rules/common/310_topology_switch_interface_breakouts.py +++ b/roles/validate/files/rules/common/310_topology_switch_interface_breakouts.py @@ -1,5 +1,6 @@ import re + class Rule: id = "310" description = "Verify if interfaces with sub-levels are part of the defined interface breakouts" From 4cc87731bdbda810065ed474e72774de0234b3e3 Mon Sep 17 00:00:00 2001 From: "Slawomir Kaszlikowski (skaszlik)" Date: Tue, 4 Nov 2025 11:40:17 +0100 Subject: [PATCH 7/8] Regex updated --- .../rules/common/310_topology_switch_interface_breakouts.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/roles/validate/files/rules/common/310_topology_switch_interface_breakouts.py b/roles/validate/files/rules/common/310_topology_switch_interface_breakouts.py index deccfa976..8d6b673b3 100644 --- a/roles/validate/files/rules/common/310_topology_switch_interface_breakouts.py +++ b/roles/validate/files/rules/common/310_topology_switch_interface_breakouts.py @@ -6,8 +6,8 @@ class Rule: description = "Verify if interfaces with sub-levels are part of the defined interface breakouts" severity = "HIGH" - # FEX regex pattern: Ethernet[101-199]/1/[1-99] - FEX_PATTERN = re.compile(r'^e(?:th(?:ernet)?)?1(0[1-9]|[1-9][0-9])/1/([1-9]|[1-9][0-9])$', re.IGNORECASE) + # regex pattern: Ethernet[101-199]/1/[1-99] + IGNORE_FEX = re.compile(r'^(?:Ethernet)(?:(?:10[1-9]|1[1-9]\d)\/1\/([1-9]|[1-5][0-9])$)', re.IGNORECASE) @classmethod def match(cls, data_model): @@ -40,7 +40,7 @@ def match(cls, data_model): normalized_interface = interface_name.lower().replace("ethernet", "e").replace("eth", "e") # Check if it's a FEX interface - skip validation - if cls.FEX_PATTERN.match(interface_name): + if cls.IGNORE_FEX.match(interface_name): continue # Skip interfaces without sub-levels (e.g., E1/x) From 008ff305ca6eb2224e0cfa690c52d18d359f5a91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C5=82awomir=20Kaszlikowski?= Date: Fri, 28 Nov 2025 12:50:07 +0100 Subject: [PATCH 8/8] Update 310_topology_switch_interface_breakouts.py Update the regex to match Miguel's expression --- .../rules/common/310_topology_switch_interface_breakouts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/validate/files/rules/common/310_topology_switch_interface_breakouts.py b/roles/validate/files/rules/common/310_topology_switch_interface_breakouts.py index 8d6b673b3..1d3e4ba3b 100644 --- a/roles/validate/files/rules/common/310_topology_switch_interface_breakouts.py +++ b/roles/validate/files/rules/common/310_topology_switch_interface_breakouts.py @@ -7,7 +7,7 @@ class Rule: severity = "HIGH" # regex pattern: Ethernet[101-199]/1/[1-99] - IGNORE_FEX = re.compile(r'^(?:Ethernet)(?:(?:10[1-9]|1[1-9]\d)\/1\/([1-9]|[1-5][0-9])$)', re.IGNORECASE) + IGNORE_FEX = re.compile(r'^(?:Ethernet)(?:10[1-9]|1[1-9]\d)/1/(?:[1-9]|[1-5]\d|6[0-4])$', re.IGNORECASE) @classmethod def match(cls, data_model):