From aa11f96a8e37c041dda6875694e0306895bb904d Mon Sep 17 00:00:00 2001 From: mihirpat1 <112018033+mihirpat1@users.noreply.github.com> Date: Mon, 27 Apr 2026 12:02:47 -0700 Subject: [PATCH] [xcvrd] Support mixed application codes in CMIS decommission logic (#794) * [xcvrd] Support mixed application codes in CMIS decommission logic Signed-off-by: Mihir Patel * Added support for gearbox host lane count Signed-off-by: Mihir Patel * Created dict for gearbox_lanes_dict Signed-off-by: Mihir Patel * Added error logs Signed-off-by: Mihir Patel * Replaced get_application with get_active_apsel_hostlane Signed-off-by: Mihir Patel * Optimized usage of gearbox_lanes_dict and created functions split getting desired application map Signed-off-by: Mihir Patel --------- Signed-off-by: Mihir Patel --- sonic-xcvrd/tests/test_xcvrd.py | 324 +++++++++++++++++++- sonic-xcvrd/xcvrd/cmis/cmis_manager_task.py | 153 ++++++++- 2 files changed, 445 insertions(+), 32 deletions(-) diff --git a/sonic-xcvrd/tests/test_xcvrd.py b/sonic-xcvrd/tests/test_xcvrd.py index 20a6e1612..3bfce0b67 100644 --- a/sonic-xcvrd/tests/test_xcvrd.py +++ b/sonic-xcvrd/tests/test_xcvrd.py @@ -2724,7 +2724,8 @@ def test_CmisManagerTask_process_single_lport_invalid_host_lanes_mask(self, mock info = task.port_dict['Ethernet0'] # Process the port - should fail due to invalid host_lanes_mask - task.process_single_lport('Ethernet0', info, {}) + task._gearbox_lanes_dict = {} + task.process_single_lport('Ethernet0', info) # Verify state transitioned to FAILED assert common.get_cmis_state_from_state_db('Ethernet0', mock_get_status_sw_tbl) == CMIS_STATE_FAILED @@ -2797,7 +2798,8 @@ def test_CmisManagerTask_process_single_lport_tx_power_config_failure(self, mock info = task.port_dict['Ethernet0'] # Process the port - should log error when tx power config fails - task.process_single_lport('Ethernet0', info, {}) + task._gearbox_lanes_dict = {} + task.process_single_lport('Ethernet0', info) # Verify configure_tx_output_power was called assert task.configure_tx_output_power.called @@ -2821,20 +2823,307 @@ def test_CmisManagerTask_task_run_stop(self, mock_chassis): cmis_manager.join() assert not cmis_manager.is_alive() - @pytest.mark.parametrize("app_new, lane_appl_code, expected", [ - (2, {0 : 1, 1 : 1, 2 : 1, 3 : 1, 4 : 2, 5 : 2, 6 : 2, 7 : 2}, True), - (0, {0 : 1, 1 : 1, 2 : 1, 3 : 1}, True), - (1, {0 : 0, 1 : 0, 2 : 0, 3 : 0, 4 : 0, 5 : 0, 6 : 0, 7 : 0}, False) + @pytest.mark.parametrize("active_map, desired_map, expected", [ + # Lanes 0-3 have app 1 but desired is 2 for all → decommission + ([1,1,1,1,2,2,2,2], [2,2,2,2,2,2,2,2], True), + # Lanes 0-3 active with app 1, desired is 0 → decommission + ([1,1,1,1,0,0,0,0], [0,0,0,0,0,0,0,0], True), + # All lanes unused, desired has app 1 → no decommission + ([0,0,0,0,0,0,0,0], [1,1,1,1,0,0,0,0], False), + # Mixed mode: adding new DPs on unused lanes → no decommission + ([3,3,3,3,0,0,0,0], [3,3,3,3,1,1,1,1], False), + # Mixed mode steady state: all lanes match → no decommission + ([3,3,3,3,1,1,1,1], [3,3,3,3,1,1,1,1], False), ]) - def test_CmisManagerTask_is_decommission_required(self, app_new, lane_appl_code, expected): + def test_CmisManagerTask_is_decommission_required(self, active_map, desired_map, expected): mock_xcvr_api = MagicMock() - def get_application(lane): - return lane_appl_code.get(lane, 0) - mock_xcvr_api.get_application = MagicMock(side_effect=get_application) + mock_xcvr_api.get_active_apsel_hostlane = MagicMock(return_value={ + 'ActiveAppSelLane{}'.format(lane + 1): active_map[lane] + for lane in range(CmisManagerTask.CMIS_MAX_HOST_LANES) + }) + mock_xcvr_api.get_application = MagicMock(side_effect=lambda lane: active_map[lane]) + port_mapping = PortMapping() + stop_event = threading.Event() + task = CmisManagerTask(DEFAULT_NAMESPACE, port_mapping, stop_event, platform_chassis=MagicMock()) + task.port_dict['Ethernet0'] = {'index': 1, 'asic_id': 0} + task.get_desired_app_map = MagicMock(return_value=desired_map) + assert task.is_decommission_required(mock_xcvr_api, 'Ethernet0') == expected + + def test_CmisManagerTask_is_decommission_required_uses_active_appsel_not_staged(self): + mock_xcvr_api = MagicMock() + # Active app map still old layout, staged app map already matches desired. + active_map = [1,1,1,1,1,1,1,1] + staged_map = [3,3,3,3,1,1,1,1] + desired_map = [3,3,3,3,1,1,1,1] + + mock_xcvr_api.get_active_apsel_hostlane = MagicMock(return_value={ + 'ActiveAppSelLane{}'.format(lane + 1): active_map[lane] + for lane in range(CmisManagerTask.CMIS_MAX_HOST_LANES) + }) + mock_xcvr_api.get_application = MagicMock(side_effect=lambda lane: staged_map[lane]) + + port_mapping = PortMapping() + stop_event = threading.Event() + task = CmisManagerTask(DEFAULT_NAMESPACE, port_mapping, stop_event, platform_chassis=MagicMock()) + task.port_dict['Ethernet0'] = {'index': 1, 'asic_id': 0} + task.get_desired_app_map = MagicMock(return_value=desired_map) + + assert task.is_decommission_required(mock_xcvr_api, 'Ethernet0') is True + + def test_CmisManagerTask_is_decommission_required_invalid_active_appsel(self): + mock_xcvr_api = MagicMock() + desired_map = [2,2,2,2,2,2,2,2] + + mock_xcvr_api.get_active_apsel_hostlane = MagicMock(return_value={ + 'ActiveAppSelLane1': 'N/A', + 'ActiveAppSelLane2': 1, + 'ActiveAppSelLane3': 1, + 'ActiveAppSelLane4': 1, + 'ActiveAppSelLane5': 2, + 'ActiveAppSelLane6': 2, + 'ActiveAppSelLane7': 2, + 'ActiveAppSelLane8': 2, + }) + + port_mapping = PortMapping() + stop_event = threading.Event() + task = CmisManagerTask(DEFAULT_NAMESPACE, port_mapping, stop_event, platform_chassis=MagicMock()) + task.port_dict['Ethernet0'] = {'index': 1, 'asic_id': 0} + task.get_desired_app_map = MagicMock(return_value=desired_map) + + assert task.is_decommission_required(mock_xcvr_api, 'Ethernet0') is True + + def test_CmisManagerTask_is_decommission_required_missing_active_appsel_lane(self): + mock_xcvr_api = MagicMock() + desired_map = [2,2,2,2,2,2,2,2] + + # Missing ActiveAppSelLane8 should trigger fail-safe decommission. + mock_xcvr_api.get_active_apsel_hostlane = MagicMock(return_value={ + 'ActiveAppSelLane1': 1, + 'ActiveAppSelLane2': 1, + 'ActiveAppSelLane3': 1, + 'ActiveAppSelLane4': 1, + 'ActiveAppSelLane5': 2, + 'ActiveAppSelLane6': 2, + 'ActiveAppSelLane7': 2, + }) + + port_mapping = PortMapping() + stop_event = threading.Event() + task = CmisManagerTask(DEFAULT_NAMESPACE, port_mapping, stop_event, platform_chassis=MagicMock()) + task.port_dict['Ethernet0'] = {'index': 1, 'asic_id': 0} + task.get_desired_app_map = MagicMock(return_value=desired_map) + + assert task.is_decommission_required(mock_xcvr_api, 'Ethernet0') is True + + def test_CmisManagerTask_get_desired_app_map(self): + """Test get_desired_app_map with mixed mode, skipped siblings, and edge cases""" + mock_xcvr_api = MagicMock() + port_mapping = PortMapping() + stop_event = threading.Event() + task = CmisManagerTask(DEFAULT_NAMESPACE, port_mapping, stop_event, platform_chassis=MagicMock()) + task.port_dict['Ethernet0'] = {'index': 1, 'asic_id': 0} + + # Mock cfg_port_tbl + cfg_port_tbl = MagicMock() + + # CONFIG_DB has 5 keys: + # Ethernet0 (index=1, 400G, 8 lanes, subport=0) — non-breakout + # Ethernet8 (index=2, different pport — should be skipped) + # Ethernet16 (index=1, but missing speed — should be skipped) + # Ethernet24 (missing index in full hash — should be skipped) + # Ethernet32 (get returns not found — should be skipped) + cfg_port_tbl.getKeys = MagicMock(return_value=[ + 'Ethernet0', 'Ethernet8', 'Ethernet16', 'Ethernet24', 'Ethernet32' + ]) + + def get_side_effect(key): + data = { + 'Ethernet0': (True, [('speed', '400000'), ('lanes', '1,2,3,4,5,6,7,8'), ('subport', '0'), ('index', '1')]), + 'Ethernet8': (True, [('speed', '100000'), ('lanes', '9'), ('subport', '1'), ('index', '2')]), + 'Ethernet16': (True, [('lanes', '1,2,3,4'), ('index', '1')]), # missing speed + 'Ethernet24': (True, [('speed', '100000'), ('lanes', '1')]), # missing index + 'Ethernet32': (False, []), + } + return data.get(key, (False, [])) + cfg_port_tbl.get = MagicMock(side_effect=get_side_effect) + + task.xcvr_table_helper.get_cfg_port_tbl = MagicMock(return_value=cfg_port_tbl) + task._gearbox_lanes_dict = {} + + # get_cmis_application_desired returns app code 3 for 8-lane 400G + mock_xcvr_api.get_host_lane_assignment_option = MagicMock(return_value=0xFF) + + with patch('xcvrd.xcvrd_utilities.common.get_cmis_application_desired', return_value=3): + result = task.get_desired_app_map(mock_xcvr_api, 'Ethernet0') + + assert result == [3, 3, 3, 3, 3, 3, 3, 3] + + def test_CmisManagerTask_get_desired_app_map_mixed_mode(self): + """Test get_desired_app_map with mixed application codes (1x400G + 4x100G)""" + mock_xcvr_api = MagicMock() + port_mapping = PortMapping() + stop_event = threading.Event() + task = CmisManagerTask(DEFAULT_NAMESPACE, port_mapping, stop_event, platform_chassis=MagicMock()) + task.port_dict['Ethernet0'] = {'index': 1, 'asic_id': 0} + + cfg_port_tbl = MagicMock() + cfg_port_tbl.getKeys = MagicMock(return_value=['Ethernet0', 'Ethernet4', 'Ethernet5', 'Ethernet6', 'Ethernet7']) + + def get_side_effect(key): + data = { + 'Ethernet0': (True, [('speed', '400000'), ('lanes', '1,2,3,4'), ('subport', '1'), ('index', '1')]), + 'Ethernet4': (True, [('speed', '100000'), ('lanes', '5'), ('subport', '5'), ('index', '1')]), + 'Ethernet5': (True, [('speed', '100000'), ('lanes', '6'), ('subport', '6'), ('index', '1')]), + 'Ethernet6': (True, [('speed', '100000'), ('lanes', '7'), ('subport', '7'), ('index', '1')]), + 'Ethernet7': (True, [('speed', '100000'), ('lanes', '8'), ('subport', '8'), ('index', '1')]), + } + return data.get(key, (False, [])) + cfg_port_tbl.get = MagicMock(side_effect=get_side_effect) + + task.xcvr_table_helper.get_cfg_port_tbl = MagicMock(return_value=cfg_port_tbl) + task._gearbox_lanes_dict = {} + + # app=3 for 4-lane 400G, app=1 for 1-lane 100G + def get_app_desired(api, lane_count, speed): + if lane_count == 4 and speed == 400000: + return 3 + if lane_count == 1 and speed == 100000: + return 1 + return None + # host_lane_assignment_option: all lanes assignable + mock_xcvr_api.get_host_lane_assignment_option = MagicMock(return_value=0xFF) + + with patch('xcvrd.xcvrd_utilities.common.get_cmis_application_desired', side_effect=get_app_desired): + result = task.get_desired_app_map(mock_xcvr_api, 'Ethernet0') + + assert result == [3, 3, 3, 3, 1, 1, 1, 1] + + def test_CmisManagerTask_get_desired_app_map_no_matching_app(self): + """Test get_desired_app_map when get_cmis_application_desired returns None""" + mock_xcvr_api = MagicMock() + port_mapping = PortMapping() + stop_event = threading.Event() + task = CmisManagerTask(DEFAULT_NAMESPACE, port_mapping, stop_event, platform_chassis=MagicMock()) + task.port_dict['Ethernet0'] = {'index': 1, 'asic_id': 0} + + cfg_port_tbl = MagicMock() + cfg_port_tbl.getKeys = MagicMock(return_value=['Ethernet0']) + cfg_port_tbl.get = MagicMock(return_value=(True, [('speed', '999999'), ('lanes', '1,2,3,4'), ('subport', '0'), ('index', '1')])) + task.xcvr_table_helper.get_cfg_port_tbl = MagicMock(return_value=cfg_port_tbl) + task._gearbox_lanes_dict = {} + + with patch('xcvrd.xcvrd_utilities.common.get_cmis_application_desired', return_value=None): + result = task.get_desired_app_map(mock_xcvr_api, 'Ethernet0') + + assert result == [0, 0, 0, 0, 0, 0, 0, 0] + + def test_CmisManagerTask_get_desired_app_map_uses_gearbox_lane_count(self): + """Test get_desired_app_map uses gearbox lane count over PORT table lanes""" + mock_xcvr_api = MagicMock() + port_mapping = PortMapping() + stop_event = threading.Event() + task = CmisManagerTask(DEFAULT_NAMESPACE, port_mapping, stop_event, platform_chassis=MagicMock()) + task.port_dict['Ethernet0'] = {'index': 1, 'asic_id': 0} + + cfg_port_tbl = MagicMock() + cfg_port_tbl.getKeys = MagicMock(return_value=['Ethernet0']) + # PORT table lanes suggest 4 lanes, but gearbox says 2 for this lport. + cfg_port_tbl.get = MagicMock(return_value=(True, [('speed', '200000'), ('lanes', '1,2,3,4'), ('subport', '1'), ('index', '1')])) + task.xcvr_table_helper.get_cfg_port_tbl = MagicMock(return_value=cfg_port_tbl) + task._gearbox_lanes_dict = {'Ethernet0': 2} + + # Return app only when lane_count comes from gearbox (2 lanes) for speed 200G. + def get_app_desired(api, lane_count, speed): + if lane_count == 2 and speed == 200000: + return 2 + return None + + # app=2 with host_lane_count=2 and subport=1 should map lanes 0 and 1. + mock_xcvr_api.get_host_lane_assignment_option = MagicMock(return_value=0xFF) + + with patch('xcvrd.xcvrd_utilities.common.get_cmis_application_desired', side_effect=get_app_desired): + result = task.get_desired_app_map(mock_xcvr_api, 'Ethernet0') + + assert result == [2, 2, 0, 0, 0, 0, 0, 0] + + def test_CmisManagerTask_get_sibling_port_configs(self): + """get_sibling_port_configs returns one entry per sibling sharing pport, + skipping rows with mismatched pport, missing index/speed/lanes, invalid + numeric fields, or get() returning not-found.""" port_mapping = PortMapping() stop_event = threading.Event() task = CmisManagerTask(DEFAULT_NAMESPACE, port_mapping, stop_event, platform_chassis=MagicMock()) - assert task.is_decommission_required(mock_xcvr_api, app_new) == expected + task.port_dict['Ethernet0'] = {'index': 1, 'asic_id': 0} + + cfg_port_tbl = MagicMock() + cfg_port_tbl.getKeys = MagicMock(return_value=[ + 'Ethernet0', # match + 'Ethernet4', # match + 'Ethernet8', # different pport — skip + 'Ethernet12', # missing index — skip + 'Ethernet16', # missing speed — skip + 'Ethernet20', # missing lanes — skip + 'Ethernet24', # invalid speed — skip + 'Ethernet28', # invalid index — skip + 'Ethernet32', # not found — skip + ]) + + def get_side_effect(key): + data = { + 'Ethernet0': (True, [('speed', '400000'), ('lanes', '1,2,3,4'), ('subport', '1'), ('index', '1')]), + 'Ethernet4': (True, [('speed', '100000'), ('lanes', '5'), ('subport', '5'), ('index', '1')]), + 'Ethernet8': (True, [('speed', '100000'), ('lanes', '9'), ('subport', '1'), ('index', '2')]), + 'Ethernet12': (True, [('speed', '100000'), ('lanes', '1')]), + 'Ethernet16': (True, [('lanes', '1,2,3,4'), ('index', '1')]), + 'Ethernet20': (True, [('speed', '100000'), ('subport', '1'), ('index', '1')]), + 'Ethernet24': (True, [('speed', 'foo'), ('lanes', '1'), ('subport', '1'), ('index', '1')]), + 'Ethernet28': (True, [('speed', '100000'), ('lanes', '1'), ('subport', '1'), ('index', 'bar')]), + 'Ethernet32': (False, []), + } + return data.get(key, (False, [])) + cfg_port_tbl.get = MagicMock(side_effect=get_side_effect) + + task.xcvr_table_helper.get_cfg_port_tbl = MagicMock(return_value=cfg_port_tbl) + task._gearbox_lanes_dict = {} + + siblings = task.get_sibling_port_configs('Ethernet0') + + assert siblings == [ + {'lport': 'Ethernet0', 'subport': 1, 'speed': 400000, 'host_lane_count': 4}, + {'lport': 'Ethernet4', 'subport': 5, 'speed': 100000, 'host_lane_count': 1}, + ] + + def test_CmisManagerTask_get_sibling_port_configs_no_cfg_port_tbl(self): + """get_sibling_port_configs returns [] when cfg_port_tbl is None.""" + port_mapping = PortMapping() + stop_event = threading.Event() + task = CmisManagerTask(DEFAULT_NAMESPACE, port_mapping, stop_event, platform_chassis=MagicMock()) + task.port_dict['Ethernet0'] = {'index': 1, 'asic_id': 0} + task.xcvr_table_helper.get_cfg_port_tbl = MagicMock(return_value=None) + task._gearbox_lanes_dict = {} + + assert task.get_sibling_port_configs('Ethernet0') == [] + + def test_CmisManagerTask_get_sibling_port_configs_uses_gearbox_lane_count(self): + """get_sibling_port_configs uses gearbox lane count when present.""" + port_mapping = PortMapping() + stop_event = threading.Event() + task = CmisManagerTask(DEFAULT_NAMESPACE, port_mapping, stop_event, platform_chassis=MagicMock()) + task.port_dict['Ethernet0'] = {'index': 1, 'asic_id': 0} + + cfg_port_tbl = MagicMock() + cfg_port_tbl.getKeys = MagicMock(return_value=['Ethernet0']) + cfg_port_tbl.get = MagicMock(return_value=( + True, [('speed', '200000'), ('lanes', '1,2,3,4'), ('subport', '1'), ('index', '1')])) + task.xcvr_table_helper.get_cfg_port_tbl = MagicMock(return_value=cfg_port_tbl) + task._gearbox_lanes_dict = {'Ethernet0': 2} + + siblings = task.get_sibling_port_configs('Ethernet0') + + assert siblings == [ + {'lport': 'Ethernet0', 'subport': 1, 'speed': 200000, 'host_lane_count': 2}, + ] DEFAULT_DP_STATE = { 'DP1State': 'DataPathActivated', @@ -3067,8 +3356,9 @@ def test_CmisManagerTask_get_host_lane_count(self, gearbox_lanes_dict, lport, po port_mapping = PortMapping() stop_event = threading.Event() task = CmisManagerTask(DEFAULT_NAMESPACE, port_mapping, stop_event, platform_chassis=MagicMock()) + task._gearbox_lanes_dict = gearbox_lanes_dict - result = task.get_host_lane_count(lport, port_config_lanes, gearbox_lanes_dict) + result = task.get_host_lane_count(lport, port_config_lanes) assert result == expected_count def test_CmisManagerTask_gearbox_integration_end_to_end(self): @@ -3103,7 +3393,8 @@ def test_CmisManagerTask_gearbox_integration_end_to_end(self): } # 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) + task._gearbox_lanes_dict = gearbox_lanes_dict + host_lane_count = task.get_host_lane_count("Ethernet0", port_config_lanes) assert host_lane_count == 2 # Should use gearbox line lanes, not port config # Test that this leads to correct CMIS application selection @@ -3123,13 +3414,14 @@ def test_CmisManagerTask_gearbox_caching_integration(self): 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) + task._gearbox_lanes_dict = mock_gearbox_lanes_dict + result1 = task.get_host_lane_count("Ethernet0", "25,26,27,28") assert result1 == 2 # Should use gearbox count - result2 = task.get_host_lane_count("Ethernet4", "29,30", mock_gearbox_lanes_dict) + result2 = task.get_host_lane_count("Ethernet4", "29,30") assert result2 == 4 # Should use gearbox count - result3 = task.get_host_lane_count("Ethernet8", "33,34,35", mock_gearbox_lanes_dict) + result3 = task.get_host_lane_count("Ethernet8", "33,34,35") assert result3 == 3 # Should fall back to port config count @patch('swsscommon.swsscommon.FieldValuePairs') diff --git a/sonic-xcvrd/xcvrd/cmis/cmis_manager_task.py b/sonic-xcvrd/xcvrd/cmis/cmis_manager_task.py index 0c275b6ff..5a252ecef 100644 --- a/sonic-xcvrd/xcvrd/cmis/cmis_manager_task.py +++ b/sonic-xcvrd/xcvrd/cmis/cmis_manager_task.py @@ -61,6 +61,8 @@ def __init__(self, namespaces, port_mapping, main_thread_stop_event, skip_cmis_m self.platform_chassis = platform_chassis self.xcvr_table_helper = XcvrTableHelper(self.namespaces) self.is_fast_reboot_enabled = False + # Cache of gearbox line lanes dict, refreshed once per task_worker iteration. + self._gearbox_lanes_dict = None def log_debug(self, message): helper_logger.log_debug(message) @@ -188,20 +190,19 @@ 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): + def get_host_lane_count(self, lport, port_config_lanes): """ 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) + # First try to get from gearbox configuration cache (refreshed each task_worker iteration) + gearbox_host_lane_count = (self._gearbox_lanes_dict or {}).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 @@ -372,24 +373,144 @@ def is_decomm_failed(self, lport): == CMIS_STATE_FAILED ) - def is_decommission_required(self, api, app_new): + def get_sibling_port_configs(self, lport): """ - Check if the CMIS decommission (i.e. reset appl code to 0 for all lanes - of the entire physical port) is required + Fetch sibling logical port configurations sharing the same physical port + as lport from the CONFIG_DB PORT table. + + Args: + lport: + String, logical port name triggering this check + + Returns: + list of dicts, one per sibling logical port on the same physical port, + each with keys: 'lport' (str), 'subport' (int), 'speed' (int), + 'host_lane_count' (int). + """ + siblings = [] + pport = self.port_dict[lport].get('index') + cfg_port_tbl = self.xcvr_table_helper.get_cfg_port_tbl(self.get_asic_id(lport)) + if cfg_port_tbl is None: + self.log_error("{}: cfg_port_tbl is None while fetching sibling port configs".format(lport)) + return siblings + + for sibling_lport in cfg_port_tbl.getKeys(): + # Single read per key: use full hash and derive index from fields. + found, port_info = cfg_port_tbl.get(sibling_lport) + if not found: + continue + port_info_dict = dict(port_info) + + sibling_pport = port_info_dict.get('index') + if sibling_pport is None: + continue + + try: + if int(sibling_pport) != pport: + continue + except (TypeError, ValueError): + self.log_error("{}: invalid index value for sibling port {}: {}".format( + lport, sibling_lport, sibling_pport)) + continue + + sibling_speed_raw = port_info_dict.get('speed', 0) + sibling_subport_raw = port_info_dict.get('subport', 0) + + try: + sibling_speed = int(sibling_speed_raw) + sibling_subport = int(sibling_subport_raw) + except (TypeError, ValueError): + self.log_error("{}: invalid speed or subport value for sibling port {}: speed={}, subport={}".format( + lport, sibling_lport, sibling_speed_raw, sibling_subport_raw)) + continue + + sibling_lanes = port_info_dict.get('lanes', '') + + if not sibling_speed or not sibling_lanes: + continue + + sibling_host_lane_count = self.get_host_lane_count(sibling_lport, sibling_lanes) + + siblings.append({ + 'lport': sibling_lport, + 'subport': sibling_subport, + 'speed': sibling_speed, + 'host_lane_count': sibling_host_lane_count, + }) + + return siblings + + def get_desired_app_map(self, api, lport): + """ + Build a per-lane desired application code map for all lanes of the + physical port that lport belongs to, using sibling logical port + configurations fetched from the CONFIG_DB PORT table. Args: api: XcvrApi object - app_new: - Integer, the new desired appl code + lport: + String, logical port name triggering this check + + Returns: + list of CMIS_MAX_HOST_LANES integers, desired app code per lane + (0 = unused/unassigned) + """ + desired_map = [0] * self.CMIS_MAX_HOST_LANES + for sibling in self.get_sibling_port_configs(lport): + sibling_appl = common.get_cmis_application_desired( + api, sibling['host_lane_count'], sibling['speed']) + if sibling_appl is None: + continue + + sibling_mask = self.get_cmis_host_lanes_mask( + api, sibling_appl, sibling['host_lane_count'], sibling['subport']) + for lane in range(self.CMIS_MAX_HOST_LANES): + if (1 << lane) & sibling_mask: + desired_map[lane] = sibling_appl + + return desired_map + + def is_decommission_required(self, api, lport): + """ + Check if CMIS decommission (reset AppSel to 0 for all lanes of the + physical port) is required. Per CMIS spec, a DP's lane width can only + be changed while in DPDeactivated state, so decommission is needed when + any currently active lane needs a different application code. + + Lanes that are currently unused (AppSel=0) are ignored — adding new DPs + on unused lanes does not require decommission. + + Args: + api: + XcvrApi object + lport: + String, logical port name triggering the check Returns: True, if decommission is required False, if decommission is not required """ + desired_map = self.get_desired_app_map(api, lport) + active_apsel = api.get_active_apsel_hostlane() + current_map = [] + for lane in range(self.CMIS_MAX_HOST_LANES): + lane_key = 'ActiveAppSelLane{}'.format(lane + 1) + if lane_key not in active_apsel: + self.log_error("{}: missing ActiveAppSel key: {}".format(lport, lane_key)) + return True + lane_value = active_apsel[lane_key] + try: + current_map.append(int(lane_value)) + except (TypeError, ValueError): + self.log_error("{}: invalid ActiveAppSel value for {}: {}".format(lport, lane_key, lane_value)) + return True + + self.log_notice("{}: current app map {}, desired app map {}".format(lport, current_map, desired_map)) + for lane in range(self.CMIS_MAX_HOST_LANES): - app_cur = api.get_application(lane) - if app_cur != 0 and app_cur != app_new: + if current_map[lane] != 0 and current_map[lane] != desired_map[lane]: return True + return False def is_cmis_application_update_required(self, api, app_new, host_lanes_mask): @@ -715,7 +836,7 @@ def is_timer_expired(self, expired_time, current_time=None): return expired_time <= current_time - def process_single_lport(self, lport, info, gearbox_lanes_dict): + def process_single_lport(self, lport, info): state = common.get_cmis_state_from_state_db(lport, self.xcvr_table_helper.get_status_sw_tbl(self.get_asic_id(lport))) if state in CMIS_TERMINAL_STATES or state == CMIS_STATE_UNKNOWN: if state != CMIS_STATE_READY: @@ -740,7 +861,7 @@ def process_single_lport(self, lport, info, gearbox_lanes_dict): # Desired port speed on the host side host_speed = speed - host_lane_count = self.get_host_lane_count(lport, lanes, gearbox_lanes_dict) + host_lane_count = self.get_host_lane_count(lport, lanes) # double-check the HW presence before moving forward sfp = self.platform_chassis.get_sfp(pport) @@ -848,7 +969,7 @@ def process_single_lport(self, lport, info, gearbox_lanes_dict): media_lanes_mask = self.port_dict[lport]['media_lanes_mask'] self.log_notice("{}: Setting media_lanemask=0x{:x}".format(lport, media_lanes_mask)) - if self.is_decommission_required(api, appl): + if self.is_decommission_required(api, lport): self.set_decomm_pending(lport) if self.is_decomm_lead_lport(lport): @@ -1104,7 +1225,7 @@ def task_worker(self): port_change_observer.handle_port_update_event() # Cache gearbox line lanes dictionary once per iteration over all ports - gearbox_lanes_dict = self.xcvr_table_helper.get_gearbox_line_lanes_dict() + self._gearbox_lanes_dict = self.xcvr_table_helper.get_gearbox_line_lanes_dict() # Cache fast reboot enabled state once per iteration over all ports self.is_fast_reboot_enabled = common.is_fast_reboot_enabled() @@ -1116,7 +1237,7 @@ def task_worker(self): if lport not in self.port_dict: continue - self.process_single_lport(lport, info, gearbox_lanes_dict) + self.process_single_lport(lport, info) self.log_notice("Stopped")