diff --git a/platform/mellanox/mlnx-platform-api/sonic_platform/chassis.py b/platform/mellanox/mlnx-platform-api/sonic_platform/chassis.py index 495be1e5a6d..b7b153eab8b 100644 --- a/platform/mellanox/mlnx-platform-api/sonic_platform/chassis.py +++ b/platform/mellanox/mlnx-platform-api/sonic_platform/chassis.py @@ -30,6 +30,7 @@ from sonic_py_common import device_info from functools import reduce from .utils import extract_RJ45_ports_index + from .utils import extract_cpo_ports_index from . import module_host_mgmt_initializer from . import utils from .device_data import DeviceDataManager @@ -120,6 +121,10 @@ def __init__(self): self._RJ45_port_inited = False self._RJ45_port_list = None + # Build the CPO port list from platform.json and hwsku.json + self._cpo_port_inited = False + self._cpo_port_list = None + Chassis.chassis_instance = self self.module_host_mgmt_initializer = module_host_mgmt_initializer.ModuleHostMgmtInitializer() @@ -139,6 +144,13 @@ def RJ45_port_list(self): self._RJ45_port_inited = True return self._RJ45_port_list + @property + def cpo_port_list(self): + if not self._cpo_port_inited: + self._cpo_port_list = extract_cpo_ports_index() + self._cpo_port_inited = True + return self._cpo_port_list + ############################################## # PSU methods ############################################## @@ -147,6 +159,9 @@ def initialize_psu(self): if not self._psu_list: from .psu import Psu, FixedPsu psu_count = DeviceDataManager.get_psu_count() + if psu_count == 0: + # For system with no PSU, for example, PDU system. + return hot_swapable = DeviceDataManager.is_psu_hotswapable() # Initialize PSU list @@ -202,9 +217,11 @@ def initialize_fan(self): if not self._fan_drawer_list: from .fan import Fan from .fan_drawer import RealDrawer, VirtualDrawer - - hot_swapable = DeviceDataManager.is_fan_hotswapable() drawer_num = DeviceDataManager.get_fan_drawer_count() + if drawer_num == 0: + # For system with no fan, for example, liquid cooling system. + return + hot_swapable = DeviceDataManager.is_fan_hotswapable() fan_num = DeviceDataManager.get_fan_count() fan_num_per_drawer = fan_num // drawer_num drawer_ctor = RealDrawer if hot_swapable else VirtualDrawer @@ -277,6 +294,8 @@ def initialize_single_sfp(self, index): sfp_module = self._import_sfp_module() if self.RJ45_port_list and index in self.RJ45_port_list: self._sfp_list[index] = sfp_module.RJ45Port(index) + elif self.cpo_port_list and index in self.cpo_port_list: + self._sfp_list[index] = sfp_module.CpoPort(index) else: self._sfp_list[index] = sfp_module.SFP(index) self.sfp_initialized_count += 1 @@ -294,6 +313,8 @@ def initialize_sfp(self): for index in range(sfp_count): if self.RJ45_port_list and index in self.RJ45_port_list: sfp_object = sfp_module.RJ45Port(index) + elif self.cpo_port_list and index in self.cpo_port_list: + sfp_object = sfp_module.CpoPort(index) else: sfp_object = sfp_module.SFP(index) self._sfp_list.append(sfp_object) @@ -304,6 +325,8 @@ def initialize_sfp(self): if self._sfp_list[index] is None: if self.RJ45_port_list and index in self.RJ45_port_list: self._sfp_list[index] = sfp_module.RJ45Port(index) + elif self.cpo_port_list and index in self.cpo_port_list: + self._sfp_list[index] = sfp_module.CpoPort(index) else: self._sfp_list[index] = sfp_module.SFP(index) self.sfp_initialized_count = len(self._sfp_list) @@ -315,13 +338,22 @@ def get_num_sfps(self): Returns: An integer, the number of sfps available on this chassis """ + num_sfps = 0 if not self._RJ45_port_inited: self._RJ45_port_list = extract_RJ45_ports_index() self._RJ45_port_inited = True + + if not self._cpo_port_inited: + self._cpo_port_list = extract_cpo_ports_index() + self._cpo_port_inited = True + + num_sfps = DeviceDataManager.get_sfp_count() if self._RJ45_port_list is not None: - return DeviceDataManager.get_sfp_count() + len(self._RJ45_port_list) - else: - return DeviceDataManager.get_sfp_count() + num_sfps += len(self._RJ45_port_list) + if self._cpo_port_list is not None: + num_sfps += len(self._cpo_port_list) + + return num_sfps def get_all_sfps(self): """ diff --git a/platform/mellanox/mlnx-platform-api/sonic_platform/device_data.py b/platform/mellanox/mlnx-platform-api/sonic_platform/device_data.py index a758d08e3d2..1e4b41eb675 100644 --- a/platform/mellanox/mlnx-platform-api/sonic_platform/device_data.py +++ b/platform/mellanox/mlnx-platform-api/sonic_platform/device_data.py @@ -229,13 +229,22 @@ def get_simx_version(cls): version = check_output_pipe(["lspci", "-vv"], ["grep", "SimX"]) parsed_version = re.search("([0-9]+\\.[0-9]+-[0-9]+)", version) return parsed_version.group(1) if parsed_version else "N/A" + + @classmethod + @utils.read_only_cache() + def get_fan_drawer_sysfs_count(cls): + return len(glob.glob('/run/hw-management/thermal/fan*_status')) @classmethod @utils.read_only_cache() def get_fan_drawer_count(cls): # Here we don't read from /run/hw-management/config/hotplug_fans because the value in it is not # always correct. - return len(glob.glob('/run/hw-management/thermal/fan*_status')) if cls.is_fan_hotswapable() else 1 + fan_status_count = cls.get_fan_drawer_sysfs_count() + if fan_status_count == 0: + # For system with no fan, for example, liquid cooling system. + return 0 + return fan_status_count if cls.is_fan_hotswapable() else 1 @classmethod @utils.read_only_cache() diff --git a/platform/mellanox/mlnx-platform-api/sonic_platform/sfp.py b/platform/mellanox/mlnx-platform-api/sonic_platform/sfp.py index 307eae7164a..5f1db70e6d3 100644 --- a/platform/mellanox/mlnx-platform-api/sonic_platform/sfp.py +++ b/platform/mellanox/mlnx-platform-api/sonic_platform/sfp.py @@ -36,7 +36,11 @@ from .device_data import DeviceDataManager from sonic_platform_base.sonic_xcvr.sfp_optoe_base import SfpOptoeBase from sonic_platform_base.sonic_xcvr.fields import consts - from sonic_platform_base.sonic_xcvr.api.public import cmis, sff8636, sff8436 + from sonic_platform_base.sonic_xcvr.api.public import sff8636, sff8436 + + from sonic_platform_base.sonic_xcvr.api.public import cmis as cmis_api + from sonic_platform_base.sonic_xcvr.codes.public import cmis as cmis_codes + from sonic_platform_base.sonic_xcvr.mem_maps.public import cmis as cmis_mem except ImportError as e: raise ImportError (str(e) + "- required module not found") @@ -72,6 +76,7 @@ ] RJ45_TYPE = "RJ45" +CPO_TYPE = "CPO" #variables for sdk REGISTER_NUM = 1 @@ -461,10 +466,10 @@ def wait_sfp_eeprom_ready(cls, sfp_list, wait_time): for s in not_ready_list: logger.log_error(f'SFP {s.sdk_index} eeprom is not ready') - # read eeprom specfic bytes beginning from offset with size as num_bytes + # read eeprom specific bytes beginning from offset with size as num_bytes def read_eeprom(self, offset, num_bytes): """ - Read eeprom specfic bytes beginning from a random offset with size as num_bytes + Read eeprom specific bytes beginning from a random offset with size as num_bytes Returns: bytearray, if raw sequence of bytes are read correctly from the offset of size num_bytes None, if the read_eeprom fails @@ -472,7 +477,7 @@ def read_eeprom(self, offset, num_bytes): return self._read_eeprom(offset, num_bytes) def _read_eeprom(self, offset, num_bytes, log_on_error=True): - """Read eeprom specfic bytes beginning from a random offset with size as num_bytes + """Read eeprom specific bytes beginning from a random offset with size as num_bytes Args: offset (int): read offset @@ -520,10 +525,10 @@ def _read_eeprom(self, offset, num_bytes, log_on_error=True): return bytearray(result) - # write eeprom specfic bytes beginning from offset with size as num_bytes + # write eeprom specific bytes beginning from offset with size as num_bytes def write_eeprom(self, offset, num_bytes, write_buffer): """ - write eeprom specfic bytes beginning from a random offset with size as num_bytes + write eeprom specific bytes beginning from a random offset with size as num_bytes and write_buffer as the required bytes Returns: Boolean, true if the write succeeded and false if it did not succeed. @@ -1086,7 +1091,7 @@ def is_cmis_api(self, xcvr_api): Returns: bool: True if the api is of type CMIS """ - return isinstance(xcvr_api, cmis.CmisApi) + return isinstance(xcvr_api, cmis_api.CmisApi) def is_sff_api(self, xcvr_api): """Check if the api type is SFF @@ -1811,3 +1816,36 @@ def get_module_status(self): """ status = super().get_module_status() return SFP_STATUS_REMOVED if status == SFP_STATUS_UNKNOWN else status + + +class CpoPort(SFP): + """class derived from SFP, representing CPO ports""" + + def __init__(self, sfp_index): + super(CpoPort, self).__init__(sfp_index) + self._sfp_type_str = None + self.sfp_type = CPO_TYPE + + def get_transceiver_info(self): + transceiver_info_dict = super().get_transceiver_info() + transceiver_info_dict['type'] = self.sfp_type + return transceiver_info_dict + + def get_xcvr_api(self): + if self._xcvr_api is None: + self._xcvr_api = self._xcvr_api_factory._create_api(cmis_codes.CmisCodes, cmis_mem.CmisMemMap, cmis_api.CmisApi) + return self._xcvr_api + + def get_presence(self): + file_path = SFP_SDK_MODULE_SYSFS_ROOT_TEMPLATE.format(self.sdk_index) + SFP_SYSFS_PRESENT + present = utils.read_int_from_file(file_path) + return present == 1 + + def reinit(self): + """ + Nothing to do for cpo. Just provide it to avoid exception + :return: + """ + return + + diff --git a/platform/mellanox/mlnx-platform-api/sonic_platform/thermal.py b/platform/mellanox/mlnx-platform-api/sonic_platform/thermal.py index e4a48784ffa..613de8ef7ef 100644 --- a/platform/mellanox/mlnx-platform-api/sonic_platform/thermal.py +++ b/platform/mellanox/mlnx-platform-api/sonic_platform/thermal.py @@ -85,11 +85,13 @@ }, { "name": "Ambient Port Side Temp", - "temperature": "port_amb" + "temperature": "port_amb", + "default_present": True }, { "name": "Ambient Fan Side Temp", - "temperature": "fan_amb" + "temperature": "fan_amb", + "default_present": True }, { "name": "Ambient COMEX Temp", @@ -139,6 +141,15 @@ "search_pattern": '/run/hw-management/thermal/sodimm*_temp_input', 'index_pattern': r'sodimm(\d+)_temp_input', "type": "discrete", + }, + { + "name": "PMIC {} Temp", + "temperature": "voltmon{}_temp1_input", + "high_threshold": "voltmon{}_temp1_max", + "high_critical_threshold": "voltmon{}_temp1_crit", + "search_pattern": '/run/hw-management/thermal/voltmon*_temp1_input', + 'index_pattern': r'voltmon(\d+)_temp1_input', + "type": "discrete", } ], 'linecard thermals': { diff --git a/platform/mellanox/mlnx-platform-api/sonic_platform/utils.py b/platform/mellanox/mlnx-platform-api/sonic_platform/utils.py index ed638526375..a66bfd964e3 100644 --- a/platform/mellanox/mlnx-platform-api/sonic_platform/utils.py +++ b/platform/mellanox/mlnx-platform-api/sonic_platform/utils.py @@ -32,6 +32,7 @@ PORT_INDEX_KEY = "index" PORT_TYPE_KEY = "port_type" RJ45_PORT_TYPE = "RJ45" +CPO_PORT_TYPE = "CPO" logger = Logger() @@ -239,19 +240,14 @@ def load_json_file(filename, log_func=logger.log_error): return None -def extract_RJ45_ports_index(): - # Cross check 'platform.json' and 'hwsku.json' to extract the RJ45 port index if exists. - hwsku_path = device_info.get_path_to_hwsku_dir() - hwsku_file = os.path.join(hwsku_path, HWSKU_JSON) - if not os.path.exists(hwsku_file): - # Platforms having no hwsku.json do not have RJ45 port +def _extract_ports_index_by_type(port_type, num_of_asics=1): + platform_file = os.path.join(device_info.get_path_to_platform_dir(), device_info.PLATFORM_JSON_FILE) + if not os.path.exists(platform_file): return None - platform_file = device_info.get_path_to_port_config_file() platform_dict = load_json_file(platform_file)['interfaces'] - hwsku_dict = load_json_file(hwsku_file)['interfaces'] port_name_to_index_map_dict = {} - RJ45_port_index_list = [] + port_index_list = [] # Compose a interface name to index mapping from 'platform.json' for i, (key, value) in enumerate(platform_dict.items()): @@ -264,12 +260,37 @@ def extract_RJ45_ports_index(): if not bool(port_name_to_index_map_dict): return None - # Check if "port_type" specified as "RJ45", if yes, add the port index to the list. + hwsku_jsons = get_path_list_to_asic_hwsku_dir(num_of_asics) + hwsku_dict = {} + for hwsku_json in hwsku_jsons: + hwsku_dict.update(load_json_file(hwsku_json)['interfaces']) + + # Check if "port_type" matches, if yes, add the port index to the list. for i, (key, value) in enumerate(hwsku_dict.items()): - if key in port_name_to_index_map_dict and PORT_TYPE_KEY in value and value[PORT_TYPE_KEY] == RJ45_PORT_TYPE: - RJ45_port_index_list.append(int(port_name_to_index_map_dict[key])-1) + if key in port_name_to_index_map_dict and PORT_TYPE_KEY in value and value[PORT_TYPE_KEY] == port_type: + port_index_list.append(int(port_name_to_index_map_dict[key]) - 1) + + # Remove duplicates + port_index_list = list(dict.fromkeys(port_index_list)) + + return port_index_list if port_index_list else None + + +def get_path_list_to_asic_hwsku_dir(num_of_asics): + platform_path = device_info.get_path_to_platform_dir() + hwsku = device_info.get_hwsku() + if num_of_asics == 1: + return [os.path.join(platform_path, hwsku, HWSKU_JSON)] + else: + return [os.path.join(platform_path, hwsku, str(asic_id), HWSKU_JSON) for asic_id in range(num_of_asics)] + + +def extract_RJ45_ports_index(num_of_asics=1): + return _extract_ports_index_by_type(RJ45_PORT_TYPE, num_of_asics) + - return RJ45_port_index_list if bool(RJ45_port_index_list) else None +def extract_cpo_ports_index(num_of_asics=1): + return _extract_ports_index_by_type(CPO_PORT_TYPE, num_of_asics) def wait_until(predict, timeout, interval=1, *args, **kwargs): diff --git a/platform/mellanox/mlnx-platform-api/tests/test_change_event.py b/platform/mellanox/mlnx-platform-api/tests/test_change_event.py index 3dca257a663..e253bb34ec1 100644 --- a/platform/mellanox/mlnx-platform-api/tests/test_change_event.py +++ b/platform/mellanox/mlnx-platform-api/tests/test_change_event.py @@ -39,6 +39,7 @@ class TestChangeEvent: @mock.patch('sonic_platform.device_data.DeviceDataManager.is_module_host_management_mode', mock.MagicMock(return_value=False)) @mock.patch('sonic_platform.device_data.DeviceDataManager.get_sfp_count', mock.MagicMock(return_value=1)) @mock.patch('sonic_platform.chassis.extract_RJ45_ports_index', mock.MagicMock(return_value=[])) + @mock.patch('sonic_platform.chassis.extract_cpo_ports_index', mock.MagicMock(return_value=[])) @mock.patch('sonic_platform.sfp.SFP.get_module_status') def test_get_change_event_legacy(self, mock_status, mock_time, mock_create_poll, mock_get_fd): c = chassis.Chassis() @@ -92,6 +93,7 @@ def test_get_change_event_legacy(self, mock_status, mock_time, mock_create_poll, @mock.patch('sonic_platform.device_data.DeviceDataManager.is_module_host_management_mode', mock.MagicMock(return_value=True)) @mock.patch('sonic_platform.device_data.DeviceDataManager.get_sfp_count', mock.MagicMock(return_value=1)) @mock.patch('sonic_platform.chassis.extract_RJ45_ports_index', mock.MagicMock(return_value=[])) + @mock.patch('sonic_platform.chassis.extract_cpo_ports_index', mock.MagicMock(return_value=[])) @mock.patch('sonic_platform.module_host_mgmt_initializer.ModuleHostMgmtInitializer.initialize', mock.MagicMock()) def test_get_change_event_for_module_host_management_mode(self, mock_time, mock_create_poll, mock_get_fd, mock_ready): """Test steps: diff --git a/platform/mellanox/mlnx-platform-api/tests/test_chassis.py b/platform/mellanox/mlnx-platform-api/tests/test_chassis.py index f07f164b730..63e06d49a0f 100644 --- a/platform/mellanox/mlnx-platform-api/tests/test_chassis.py +++ b/platform/mellanox/mlnx-platform-api/tests/test_chassis.py @@ -1,6 +1,6 @@ # # SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES -# Copyright (c) 2021-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright (c) 2021-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -39,6 +39,7 @@ from sonic_platform.device_data import DeviceDataManager sonic_platform.chassis.extract_RJ45_ports_index = mock.MagicMock(return_value=[]) +sonic_platform.chassis.extract_cpo_ports_index = mock.MagicMock(return_value=[]) class TestChassis: """Test class to test chassis.py. The test cases covers: @@ -87,10 +88,12 @@ def test_psu(self): chassis._psu_list = [] assert chassis.get_num_psus() == 3 - def test_fan(self): + @mock.patch('sonic_platform.device_data.DeviceDataManager.get_fan_drawer_sysfs_count') + def test_fan(self, mock_sysfs_count): from sonic_platform.fan_drawer import RealDrawer, VirtualDrawer # Test creating fixed fan + mock_sysfs_count.return_value = 4 DeviceDataManager.is_fan_hotswapable = mock.MagicMock(return_value=False) assert DeviceDataManager.get_fan_drawer_count() == 1 DeviceDataManager.get_fan_count = mock.MagicMock(return_value=4) @@ -179,6 +182,13 @@ def test_sfp(self): assert chassis.get_num_sfps() == 6 sonic_platform.chassis.extract_RJ45_ports_index = mock.MagicMock(return_value=[]) + # Get all SFPs, with CPO ports + sonic_platform.chassis.extract_cpo_ports_index = mock.MagicMock(return_value=[3, 4]) + DeviceDataManager.get_sfp_count = mock.MagicMock(return_value=3) + chassis = Chassis() + assert chassis.get_num_sfps() == 5 + sonic_platform.chassis.extract_cpo_ports_index = mock.MagicMock(return_value=[]) + @mock.patch('sonic_platform.device_data.DeviceDataManager.is_module_host_management_mode', mock.MagicMock(return_value=False)) def test_create_sfp_in_multi_thread(self): DeviceDataManager.get_sfp_count = mock.MagicMock(return_value=3) diff --git a/platform/mellanox/mlnx-platform-api/tests/test_module_initializer.py b/platform/mellanox/mlnx-platform-api/tests/test_module_initializer.py index ad833a70f85..05612654e9b 100644 --- a/platform/mellanox/mlnx-platform-api/tests/test_module_initializer.py +++ b/platform/mellanox/mlnx-platform-api/tests/test_module_initializer.py @@ -1,5 +1,6 @@ # -# Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. +# SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES +# Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -59,9 +60,9 @@ def test_wait_module_ready(self, mock_is_host, mock_wait, mock_exists): mock_exists.return_value = False initializer.wait_module_ready() assert not initializer.initialized - @mock.patch('sonic_platform.chassis.extract_RJ45_ports_index', mock.MagicMock(return_value=[])) + @mock.patch('sonic_platform.chassis.extract_cpo_ports_index', mock.MagicMock(return_value=[])) @mock.patch('sonic_platform.device_data.DeviceDataManager.get_sfp_count', mock.MagicMock(return_value=1)) @mock.patch('sonic_platform.sfp.SFP.initialize_sfp_modules', mock.MagicMock()) @mock.patch('sonic_platform.module_host_mgmt_initializer.ModuleHostMgmtInitializer.is_initialization_owner') diff --git a/platform/mellanox/mlnx-platform-api/tests/test_sfp.py b/platform/mellanox/mlnx-platform-api/tests/test_sfp.py index a3efb395bce..b6afca155ea 100644 --- a/platform/mellanox/mlnx-platform-api/tests/test_sfp.py +++ b/platform/mellanox/mlnx-platform-api/tests/test_sfp.py @@ -29,7 +29,7 @@ modules_path = os.path.dirname(test_path) sys.path.insert(0, modules_path) -from sonic_platform.sfp import SFP, RJ45Port, SX_PORT_MODULE_STATUS_INITIALIZING, SX_PORT_MODULE_STATUS_PLUGGED, SX_PORT_MODULE_STATUS_UNPLUGGED, SX_PORT_MODULE_STATUS_PLUGGED_WITH_ERROR, SX_PORT_MODULE_STATUS_PLUGGED_DISABLED +from sonic_platform.sfp import SFP, RJ45Port, CpoPort, CPO_TYPE, cmis_api, SX_PORT_MODULE_STATUS_INITIALIZING, SX_PORT_MODULE_STATUS_PLUGGED, SX_PORT_MODULE_STATUS_UNPLUGGED, SX_PORT_MODULE_STATUS_PLUGGED_WITH_ERROR, SX_PORT_MODULE_STATUS_PLUGGED_DISABLED from sonic_platform.chassis import Chassis @@ -319,6 +319,18 @@ def test_rj45_basic(self): assert sfp.get_transceiver_threshold_info() sfp.reinit() + @mock.patch('sonic_platform.sfp.CpoPort.read_eeprom') + def test_cpo_get_xcvr_api(self, mock_read): + sfp = CpoPort(0) + api = sfp.get_xcvr_api() + assert isinstance(api, cmis_api.CmisApi) + + @mock.patch('sonic_platform.sfp.SfpOptoeBase.get_transceiver_info', return_value={}) + def test_cpo_get_transceiver_info(self, mock_get_info): + sfp = CpoPort(0) + info = sfp.get_transceiver_info() + assert info['type'] == CPO_TYPE + @mock.patch('os.path.exists') @mock.patch('sonic_platform.utils.read_int_from_file') def test_get_temperature(self, mock_read, mock_exists): @@ -507,6 +519,7 @@ def test_get_error_info_from_sdk_error_type(self, mock_read): assert error_desc is None @mock.patch('sonic_platform.chassis.extract_RJ45_ports_index', mock.MagicMock(return_value=[])) + @mock.patch('sonic_platform.chassis.extract_cpo_ports_index', mock.MagicMock(return_value=[])) @mock.patch('sonic_platform.device_data.DeviceDataManager.get_sfp_count', mock.MagicMock(return_value=1)) def test_initialize_sfp_modules(self): c = Chassis() diff --git a/platform/mellanox/mlnx-platform-api/tests/test_thermal.py b/platform/mellanox/mlnx-platform-api/tests/test_thermal.py index f080462ae10..e45f16ec6cb 100644 --- a/platform/mellanox/mlnx-platform-api/tests/test_thermal.py +++ b/platform/mellanox/mlnx-platform-api/tests/test_thermal.py @@ -40,12 +40,18 @@ class TestThermal: @mock.patch('os.path.exists', mock.MagicMock(return_value=True)) @mock.patch('sonic_platform.device_data.DeviceDataManager.get_gearbox_count', mock.MagicMock(return_value=2)) @mock.patch('sonic_platform.device_data.DeviceDataManager.get_cpu_thermal_count', mock.MagicMock(return_value=2)) - @mock.patch('sonic_platform.thermal.glob.iglob', mock.MagicMock( - return_value=['/run/hw-management/thermal/sodimm1_temp_input', - '/run/hw-management/thermal/sodimm2_temp_input'])) + @mock.patch('sonic_platform.thermal.glob.iglob') @mock.patch('sonic_platform.device_data.DeviceDataManager.get_platform_name', mock.MagicMock(return_value='x86_64-mlnx_msn2700-r0')) - def test_chassis_thermal(self): + def test_chassis_thermal(self, mock_glob): from sonic_platform.thermal import THERMAL_NAMING_RULE + def mocked_glob(pattern): + if 'sodimm' in pattern: + return ['/run/hw-management/thermal/sodimm1_temp_input', + '/run/hw-management/thermal/sodimm2_temp_input'] + elif 'voltmon' in pattern: + return ['/run/hw-management/thermal/voltmon1_temp1_input', + '/run/hw-management/thermal/voltmon2_temp1_input'] + mock_glob.side_effect = mocked_glob chassis = Chassis() thermal_list = chassis.get_all_thermals() assert thermal_list diff --git a/platform/mellanox/mlnx-platform-api/tests/test_utils.py b/platform/mellanox/mlnx-platform-api/tests/test_utils.py index b6ec67975f8..e79641496be 100644 --- a/platform/mellanox/mlnx-platform-api/tests/test_utils.py +++ b/platform/mellanox/mlnx-platform-api/tests/test_utils.py @@ -1,5 +1,6 @@ # -# Copyright (c) 2021-2023 NVIDIA CORPORATION & AFFILIATES. +# SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES +# Copyright (c) 2021-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -128,10 +129,11 @@ def test_run_command_exception(self): output = utils.run_command(['ls']) assert output + @mock.patch('sonic_py_common.device_info.get_path_to_platform_dir', mock.MagicMock(return_value='.')) + @mock.patch('sonic_py_common.device_info.get_path_to_hwsku_dir', mock.MagicMock(return_value='/tmp')) + @mock.patch('sonic_py_common.device_info.get_hwsku', mock.MagicMock(return_value='')) @mock.patch('sonic_platform.utils.load_json_file') @mock.patch('os.path.exists') - @mock.patch('sonic_py_common.device_info.get_path_to_port_config_file', mock.MagicMock(return_value='')) - @mock.patch('sonic_py_common.device_info.get_path_to_hwsku_dir', mock.MagicMock(return_value='/tmp')) def test_extract_RJ45_ports_index(self, mock_exists, mock_load_json): mock_exists.return_value = False rj45_list = utils.extract_RJ45_ports_index() @@ -161,6 +163,93 @@ def test_extract_RJ45_ports_index(self, mock_exists, mock_load_json): mock_load_json.side_effect = [platform_json, hwsku_json] assert utils.extract_RJ45_ports_index() == [0] + @pytest.mark.parametrize( + "mock_exists_value, platform_json, hwsku_json, expected_result", + [ + # Case 1: hwsku.json file does not exist + (False, {}, {}, None), + + # Case 2: file exists, but no CPO ports + ( + True, + { + 'interfaces': { + "Ethernet0": { + "index": "1", + "lanes": "0", + "breakout_modes": { + "2x400G[200G]": ["etp1a", "etp1b"] + } + } + } + }, + { + 'interfaces': { + "Ethernet0": { + "default_brkout_mode": "2x400G[200G]", + "port_type": "SFP" + } + } + }, + None + ), + + # Case 3: one CPO port + ( + True, + { + 'interfaces': { + "Ethernet0": { + "index": "1", + "lanes": "0", + "breakout_modes": { + "2x400G[200G]": ["etp1a", "etp1b"] + } + } + } + }, + { + 'interfaces': { + "Ethernet0": { + "default_brkout_mode": "2x400G[200G]", + "port_type": "CPO" + } + } + }, + [0] + ), + + # Case 4: multiple CPO ports + ( + True, + { + 'interfaces': { + "Ethernet0": {"index": "1"}, + "Ethernet4": {"index": "5"}, + "Ethernet8": {"index": "9"} + } + }, + { + 'interfaces': { + "Ethernet0": {"port_type": "CPO"}, + "Ethernet4": {"port_type": "CPO"}, + "Ethernet8": {"port_type": "CPO"} + } + }, + [0, 4, 8] + ), + ] + ) + @mock.patch('sonic_py_common.device_info.get_path_to_platform_dir', mock.MagicMock(return_value='.')) + @mock.patch('sonic_py_common.device_info.get_path_to_hwsku_dir', mock.MagicMock(return_value='/tmp')) + @mock.patch('sonic_py_common.device_info.get_hwsku', mock.MagicMock(return_value='')) + @mock.patch('sonic_platform.utils.load_json_file') + @mock.patch('os.path.exists') + def test_extract_cpo_ports_index(self, mock_exists, mock_load_json, mock_exists_value, platform_json, hwsku_json, expected_result): + mock_exists.return_value = mock_exists_value + mock_load_json.side_effect = [platform_json, hwsku_json] + assert utils.extract_cpo_ports_index() == expected_result + def test_wait_until(self): values = [] assert utils.wait_until(lambda: len(values) == 0, timeout=1)