diff --git a/sonic-xcvrd/tests/test_xcvrd.py b/sonic-xcvrd/tests/test_xcvrd.py index 4e97f4edb..12a52f743 100644 --- a/sonic-xcvrd/tests/test_xcvrd.py +++ b/sonic-xcvrd/tests/test_xcvrd.py @@ -2575,6 +2575,173 @@ def get_host_lane_assignment_option_side_effect(app): appl = common.get_cmis_application_desired(mock_xcvr_api, host_lane_count, speed) assert task.get_cmis_host_lanes_mask(mock_xcvr_api, appl, host_lane_count, subport) == expected + @pytest.mark.parametrize("gearbox_data, expected_dict", [ + # Test case 1: Gearbox port with 2 line lanes + ({ + "interface:0": { + "name": "Ethernet0", + "index": "0", + "phy_id": "1", + "system_lanes": "300,301,302,303", + "line_lanes": "304,305" + } + }, {"Ethernet0": 2}), + # Test case 2: Multiple gearbox ports + ({ + "interface:0": { + "name": "Ethernet0", + "index": "0", + "phy_id": "1", + "system_lanes": "300,301,302,303", + "line_lanes": "304,305,306,307" + }, + "interface:200": { + "name": "Ethernet200", + "index": "200", + "phy_id": "2", + "system_lanes": "400,401", + "line_lanes": "404,405" + } + }, {"Ethernet0": 4, "Ethernet200": 2}), + # Test case 3: Empty gearbox data + ({}, {}), + # Test case 4: Gearbox interface with empty line_lanes + ({ + "interface:0": { + "name": "Ethernet0", + "index": "0", + "phy_id": "1", + "system_lanes": "300,301,302,303", + "line_lanes": "" + } + }, {}), + # Test case 5: Non-interface keys (should be ignored) + ({ + "interface:0": { + "name": "Ethernet0", + "index": "0", + "phy_id": "1", + "system_lanes": "300,301,302,303", + "line_lanes": "304,305" + }, + "phy:1": { + "name": "phy1", + "some_field": "some_value" + } + }, {"Ethernet0": 2}) + ]) + def test_XcvrTableHelper_get_gearbox_line_lanes_dict(self, gearbox_data, expected_dict): + # Mock the XcvrTableHelper and APPL_DB access + mock_appl_db = MagicMock() + mock_gearbox_table = MagicMock() + + # Mock table.getKeys() to return gearbox interface keys + mock_gearbox_table.getKeys.return_value = list(gearbox_data.keys()) + + # Mock table.get() to return gearbox interface data + def mock_get_side_effect(key): + if key in gearbox_data: + # Convert dict to list of tuples for fvs format + interface_data = gearbox_data[key] + fvs_list = [(k, v) for k, v in interface_data.items()] + return (True, fvs_list) + return (False, []) + + mock_gearbox_table.get.side_effect = mock_get_side_effect + + # Mock swsscommon.Table constructor to return our mock table + with patch('xcvrd.xcvrd_utilities.xcvr_table_helper.swsscommon.Table', return_value=mock_gearbox_table): + # Mock the helper_logger to avoid logging during tests + with patch('xcvrd.xcvrd_utilities.xcvr_table_helper.helper_logger'): + helper = XcvrTableHelper(DEFAULT_NAMESPACE) + helper.appl_db = {0: mock_appl_db} # Mock the appl_db dict + + result = helper.get_gearbox_line_lanes_dict() + assert result == expected_dict + + @pytest.mark.parametrize("gearbox_lanes_dict, lport, port_config_lanes, expected_count", [ + # Test case 1: Gearbox data available, should use gearbox count + ({"Ethernet0": 2}, "Ethernet0", "25,26,27,28", 2), + # Test case 2: Gearbox data available with 4 lanes + ({"Ethernet0": 4}, "Ethernet0", "29,30", 4), + # Test case 3: No gearbox data for this port, should use port config + ({"Ethernet4": 2}, "Ethernet0", "33,34,35,36", 4), + # Test case 4: Empty gearbox dict, should use port config + ({}, "Ethernet0", "37,38", 2), + # Test case 5: Multiple ports in gearbox dict + ({"Ethernet0": 2, "Ethernet4": 4}, "Ethernet0", "25,26,27,28", 2), + # Test case 6: Port not in gearbox dict + ({"Ethernet4": 4}, "Ethernet8", "41,42,43", 3) + ]) + def test_CmisManagerTask_get_host_lane_count(self, gearbox_lanes_dict, lport, port_config_lanes, expected_count): + port_mapping = PortMapping() + stop_event = threading.Event() + task = CmisManagerTask(DEFAULT_NAMESPACE, port_mapping, stop_event) + + result = task.get_host_lane_count(lport, port_config_lanes, gearbox_lanes_dict) + assert result == expected_count + + def test_CmisManagerTask_gearbox_integration_end_to_end(self): + """Test end-to-end integration of gearbox line lanes with CMIS application selection""" + port_mapping = PortMapping() + stop_event = threading.Event() + task = CmisManagerTask(DEFAULT_NAMESPACE, port_mapping, stop_event) + + # Mock gearbox lanes dictionary - port has 4 system lanes but only 2 line lanes + gearbox_lanes_dict = {"Ethernet0": 2} # 2 line lanes from gearbox + + # Mock port config - would normally give 4 lanes + port_config_lanes = "25,26,27,28" # 4 lanes from port config + + # Mock CMIS API with application advertisement + mock_xcvr_api = MagicMock() + mock_xcvr_api.get_application_advertisement.return_value = { + 1: { + 'host_electrical_interface_id': '100GAUI-2 C2M (Annex 135G)', + 'module_media_interface_id': '100G-FR/100GBASE-FR1 (Cl 140)', + 'media_lane_count': 1, + 'host_lane_count': 2, # Matches our gearbox line lanes + 'host_lane_assignment_options': 85 + }, + 2: { + 'host_electrical_interface_id': 'CAUI-4 C2M (Annex 83E)', + 'module_media_interface_id': 'Active Cable assembly', + 'media_lane_count': 4, + 'host_lane_count': 4, # Would match port config lanes + 'host_lane_assignment_options': 17 + } + } + + # Test the integration: should use gearbox line lanes (2) not port config lanes (4) + host_lane_count = task.get_host_lane_count("Ethernet0", port_config_lanes, gearbox_lanes_dict) + assert host_lane_count == 2 # Should use gearbox line lanes, not port config + + # Test that this leads to correct CMIS application selection + with patch('xcvrd.xcvrd_utilities.common.is_cmis_api', return_value=True): + appl = common.get_cmis_application_desired(mock_xcvr_api, host_lane_count, 100000) + assert appl == 1 # Should select application 1 (2 lanes) not application 2 (4 lanes) + + def test_CmisManagerTask_gearbox_caching_integration(self): + """Test that gearbox lanes dictionary is properly cached and used in task worker""" + port_mapping = PortMapping() + stop_event = threading.Event() + task = CmisManagerTask(DEFAULT_NAMESPACE, port_mapping, stop_event) + + # Mock the XcvrTableHelper to return a gearbox lanes dictionary + mock_gearbox_lanes_dict = {"Ethernet0": 2, "Ethernet4": 4} + task.xcvr_table_helper = MagicMock() + task.xcvr_table_helper.get_gearbox_line_lanes_dict.return_value = mock_gearbox_lanes_dict + + # Test that get_host_lane_count uses the cached dictionary correctly + result1 = task.get_host_lane_count("Ethernet0", "25,26,27,28", mock_gearbox_lanes_dict) + assert result1 == 2 # Should use gearbox count + + result2 = task.get_host_lane_count("Ethernet4", "29,30", mock_gearbox_lanes_dict) + assert result2 == 4 # Should use gearbox count + + result3 = task.get_host_lane_count("Ethernet8", "33,34,35", mock_gearbox_lanes_dict) + assert result3 == 3 # Should fall back to port config count + @patch('swsscommon.swsscommon.FieldValuePairs') def test_CmisManagerTask_post_port_active_apsel_to_db_error_cases(self, mock_field_value_pairs): mock_xcvr_api = MagicMock() diff --git a/sonic-xcvrd/xcvrd/xcvrd.py b/sonic-xcvrd/xcvrd/xcvrd.py index a6ca4997a..cb82623f4 100644 --- a/sonic-xcvrd/xcvrd/xcvrd.py +++ b/sonic-xcvrd/xcvrd/xcvrd.py @@ -465,6 +465,29 @@ def get_cmis_module_power_up_duration_secs(self, api): def get_cmis_module_power_down_duration_secs(self, api): return api.get_module_pwr_down_duration()/1000 + def get_host_lane_count(self, lport, port_config_lanes, gearbox_lanes_dict): + """ + Get host lane count from gearbox configuration if available, otherwise from port config + + Args: + lport: logical port name (e.g., "Ethernet0") + port_config_lanes: lanes string from port config (e.g., "25,26,27,28") + gearbox_lanes_dict: dictionary of gearbox line lanes counts + + Returns: + Integer: number of host lanes + """ + # First try to get from gearbox configuration + gearbox_host_lane_count = gearbox_lanes_dict.get(lport, 0) + if gearbox_host_lane_count > 0: + self.log_debug("{}: Using gearbox line lanes count: {}".format(lport, gearbox_host_lane_count)) + return gearbox_host_lane_count + + # Fallback to port config lanes + host_lane_count = len(port_config_lanes.split(',')) + self.log_debug("{}: Using port config lanes count: {}".format(lport, host_lane_count)) + return host_lane_count + def get_cmis_host_lanes_mask(self, api, appl, host_lane_count, subport): """ Retrieves mask of active host lanes based on appl, host lane count and subport @@ -972,6 +995,9 @@ def task_worker(self): # Handle port change event from main thread port_change_observer.handle_port_update_event() + # Cache gearbox line lanes dictionary for this set of iterations over the port_dict + gearbox_lanes_dict = self.xcvr_table_helper.get_gearbox_line_lanes_dict() + for lport, info in self.port_dict.items(): if self.task_stopping_event.is_set(): break @@ -1003,7 +1029,7 @@ def task_worker(self): # Desired port speed on the host side host_speed = speed - host_lane_count = len(lanes.split(',')) + host_lane_count = self.get_host_lane_count(lport, lanes, gearbox_lanes_dict) # double-check the HW presence before moving forward sfp = platform_chassis.get_sfp(pport) diff --git a/sonic-xcvrd/xcvrd/xcvrd_utilities/xcvr_table_helper.py b/sonic-xcvrd/xcvrd/xcvrd_utilities/xcvr_table_helper.py index c9801d959..36cb38140 100644 --- a/sonic-xcvrd/xcvrd/xcvrd_utilities/xcvr_table_helper.py +++ b/sonic-xcvrd/xcvrd/xcvrd_utilities/xcvr_table_helper.py @@ -56,6 +56,7 @@ def __init__(self, namespaces): self.int_tbl, self.dom_tbl, self.dom_threshold_tbl, self.status_tbl, self.app_port_tbl, \ self.cfg_port_tbl, self.state_port_tbl, self.pm_tbl, self.firmware_info_tbl = {}, {}, {}, {}, {}, {}, {}, {}, {} self.state_db = {} + self.appl_db = {} self.cfg_db = {} self.dom_flag_tbl = {} self.dom_flag_change_count_tbl = {} @@ -92,8 +93,8 @@ def __init__(self, namespaces): self.pm_tbl[asic_id] = swsscommon.Table(self.state_db[asic_id], TRANSCEIVER_PM_TABLE) self.firmware_info_tbl[asic_id] = swsscommon.Table(self.state_db[asic_id], TRANSCEIVER_FIRMWARE_INFO_TABLE) self.state_port_tbl[asic_id] = swsscommon.Table(self.state_db[asic_id], swsscommon.STATE_PORT_TABLE_NAME) - appl_db = daemon_base.db_connect("APPL_DB", namespace) - self.app_port_tbl[asic_id] = swsscommon.ProducerStateTable(appl_db, swsscommon.APP_PORT_TABLE_NAME) + self.appl_db[asic_id] = daemon_base.db_connect("APPL_DB", namespace) + self.app_port_tbl[asic_id] = swsscommon.ProducerStateTable(self.appl_db[asic_id], swsscommon.APP_PORT_TABLE_NAME) self.cfg_db[asic_id] = daemon_base.db_connect("CONFIG_DB", namespace) self.cfg_port_tbl[asic_id] = swsscommon.Table(self.cfg_db[asic_id], swsscommon.CFG_PORT_TABLE_NAME) self.vdm_real_value_tbl[asic_id] = swsscommon.Table(self.state_db[asic_id], TRANSCEIVER_VDM_REAL_VALUE_TABLE) @@ -238,3 +239,52 @@ def is_npu_si_settings_update_required(self, lport, port_mapping): # If npu_si_settings_sync_val is None, it can also mean that the key is not present in the table return npu_si_settings_sync_val is None or npu_si_settings_sync_val == NPU_SI_SETTINGS_DEFAULT_VALUE + + def get_gearbox_line_lanes_dict(self): + """ + Retrieves the gearbox line lanes dictionary from APPL_DB + + This method scans all ASICs for gearbox interface configurations and extracts + the line_lanes count for each logical port. The line_lanes represent the + number of lanes on the line side (towards the optical module) which is the + correct count to use for CMIS host lane configuration. + + Returns: + dict: A dictionary where: + - key (str): logical port name (e.g., "Ethernet0") + - value (int): number of line-side lanes for that port + + Example: + {"Ethernet0": 2, "Ethernet200": 4} + + Note: + - Returns empty dict if no gearbox configuration is found + - Silently skips invalid or malformed entries + - Only processes keys that start with "interface:" + """ + gearbox_line_lanes_dict = {} + try: + for asic_id in self.appl_db: + appl_db = self.appl_db[asic_id] + gearbox_table = swsscommon.Table(appl_db, "_GEARBOX_TABLE") + interface_keys = gearbox_table.getKeys() + for key in interface_keys: + if key.startswith("interface:"): + (found, fvs) = gearbox_table.get(key) + if found: + fvs_dict = dict(fvs) + interface_name = fvs_dict.get('name', '') + line_lanes_str = fvs_dict.get('line_lanes', '') + if interface_name and line_lanes_str: + line_lanes_count = len(line_lanes_str.split(',')) + gearbox_line_lanes_dict[interface_name] = line_lanes_count + else: + if not interface_name: + helper_logger.log_warning("get_gearbox_line_lanes_dict: ASIC {}: Interface {} missing 'name' field".format(asic_id, key)) + if not line_lanes_str: + helper_logger.log_debug("get_gearbox_line_lanes_dict: ASIC {}: Interface {} has empty 'line_lanes' field".format(asic_id, interface_name)) + except Exception as e: + helper_logger.log_error("Error in get_gearbox_line_lanes_dict: {}".format(str(e))) + return gearbox_line_lanes_dict + + return gearbox_line_lanes_dict