Skip to content

Commit 7e594d1

Browse files
Fix up xcvrd to use line lanes from gearbox table if present (#689)
* Fix up xcvrd to look at line lanes from gearbox if present - Implement new method get_gearbox_line_lanes_dict in xcvr_table_helper.py to return a dict of number of gearbox line lanes indexed by logical port. - Building this dict and caching it in CmisTaskManager before a new iteration over port_dict - Implement new method get_host_lane_count to look up the gearbox line lanes in this cached dict for every logical port. If found, use this as the host_lane_count, else use the host_lane_count from port_config - Use this new method when setting host_lane_count for every logical port iteration * Addressed nit in comment - line -> line-side
1 parent cf87190 commit 7e594d1

File tree

3 files changed

+246
-3
lines changed

3 files changed

+246
-3
lines changed

sonic-xcvrd/tests/test_xcvrd.py

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2575,6 +2575,173 @@ def get_host_lane_assignment_option_side_effect(app):
25752575
appl = common.get_cmis_application_desired(mock_xcvr_api, host_lane_count, speed)
25762576
assert task.get_cmis_host_lanes_mask(mock_xcvr_api, appl, host_lane_count, subport) == expected
25772577

2578+
@pytest.mark.parametrize("gearbox_data, expected_dict", [
2579+
# Test case 1: Gearbox port with 2 line lanes
2580+
({
2581+
"interface:0": {
2582+
"name": "Ethernet0",
2583+
"index": "0",
2584+
"phy_id": "1",
2585+
"system_lanes": "300,301,302,303",
2586+
"line_lanes": "304,305"
2587+
}
2588+
}, {"Ethernet0": 2}),
2589+
# Test case 2: Multiple gearbox ports
2590+
({
2591+
"interface:0": {
2592+
"name": "Ethernet0",
2593+
"index": "0",
2594+
"phy_id": "1",
2595+
"system_lanes": "300,301,302,303",
2596+
"line_lanes": "304,305,306,307"
2597+
},
2598+
"interface:200": {
2599+
"name": "Ethernet200",
2600+
"index": "200",
2601+
"phy_id": "2",
2602+
"system_lanes": "400,401",
2603+
"line_lanes": "404,405"
2604+
}
2605+
}, {"Ethernet0": 4, "Ethernet200": 2}),
2606+
# Test case 3: Empty gearbox data
2607+
({}, {}),
2608+
# Test case 4: Gearbox interface with empty line_lanes
2609+
({
2610+
"interface:0": {
2611+
"name": "Ethernet0",
2612+
"index": "0",
2613+
"phy_id": "1",
2614+
"system_lanes": "300,301,302,303",
2615+
"line_lanes": ""
2616+
}
2617+
}, {}),
2618+
# Test case 5: Non-interface keys (should be ignored)
2619+
({
2620+
"interface:0": {
2621+
"name": "Ethernet0",
2622+
"index": "0",
2623+
"phy_id": "1",
2624+
"system_lanes": "300,301,302,303",
2625+
"line_lanes": "304,305"
2626+
},
2627+
"phy:1": {
2628+
"name": "phy1",
2629+
"some_field": "some_value"
2630+
}
2631+
}, {"Ethernet0": 2})
2632+
])
2633+
def test_XcvrTableHelper_get_gearbox_line_lanes_dict(self, gearbox_data, expected_dict):
2634+
# Mock the XcvrTableHelper and APPL_DB access
2635+
mock_appl_db = MagicMock()
2636+
mock_gearbox_table = MagicMock()
2637+
2638+
# Mock table.getKeys() to return gearbox interface keys
2639+
mock_gearbox_table.getKeys.return_value = list(gearbox_data.keys())
2640+
2641+
# Mock table.get() to return gearbox interface data
2642+
def mock_get_side_effect(key):
2643+
if key in gearbox_data:
2644+
# Convert dict to list of tuples for fvs format
2645+
interface_data = gearbox_data[key]
2646+
fvs_list = [(k, v) for k, v in interface_data.items()]
2647+
return (True, fvs_list)
2648+
return (False, [])
2649+
2650+
mock_gearbox_table.get.side_effect = mock_get_side_effect
2651+
2652+
# Mock swsscommon.Table constructor to return our mock table
2653+
with patch('xcvrd.xcvrd_utilities.xcvr_table_helper.swsscommon.Table', return_value=mock_gearbox_table):
2654+
# Mock the helper_logger to avoid logging during tests
2655+
with patch('xcvrd.xcvrd_utilities.xcvr_table_helper.helper_logger'):
2656+
helper = XcvrTableHelper(DEFAULT_NAMESPACE)
2657+
helper.appl_db = {0: mock_appl_db} # Mock the appl_db dict
2658+
2659+
result = helper.get_gearbox_line_lanes_dict()
2660+
assert result == expected_dict
2661+
2662+
@pytest.mark.parametrize("gearbox_lanes_dict, lport, port_config_lanes, expected_count", [
2663+
# Test case 1: Gearbox data available, should use gearbox count
2664+
({"Ethernet0": 2}, "Ethernet0", "25,26,27,28", 2),
2665+
# Test case 2: Gearbox data available with 4 lanes
2666+
({"Ethernet0": 4}, "Ethernet0", "29,30", 4),
2667+
# Test case 3: No gearbox data for this port, should use port config
2668+
({"Ethernet4": 2}, "Ethernet0", "33,34,35,36", 4),
2669+
# Test case 4: Empty gearbox dict, should use port config
2670+
({}, "Ethernet0", "37,38", 2),
2671+
# Test case 5: Multiple ports in gearbox dict
2672+
({"Ethernet0": 2, "Ethernet4": 4}, "Ethernet0", "25,26,27,28", 2),
2673+
# Test case 6: Port not in gearbox dict
2674+
({"Ethernet4": 4}, "Ethernet8", "41,42,43", 3)
2675+
])
2676+
def test_CmisManagerTask_get_host_lane_count(self, gearbox_lanes_dict, lport, port_config_lanes, expected_count):
2677+
port_mapping = PortMapping()
2678+
stop_event = threading.Event()
2679+
task = CmisManagerTask(DEFAULT_NAMESPACE, port_mapping, stop_event)
2680+
2681+
result = task.get_host_lane_count(lport, port_config_lanes, gearbox_lanes_dict)
2682+
assert result == expected_count
2683+
2684+
def test_CmisManagerTask_gearbox_integration_end_to_end(self):
2685+
"""Test end-to-end integration of gearbox line lanes with CMIS application selection"""
2686+
port_mapping = PortMapping()
2687+
stop_event = threading.Event()
2688+
task = CmisManagerTask(DEFAULT_NAMESPACE, port_mapping, stop_event)
2689+
2690+
# Mock gearbox lanes dictionary - port has 4 system lanes but only 2 line lanes
2691+
gearbox_lanes_dict = {"Ethernet0": 2} # 2 line lanes from gearbox
2692+
2693+
# Mock port config - would normally give 4 lanes
2694+
port_config_lanes = "25,26,27,28" # 4 lanes from port config
2695+
2696+
# Mock CMIS API with application advertisement
2697+
mock_xcvr_api = MagicMock()
2698+
mock_xcvr_api.get_application_advertisement.return_value = {
2699+
1: {
2700+
'host_electrical_interface_id': '100GAUI-2 C2M (Annex 135G)',
2701+
'module_media_interface_id': '100G-FR/100GBASE-FR1 (Cl 140)',
2702+
'media_lane_count': 1,
2703+
'host_lane_count': 2, # Matches our gearbox line lanes
2704+
'host_lane_assignment_options': 85
2705+
},
2706+
2: {
2707+
'host_electrical_interface_id': 'CAUI-4 C2M (Annex 83E)',
2708+
'module_media_interface_id': 'Active Cable assembly',
2709+
'media_lane_count': 4,
2710+
'host_lane_count': 4, # Would match port config lanes
2711+
'host_lane_assignment_options': 17
2712+
}
2713+
}
2714+
2715+
# Test the integration: should use gearbox line lanes (2) not port config lanes (4)
2716+
host_lane_count = task.get_host_lane_count("Ethernet0", port_config_lanes, gearbox_lanes_dict)
2717+
assert host_lane_count == 2 # Should use gearbox line lanes, not port config
2718+
2719+
# Test that this leads to correct CMIS application selection
2720+
with patch('xcvrd.xcvrd_utilities.common.is_cmis_api', return_value=True):
2721+
appl = common.get_cmis_application_desired(mock_xcvr_api, host_lane_count, 100000)
2722+
assert appl == 1 # Should select application 1 (2 lanes) not application 2 (4 lanes)
2723+
2724+
def test_CmisManagerTask_gearbox_caching_integration(self):
2725+
"""Test that gearbox lanes dictionary is properly cached and used in task worker"""
2726+
port_mapping = PortMapping()
2727+
stop_event = threading.Event()
2728+
task = CmisManagerTask(DEFAULT_NAMESPACE, port_mapping, stop_event)
2729+
2730+
# Mock the XcvrTableHelper to return a gearbox lanes dictionary
2731+
mock_gearbox_lanes_dict = {"Ethernet0": 2, "Ethernet4": 4}
2732+
task.xcvr_table_helper = MagicMock()
2733+
task.xcvr_table_helper.get_gearbox_line_lanes_dict.return_value = mock_gearbox_lanes_dict
2734+
2735+
# Test that get_host_lane_count uses the cached dictionary correctly
2736+
result1 = task.get_host_lane_count("Ethernet0", "25,26,27,28", mock_gearbox_lanes_dict)
2737+
assert result1 == 2 # Should use gearbox count
2738+
2739+
result2 = task.get_host_lane_count("Ethernet4", "29,30", mock_gearbox_lanes_dict)
2740+
assert result2 == 4 # Should use gearbox count
2741+
2742+
result3 = task.get_host_lane_count("Ethernet8", "33,34,35", mock_gearbox_lanes_dict)
2743+
assert result3 == 3 # Should fall back to port config count
2744+
25782745
@patch('swsscommon.swsscommon.FieldValuePairs')
25792746
def test_CmisManagerTask_post_port_active_apsel_to_db_error_cases(self, mock_field_value_pairs):
25802747
mock_xcvr_api = MagicMock()

sonic-xcvrd/xcvrd/xcvrd.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,29 @@ def get_cmis_module_power_up_duration_secs(self, api):
465465
def get_cmis_module_power_down_duration_secs(self, api):
466466
return api.get_module_pwr_down_duration()/1000
467467

468+
def get_host_lane_count(self, lport, port_config_lanes, gearbox_lanes_dict):
469+
"""
470+
Get host lane count from gearbox configuration if available, otherwise from port config
471+
472+
Args:
473+
lport: logical port name (e.g., "Ethernet0")
474+
port_config_lanes: lanes string from port config (e.g., "25,26,27,28")
475+
gearbox_lanes_dict: dictionary of gearbox line lanes counts
476+
477+
Returns:
478+
Integer: number of host lanes
479+
"""
480+
# First try to get from gearbox configuration
481+
gearbox_host_lane_count = gearbox_lanes_dict.get(lport, 0)
482+
if gearbox_host_lane_count > 0:
483+
self.log_debug("{}: Using gearbox line lanes count: {}".format(lport, gearbox_host_lane_count))
484+
return gearbox_host_lane_count
485+
486+
# Fallback to port config lanes
487+
host_lane_count = len(port_config_lanes.split(','))
488+
self.log_debug("{}: Using port config lanes count: {}".format(lport, host_lane_count))
489+
return host_lane_count
490+
468491
def get_cmis_host_lanes_mask(self, api, appl, host_lane_count, subport):
469492
"""
470493
Retrieves mask of active host lanes based on appl, host lane count and subport
@@ -972,6 +995,9 @@ def task_worker(self):
972995
# Handle port change event from main thread
973996
port_change_observer.handle_port_update_event()
974997

998+
# Cache gearbox line lanes dictionary for this set of iterations over the port_dict
999+
gearbox_lanes_dict = self.xcvr_table_helper.get_gearbox_line_lanes_dict()
1000+
9751001
for lport, info in self.port_dict.items():
9761002
if self.task_stopping_event.is_set():
9771003
break
@@ -1003,7 +1029,7 @@ def task_worker(self):
10031029

10041030
# Desired port speed on the host side
10051031
host_speed = speed
1006-
host_lane_count = len(lanes.split(','))
1032+
host_lane_count = self.get_host_lane_count(lport, lanes, gearbox_lanes_dict)
10071033

10081034
# double-check the HW presence before moving forward
10091035
sfp = platform_chassis.get_sfp(pport)

sonic-xcvrd/xcvrd/xcvrd_utilities/xcvr_table_helper.py

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ def __init__(self, namespaces):
5656
self.int_tbl, self.dom_tbl, self.dom_threshold_tbl, self.status_tbl, self.app_port_tbl, \
5757
self.cfg_port_tbl, self.state_port_tbl, self.pm_tbl, self.firmware_info_tbl = {}, {}, {}, {}, {}, {}, {}, {}, {}
5858
self.state_db = {}
59+
self.appl_db = {}
5960
self.cfg_db = {}
6061
self.dom_flag_tbl = {}
6162
self.dom_flag_change_count_tbl = {}
@@ -92,8 +93,8 @@ def __init__(self, namespaces):
9293
self.pm_tbl[asic_id] = swsscommon.Table(self.state_db[asic_id], TRANSCEIVER_PM_TABLE)
9394
self.firmware_info_tbl[asic_id] = swsscommon.Table(self.state_db[asic_id], TRANSCEIVER_FIRMWARE_INFO_TABLE)
9495
self.state_port_tbl[asic_id] = swsscommon.Table(self.state_db[asic_id], swsscommon.STATE_PORT_TABLE_NAME)
95-
appl_db = daemon_base.db_connect("APPL_DB", namespace)
96-
self.app_port_tbl[asic_id] = swsscommon.ProducerStateTable(appl_db, swsscommon.APP_PORT_TABLE_NAME)
96+
self.appl_db[asic_id] = daemon_base.db_connect("APPL_DB", namespace)
97+
self.app_port_tbl[asic_id] = swsscommon.ProducerStateTable(self.appl_db[asic_id], swsscommon.APP_PORT_TABLE_NAME)
9798
self.cfg_db[asic_id] = daemon_base.db_connect("CONFIG_DB", namespace)
9899
self.cfg_port_tbl[asic_id] = swsscommon.Table(self.cfg_db[asic_id], swsscommon.CFG_PORT_TABLE_NAME)
99100
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):
238239

239240
# If npu_si_settings_sync_val is None, it can also mean that the key is not present in the table
240241
return npu_si_settings_sync_val is None or npu_si_settings_sync_val == NPU_SI_SETTINGS_DEFAULT_VALUE
242+
243+
def get_gearbox_line_lanes_dict(self):
244+
"""
245+
Retrieves the gearbox line lanes dictionary from APPL_DB
246+
247+
This method scans all ASICs for gearbox interface configurations and extracts
248+
the line_lanes count for each logical port. The line_lanes represent the
249+
number of lanes on the line side (towards the optical module) which is the
250+
correct count to use for CMIS host lane configuration.
251+
252+
Returns:
253+
dict: A dictionary where:
254+
- key (str): logical port name (e.g., "Ethernet0")
255+
- value (int): number of line-side lanes for that port
256+
257+
Example:
258+
{"Ethernet0": 2, "Ethernet200": 4}
259+
260+
Note:
261+
- Returns empty dict if no gearbox configuration is found
262+
- Silently skips invalid or malformed entries
263+
- Only processes keys that start with "interface:"
264+
"""
265+
gearbox_line_lanes_dict = {}
266+
try:
267+
for asic_id in self.appl_db:
268+
appl_db = self.appl_db[asic_id]
269+
gearbox_table = swsscommon.Table(appl_db, "_GEARBOX_TABLE")
270+
interface_keys = gearbox_table.getKeys()
271+
for key in interface_keys:
272+
if key.startswith("interface:"):
273+
(found, fvs) = gearbox_table.get(key)
274+
if found:
275+
fvs_dict = dict(fvs)
276+
interface_name = fvs_dict.get('name', '')
277+
line_lanes_str = fvs_dict.get('line_lanes', '')
278+
if interface_name and line_lanes_str:
279+
line_lanes_count = len(line_lanes_str.split(','))
280+
gearbox_line_lanes_dict[interface_name] = line_lanes_count
281+
else:
282+
if not interface_name:
283+
helper_logger.log_warning("get_gearbox_line_lanes_dict: ASIC {}: Interface {} missing 'name' field".format(asic_id, key))
284+
if not line_lanes_str:
285+
helper_logger.log_debug("get_gearbox_line_lanes_dict: ASIC {}: Interface {} has empty 'line_lanes' field".format(asic_id, interface_name))
286+
except Exception as e:
287+
helper_logger.log_error("Error in get_gearbox_line_lanes_dict: {}".format(str(e)))
288+
return gearbox_line_lanes_dict
289+
290+
return gearbox_line_lanes_dict

0 commit comments

Comments
 (0)