Skip to content

Commit ac21d10

Browse files
ZhaohuiSselldinesh
authored andcommitted
[test gap] Add a new script to check ecmp balance (#18255)
Description of PR Summary: Fixes # (issue) Add a test gap for covering ecmp imbalance issue found before. Test plan is here #19021 In this PR, it separates the ECMP balance test into Platform-Independent (PI) - test_ecmp_balance.py and Platform-Dependent (PD) code - platform_handler.py to improve maintainability and extensibility. Also move the skip condition to conditional mark file to avoid setup error for skipped platforms and also save time. Platform-Dependent Code (PD) File: platform_handler.py Contains all platform-specific logic including: ECMPHashPlatformHandler (Abstract Base Class) Defines the interface for platform-specific operations Methods: get_supported_skus(), is_supported(), get_hash_offset_command(), etc. BroadcomPlatformHandler Implements Broadcom-specific logic Handles BCM commands for ECMP hash offset operations Manages supported Arista SKUs Default and test offset values MellanoxPlatformHandler Placeholder for future Mellanox support Can be extended with Mellanox-specific commands PlatformHandlerFactory Creates appropriate platform handlers Auto-detects platform based on hardware SKU Extensible for new platforms ECMPHashManager High-level interface for ECMP hash operations Platform-agnostic API for test code Handles backup/restore of original values Platform-Independent Code (PI) File: test_ecmp_balance.py Contains the core test logic: Test Infrastructure Packet generation and verification Test result collection and analysis File I/O operations Test patterns (varying source ports, IPs, destination ports, IPs) Test Functions test_udp_packets(): Basic ECMP balance testing test_udp_packets_ecmp(): ECMP hash offset impact testing Platform checks are handled by the platform handler Helper Functions Packet creation and masking Result comparison and validation Statistics collection Signed-off-by: selldinesh <[email protected]>
1 parent 8ecd5a2 commit ac21d10

File tree

3 files changed

+1085
-0
lines changed

3 files changed

+1085
-0
lines changed

tests/common/plugins/conditional_mark/tests_mark_conditions.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1256,6 +1256,14 @@ ecmp/inner_hashing/test_wr_inner_hashing_lag.py:
12561256
- "asic_type not in ['mellanox', 'vs']"
12571257
- "'dualtor' in topo_name"
12581258

1259+
ecmp/test_ecmp_balance.py:
1260+
skip:
1261+
reason: "Only support Broadcom T1/T0 topology for now"
1262+
conditions_logical_operator: or
1263+
conditions:
1264+
- "topo_type not in ['t1', 't0']"
1265+
- "asic_type not in ['broadcom']"
1266+
12591267
ecmp/test_ecmp_sai_value.py:
12601268
skip:
12611269
reason: "Only support Broadcom T1/T0 topology with 20230531 and above image, 7050cx3 T1 doesn't enable this feature"

tests/ecmp/platform_handler.py

Lines changed: 358 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,358 @@
1+
"""
2+
Platform Dependency Code (PD) - ECMP Hash Platform Handler
3+
4+
This module contains all platform-specific logic for ECMP hash testing.
5+
"""
6+
7+
import logging
8+
import pytest
9+
from abc import ABC, abstractmethod
10+
11+
logger = logging.getLogger(__name__)
12+
13+
14+
class ECMPHashPlatformHandler(ABC):
15+
"""Abstract base class for platform-specific ECMP hash operations."""
16+
17+
@abstractmethod
18+
def get_supported_skus(self):
19+
"""Return list of supported hardware SKUs for this platform."""
20+
pass
21+
22+
@abstractmethod
23+
def is_supported(self, duthost=None, hwsku=None, asic_type=None, topology=None):
24+
"""Check if the given hardware configuration is supported.
25+
26+
Args:
27+
duthost: DUT host object (optional, for extracting facts)
28+
hwsku: Hardware SKU string (optional)
29+
asic_type: ASIC type string (optional)
30+
topology: Topology name string (optional)
31+
"""
32+
pass
33+
34+
@abstractmethod
35+
def get_hash_offset_command(self, action="get", value=None):
36+
"""Get the command to read or set hash offset value."""
37+
pass
38+
39+
@abstractmethod
40+
def parse_hash_offset_output(self, output):
41+
"""Parse the hash offset value from command output."""
42+
pass
43+
44+
@abstractmethod
45+
def get_default_offset_value(self):
46+
"""Get the default hash offset value for this platform."""
47+
pass
48+
49+
50+
class BroadcomPlatformHandler(ECMPHashPlatformHandler):
51+
"""Platform handler for Broadcom-based devices."""
52+
53+
SUPPORTED_SKUS = [
54+
"Arista-7060CX-32S-C32",
55+
"Arista-7060CX-32S-D48C8",
56+
"Arista-7060CX-32S-Q32",
57+
"Arista-7260CX3-C64",
58+
"Arista-7260CX3-D108C10",
59+
"Arista-7260CX3-D108C8"
60+
]
61+
62+
DEFAULT_OFFSET = "0x1a"
63+
TEST_OFFSET = "0x1c"
64+
65+
def get_supported_skus(self):
66+
"""Return list of supported Broadcom hardware SKUs."""
67+
return self.SUPPORTED_SKUS
68+
69+
def is_supported(self, duthost=None, hwsku=None, asic_type=None, topology=None):
70+
"""Check if the given hardware configuration is supported by Broadcom platform.
71+
72+
Args:
73+
duthost: DUT host object (optional, for extracting facts)
74+
hwsku: Hardware SKU string (optional)
75+
asic_type: ASIC type string (optional)
76+
topology: Topology name string (optional)
77+
"""
78+
# Check ASIC type - must be Broadcom
79+
if asic_type and asic_type.lower() != "broadcom":
80+
logger.debug(f"ASIC type '{asic_type}' not supported by Broadcom platform handler")
81+
return False
82+
83+
# Check topology - must be t0 or t1
84+
if topology and not any(topo in topology.lower() for topo in ["t0", "t1"]):
85+
logger.info(f"Topology '{topology}' not supported by Broadcom platform handler")
86+
return False
87+
88+
# Check hardware SKU
89+
if hwsku and hwsku not in self.SUPPORTED_SKUS:
90+
logger.info(f"Hardware SKU '{hwsku}' not supported by Broadcom platform handler")
91+
return False
92+
93+
return True
94+
95+
def get_hash_offset_command(self, action="get", value=None):
96+
"""Get the BCM command to read or set ECMP hash offset value."""
97+
if action == "get":
98+
return 'bcmcmd "sc ECMPHashSet0Offset"'
99+
elif action == "set" and value:
100+
return f'bcmcmd "sc ECMPHashSet0Offset={value}"'
101+
else:
102+
raise ValueError(f"Invalid action '{action}' or missing value for set operation")
103+
104+
def parse_hash_offset_output(self, output):
105+
"""Parse the ECMP hash offset value from BCM command output."""
106+
if output.get("rc") != 0:
107+
logger.warning("Command failed to execute successfully")
108+
return None
109+
110+
for line in output.get("stdout_lines", []):
111+
if "0x" in line:
112+
return line.strip()
113+
return None
114+
115+
def get_default_offset_value(self):
116+
"""Get the default hash offset value for Broadcom platform."""
117+
return self.DEFAULT_OFFSET
118+
119+
def get_test_offset_value(self):
120+
"""Get the test hash offset value for Broadcom platform."""
121+
return self.TEST_OFFSET
122+
123+
124+
class MellanoxPlatformHandler(ECMPHashPlatformHandler):
125+
"""Platform handler for Mellanox-based devices."""
126+
127+
SUPPORTED_SKUS = [
128+
# Add Mellanox SKUs here when supported
129+
]
130+
131+
def get_supported_skus(self):
132+
"""Return list of supported Mellanox hardware SKUs."""
133+
return self.SUPPORTED_SKUS
134+
135+
def is_supported(self, duthost=None, hwsku=None, asic_type=None, topology=None):
136+
"""Check if the given hardware configuration is supported by Mellanox platform.
137+
138+
Args:
139+
duthost: DUT host object (optional, for extracting facts)
140+
hwsku: Hardware SKU string (optional)
141+
asic_type: ASIC type string (optional)
142+
topology: Topology name string (optional)
143+
"""
144+
# Check ASIC type - must be Mellanox
145+
if asic_type and asic_type.lower() != "mellanox":
146+
logger.debug(f"ASIC type '{asic_type}' not supported by Mellanox platform handler")
147+
return False
148+
149+
# Check topology - must be t0 or t1
150+
if topology and not any(topo in topology.lower() for topo in ["t0", "t1"]):
151+
logger.info(f"Topology '{topology}' not supported by Mellanox platform handler")
152+
return False
153+
154+
# Check if we have any supported SKUs at all - if not, Mellanox platform is not implemented yet
155+
if not self.SUPPORTED_SKUS:
156+
logger.info("Mellanox platform handler has no supported SKUs defined - platform not implemented yet")
157+
return False
158+
159+
# Check hardware SKU
160+
if hwsku and hwsku not in self.SUPPORTED_SKUS:
161+
logger.info(f"Hardware SKU '{hwsku}' not supported by Mellanox platform handler")
162+
return False
163+
164+
return True
165+
166+
def get_hash_offset_command(self, action="get", value=None):
167+
"""Get the Mellanox command to read or set ECMP hash offset value."""
168+
# TODO: Implement Mellanox-specific commands
169+
raise NotImplementedError("Mellanox platform support not implemented yet")
170+
171+
def parse_hash_offset_output(self, output):
172+
"""Parse the ECMP hash offset value from Mellanox command output."""
173+
# TODO: Implement Mellanox-specific parsing
174+
raise NotImplementedError("Mellanox platform support not implemented yet")
175+
176+
def get_default_offset_value(self):
177+
"""Get the default hash offset value for Mellanox platform."""
178+
# TODO: Implement Mellanox-specific default value
179+
raise NotImplementedError("Mellanox platform support not implemented yet")
180+
181+
182+
class PlatformHandlerFactory:
183+
"""Factory class to create appropriate platform handlers."""
184+
185+
_handlers = {
186+
"broadcom": BroadcomPlatformHandler,
187+
"mellanox": MellanoxPlatformHandler,
188+
}
189+
190+
@classmethod
191+
def get_handler(cls, platform_type):
192+
"""Get the appropriate platform handler."""
193+
handler_class = cls._handlers.get(platform_type.lower())
194+
if not handler_class:
195+
raise ValueError(f"Unsupported platform type: {platform_type}")
196+
return handler_class()
197+
198+
@classmethod
199+
def auto_detect_handler(cls, duthost=None, tbinfo=None):
200+
"""Auto-detect platform handler based on hardware configuration.
201+
202+
Args:
203+
duthost: DUT host object (optional, for extracting facts)
204+
tbinfo: Testbed information (optional, for extracting facts)
205+
"""
206+
if isinstance(duthost, str):
207+
hwsku = duthost
208+
duthost = None
209+
210+
# Extract platform info for better error messages
211+
if duthost and tbinfo:
212+
hwsku = duthost.facts.get('hwsku', 'unknown')
213+
asic_type = duthost.facts.get('asic_type', 'unknown')
214+
topo_type = tbinfo["topo"]["type"]
215+
else:
216+
asic_type = 'unknown'
217+
topo_type = 'unknown'
218+
219+
logger.info(f"Auto-detecting platform handler for: ASIC={asic_type}, SKU={hwsku}, Topology Type={topo_type}")
220+
221+
# Direct ASIC type to platform mapping for faster and more accurate detection
222+
asic_to_platform_map = {
223+
'broadcom': 'broadcom',
224+
'mellanox': 'mellanox'
225+
}
226+
227+
# Try direct ASIC type mapping first
228+
if asic_type and asic_type.lower() in asic_to_platform_map:
229+
platform_name = asic_to_platform_map[asic_type.lower()]
230+
if platform_name in cls._handlers:
231+
handler_class = cls._handlers[platform_name]
232+
handler = handler_class()
233+
logger.info(f"Trying {platform_name} platform handler based on ASIC type '{asic_type}'...")
234+
if handler.is_supported(duthost=duthost, hwsku=hwsku, asic_type=asic_type, topology=topo_type):
235+
logger.info(f"Auto-detected platform: {platform_name}")
236+
return handler
237+
else:
238+
# If the direct mapping fails, return None to skip the test
239+
logger.info(
240+
f"ASIC type '{asic_type}' maps to {platform_name} platform, but configuration is not supported:"
241+
f"SKU={hwsku}, Topology={topo_type}. "
242+
f"the HWSKU is not in the supported list or topo_type is not supported, "
243+
f"or the platform is not implemented yet."
244+
)
245+
return None
246+
247+
logger.info(f"ASIC type '{asic_type}' not in direct mapping, trying all available handlers...")
248+
249+
# No platform handler found - return None to skip the test
250+
logger.info(
251+
f"No platform handler found for configuration: "
252+
f"ASIC={asic_type}, SKU={hwsku}, Topology={topo_type}. "
253+
f"Available platforms: {list(cls._handlers.keys())}"
254+
)
255+
return None
256+
257+
@classmethod
258+
def register_handler(cls, platform_type, handler_class):
259+
"""Register a new platform handler."""
260+
cls._handlers[platform_type.lower()] = handler_class
261+
262+
263+
class ECMPHashManager:
264+
"""Manager class for ECMP hash operations across different platforms."""
265+
266+
def __init__(self, duthost, tbinfo=None):
267+
"""Initialize the ECMP hash manager with a DUT host.
268+
269+
Args:
270+
duthost: DUT host object
271+
tbinfo: Testbed info (optional, for topology information)
272+
"""
273+
self.duthost = duthost
274+
self.hwsku = duthost.facts['hwsku']
275+
self.asic_type = duthost.facts.get('asic_type')
276+
277+
# Extract topology from tbinfo if available, fallback to duthost facts
278+
if tbinfo:
279+
self.topology = tbinfo.get("topo", {}).get("name", "")
280+
self.topo_type = tbinfo["topo"]["type"]
281+
else:
282+
self.topology = 'unknown'
283+
self.topo_type = 'unknown'
284+
285+
self.handler = PlatformHandlerFactory.auto_detect_handler(duthost=duthost, tbinfo=tbinfo)
286+
287+
# If no handler is found, skip the test
288+
if self.handler is None:
289+
skip_msg = (
290+
f"ECMP hash test not supported on {duthost.hostname}: "
291+
f"ASIC={self.asic_type}, SKU={self.hwsku}, Topology={self.topology}, Topology Type={self.topo_type}. "
292+
f"Platform is either not implemented or not supported."
293+
)
294+
pytest.skip(skip_msg)
295+
296+
self._original_value = None
297+
298+
def is_supported(self):
299+
"""Check if the current platform supports ECMP hash offset testing."""
300+
if self.handler is None:
301+
return False
302+
return self.handler.is_supported(duthost=self.duthost,
303+
hwsku=self.hwsku,
304+
asic_type=self.asic_type,
305+
topology=self.topo_type)
306+
307+
def get_current_offset(self):
308+
"""Get the current ECMP hash offset value."""
309+
command = self.handler.get_hash_offset_command(action="get")
310+
output = self.duthost.command(command, module_ignore_errors=True)
311+
logger.info(f"ECMP hash offset command output: {output.get('stdout_lines', [])}")
312+
return self.handler.parse_hash_offset_output(output)
313+
314+
def set_offset(self, value):
315+
"""Set the ECMP hash offset value."""
316+
command = self.handler.get_hash_offset_command(action="set", value=value)
317+
logger.info(f"Setting ECMP hash offset to {value}")
318+
return self.duthost.command(command, module_ignore_errors=True)
319+
320+
def backup_current_offset(self):
321+
"""Backup the current ECMP hash offset value."""
322+
self._original_value = self.get_current_offset()
323+
if self._original_value is None:
324+
logger.warning("Could not retrieve original ECMP hash offset value")
325+
self._original_value = self.handler.get_default_offset_value()
326+
logger.info(f"Backed up original ECMP hash offset: {self._original_value}")
327+
return self._original_value
328+
329+
def restore_original_offset(self):
330+
"""Restore the original ECMP hash offset value."""
331+
if self._original_value:
332+
logger.info(f"Restoring ECMP hash offset to {self._original_value}")
333+
return self.set_offset(self._original_value)
334+
else:
335+
logger.warning("No original value to restore")
336+
return None
337+
338+
def set_test_offset(self):
339+
"""Set the ECMP hash offset to test value."""
340+
if hasattr(self.handler, 'get_test_offset_value'):
341+
test_value = self.handler.get_test_offset_value()
342+
return self.set_offset(test_value)
343+
else:
344+
raise NotImplementedError("Test offset value not defined for this platform")
345+
346+
def get_support_info(self):
347+
"""Get detailed support information for debugging.
348+
349+
Returns:
350+
dict: Dictionary with support details
351+
"""
352+
return {
353+
"hwsku": self.hwsku,
354+
"asic_type": self.asic_type,
355+
"topology": self.topology,
356+
"handler_type": type(self.handler).__name__ if self.handler else "None",
357+
"is_supported": self.is_supported()
358+
}

0 commit comments

Comments
 (0)