Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 167 additions & 0 deletions sonic-xcvrd/tests/test_xcvrd.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
28 changes: 27 additions & 1 deletion sonic-xcvrd/xcvrd/xcvrd.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
54 changes: 52 additions & 2 deletions sonic-xcvrd/xcvrd/xcvrd_utilities/xcvr_table_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 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
Loading