diff --git a/platform/mellanox/mlnx-platform-api/sonic_platform/chassis.py b/platform/mellanox/mlnx-platform-api/sonic_platform/chassis.py index 495be1e5a6d..f8547c57fce 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 ############################################## @@ -277,6 +289,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 +308,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 +320,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 +333,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): """ @@ -330,7 +357,7 @@ def get_all_sfps(self): Returns: A list of objects derived from SfpBase representing all sfps available on this chassis - """ + """ if DeviceDataManager.is_module_host_management_mode(): self.module_host_mgmt_initializer.initialize(self) else: @@ -410,7 +437,7 @@ def get_change_event(self, timeout=0): else: self.initialize_sfp() return self.get_change_event_legacy(timeout) - + def get_change_event_for_module_host_management_mode(self, timeout): """Get SFP change event when module host management mode is enabled. @@ -442,9 +469,9 @@ def get_change_event_for_module_host_management_mode(self, timeout): self.registered_fds[fd.fileno()] = (s.sdk_index, fd, fd_type) logger.log_debug(f'Registered SFP file descriptors for polling: {self.registered_fds}') - + from . import sfp - + wait_forever = (timeout == 0) # poll timeout should be no more than 1000ms to ensure fast shutdown flow timeout = 1000.0 if timeout >= 1000 else float(timeout) @@ -452,14 +479,14 @@ def get_change_event_for_module_host_management_mode(self, timeout): error_dict = {} begin = time.monotonic() wait_ready_task = sfp.SFP.get_wait_ready_task() - - while True: + + while True: fds_events = self.poll_obj.poll(timeout) for fileno, _ in fds_events: if fileno not in self.registered_fds: logger.log_error(f'Unknown file no {fileno} from poll event, registered files are {self.registered_fds}') continue - + sfp_index, fd, fd_type = self.registered_fds[fileno] s = self._sfp_list[sfp_index] fd.seek(0) @@ -483,7 +510,7 @@ def get_change_event_for_module_host_management_mode(self, timeout): # FW control cable got an error, no need trigger state machine sfp_status, error_desc = s.get_error_info_from_sdk_error_type() port_dict[sfp_index + 1] = sfp_status - if error_desc: + if error_desc: error_dict[sfp_index + 1] = error_desc continue elif str(fd_value) == sfp.SFP_STATUS_INSERTED: @@ -499,14 +526,14 @@ def get_change_event_for_module_host_management_mode(self, timeout): # event could be EVENT_POWER_GOOD or EVENT_POWER_BAD event = sfp.EVENT_POWER_BAD if fd_value == 0 else sfp.EVENT_POWER_GOOD s.on_event(event) - + if s.in_stable_state(): self.sfp_module.SFP.wait_sfp_eeprom_ready([s], 2) s.fill_change_event(port_dict) s.refresh_poll_obj(self.poll_obj, self.registered_fds) else: logger.log_debug(f'SFP {sfp_index} does not reach stable state, state={s.state}') - + ready_sfp_set = wait_ready_task.get_ready_set() for sfp_index in ready_sfp_set: s = self._sfp_list[sfp_index] @@ -517,7 +544,7 @@ def get_change_event_for_module_host_management_mode(self, timeout): s.refresh_poll_obj(self.poll_obj, self.registered_fds) else: logger.log_error(f'SFP {sfp_index} failed to reach stable state, state={s.state}') - + if port_dict: logger.log_notice(f'Sending SFP change event: {port_dict}, error event: {error_dict}') self.reinit_sfps(port_dict) @@ -564,23 +591,23 @@ def get_change_event_legacy(self, timeout): self.sfp_states_before_first_poll[s.sdk_index] = s.get_module_status() logger.log_debug(f'Registered SFP file descriptors for polling: {self.registered_fds}') - + from . import sfp - + wait_forever = (timeout == 0) # poll timeout should be no more than 1000ms to ensure fast shutdown flow timeout = 1000.0 if timeout >= 1000 else float(timeout) port_dict = {} error_dict = {} begin = time.monotonic() - + while True: fds_events = self.poll_obj.poll(timeout) for fileno, _ in fds_events: if fileno not in self.registered_fds: logger.log_error(f'Unknown file no {fileno} from poll event, registered files are {self.registered_fds}') continue - + sfp_index, fd = self.registered_fds[fileno] fd.seek(0) fd.read() @@ -948,7 +975,7 @@ def _parse_vpd_data(self, filename): return result result = utils.read_key_value_file(filename, delimeter=": ") - + except Exception as e: logger.log_error("Fail to decode vpd_data {} due to {}".format(filename, repr(e))) diff --git a/platform/mellanox/mlnx-platform-api/sonic_platform/sfp.py b/platform/mellanox/mlnx-platform-api/sonic_platform/sfp.py index 307eae7164a..dd448626802 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 @@ -120,8 +125,8 @@ # 0x1 plug in # 0x2 plug out # 0x3 plug in with error -# 0x4 disabled, at this status SFP eeprom is not accessible, -# and presence status also will be not present, +# 0x4 disabled, at this status SFP eeprom is not accessible, +# and presence status also will be not present, # so treate it as plug out. SDK_SFP_STATE_IN = 0x1 SDK_SFP_STATE_OUT = 0x2 @@ -272,7 +277,7 @@ class NvidiaSFPCommon(SfpOptoeBase): sfp_index_to_logical_port_dict = {} sfp_index_to_logical_lock = threading.Lock() - + SFP_MLNX_ERROR_DESCRIPTION_LONGRANGE_NON_MLNX_CABLE = 'Long range for non-Mellanox cable or module' SFP_MLNX_ERROR_DESCRIPTION_ENFORCE_PART_NUMBER_LIST = 'Enforce part number list' SFP_MLNX_ERROR_DESCRIPTION_PMD_TYPE_NOT_ENABLED = 'PMD type not enabled' @@ -375,19 +380,19 @@ def get_error_info_from_sdk_error_type(self): error_description = NvidiaSFPCommon.SDK_ERRORS_TO_DESCRIPTION.get(error_type) sfp_state = str(sfp_state_bits) return sfp_state, error_description - + class SFP(NvidiaSFPCommon): """Platform-specific SFP class""" shared_sdk_handle = None - + # Class level state machine object, only applicable for module host management sm = None - + # Class level wait SFP ready task, the task waits for module to load its firmware after resetting, # only applicable for module host management wait_ready_task = None - + # Class level action table which stores the mapping from action name to action function, # only applicable for module host management action_table = None @@ -445,11 +450,11 @@ def get_presence(self): return False eeprom_raw = self._read_eeprom(0, 1, log_on_error=False) return eeprom_raw is not None - + @classmethod def wait_sfp_eeprom_ready(cls, sfp_list, wait_time): not_ready_list = sfp_list - + while wait_time > 0: not_ready_list = [s for s in not_ready_list if s.state == STATE_FW_CONTROL and s._read_eeprom(0, 2,False) is None] if not_ready_list: @@ -457,7 +462,7 @@ def wait_sfp_eeprom_ready(cls, sfp_list, wait_time): wait_time -= 0.1 else: return - + for s in not_ready_list: logger.log_error(f'SFP {s.sdk_index} eeprom is not ready') @@ -841,7 +846,7 @@ def get_tx_fault(self): except Exception as e: print(e) return [False] * api.NUM_CHANNELS if api else None - + def reinit_if_sn_changed(self): """Reinitialize the SFP if the module ID has changed """ @@ -853,7 +858,7 @@ def reinit_if_sn_changed(self): self.temp_critical_threshold = None return True return False - + def get_temperature_info(self): """Get SFP temperature info in a fast way. This function is faster than calling following functions one by one: get_temperature, get_temperature_warning_threshold, get_temperature_critical_threshold. @@ -931,7 +936,7 @@ def get_temperature_warning_threshold(self): self.is_sw_control() except: return 0.0 - + self.temp_high_threshold = self._get_temperature_threshold(consts.TEMP_HIGH_WARNING_FIELD) return self.temp_high_threshold @@ -953,7 +958,7 @@ def get_temperature_critical_threshold(self): def _get_temperature_threshold(self, thresh_field): """Get temperature thresholds data from EEPROM - + Args: thresh_field (str): threshold field name @@ -990,12 +995,12 @@ def is_sw_control(self): if not DeviceDataManager.is_module_host_management_mode(): return False try: - return utils.read_int_from_file(f'/sys/module/sx_core/asic0/module{self.sdk_index}/control', + return utils.read_int_from_file(f'/sys/module/sx_core/asic0/module{self.sdk_index}/control', raise_exception=True, log_func=None) == 1 except: # just in case control file does not exist raise Exception(f'control sysfs for SFP {self.sdk_index} does not exist') - + def get_hw_present(self): """Get hardware present status, only applicable on host management mode @@ -1003,7 +1008,7 @@ def get_hw_present(self): bool: True if module is in the cage """ return utils.read_int_from_file(f'/sys/module/sx_core/asic0/module{self.sdk_index}/hw_present') == 1 - + def get_power_on(self): """Get power on status, only applicable on host management mode @@ -1011,7 +1016,7 @@ def get_power_on(self): bool: True if the module is powered on """ return utils.read_int_from_file(f'/sys/module/sx_core/asic0/module{self.sdk_index}/power_on') == 1 - + def set_power(self, on): """Control the power of this module, only applicable on host management mode @@ -1020,7 +1025,7 @@ def set_power(self, on): """ value = 1 if on else 0 utils.write_file(f'/sys/module/sx_core/asic0/module{self.sdk_index}/power_on', value) - + def get_reset_state(self): """Get reset state of this module, only applicable on host management mode @@ -1028,7 +1033,7 @@ def get_reset_state(self): bool: True if module is not in reset status """ return utils.read_int_from_file(f'/sys/module/sx_core/asic0/module{self.sdk_index}/hw_reset') == 1 - + def set_hw_reset(self, value): """Set the module reset status @@ -1036,7 +1041,7 @@ def set_hw_reset(self, value): value (int): 1 for reset, 0 for leaving reset """ utils.write_file(f'/sys/module/sx_core/asic0/module{self.sdk_index}/hw_reset', value) - + def get_power_good(self): """Get power good status of this module, only applicable on host management mode @@ -1044,7 +1049,7 @@ def get_power_good(self): bool: True if the power is in good status """ return utils.read_int_from_file(f'/sys/module/sx_core/asic0/module{self.sdk_index}/power_good') == 1 - + def get_control_type(self): """Get control type of this module, only applicable on host management mode @@ -1052,7 +1057,7 @@ def get_control_type(self): int: 1 - software control, 0 - firmware control """ return utils.read_int_from_file(f'/sys/module/sx_core/asic0/module{self.sdk_index}/control') - + def set_control_type(self, control_type): """Set control type for the module @@ -1060,7 +1065,7 @@ def set_control_type(self, control_type): control_type (int): 0 for firmware control, currently only 0 is allowed """ utils.write_file(f'/sys/module/sx_core/asic0/module{self.sdk_index}/control', control_type) - + def determine_control_type(self): """Determine control type according to module type @@ -1071,12 +1076,12 @@ def determine_control_type(self): if not api: logger.log_error(f'Failed to get api object for SFP {self.sdk_index}, probably module EEPROM is not ready') return SFP_FW_CONTROL - + if not self.is_supported_for_software_control(api): return SFP_FW_CONTROL else: return SFP_SW_CONTROL - + def is_cmis_api(self, xcvr_api): """Check if the api type is CMIS @@ -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 @@ -1135,7 +1140,7 @@ def check_power_capability(self): max_power = self.get_module_max_power() if max_power < 0: return False - + power_limit = self.get_power_limit() logger.log_info(f'SFP {self.sdk_index}: max_power={max_power}, power_limit={power_limit}') if max_power <= power_limit: @@ -1143,7 +1148,7 @@ def check_power_capability(self): else: logger.log_error(f'SFP {self.sdk_index} exceed power limit: max_power={max_power}, power_limit={power_limit}') return False - + def get_power_limit(self): """Get power limit of this module @@ -1151,7 +1156,7 @@ def get_power_limit(self): int: Power limit in unit of 0.25W """ return utils.read_int_from_file(f'/sys/module/sx_core/asic0/module{self.sdk_index}/power_limit') - + def get_module_max_power(self): """Get module max power from EEPROM @@ -1171,7 +1176,7 @@ def get_module_max_power(self): # According to standard: # Byte 128: # if bit 5 is 1, "Power Class 8 implemented (Max power declared in byte 107)" - # Byte 107: + # Byte 107: # "Maximum power consumption of module. Unsigned integer with LSB = 0.1 W." power_class_8_byte = self.read_eeprom(SFF_POWER_CLASS_8_OFFSET, 1) powercap = power_class_8_byte[0] * 0.1 @@ -1179,14 +1184,14 @@ def get_module_max_power(self): logger.log_error(f'SFP {self.sdk_index} got invalid value for power class field: {power_class_bit}') return -1 - # Multiplying the sysfs value (0.25 Watt units) by 4 aligns it with the EEPROM max power value (1 Watt units), + # Multiplying the sysfs value (0.25 Watt units) by 4 aligns it with the EEPROM max power value (1 Watt units), # ensuring both are in the same unit for a meaningful comparison return powercap * 4 # else: # Should never hit, just in case logger.log_error(f'SFP {self.sdk_index} with api type {xcvr_api} does not support getting max power') return -1 - + def update_i2c_frequency(self): """Update I2C frequency for the module. """ @@ -1208,10 +1213,10 @@ def update_i2c_frequency(self): # Should never hit, just in case logger.log_error(f'SFP {self.sdk_index} with api type {api} does not support updating frequency but frequency_support sysfs return 1') return - + logger.log_info(f"Read mci max frequency bits {frequency} for SFP {self.sdk_index}") self.set_frequency(frequency) - + def get_frequency_support(self): """Get frequency support for this module @@ -1219,7 +1224,7 @@ def get_frequency_support(self): bool: True if supported """ return utils.read_int_from_file(f'/sys/module/sx_core/asic0/module{self.sdk_index}/frequency_support') == 1 - + def set_frequency(self, freqeuncy): """Set module frequency. @@ -1227,7 +1232,7 @@ def set_frequency(self, freqeuncy): freqeuncy (int): 0 - up to 400KHz, 1 - up to 1MHz """ utils.write_file(f'/sys/module/sx_core/asic0/module{self.sdk_index}/frequency', freqeuncy) - + def disable_tx_for_sff_optics(self): """Disable TX for SFF optics """ @@ -1235,7 +1240,7 @@ def disable_tx_for_sff_optics(self): if self.is_sff_api(api) and api.get_tx_disable_support(): logger.log_info(f'Disabling tx for SFP {self.sdk_index}') api.tx_disable(True) - + @classmethod def get_state_machine(cls): """Get state machine object, create if not exists @@ -1271,7 +1276,7 @@ def get_state_machine(cls): sm.add_state(STATE_POWER_LIMIT_ERROR).set_entry_action(ACTION_ON_POWER_LIMIT_ERROR) \ .add_transition(EVENT_POWER_GOOD, STATE_POWERED_ON) \ .add_transition(EVENT_NOT_PRESENT, STATE_NOT_PRESENT) - + cls.action_table = {} cls.action_table[ACTION_ON_START] = cls.action_on_start cls.action_table[ACTION_ON_RESET] = cls.action_on_reset @@ -1280,7 +1285,7 @@ def get_state_machine(cls): cls.action_table[ACTION_ON_FW_CONTROL] = cls.action_on_fw_control cls.action_table[ACTION_ON_CANCEL_WAIT] = cls.action_on_cancel_wait cls.action_table[ACTION_ON_POWER_LIMIT_ERROR] = cls.action_on_power_limit_error - + # For always firewire control ports sm.add_state(STATE_FCP_DOWN).add_transition(EVENT_START, STATE_FCP_INIT) sm.add_state(STATE_FCP_INIT).set_entry_action(ACTION_FCP_ON_START) \ @@ -1288,25 +1293,25 @@ def get_state_machine(cls): .add_transition(EVENT_PRESENT, STATE_FCP_PRESENT) sm.add_state(STATE_FCP_NOT_PRESENT).add_transition(EVENT_PRESENT, STATE_FCP_PRESENT) sm.add_state(STATE_FCP_PRESENT).add_transition(EVENT_NOT_PRESENT, STATE_FCP_NOT_PRESENT) - + cls.action_table[ACTION_FCP_ON_START] = cls.action_fcp_on_start - + cls.sm = sm - + return cls.sm - + @classmethod def action_on_start(cls, sfp): if sfp.get_control_type() == SFP_FW_CONTROL: logger.log_info(f'SFP {sfp.sdk_index} is already FW control, probably in warm reboot') sfp.on_event(EVENT_FW_CONTROL) return - + if not sfp.get_hw_present(): logger.log_info(f'SFP {sfp.sdk_index} is not present') sfp.on_event(EVENT_NOT_PRESENT) return - + if not sfp.get_power_on(): logger.log_info(f'SFP {sfp.sdk_index} is not powered on') sfp.set_power(True) @@ -1332,51 +1337,51 @@ def action_fcp_on_start(cls, sfp): sfp.on_event(EVENT_PRESENT) else: sfp.on_event(EVENT_NOT_PRESENT) - + @classmethod def action_on_reset(cls, sfp): logger.log_info(f'SFP {sfp.sdk_index} is scheduled to wait for resetting done') cls.get_wait_ready_task().schedule_wait(sfp.sdk_index) - + @classmethod def action_on_powered(cls, sfp): if not sfp.get_power_good(): logger.log_error(f'SFP {sfp.sdk_index} is not in power good state') sfp.on_event(EVENT_POWER_BAD) return - + control_type = sfp.determine_control_type() if control_type == SFP_SW_CONTROL: sfp.on_event(EVENT_SW_CONTROL) else: sfp.on_event(EVENT_FW_CONTROL) - + @classmethod def action_on_sw_control(cls, sfp): if not sfp.check_power_capability(): sfp.on_event(EVENT_POWER_LIMIT_EXCEED) return - + sfp.update_i2c_frequency() sfp.disable_tx_for_sff_optics() logger.log_info(f'SFP {sfp.sdk_index} is set to software control') - + @classmethod def action_on_fw_control(cls, sfp): if sfp.get_control_type() != SFP_FW_CONTROL: logger.log_info(f'SFP {sfp.sdk_index} is set to firmware control') sfp.set_control_type(SFP_FW_CONTROL) - + @classmethod def action_on_cancel_wait(cls, sfp): cls.get_wait_ready_task().cancel_wait(sfp.sdk_index) - + @classmethod def action_on_power_limit_error(cls, sfp): logger.log_info(f'SFP {sfp.sdk_index} is powered off due to exceeding power limit') sfp.set_power(False) sfp.set_hw_reset(0) - + @classmethod def get_wait_ready_task(cls): """Get SFP wait ready task. Create if not exists. @@ -1388,7 +1393,7 @@ def get_wait_ready_task(cls): from .wait_sfp_ready_task import WaitSfpReadyTask cls.wait_ready_task = WaitSfpReadyTask() return cls.wait_ready_task - + def get_state(self): """Return the current state. @@ -1396,7 +1401,7 @@ def get_state(self): str: current state """ return self.state - + def change_state(self, new_state): """Change from old state to new state @@ -1412,7 +1417,7 @@ def on_action(self, action_name): action_name (str): action name """ SFP.action_table[action_name](self) - + def on_event(self, event): """Called when a state machine event arrives @@ -1420,7 +1425,7 @@ def on_event(self, event): event (str): State machine event """ SFP.get_state_machine().on_event(self, event) - + def in_stable_state(self): """Indicate whether this module is in a stable state. 'Stable state' means the module is pending on a polling event from SDK. @@ -1428,21 +1433,21 @@ def in_stable_state(self): Returns: bool: True if the module is in a stable state """ - return self.state in (STATE_NOT_PRESENT, STATE_SW_CONTROL, STATE_FW_CONTROL, + return self.state in (STATE_NOT_PRESENT, STATE_SW_CONTROL, STATE_FW_CONTROL, STATE_POWER_BAD, STATE_POWER_LIMIT_ERROR, STATE_FCP_NOT_PRESENT, STATE_FCP_PRESENT) - - def get_fds_for_poling(self): + + def get_fds_for_poling(self): if self.state == STATE_FW_CONTROL or self.state == STATE_FCP_NOT_PRESENT or self.state == STATE_FCP_PRESENT: return { 'present': self.get_fd('present') - } + } else: return { 'hw_present': self.get_fd('hw_present'), 'power_good': self.get_fd('power_good') - } - + } + def fill_change_event(self, port_dict): """Fill change event data based on current state. @@ -1456,7 +1461,7 @@ def fill_change_event(self, port_dict): elif self.state == STATE_POWER_BAD or self.state == STATE_POWER_LIMIT_ERROR: sfp_state = SFP.SFP_ERROR_BIT_POWER_BUDGET_EXCEEDED | SFP.SFP_STATUS_BIT_INSERTED port_dict[self.sdk_index + 1] = str(sfp_state) - + def refresh_poll_obj(self, poll_obj, all_registered_fds): """Refresh polling object and registered fds. This function is usually called when a cable plugin event occurs. For example, user plugs out a software control module and replaces with a firmware @@ -1474,7 +1479,7 @@ def refresh_poll_obj(self, poll_obj, all_registered_fds): target_poll_types = ['present'] else: target_poll_types = ['hw_present', 'power_good'] - + for target_poll_type in target_poll_types: if target_poll_type not in current_registered_fds: # need add new fd for polling @@ -1526,10 +1531,10 @@ def initialize_sfp_modules(cls, sfp_list): """ wait_ready_task = cls.get_wait_ready_task() wait_ready_task.start() - + for s in sfp_list: s.on_event(EVENT_START) - + if not wait_ready_task.empty(): # Wait until wait_ready_task is up while not wait_ready_task.is_alive(): @@ -1558,7 +1563,7 @@ def initialize_sfp_modules(cls, sfp_list): logger.log_notice(f'SFP {index} is in state {s.state} after module initialization') cls.wait_sfp_eeprom_ready(sfp_list, 2) - + class RJ45Port(NvidiaSFPCommon): """class derived from SFP, representing RJ45 ports""" @@ -1811,3 +1816,34 @@ 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/utils.py b/platform/mellanox/mlnx-platform-api/sonic_platform/utils.py index ed638526375..00c0bf034e3 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)] - return RJ45_port_index_list if bool(RJ45_port_index_list) else None + +def extract_RJ45_ports_index(num_of_asics=1): + return _extract_ports_index_by_type(RJ45_PORT_TYPE, num_of_asics) + + +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): @@ -316,7 +337,7 @@ def wait_until_conditions(conditions, timeout, interval=1): timeout -= interval return False - + class TimerEvent: def __init__(self, interval, cb, repeat): self.interval = interval 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..29aad863bee 100644 --- a/platform/mellanox/mlnx-platform-api/tests/test_change_event.py +++ b/platform/mellanox/mlnx-platform-api/tests/test_change_event.py @@ -39,13 +39,14 @@ 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() s = c.get_sfp(1) - + mock_status.return_value = sfp.SFP_STATUS_INSERTED - + # mock poll object mock_poll = mock.MagicMock() mock_create_poll.return_value = mock_poll @@ -84,14 +85,15 @@ def test_get_change_event_legacy(self, mock_status, mock_time, mock_create_poll, _, change_event = c.get_change_event(timeout) assert 'sfp' in change_event and sfp_index in change_event['sfp'] and change_event['sfp'][sfp_index] == '2' assert 'sfp_error' in change_event and sfp_index in change_event['sfp_error'] and change_event['sfp_error'][sfp_index] == 'some error' - - @mock.patch('sonic_platform.wait_sfp_ready_task.WaitSfpReadyTask.get_ready_set') + + @mock.patch('sonic_platform.wait_sfp_ready_task.WaitSfpReadyTask.get_ready_set') @mock.patch('sonic_platform.sfp.SFP.get_fd') @mock.patch('select.poll') @mock.patch('time.monotonic') @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: @@ -109,12 +111,12 @@ def test_get_change_event_for_module_host_management_mode(self, mock_time, mock_ c.initialize_sfp() s = c._sfp_list[0] s.state = sfp.STATE_SW_CONTROL - + # mock poll object mock_poll = mock.MagicMock() mock_create_poll.return_value = mock_poll mock_poll.poll = mock.MagicMock(return_value = []) - + # mock file descriptors for polling mock_hw_present_file = mock.MagicMock() mock_power_good_file = mock.MagicMock() @@ -133,29 +135,29 @@ def get_fd(fd_type): else: return mock_present_file mock_get_fd.side_effect = get_fd - + timeout = 1000 # mock time function so that the while loop exit early mock_time.side_effect = [0, timeout] - + # no event, expect returning empty change event _, change_event = c.get_change_event(timeout) assert 'sfp' in change_event and not change_event['sfp'] - + # dummy event, expect returning empty change event sfp_index = s.sdk_index + 1 mock_poll.poll.return_value = [(1, 10)] mock_time.side_effect = [0, timeout] _, change_event = c.get_change_event(timeout) assert 'sfp' in change_event and not change_event['sfp'] - + # plug out event, expect returning remove event mock_time.side_effect = [0, timeout] mock_hw_present_file.read.return_value = sfp.SFP_STATUS_REMOVED _, change_event = c.get_change_event(timeout) assert 'sfp' in change_event and sfp_index in change_event['sfp'] and change_event['sfp'][sfp_index] == sfp.SFP_STATUS_REMOVED assert s.state == sfp.STATE_NOT_PRESENT - + # plug in with a fw control cable, expect returning insert event s.get_control_type = mock.MagicMock(return_value=sfp.SFP_SW_CONTROL) s.get_hw_present = mock.MagicMock(return_value=True) @@ -174,7 +176,7 @@ def get_fd(fd_type): assert 2 not in c.registered_fds # stop polling power_good assert 3 in c.registered_fds # start polling present because it is firmware control print(c.registered_fds) - + # error event, expect returning error mock_ready.return_value = [] mock_time.side_effect = [0, timeout] @@ -184,7 +186,7 @@ def get_fd(fd_type): _, change_event = c.get_change_event(timeout) assert 'sfp' in change_event and sfp_index in change_event['sfp'] and change_event['sfp'][sfp_index] == '2' assert 'sfp_error' in change_event and sfp_index in change_event['sfp_error'] and change_event['sfp_error'][sfp_index] == 'some error' - + # plug out the firmware control cable, expect returning remove event mock_time.side_effect = [0, timeout] mock_present_file.read.return_value = sfp.SFP_STATUS_REMOVED @@ -194,7 +196,7 @@ def get_fd(fd_type): assert 1 in c.registered_fds # start polling hw_present because cable is not present, always assume software control assert 2 in c.registered_fds # start polling power_good because cable is not present, always assume software control assert 3 not in c.registered_fds # stop polling present - + # plug in a software control cable, expect returning insert event mock_time.side_effect = [0, timeout] mock_ready.return_value = set([0]) @@ -207,7 +209,7 @@ def get_fd(fd_type): _, change_event = c.get_change_event(timeout) assert 'sfp' in change_event and sfp_index in change_event['sfp'] and change_event['sfp'][sfp_index] == sfp.SFP_STATUS_INSERTED assert s.state == sfp.STATE_SW_CONTROL - + # power bad event, expect returning error event mock_time.side_effect = [0, timeout] mock_poll.poll.return_value = [(2, 10)] @@ -215,7 +217,7 @@ def get_fd(fd_type): _, change_event = c.get_change_event(timeout) assert 'sfp' in change_event and sfp_index in change_event['sfp'] and change_event['sfp'][sfp_index] == '5' assert s.state == sfp.STATE_POWER_BAD - + # power good event, expect returning insert event mock_time.side_effect = [0, timeout] mock_poll.poll.return_value = [(2, 10)] diff --git a/platform/mellanox/mlnx-platform-api/tests/test_chassis.py b/platform/mellanox/mlnx-platform-api/tests/test_chassis.py index f07f164b730..5f15bf7660c 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: @@ -179,6 +180,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..3a476aef226 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"); @@ -43,25 +44,25 @@ def test_wait_module_ready(self, mock_is_host, mock_wait, mock_exists): initializer.wait_module_ready() mock_exists.assert_called_with(module_host_mgmt_initializer.MODULE_READY_HOST_FILE) assert initializer.initialized - + initializer.initialized = False mock_is_host.return_value = False initializer.wait_module_ready() mock_exists.assert_called_with(module_host_mgmt_initializer.MODULE_READY_CONTAINER_FILE) - + initializer.initialized = False mock_exists.return_value = True initializer.wait_module_ready() assert initializer.initialized - + initializer.initialized = False mock_wait.return_value = False 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') @@ -76,20 +77,20 @@ def test_initialize(self, mock_is_host, mock_wait_ready, mock_owner): initializer.initialize(c) mock_wait_ready.assert_called_once() mock_wait_ready.reset_mock() - + mock_is_host.return_value = False # non-initializer-owner called from container side, just wait initializer.initialize(c) mock_wait_ready.assert_called_once() mock_wait_ready.reset_mock() - + mock_owner.return_value = True initializer.initialize(c) mock_wait_ready.assert_not_called() assert initializer.initialized assert module_host_mgmt_initializer.initialization_owner assert os.path.exists(module_host_mgmt_initializer.MODULE_READY_CONTAINER_FILE) - + module_host_mgmt_initializer.clean_up() assert not os.path.exists(module_host_mgmt_initializer.MODULE_READY_CONTAINER_FILE) diff --git a/platform/mellanox/mlnx-platform-api/tests/test_sfp.py b/platform/mellanox/mlnx-platform-api/tests/test_sfp.py index a3efb395bce..f5512f3f8c7 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 @@ -94,7 +94,7 @@ def test_sfp_get_error_status(self, mock_get_error_code, mock_control): mock_control.side_effect = RuntimeError('') description = sfp.get_error_description() assert description == 'Initializing' - + mock_control.side_effect = NotImplementedError('') description = sfp.get_error_description() assert description == 'Not supported' @@ -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): @@ -347,7 +359,7 @@ def test_get_temperature_threshold(self): sfp.get_xcvr_api = mock.MagicMock(return_value=None) assert sfp.get_temperature_warning_threshold() is None assert sfp.get_temperature_critical_threshold() is None - + sfp.get_xcvr_api.return_value = mock_api assert sfp.get_temperature_warning_threshold() == 0.0 assert sfp.get_temperature_critical_threshold() == 0.0 @@ -355,17 +367,17 @@ def test_get_temperature_threshold(self): from sonic_platform_base.sonic_xcvr.fields import consts mock_api.get_transceiver_thresholds_support.return_value = True mock_api.xcvr_eeprom = mock.MagicMock() - + def mock_read(field): if field == consts.TEMP_HIGH_ALARM_FIELD: return 85.0 elif field == consts.TEMP_HIGH_WARNING_FIELD: return 75.0 - + mock_api.xcvr_eeprom.read = mock.MagicMock(side_effect=mock_read) assert sfp.get_temperature_warning_threshold() == 75.0 assert sfp.get_temperature_critical_threshold() == 85.0 - + sfp.reinit_if_sn_changed.return_value = False assert sfp.get_temperature_warning_threshold() == 75.0 assert sfp.get_temperature_critical_threshold() == 85.0 @@ -377,12 +389,12 @@ def test_is_sw_control(self, mock_mode, mock_read): mock_mode.return_value = False assert not sfp.is_sw_control() mock_mode.return_value = True - + mock_read.return_value = 0 assert not sfp.is_sw_control() mock_read.return_value = 1 assert sfp.is_sw_control() - + @mock.patch('sonic_platform.device_data.DeviceDataManager.is_module_host_management_mode', mock.MagicMock(return_value=True)) @mock.patch('sonic_platform.utils.read_int_from_file') @mock.patch('sonic_platform.sfp.SFP.is_sw_control', mock.MagicMock(return_value=True)) @@ -390,12 +402,12 @@ def test_get_lpmode_cmis_host_mangagement(self, mock_read): sfp = SFP(0) sfp.get_xcvr_api = mock.MagicMock(return_value=None) assert not sfp.get_lpmode() - + mock_api = mock.MagicMock() sfp.get_xcvr_api.return_value = mock_api mock_api.get_lpmode = mock.MagicMock(return_value=False) assert not sfp.get_lpmode() - + mock_api.get_lpmode.return_value = True assert sfp.get_lpmode() @@ -405,7 +417,7 @@ def test_set_lpmode_cmis_host_mangagement(self): sfp = SFP(0) sfp.get_xcvr_api = mock.MagicMock(return_value=None) assert not sfp.set_lpmode(False) - + mock_api = mock.MagicMock() sfp.get_xcvr_api.return_value = mock_api mock_api.get_lpmode = mock.MagicMock(return_value=False) @@ -416,69 +428,69 @@ def test_determine_control_type(self): sfp = SFP(0) sfp.get_xcvr_api = mock.MagicMock(return_value=None) assert sfp.determine_control_type() == 0 - + sfp.get_xcvr_api.return_value = 1 # Just make it not None sfp.is_supported_for_software_control = mock.MagicMock(return_value=True) assert sfp.determine_control_type() == 1 - + sfp.is_supported_for_software_control.return_value = False assert sfp.determine_control_type() == 0 - + def test_check_power_capability(self): sfp = SFP(0) sfp.get_module_max_power = mock.MagicMock(return_value=-1) assert not sfp.check_power_capability() - + sfp.get_module_max_power.return_value = 48 sfp.get_power_limit = mock.MagicMock(return_value=48) assert sfp.check_power_capability() - + sfp.get_power_limit.return_value = 1 assert not sfp.check_power_capability() - + def test_get_module_max_power(self): sfp = SFP(0) sfp.is_cmis_api = mock.MagicMock(return_value=True) sfp.read_eeprom = mock.MagicMock(return_value=bytearray([48])) assert sfp.get_module_max_power() == 48 - + sfp.is_cmis_api.return_value = False sfp.is_sff_api = mock.MagicMock(return_value=True) sfp.read_eeprom.return_value = bytearray([128]) assert sfp.get_module_max_power() == 2.5 * 4 - + sfp.read_eeprom.return_value = bytearray([32]) assert sfp.get_module_max_power() == 3.2 * 4 - + # Simulate invalid value sfp.read_eeprom.return_value = bytearray([33]) assert sfp.get_module_max_power() == -1 - + # Simulate unsupported module type sfp.is_sff_api .return_value = False assert sfp.get_module_max_power() == -1 - + def test_update_i2c_frequency(self): sfp = SFP(0) sfp.get_frequency_support = mock.MagicMock(return_value=False) sfp.set_frequency = mock.MagicMock() sfp.update_i2c_frequency() sfp.set_frequency.assert_not_called() - + sfp.get_frequency_support.return_value = True sfp.update_i2c_frequency() sfp.set_frequency.assert_not_called() - + sfp.is_cmis_api = mock.MagicMock(return_value=True) sfp.read_eeprom = mock.MagicMock(return_value=bytearray([0])) sfp.update_i2c_frequency() sfp.set_frequency.assert_called_with(0) - + sfp.is_cmis_api.return_value = False sfp.is_sff_api = mock.MagicMock(return_value=True) sfp.update_i2c_frequency() sfp.set_frequency.assert_called_with(0) - + def test_disable_tx_for_sff_optics(self): sfp = SFP(0) mock_api = mock.MagicMock() @@ -486,12 +498,12 @@ def test_disable_tx_for_sff_optics(self): mock_api.tx_disable = mock.MagicMock() sfp.disable_tx_for_sff_optics() mock_api.tx_disable.assert_not_called() - + sfp.is_sff_api = mock.MagicMock(return_value=True) mock_api.get_tx_disable_support = mock.MagicMock(return_value=True) sfp.disable_tx_for_sff_optics() mock_api.tx_disable.assert_called_with(True) - + @mock.patch('sonic_platform.utils.read_int_from_file') def test_get_error_info_from_sdk_error_type(self, mock_read): sfp = SFP(0) @@ -500,13 +512,14 @@ def test_get_error_info_from_sdk_error_type(self, mock_read): sfp_state, error_desc = sfp.get_error_info_from_sdk_error_type() assert sfp_state == '2' assert 'Unknown error' in error_desc - + mock_read.return_value = 2 sfp_state, error_desc = sfp.get_error_info_from_sdk_error_type() assert sfp_state == '11' 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() @@ -571,13 +584,13 @@ def mock_read(field): sfp.is_sw_control.return_value = True mock_super_get_temperature.return_value = 58.0 assert sfp.get_temperature_info() == (True, 58.0, 75.0, 85.0) - + mock_api.get_transceiver_thresholds_support.return_value = None assert sfp.get_temperature_info() == (True, 58.0, None, None) - + mock_api.get_transceiver_thresholds_support.return_value = False assert sfp.get_temperature_info() == (True, 58.0, 0.0, 0.0) - + sfp.reinit_if_sn_changed.return_value = False assert sfp.get_temperature_info() == (True, 58.0, 75.0, 85.0) sfp.is_sw_control.side_effect = Exception('') diff --git a/platform/mellanox/mlnx-platform-api/tests/test_utils.py b/platform/mellanox/mlnx-platform-api/tests/test_utils.py index b6ec67975f8..c8d3b32662a 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) @@ -195,7 +284,7 @@ def test_read_key_value_file(self): mock_os_open = mock.mock_open(read_data='a=b') with mock.patch('sonic_platform.utils.open', mock_os_open): assert utils.read_key_value_file('some_file', delimeter='=') == {'a':'b'} - + @mock.patch('sonic_platform.utils.time.sleep', mock.MagicMock()) def test_wait_until_conditions(self): conditions = [lambda: True]