|
| 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