From f55d15d497870f69414c6002c002ad750f74fe5f Mon Sep 17 00:00:00 2001 From: Prince George <45705344+prgeor@users.noreply.github.com> Date: Wed, 4 Mar 2026 12:14:29 -0800 Subject: [PATCH 1/2] Refactor media-settings parser (#762) * Refactor media-setting parser Signed-off-by: Prince George --- sonic-xcvrd/tests/test_xcvrd.py | 31 ++- sonic-xcvrd/xcvrd/xcvrd_utilities/common.py | 9 + .../xcvrd_utilities/media_settings_parser.py | 206 +++++++++++------- 3 files changed, 156 insertions(+), 90 deletions(-) diff --git a/sonic-xcvrd/tests/test_xcvrd.py b/sonic-xcvrd/tests/test_xcvrd.py index 7608896..47e4265 100644 --- a/sonic-xcvrd/tests/test_xcvrd.py +++ b/sonic-xcvrd/tests/test_xcvrd.py @@ -184,6 +184,23 @@ media_settings_global_default_port_media_key_lane_speed_si['GLOBAL_MEDIA_SETTINGS'] = {'0-31': {'Default': asic_serdes_si_settings_example}} media_settings_global_default_port_media_key_lane_speed_si['PORT_MEDIA_SETTINGS'] = port_media_settings_data +# Fixture: PORT_MEDIA_SETTINGS vendor match should override GLOBAL_MEDIA_SETTINGS Default +# for the same port. GLOBAL has only Default; PORT has a specific vendor entry. +media_settings_port_overrides_global_default = { + 'GLOBAL_MEDIA_SETTINGS': { + '0-31': { + 'Default': asic_serdes_si_settings_example + } + }, + 'PORT_MEDIA_SETTINGS': { + '7': { + 'AMPHANOL-5678': { + 'speed:100GAUI-2': asic_serdes_si_settings_example2 + } + } + } +} + media_settings_port_default_media_key_lane_speed_si = copy.deepcopy(media_settings_port_media_key_lane_speed_si) media_settings_port_default_media_key_lane_speed_si['PORT_MEDIA_SETTINGS']['7']['Default'] = { LANE_SPEED_DEFAULT_KEY: asic_serdes_si_settings_example, @@ -318,7 +335,7 @@ def test_CmisManagerTask_get_xcvr_api_exception(self, mock_platform_chassis, moc task.xcvr_table_helper = XcvrTableHelper(DEFAULT_NAMESPACE) task.xcvr_table_helper.get_status_sw_tbl.return_value = mock_get_status_sw_tbl port_change_event = PortChangeEvent('Ethernet0', 1, 0, PortChangeEvent.PORT_SET, - {'speed':'400000', 'lanes':'1,2,3,4,5,6,7,8', + {'speed':'400000', 'lanes':'1,2,3,4,5,6,7,8', 'admin_status':'up', 'host_tx_status':'true'}) # Case 1: get_xcvr_api() raises an exception @@ -1548,6 +1565,8 @@ def test_is_si_per_speed_supported(self): (media_settings_port_generic_vendor_key_si, 7, {'vendor_key': 'GENERIC_VENDOR-1234', 'media_key': 'UNKOWN', 'lane_speed_key': 'UNKOWN', 'medium_lane_speed_key': 'UNKNOWN'}, {'pre1': {'lane0': '0x00000002', 'lane1': '0x00000002'}, 'main': {'lane0': '0x00000020', 'lane1': '0x00000020'}, 'post1': {'lane0': '0x00000006', 'lane1': '0x00000006'}, 'regn_bfm1n': {'lane0': '0x000000aa', 'lane1': '0x000000aa'}}), (media_settings_port_default_media_key_lane_speed_si, 7, {'vendor_key': 'MISSING', 'media_key': 'MISSING', 'lane_speed_key': 'MISSING', 'medium_lane_speed_key': 'COPPER50'}, asic_serdes_si_settings_example), (media_settings_global_default_port_media_key_lane_speed_si, 7, {'vendor_key': 'MISSING', 'media_key': 'MISSING', 'lane_speed_key': 'MISSING', 'medium_lane_speed_key': 'UNKNOWN'}, asic_serdes_si_settings_example), + (media_settings_port_overrides_global_default, 7, {'vendor_key': 'AMPHANOL-5678', 'media_key': 'UNKOWN', 'lane_speed_key': 'speed:100GAUI-2', 'medium_lane_speed_key': 'UNKNOWN'}, asic_serdes_si_settings_example2), + (media_settings_port_overrides_global_default, 7, {'vendor_key': 'MISSING', 'media_key': 'MISSING', 'lane_speed_key': 'MISSING', 'medium_lane_speed_key': 'UNKNOWN'}, asic_serdes_si_settings_example), (media_settings_global_list_of_ranges_media_key_lane_speed_si_with_default_section, 7, {'vendor_key': 'MISSING', 'media_key': 'MISSING', 'lane_speed_key': 'MISSING', 'medium_lane_speed_key': 'COPPER50'}, asic_serdes_si_settings_example), (media_settings_empty, 7, {'vendor_key': 'AMPHANOL-5678', 'media_key': 'QSFP-DD-active_cable_media_interface', 'lane_speed_key': 'speed:100GAUI-2', 'medium_lane_speed_key': 'COPPER50'}, {}), (media_settings_with_regular_expression_dict, 7, {'vendor_key': 'UNKOWN', 'media_key': 'QSFP28-40GBASE-CR4-1M', 'lane_speed_key': 'UNKOWN', 'medium_lane_speed_key': 'UNKNOWN'}, {'preemphasis': {'lane0': '0x16440A', 'lane1': '0x16440A', 'lane2': '0x16440A', 'lane3': '0x16440A'}}), @@ -1561,11 +1580,11 @@ def test_get_media_settings_value(self, media_settings_dict, port, key, expected result = media_settings_parser.get_media_settings_value(port, key) assert result == expected - @patch('xcvrd.xcvrd.platform_chassis') - def test_get_is_copper_exception(self, mock_chassis): + @patch('xcvrd.xcvrd_utilities.common.platform_chassis') + def test_is_copper_exception(self, mock_chassis): mock_sfp = MagicMock() mock_chassis.get_sfp = MagicMock(return_value=mock_sfp, side_effect=AttributeError) - result = media_settings_parser.get_is_copper(0) + result = common.is_copper(0) assert result == True @patch('xcvrd.xcvrd_utilities.common._wrapper_get_presence', MagicMock(return_value=True)) @@ -2058,7 +2077,7 @@ def test_handle_port_config_change(self, mock_select, mock_sub_table): def test_get_port_mapping(self, mock_swsscommon_table): mock_table = MagicMock() mock_table.getKeys = MagicMock(return_value=['Ethernet0', 'Ethernet4', 'Ethernet-IB0', 'Ethernet8']) - mock_table.get = MagicMock(side_effect=[(True, (('index', 1), )), (True, (('index', 2), )), + mock_table.get = MagicMock(side_effect=[(True, (('index', 1), )), (True, (('index', 2), )), (True, (('index', 3), )), (True, (('index', 4), ('role', 'Dpc')))]) mock_swsscommon_table.return_value = mock_table port_mapping = get_port_mapping(DEFAULT_NAMESPACE) @@ -6150,7 +6169,7 @@ def test_load_optics_si_settings_no_file(self): """Test load_optics_si_settings when no file exists""" from xcvrd.xcvrd_utilities.optics_si_parser import load_optics_si_settings - with patch('sonic_py_common.device_info.get_paths_to_platform_and_hwsku_dirs', + with patch('sonic_py_common.device_info.get_paths_to_platform_and_hwsku_dirs', return_value=('/nonexistent/platform', '/nonexistent/hwsku')): with patch('os.path.isfile', return_value=False): result = load_optics_si_settings() diff --git a/sonic-xcvrd/xcvrd/xcvrd_utilities/common.py b/sonic-xcvrd/xcvrd/xcvrd_utilities/common.py index 65f124a..a370b81 100644 --- a/sonic-xcvrd/xcvrd/xcvrd_utilities/common.py +++ b/sonic-xcvrd/xcvrd/xcvrd_utilities/common.py @@ -95,6 +95,15 @@ def update_port_transceiver_status_table_sw(logical_port_name, status_sw_tbl, st fvs = swsscommon.FieldValuePairs([('status', status), ('error', error_descriptions)]) status_sw_tbl.set(logical_port_name, fvs) +def is_copper(physical_port): + """Check if the transceiver on the given physical port is copper""" + if platform_chassis: + try: + return platform_chassis.get_sfp(physical_port).get_xcvr_api().is_copper() + except (NotImplementedError, AttributeError): + helper_logger.log_debug(f"No is_copper() defined for xcvr api on physical port {physical_port}, assuming Copper") + return True + def _wrapper_get_presence(physical_port): """Wrapper function to get SFP presence status""" if platform_chassis is not None: diff --git a/sonic-xcvrd/xcvrd/xcvrd_utilities/media_settings_parser.py b/sonic-xcvrd/xcvrd/xcvrd_utilities/media_settings_parser.py index af357ab..6060eb3 100644 --- a/sonic-xcvrd/xcvrd/xcvrd_utilities/media_settings_parser.py +++ b/sonic-xcvrd/xcvrd/xcvrd_utilities/media_settings_parser.py @@ -13,9 +13,9 @@ from xcvrd import xcvrd from .xcvr_table_helper import * from . import common +from abc import ABC, abstractmethod -g_dict = {} - +# Constants LANE_SPEED_KEY_PREFIX = "speed:" VENDOR_KEY = 'vendor_key' MEDIA_KEY = 'media_key' @@ -25,9 +25,98 @@ # This is useful if default value is desired when no match is found for lane speed key LANE_SPEED_DEFAULT_KEY = LANE_SPEED_KEY_PREFIX + DEFAULT_KEY SYSLOG_IDENTIFIER = "xcvrd" +GLOBAL_MEDIA_SETTINGS_KEY = 'GLOBAL_MEDIA_SETTINGS' +PORT_MEDIA_SETTINGS_KEY = 'PORT_MEDIA_SETTINGS' +CUSTOM_MEDIA_SETTINGS_KEY = 'CUSTOM_MEDIA_SETTINGS' +PHYSICAL_PORT_NOT_EXIST = -1 + helper_logger = syslogger.SysLogger(SYSLOG_IDENTIFIER, enable_runtime_config=True) -PHYSICAL_PORT_NOT_EXIST = -1 +g_dict = {} + +# Parser base and implementations for modular media settings handling +class MediaSettingsParserBase(ABC): + @abstractmethod + def parse(self, settings, physical_port, key): + """Parse the settings and return (result, default_fallback). + + Returns: + tuple: (result, default_fallback) where *result* is an explicit + vendor/media/speed match and *default_fallback* is the + Default-key match. Either element can be {}. + """ + pass + + @staticmethod + def get_media_settings(key, media_dict): + """Look up media settings by vendor key, media key, or medium lane speed key.""" + for dict_key in media_dict.keys(): + if (re.match(dict_key, key[VENDOR_KEY]) or \ + re.match(dict_key, key[VENDOR_KEY].split('-')[0]) or \ + re.match(dict_key, key[MEDIA_KEY])): + return get_media_settings_for_speed(media_dict[dict_key], key[LANE_SPEED_KEY]) + for dict_key in media_dict.keys(): + if re.match(dict_key, key[MEDIUM_LANE_SPEED_KEY]): + return get_media_settings_for_speed(media_dict[dict_key], key[LANE_SPEED_KEY]) + return None + +class GlobalMediaSettingsParser(MediaSettingsParserBase): + def parse(self, settings, physical_port, key): + RANGE_SEPARATOR = '-' + COMMA_SEPARATOR = ',' + default_dict = {} + lane_speed_key = key[LANE_SPEED_KEY] + + for keys in settings: + media_dict = {} + if COMMA_SEPARATOR in keys: + port_list = keys.split(COMMA_SEPARATOR) + for port in port_list: + if RANGE_SEPARATOR in port: + if common.check_port_in_range(port, physical_port): + media_dict = settings[keys] + break + elif str(physical_port) == port: + media_dict = settings[keys] + break + elif RANGE_SEPARATOR in keys: + if common.check_port_in_range(keys, physical_port): + media_dict = settings[keys] + + if media_dict: + media_settings = self.get_media_settings(key, media_dict) + if media_settings is not None: + return media_settings, {} + elif DEFAULT_KEY in media_dict: + default_dict = get_media_settings_for_speed(media_dict[DEFAULT_KEY], lane_speed_key) + + return {}, default_dict + +class PortMediaSettingsParser(MediaSettingsParserBase): + def parse(self, settings, physical_port, key): + media_dict = {} + lane_speed_key = key[LANE_SPEED_KEY] + + for keys in settings: + if int(keys) == physical_port: + media_dict = settings[keys] + break + + if len(media_dict) == 0: + return {}, {} + + media_settings = self.get_media_settings(key, media_dict) + if media_settings is not None: + return media_settings, {} + elif DEFAULT_KEY in media_dict: + return {}, get_media_settings_for_speed(media_dict[DEFAULT_KEY], lane_speed_key) + return {}, {} + +class CustomMediaSettingsParser(MediaSettingsParserBase): + def parse(self, settings, physical_port, key): + # TODO: Placeholder for custom media setting parser + return {}, {} + def load_media_settings(): global g_dict @@ -54,13 +143,6 @@ def media_settings_present(): return True return False -def get_is_copper(physical_port): - if xcvrd.platform_chassis: - try: - return xcvrd.platform_chassis.get_sfp(physical_port).get_xcvr_api().is_copper() - except (NotImplementedError, AttributeError): - helper_logger.log_debug(f"No is_copper() defined for xcvr api on physical port {physical_port}, assuming Copper") - return True def get_lane_speed_key(physical_port, port_speed, lane_count): """ @@ -148,7 +230,7 @@ def get_media_settings_key(physical_port, transceiver_dict, port_speed, lane_cou media_key += '-' + '*' lane_speed_key = get_lane_speed_key(physical_port, port_speed, lane_count) - medium = "COPPER" if get_is_copper(physical_port) else "OPTICAL" + medium = "COPPER" if common.is_copper(physical_port) else "OPTICAL" speed = int(int(int(port_speed) /lane_count)/1000) medium_lane_speed_key = medium + str(speed) # return (vendor_key, media_key, lane_speed_key) @@ -222,84 +304,40 @@ def get_media_settings_for_speed(settings_dict, lane_speed_key): def get_media_settings_value(physical_port, key): - GLOBAL_MEDIA_SETTINGS_KEY = 'GLOBAL_MEDIA_SETTINGS' - PORT_MEDIA_SETTINGS_KEY = 'PORT_MEDIA_SETTINGS' - RANGE_SEPARATOR = '-' - COMMA_SEPARATOR = ',' - media_dict = {} - default_dict = {} - lane_speed_key = key[LANE_SPEED_KEY] - - def get_media_settings(key, media_dict): - for dict_key in media_dict.keys(): - if (re.match(dict_key, key[VENDOR_KEY]) or \ - re.match(dict_key, key[VENDOR_KEY].split('-')[0]) # e.g: 'AMPHENOL-1234' - or re.match(dict_key, key[MEDIA_KEY]) ): # e.g: 'QSFP28-40GBASE-CR4-1M' - return get_media_settings_for_speed(media_dict[dict_key], key[LANE_SPEED_KEY]) + global_default = {} - if key[MEDIUM_LANE_SPEED_KEY] in media_dict: - return media_dict[key[MEDIUM_LANE_SPEED_KEY]] - - return None - - # Keys under global media settings can be a list or range or list of ranges - # of physical port numbers. Below are some examples - # 1-32 - # 1,2,3,4,5 - # 1-4,9-12 + # Priority order: + # 1. GLOBAL explicit match (vendor/media/speed) + # 2. PORT explicit match + # 3. PORT Default + # 4. CUSTOM explicit match + # 5. GLOBAL Default (last-resort fallback) + # Check global media settings first (can apply to ranges/lists of ports) if GLOBAL_MEDIA_SETTINGS_KEY in g_dict: - for keys in g_dict[GLOBAL_MEDIA_SETTINGS_KEY]: - if COMMA_SEPARATOR in keys: - port_list = keys.split(COMMA_SEPARATOR) - for port in port_list: - if RANGE_SEPARATOR in port: - if common.check_port_in_range(port, physical_port): - media_dict = g_dict[GLOBAL_MEDIA_SETTINGS_KEY][keys] - break - elif str(physical_port) == port: - media_dict = g_dict[GLOBAL_MEDIA_SETTINGS_KEY][keys] - break - - elif RANGE_SEPARATOR in keys: - if common.check_port_in_range(keys, physical_port): - media_dict = g_dict[GLOBAL_MEDIA_SETTINGS_KEY][keys] - - # If there is a match in the global profile for a media type, - # fetch those values - media_settings = get_media_settings(key, media_dict) - if media_settings is not None: - return media_settings - # Try to match 'default' key if it does not match any keys - elif DEFAULT_KEY in media_dict: - default_dict = get_media_settings_for_speed(media_dict[DEFAULT_KEY], lane_speed_key) - - media_dict = {} + result, global_default = GlobalMediaSettingsParser().parse( + g_dict[GLOBAL_MEDIA_SETTINGS_KEY], physical_port, key) + if result: + return result + # Then check port-specific media settings if PORT_MEDIA_SETTINGS_KEY in g_dict: - for keys in g_dict[PORT_MEDIA_SETTINGS_KEY]: - if int(keys) == physical_port: - media_dict = g_dict[PORT_MEDIA_SETTINGS_KEY][keys] - break - - if len(media_dict) == 0: - if len(default_dict) != 0: - return default_dict - else: - helper_logger.log_notice("No values for physical port '{}'".format(physical_port)) - return {} - - media_settings = get_media_settings(key, media_dict) - if media_settings is not None: - return media_settings - # Try to match 'default' key if it does not match any keys - elif DEFAULT_KEY in media_dict: - return get_media_settings_for_speed(media_dict[DEFAULT_KEY], lane_speed_key) - elif len(default_dict) != 0: - return default_dict - else: - if len(default_dict) != 0: - return default_dict + result, port_default = PortMediaSettingsParser().parse( + g_dict[PORT_MEDIA_SETTINGS_KEY], physical_port, key) + if result: + return result + if port_default: + return port_default + + if CUSTOM_MEDIA_SETTINGS_KEY in g_dict: + result, _ = CustomMediaSettingsParser().parse( + g_dict[CUSTOM_MEDIA_SETTINGS_KEY], physical_port, key) + if result: + return result + + # Fall back to global default if no explicit or port-default match found + if global_default: + return global_default return {} From c6ad4d90ef97d7358f72bfaa1dcb7330d2a46507 Mon Sep 17 00:00:00 2001 From: longhuan-cisco <84595962+longhuan-cisco@users.noreply.github.com> Date: Wed, 22 Apr 2026 17:44:12 -0700 Subject: [PATCH 2/2] Add support for custom serdes attributes (#643) * xcvrd: add CUSTOM_MEDIA_SETTINGS support after parser refactor Signed-off-by: Longyin Huang * xcvrd: align media settings parser structure Signed-off-by: Longyin Huang * tests: remove gearbox docstring churn Signed-off-by: Longyin Huang --------- Signed-off-by: Longyin Huang --- sonic-xcvrd/tests/test_xcvrd.py | 441 +++++++++++++++++- .../xcvrd_utilities/media_settings_parser.py | 310 +++++++++--- 2 files changed, 690 insertions(+), 61 deletions(-) diff --git a/sonic-xcvrd/tests/test_xcvrd.py b/sonic-xcvrd/tests/test_xcvrd.py index 47e4265..677a512 100644 --- a/sonic-xcvrd/tests/test_xcvrd.py +++ b/sonic-xcvrd/tests/test_xcvrd.py @@ -232,6 +232,98 @@ media_settings_empty = {} +custom_serdes_attrs_xyz_10 = {'lane0': 10, 'lane1': 11, 'lane2': 12, 'lane3': 13} +custom_serdes_attrs_xyz_20 = {'lane0': 20, 'lane1': 21, 'lane2': 22, 'lane3': 23} +custom_serdes_attrs_abc_mode = {'lane0': 'mode_a', 'lane1': 'mode_b', 'lane2': 'mode_c', 'lane3': 'mode_d'} + +media_settings_custom_attrs = { + 'CUSTOM_MEDIA_SETTINGS': { + '1, 3-4, 8': { + 'QSFP-DD-active_cable_media_interface': { + 'speed:100GAUI-2': { + 'CUSTOM:XYZ': custom_serdes_attrs_xyz_10, + 'CUSTOM:ABC': custom_serdes_attrs_abc_mode, + }, + }, + 'Default': { + 'speed:100GAUI-2': { + 'CUSTOM:XYZ': custom_serdes_attrs_xyz_20, + }, + }, + }, + '7-9': { + 'Default': { + 'speed:100GAUI-2': { + 'CUSTOM:XYZ': custom_serdes_attrs_xyz_20, + }, + }, + }, + } +} + +media_settings_custom_attrs_no_space = { + 'CUSTOM_MEDIA_SETTINGS': { + '1,3-4,8': { + 'QSFP-DD-active_cable_media_interface': { + 'speed:100GAUI-2': { + 'CUSTOM:XYZ': custom_serdes_attrs_xyz_10, + }, + }, + }, + } +} + +media_settings_custom_attrs_non_string_selector = { + 'CUSTOM_MEDIA_SETTINGS': { + 9: { + 'Default': { + 'speed:100GAUI-2': { + 'CUSTOM:XYZ': custom_serdes_attrs_xyz_20, + }, + }, + }, + } +} + +media_settings_custom_attrs_with_port_and_global = copy.deepcopy(media_settings_port_media_key_lane_speed_si) +media_settings_custom_attrs_with_port_and_global['CUSTOM_MEDIA_SETTINGS'] = media_settings_custom_attrs['CUSTOM_MEDIA_SETTINGS'] +media_settings_custom_attrs_with_port_and_global['GLOBAL_MEDIA_SETTINGS'] = { + '0-31': { + 'NO_MATCH': { + 'speed:100GAUI-2': { + 'pre1': {'lane0': '0x000000ff'}, + }, + }, + }, +} + +media_settings_custom_attrs_medium_lane = { + 'CUSTOM_MEDIA_SETTINGS': { + '7-9': { + 'COPPER50': { + 'CUSTOM:XYZ': custom_serdes_attrs_xyz_20, + }, + }, + } +} + +media_settings_custom_attrs_empty_explicit_then_default = { + 'CUSTOM_MEDIA_SETTINGS': { + '7-9': { + 'QSFP-DD-active_cable_media_interface': { + 'speed:200GAUI-4': { + 'CUSTOM:XYZ': custom_serdes_attrs_xyz_10, + }, + }, + 'Default': { + 'speed:100GAUI-2': { + 'CUSTOM:XYZ': custom_serdes_attrs_xyz_20, + }, + }, + }, + } +} + def gen_cmis_lanes_dict(key_format_str, value, one_based=True): start_idx = 1 if one_based else 0 lanes_dict = {} @@ -1645,6 +1737,140 @@ def test_notify_media_setting_with_comma(self): self._check_notify_media_setting(1, True, {'preemphasis': ','.join(['0x164509'] * 2)}) self._check_notify_media_setting(6, True, {'preemphasis': ','.join(['0x124A08'] * 2)}) + @patch('xcvrd.xcvrd_utilities.common._wrapper_get_presence', MagicMock(return_value=True)) + @patch('xcvrd.xcvrd_utilities.media_settings_parser.media_settings_present', MagicMock(return_value=True)) + @patch('xcvrd.xcvrd_utilities.media_settings_parser.get_media_settings_key', + MagicMock(return_value={'vendor_key': 'MOLEX-1064141421', 'media_key': 'QSFP-DD-active_cable_media_interface', 'lane_speed_key': 'speed:100GAUI-2', 'medium_lane_speed_key': 'UNKNOWN'})) + @patch('xcvrd.xcvrd_utilities.media_settings_parser.get_speed_lane_count_and_subport', MagicMock(return_value=(100000, 2, 1))) + def test_notify_media_setting_custom_only(self): + custom_media_dict = { + 'CUSTOM:XYZ': {'lane0': 10, 'lane1': 11, 'lane2': 12, 'lane3': 13}, + } + expected = '{"attributes":[{"XYZ":{"value":[10,11]}}]}' + + xcvr_table_helper = MagicMock() + xcvr_table_helper.is_npu_si_settings_update_required = MagicMock(return_value=True) + xcvr_table_helper.get_cfg_port_tbl = MagicMock(return_value=MagicMock()) + xcvr_table_helper.get_gearbox_line_lanes_dict = MagicMock(return_value={}) + app_port_tbl = MagicMock() + xcvr_table_helper.get_app_port_tbl = MagicMock(return_value=app_port_tbl) + state_port_tbl = MagicMock() + xcvr_table_helper.get_state_port_tbl = MagicMock(return_value=state_port_tbl) + + port_mapping = MagicMock() + port_mapping.get_asic_id_for_logical_port = MagicMock(return_value=0) + port_mapping.logical_port_name_to_physical_port_list = MagicMock(return_value=[1]) + + transceiver_dict = { + 1: { + 'manufacturer': 'Molex', + 'model': '1064141421', + 'cable_type': 'Length Cable Assembly(m)', + 'cable_length': '255', + 'specification_compliance': "{'10/40G Ethernet Compliance Code': '10GBase-SR'}", + 'type_abbrv_name': 'QSFP+' + } + } + with patch.multiple('xcvrd.xcvrd_utilities.media_settings_parser', + get_media_settings_value=MagicMock(return_value={}), + get_custom_media_settings_value=MagicMock(return_value=custom_media_dict)): + media_settings_parser.notify_media_setting('Ethernet0', transceiver_dict, xcvr_table_helper, port_mapping) + + assert app_port_tbl.set.called + set_key, fvs = app_port_tbl.set.call_args[0] + assert set_key == 'Ethernet0' + result_dict = dict(fvs) + assert result_dict == { + media_settings_parser.CustomMediaSettingsParser.CUSTOM_SERDES_ATTRS_KEY_IN_DB: expected + } + + @patch('xcvrd.xcvrd_utilities.common._wrapper_get_presence', MagicMock(return_value=True)) + @patch('xcvrd.xcvrd_utilities.media_settings_parser.media_settings_present', MagicMock(return_value=True)) + @patch('xcvrd.xcvrd_utilities.media_settings_parser.get_media_settings_key', + MagicMock(return_value={'vendor_key': 'MOLEX-1064141421', 'media_key': 'QSFP-DD-active_cable_media_interface', 'lane_speed_key': 'speed:100GAUI-2', 'medium_lane_speed_key': 'UNKNOWN'})) + @patch('xcvrd.xcvrd_utilities.media_settings_parser.get_speed_lane_count_and_subport', MagicMock(return_value=(100000, 2, 1))) + def test_notify_media_setting_empty_serialized_payload(self): + custom_media_dict = { + 'NOT_CUSTOM': {'lane0': 10, 'lane1': 11, 'lane2': 12, 'lane3': 13}, + } + + xcvr_table_helper = MagicMock() + xcvr_table_helper.is_npu_si_settings_update_required = MagicMock(return_value=True) + xcvr_table_helper.get_cfg_port_tbl = MagicMock(return_value=MagicMock()) + xcvr_table_helper.get_gearbox_line_lanes_dict = MagicMock(return_value={}) + app_port_tbl = MagicMock() + xcvr_table_helper.get_app_port_tbl = MagicMock(return_value=app_port_tbl) + state_port_tbl = MagicMock() + xcvr_table_helper.get_state_port_tbl = MagicMock(return_value=state_port_tbl) + + port_mapping = MagicMock() + port_mapping.get_asic_id_for_logical_port = MagicMock(return_value=0) + port_mapping.logical_port_name_to_physical_port_list = MagicMock(return_value=[1]) + + transceiver_dict = { + 1: { + 'manufacturer': 'Molex', + 'model': '1064141421', + 'cable_type': 'Length Cable Assembly(m)', + 'cable_length': '255', + 'specification_compliance': "{'10/40G Ethernet Compliance Code': '10GBase-SR'}", + 'type_abbrv_name': 'QSFP+' + } + } + with patch.multiple('xcvrd.xcvrd_utilities.media_settings_parser', + get_media_settings_value=MagicMock(return_value={}), + get_custom_media_settings_value=MagicMock(return_value=custom_media_dict)): + media_settings_parser.notify_media_setting('Ethernet0', transceiver_dict, xcvr_table_helper, port_mapping) + + assert not app_port_tbl.set.called + assert not state_port_tbl.set.called + + @patch('xcvrd.xcvrd_utilities.common._wrapper_get_presence', MagicMock(return_value=True)) + @patch('xcvrd.xcvrd_utilities.media_settings_parser.media_settings_present', MagicMock(return_value=True)) + @patch('xcvrd.xcvrd_utilities.media_settings_parser.get_media_settings_key', + MagicMock(return_value={'vendor_key': 'UNKOWN', 'media_key': 'QSFP-DD-active_cable_media_interface', 'lane_speed_key': 'speed:100GAUI-2', 'medium_lane_speed_key': 'UNKNOWN'})) + @patch('xcvrd.xcvrd_utilities.media_settings_parser.get_speed_lane_count_and_subport', MagicMock(return_value=(100000, 2, 1))) + def test_notify_media_setting_mixed_settings(self): + expected_custom = '{"attributes":[{"XYZ":{"value":[20,21]}}]}' + xcvr_table_helper = MagicMock() + xcvr_table_helper.is_npu_si_settings_update_required = MagicMock(return_value=True) + xcvr_table_helper.get_cfg_port_tbl = MagicMock(return_value=MagicMock()) + xcvr_table_helper.get_gearbox_line_lanes_dict = MagicMock(return_value={}) + app_port_tbl = MagicMock() + xcvr_table_helper.get_app_port_tbl = MagicMock(return_value=app_port_tbl) + state_port_tbl = MagicMock() + xcvr_table_helper.get_state_port_tbl = MagicMock(return_value=state_port_tbl) + + port_mapping = MagicMock() + port_mapping.get_asic_id_for_logical_port = MagicMock(return_value=0) + port_mapping.logical_port_name_to_physical_port_list = MagicMock(return_value=[7]) + + transceiver_dict = { + 7: { + 'manufacturer': 'Molex', + 'model': '1064141421', + 'cable_type': 'Length Cable Assembly(m)', + 'cable_length': '255', + 'specification_compliance': "{'10/40G Ethernet Compliance Code': '10GBase-SR'}", + 'type_abbrv_name': 'QSFP+' + } + } + + with patch('xcvrd.xcvrd_utilities.media_settings_parser.g_dict', + media_settings_custom_attrs_with_port_and_global): + media_settings_parser.notify_media_setting('Ethernet0', transceiver_dict, xcvr_table_helper, port_mapping) + + set_key, fvs = app_port_tbl.set.call_args[0] + assert set_key == 'Ethernet0' + result_dict = dict(fvs) + assert result_dict == { + 'pre1': '0x00000002,0x00000002', + 'main': '0x00000020,0x00000020', + 'post1': '0x00000006,0x00000006', + 'regn_bfm1n': '0x000000aa,0x000000aa', + media_settings_parser.CustomMediaSettingsParser.CUSTOM_SERDES_ATTRS_KEY_IN_DB: expected_custom, + } + @patch('xcvrd.xcvrd_utilities.common._wrapper_get_presence', MagicMock(return_value=True)) @patch('xcvrd.xcvrd.XcvrTableHelper', MagicMock()) @patch('xcvrd.xcvrd.XcvrTableHelper.get_cfg_port_tbl', MagicMock()) @@ -1716,6 +1942,187 @@ def _check_notify_media_setting(self, index, expected_found=False, expected_valu assert found == expected_found assert result_dict == expected_value + @pytest.mark.parametrize("media_dict, lane_count, subport_num, expected", [ + ( + { + 'CUSTOM:XYZ': {'lane0': 10, 'lane1': 11, 'lane2': 12, 'lane3': 13}, + 'CUSTOM:ABC': {'lane0': 1, 'lane1': 2, 'lane2': 3, 'lane3': 4}, + 'main': {'lane0': '0x11', 'lane1': '0x12', 'lane2': '0x13', 'lane3': '0x14'}, + }, + 2, 2, + '{"attributes":[{"XYZ":{"value":[12,13]}},{"ABC":{"value":[3,4]}}]}', + ), + ( + { + 'CUSTOM:XYZ': {'lane0': 'ADAPTIVE', 'lane1': 'ADAPTIVE', 'lane2': 'ADAPTIVE', 'lane3': 'ADAPTIVE'}, + 'CUSTOM:ABC': {'lane0': 1, 'lane1': 2, 'lane2': 3, 'lane3': 4}, + }, + 2, 2, + '{"attributes":[{"XYZ":{"value":["ADAPTIVE","ADAPTIVE"]}},{"ABC":{"value":[3,4]}}]}', + ), + ( + {'main': {'lane0': '0x11', 'lane1': '0x12', 'lane2': '0x13', 'lane3': '0x14'}}, + 2, 2, + None, + ), + ]) + def test_custom_media_settings_to_db_value(self, media_dict, lane_count, subport_num, expected): + assert expected == media_settings_parser.CustomMediaSettingsParser.to_db_value( + media_dict, lane_count, subport_num) + + def test_custom_media_settings_get_lane_values(self): + lane_dict = {'lane0': 1, 'lane1': 2, 'lane2': 3, 'lane3': 4} + lane_values = media_settings_parser.CustomMediaSettingsParser._get_lane_values( + lane_dict, 2, 2 + ) + assert lane_values == [3, 4] + + lane_values = media_settings_parser.CustomMediaSettingsParser._get_lane_values( + lane_dict, 2, 3 + ) + assert lane_values == [1, 2] + + def test_custom_media_settings_is_port_selected(self): + assert media_settings_parser.CustomMediaSettingsParser.is_port_selected('1, 3-4, 8', 8) + assert media_settings_parser.CustomMediaSettingsParser.is_port_selected('1,3-4,8', 4) + assert media_settings_parser.CustomMediaSettingsParser.is_port_selected('01', 1) + assert media_settings_parser.CustomMediaSettingsParser.is_port_selected('1 - 3', 2) + assert not media_settings_parser.CustomMediaSettingsParser.is_port_selected('1,3-4,8', 2) + assert not media_settings_parser.CustomMediaSettingsParser.is_port_selected(' ', 1) + assert not media_settings_parser.CustomMediaSettingsParser.is_port_selected('1,,3', 2) + assert not media_settings_parser.CustomMediaSettingsParser.is_port_selected('1-a', 1) + assert not media_settings_parser.CustomMediaSettingsParser.is_port_selected('1-2-3', 2) + assert not media_settings_parser.CustomMediaSettingsParser.is_port_selected('a', 2) + assert not media_settings_parser.CustomMediaSettingsParser.is_port_selected(123, 1) + + def test_get_custom_media_settings_value(self): + key = { + 'vendor_key': 'UNKOWN', + 'media_key': 'QSFP-DD-active_cable_media_interface', + 'lane_speed_key': 'speed:100GAUI-2', + 'medium_lane_speed_key': 'UNKNOWN', + } + with patch('xcvrd.xcvrd_utilities.media_settings_parser.g_dict', media_settings_custom_attrs): + result = media_settings_parser.get_custom_media_settings_value(8, key) + assert result == { + 'CUSTOM:XYZ': custom_serdes_attrs_xyz_10, + 'CUSTOM:ABC': custom_serdes_attrs_abc_mode, + } + + result = media_settings_parser.get_custom_media_settings_value(7, key) + assert result == { + 'CUSTOM:XYZ': custom_serdes_attrs_xyz_20, + } + + key_no_match = copy.deepcopy(key) + key_no_match['media_key'] = 'UNMATCHED_MEDIA' + result = media_settings_parser.get_custom_media_settings_value(8, key_no_match) + assert result == { + 'CUSTOM:XYZ': custom_serdes_attrs_xyz_20, + } + + with patch('xcvrd.xcvrd_utilities.media_settings_parser.g_dict', media_settings_custom_attrs_no_space): + result = media_settings_parser.get_custom_media_settings_value(4, key) + assert result == { + 'CUSTOM:XYZ': custom_serdes_attrs_xyz_10, + } + + with patch('xcvrd.xcvrd_utilities.media_settings_parser.g_dict', media_settings_custom_attrs_non_string_selector): + result = media_settings_parser.get_custom_media_settings_value(9, key) + assert result == {} + + with patch('xcvrd.xcvrd_utilities.media_settings_parser.g_dict', + media_settings_custom_attrs_empty_explicit_then_default): + result = media_settings_parser.get_custom_media_settings_value(8, key) + assert result == { + 'CUSTOM:XYZ': custom_serdes_attrs_xyz_20, + } + + def test_custom_media_settings_mixed_with_port_and_global(self): + key = { + 'vendor_key': 'UNKOWN', + 'media_key': 'QSFP-DD-active_cable_media_interface', + 'lane_speed_key': 'speed:100GAUI-2', + 'medium_lane_speed_key': 'UNKNOWN', + } + with patch('xcvrd.xcvrd_utilities.media_settings_parser.g_dict', + media_settings_custom_attrs_with_port_and_global): + result = media_settings_parser.get_media_settings_value(7, key) + assert result == { + 'pre1': {'lane0': '0x00000002', 'lane1': '0x00000002'}, + 'main': {'lane0': '0x00000020', 'lane1': '0x00000020'}, + 'post1': {'lane0': '0x00000006', 'lane1': '0x00000006'}, + 'regn_bfm1n': {'lane0': '0x000000aa', 'lane1': '0x000000aa'}, + } + + result = media_settings_parser.get_custom_media_settings_value(7, key) + assert result == { + 'CUSTOM:XYZ': custom_serdes_attrs_xyz_20, + } + + def test_custom_media_settings_medium_lane_key(self): + key = { + 'vendor_key': 'UNKOWN', + 'media_key': 'UNMATCHED_MEDIA', + 'lane_speed_key': 'speed:100GAUI-2', + 'medium_lane_speed_key': 'COPPER50', + } + with patch('xcvrd.xcvrd_utilities.media_settings_parser.g_dict', + media_settings_custom_attrs_medium_lane): + result = media_settings_parser.get_custom_media_settings_value(7, key) + assert result == { + 'CUSTOM:XYZ': custom_serdes_attrs_xyz_20, + } + + @pytest.mark.parametrize("settings", [{}, [], None]) + def test_custom_media_settings_parser_empty_or_invalid_settings(self, settings): + key = { + 'vendor_key': 'UNKOWN', + 'media_key': 'UNMATCHED_MEDIA', + 'lane_speed_key': 'speed:100GAUI-2', + 'medium_lane_speed_key': 'COPPER50', + } + parser = media_settings_parser.CustomMediaSettingsParser() + assert parser.parse(settings, 7, key) == ({}, {}) + + @pytest.mark.parametrize("media_dict, lane_count, subport_num, gearbox_line_lane_count, expected", [ + ( + {'main': {'lane0': '0x11', 'lane1': '0x12', 'lane2': '0x13', 'lane3': '0x14'}}, + 2, 2, None, + [('main', '0x13,0x14')], + ), + ( + {'main': {'lane0': '0x11', 'lane1': '0x12'}, 'los_thresh': '7'}, + 2, 0, None, + [('main', '0x11,0x12'), ('los_thresh', '7')], + ), + ( + {}, + 2, 2, None, + [], + ), + ( + { + 'gb_line_main': { + 'lane0': '0x10', 'lane1': '0x11', 'lane2': '0x12', 'lane3': '0x13', + 'lane4': '0x14', 'lane5': '0x15', 'lane6': '0x16', 'lane7': '0x17', + }, + 'gb_system_main': { + 'lane0': '0x20', 'lane1': '0x21', 'lane2': '0x22', 'lane3': '0x23', + }, + }, + 4, 0, 8, + [ + ('gb_line_main', '0x10,0x11,0x12,0x13,0x14,0x15,0x16,0x17'), + ('gb_system_main', '0x20,0x21,0x22,0x23'), + ], + ), + ]) + def test_media_settings_to_db_value(self, media_dict, lane_count, subport_num, + gearbox_line_lane_count, expected): + assert expected == media_settings_parser.MediaSettingsParserBase.to_db_value( + media_dict, lane_count, subport_num, gearbox_line_lane_count) + def _check_notify_media_setting_with_gearbox(self, index, gearbox_line_lanes, system_lanes, xcvr_info_dict=None): """ Helper method to test notify_media_setting with gearbox configuration. @@ -5616,40 +6023,60 @@ def test_check_port_in_range(self): physical_port = 33 assert not common.check_port_in_range(range_str, physical_port) - def test_get_serdes_si_setting_val_str(self): + def test_media_settings_parser_base_get_lane_values_str(self): lane_dict = {'lane0': '1', 'lane1': '2', 'lane2': '3', 'lane3': '4'} # non-breakout case lane_count = 4 subport_num = 0 - media_str = get_serdes_si_setting_val_str(lane_dict, lane_count, subport_num) + media_str = media_settings_parser.MediaSettingsParserBase._get_lane_values_str( + lane_dict, lane_count, subport_num + ) assert media_str == '1,2,3,4' # breakout case lane_count = 2 subport_num = 2 - media_str = get_serdes_si_setting_val_str(lane_dict, lane_count, subport_num) + media_str = media_settings_parser.MediaSettingsParserBase._get_lane_values_str( + lane_dict, lane_count, subport_num + ) assert media_str == '3,4' # breakout case without subport number specified in config lane_count = 2 subport_num = 0 - media_str = get_serdes_si_setting_val_str(lane_dict, lane_count, subport_num) + media_str = media_settings_parser.MediaSettingsParserBase._get_lane_values_str( + lane_dict, lane_count, subport_num + ) assert media_str == '1,2' # breakout case with out-of-range subport number lane_count = 2 subport_num = 3 - media_str = get_serdes_si_setting_val_str(lane_dict, lane_count, subport_num) + media_str = media_settings_parser.MediaSettingsParserBase._get_lane_values_str( + lane_dict, lane_count, subport_num + ) assert media_str == '1,2' # breakout case with smaler lane_dict lane_dict = {'lane0': '1', 'lane1': '2'} lane_count = 2 subport_num = 2 - media_str = get_serdes_si_setting_val_str(lane_dict, lane_count, subport_num) + media_str = media_settings_parser.MediaSettingsParserBase._get_lane_values_str( + lane_dict, lane_count, subport_num + ) assert media_str == '1,2' # lane key-value pair inserted in non-asceding order lane_dict = {'lane0': 'a', 'lane2': 'c', 'lane1': 'b', 'lane3': 'd'} lane_count = 2 subport_num = 2 - media_str = get_serdes_si_setting_val_str(lane_dict, lane_count, subport_num) + media_str = media_settings_parser.MediaSettingsParserBase._get_lane_values_str( + lane_dict, lane_count, subport_num + ) assert media_str == 'c,d' + # non-string lane values are coerced defensively for string output + lane_dict = {'lane0': 1, 'lane1': 2, 'lane2': 3, 'lane3': 4} + lane_count = 2 + subport_num = 2 + media_str = media_settings_parser.MediaSettingsParserBase._get_lane_values_str( + lane_dict, lane_count, subport_num + ) + assert media_str == '3,4' class MockPortMapping: logical_port_list = [0, 1, 2] diff --git a/sonic-xcvrd/xcvrd/xcvrd_utilities/media_settings_parser.py b/sonic-xcvrd/xcvrd/xcvrd_utilities/media_settings_parser.py index 6060eb3..3d58bac 100644 --- a/sonic-xcvrd/xcvrd/xcvrd_utilities/media_settings_parser.py +++ b/sonic-xcvrd/xcvrd/xcvrd_utilities/media_settings_parser.py @@ -22,6 +22,8 @@ LANE_SPEED_KEY = 'lane_speed_key' MEDIUM_LANE_SPEED_KEY = 'medium_lane_speed_key' DEFAULT_KEY = 'Default' +RANGE_SEPARATOR = '-' +COMMA_SEPARATOR = ',' # This is useful if default value is desired when no match is found for lane speed key LANE_SPEED_DEFAULT_KEY = LANE_SPEED_KEY_PREFIX + DEFAULT_KEY SYSLOG_IDENTIFIER = "xcvrd" @@ -60,10 +62,67 @@ def get_media_settings(key, media_dict): return get_media_settings_for_speed(media_dict[dict_key], key[LANE_SPEED_KEY]) return None + @staticmethod + def _get_lane_values_str(val_dict, lane_count, subport_num=0): + """ + Slice per-lane SerDes settings and return them as a comma-separated string. + + Args: + val_dict: dictionary containing SerDes settings for all lanes of the port + lane_count: number of lanes for this subport + subport_num: subport number (1-based), 0 for non-breakout case + + Returns: + String containing SerDes settings for the given subport, separated by commas. + """ + start_lane_idx = (subport_num - 1) * lane_count if subport_num else 0 + if start_lane_idx + lane_count > len(val_dict): + helper_logger.log_notice( + "start_lane_idx + lane_count ({}) is beyond length of {}, " + "default start_lane_idx to 0 as a best effort".format(start_lane_idx + lane_count, val_dict) + ) + start_lane_idx = 0 + val_list = [val_dict[lane_key] for lane_key in natsorted(val_dict)] + return ','.join(str(val) for val in val_list[start_lane_idx:start_lane_idx + lane_count]) + + @staticmethod + def to_db_value(media_dict, lane_count, subport_num, gearbox_line_lane_count=None): + """ + Convert traditional media settings to APP DB field/value tuples. + + Args: + media_dict: dictionary containing traditional media settings + lane_count: number of lanes for this subport + subport_num: subport number (1-based), 0 for non-breakout case + gearbox_line_lane_count: optional gearbox line-side lane count. + When provided, traditional keys prefixed with ``gb_line`` use + this width instead of the system-side ``lane_count``. + + Returns: + List of ``(field, value)`` tuples ready to publish to APP_DB. + """ + if not media_dict: + return [] + + fvs_list = [] + + for media_key, media_value in media_dict.items(): + if isinstance(media_value, dict): + lane_count_si = lane_count + if gearbox_line_lane_count is not None and "gb_line" in media_key: + lane_count_si = gearbox_line_lane_count + val_str = MediaSettingsParserBase._get_lane_values_str( + media_value, lane_count_si, subport_num + ) + else: + val_str = media_value + + fvs_list.append((str(media_key), str(val_str))) + + return fvs_list + class GlobalMediaSettingsParser(MediaSettingsParserBase): def parse(self, settings, physical_port, key): - RANGE_SEPARATOR = '-' - COMMA_SEPARATOR = ',' default_dict = {} lane_speed_key = key[LANE_SPEED_KEY] @@ -113,9 +172,125 @@ def parse(self, settings, physical_port, key): return {}, {} class CustomMediaSettingsParser(MediaSettingsParserBase): + CUSTOM_SERDES_ATTR_PREFIX = 'CUSTOM:' + CUSTOM_SERDES_ATTRS_TOP_LEVEL_KEY = 'attributes' + CUSTOM_SERDES_ATTRS_KEY_IN_DB = 'custom_serdes_attrs' + + @staticmethod + def is_port_selected(port_selector, physical_port): + """ + Return True if the port selector matches the given physical_port. + + Supports: + - Single port: "7" + - Range: "1-4" + - List / list-of-ranges: "1,3-4,8" + Whitespace is ignored. Non-string selectors return False. + """ + if not isinstance(port_selector, str): + helper_logger.log_notice("Malformed port selector '{}'".format(port_selector)) + return False + + for token in port_selector.split(COMMA_SEPARATOR): + token = token.strip() + if not token: + helper_logger.log_notice( + "Malformed port selector token '' in '{}'".format(port_selector) + ) + continue + + if RANGE_SEPARATOR in token: + start_str, end_str = token.split(RANGE_SEPARATOR, 1) + else: + start_str = end_str = token + + try: + start = int(start_str.strip()) + end = int(end_str.strip()) + except ValueError: + helper_logger.log_notice( + "Malformed port selector token '{}' in '{}'".format(token, port_selector) + ) + continue + + if start <= physical_port <= end: + return True + + return False + + @staticmethod + def _get_lane_values(val_dict, lane_count, subport_num): + """ + Slice per-lane SerDes settings and return them as a list. + + Args: + val_dict: dictionary containing SerDes settings for all lanes of the port + lane_count: number of lanes for this subport + subport_num: subport number (1-based), 0 for non-breakout case + + Returns: + List containing SerDes settings for the requested subport. + """ + start_lane_idx = (subport_num - 1) * lane_count if subport_num else 0 + val_list = [val_dict[lane_key] for lane_key in natsorted(val_dict)] + if start_lane_idx + lane_count > len(val_list): + start_lane_idx = 0 + return val_list[start_lane_idx:start_lane_idx + lane_count] + + @staticmethod + def to_db_value(custom_media_dict, lane_count, subport_num): + """ + Convert custom SerDes attributes to the JSON string used in APP DB. + + Args: + custom_media_dict: dictionary containing custom SerDes settings for all lanes of the port + lane_count: number of lanes for this subport + subport_num: subport number (1-based), 0 for non-breakout case + + Returns: + JSON string for custom SerDes attributes, or None if no custom attributes are present. + """ + if not custom_media_dict: + return None + + attrs_list = [] + + for key, value in custom_media_dict.items(): + if not isinstance(key, str) or not key.startswith(CustomMediaSettingsParser.CUSTOM_SERDES_ATTR_PREFIX): + continue + + attrs_list.append({ + key[len(CustomMediaSettingsParser.CUSTOM_SERDES_ATTR_PREFIX):]: { + 'value': CustomMediaSettingsParser._get_lane_values(value, lane_count, subport_num) + } + }) + + if not attrs_list: + return None + + return json.dumps( + {CustomMediaSettingsParser.CUSTOM_SERDES_ATTRS_TOP_LEVEL_KEY: attrs_list}, + separators=(',', ':') + ) + def parse(self, settings, physical_port, key): - # TODO: Placeholder for custom media setting parser - return {}, {} + if not isinstance(settings, dict) or not settings: + return {}, {} + + default_dict = {} + lane_speed_key = key[LANE_SPEED_KEY] + + for port_selector, media_dict in settings.items(): + if not self.is_port_selected(port_selector, physical_port): + continue + + media_settings = self.get_media_settings(key, media_dict) + if media_settings: + return media_settings, {} + if DEFAULT_KEY in media_dict and not default_dict: + default_dict = get_media_settings_for_speed(media_dict[DEFAULT_KEY], lane_speed_key) + + return {}, default_dict def load_media_settings(): @@ -246,32 +421,6 @@ def is_si_per_speed_supported(media_dict): return LANE_SPEED_KEY_PREFIX in list(media_dict.keys())[0] -def get_serdes_si_setting_val_str(val_dict, lane_count, subport_num=0): - """ - Get ASIC side SerDes SI settings for the given logical port (subport) - - Args: - val_dict: dictionary containing SerDes settings for all lanes of the port - e.g. {'lane0': '0x1f', 'lane1': '0x1f', 'lane2': '0x1f', 'lane3': '0x1f'} - lane_count: number of lanes for this subport - subport_num: subport number (1-based), 0 for non-breakout case - - Returns: - string containing SerDes settings for the given subport, separated by comma - e.g. '0x1f,0x1f,0x1f,0x1f' - """ - start_lane_idx = (subport_num - 1) * lane_count if subport_num else 0 - if start_lane_idx + lane_count > len(val_dict): - helper_logger.log_notice( - "start_lane_idx + lane_count ({}) is beyond length of {}, " - "default start_lane_idx to 0 as a best effort".format(start_lane_idx + lane_count, val_dict) - ) - start_lane_idx = 0 - val_list = [val_dict[lane_key] for lane_key in natsorted(val_dict)] - # If subport_num ('subport') is not specified in config_db, return values for first lane_count number of lanes - return ','.join(val_list[start_lane_idx:start_lane_idx + lane_count]) - - def get_media_settings_for_speed(settings_dict, lane_speed_key): """ Get settings for the given lane speed key @@ -304,14 +453,33 @@ def get_media_settings_for_speed(settings_dict, lane_speed_key): def get_media_settings_value(physical_port, key): + """ + Resolve traditional media settings for a physical port. + + Traditional settings are selected from GLOBAL_MEDIA_SETTINGS and + PORT_MEDIA_SETTINGS using the standard precedence order, and the returned + value is the raw media-settings dictionary before APP_DB serialization. + + Args: + physical_port: physical port number for this logical port + key: media settings key dictionary with vendor/media and lane speed info + + Returns: + Dictionary containing the matched traditional media settings before + APP_DB serialization. Returns {} if no traditional settings match. + + Example: + A matching traditional profile may return: + {'main': {'lane0': '0x11', 'lane1': '0x12', 'lane2': '0x13', + 'lane3': '0x14'}} + """ global_default = {} - # Priority order: + # Priority order for traditional media settings: # 1. GLOBAL explicit match (vendor/media/speed) # 2. PORT explicit match # 3. PORT Default - # 4. CUSTOM explicit match - # 5. GLOBAL Default (last-resort fallback) + # 4. GLOBAL Default (last-resort fallback) # Check global media settings first (can apply to ranges/lists of ports) if GLOBAL_MEDIA_SETTINGS_KEY in g_dict: @@ -329,12 +497,6 @@ def get_media_settings_value(physical_port, key): if port_default: return port_default - if CUSTOM_MEDIA_SETTINGS_KEY in g_dict: - result, _ = CustomMediaSettingsParser().parse( - g_dict[CUSTOM_MEDIA_SETTINGS_KEY], physical_port, key) - if result: - return result - # Fall back to global default if no explicit or port-default match found if global_default: return global_default @@ -342,6 +504,39 @@ def get_media_settings_value(physical_port, key): return {} +def get_custom_media_settings_value(physical_port, key): + """ + Resolve custom media settings for a physical port. + + Custom settings are selected only from CUSTOM_MEDIA_SETTINGS and the + returned value is the raw custom-attribute dictionary before it is + converted to the APP_DB custom_serdes_attrs payload. + + Args: + physical_port: physical port number for this logical port + key: media settings key dictionary with vendor/media and lane speed info + + Returns: + Dictionary containing the matched custom media settings before they are + converted to the APP_DB custom_serdes_attrs payload. Returns {} if no + custom settings match. + + Example: + A matching custom profile may return: + {'CUSTOM:ABC': {'lane0': 1, 'lane1': 2, 'lane2': 3, 'lane3': 4}} + """ + custom_settings = g_dict.get(CUSTOM_MEDIA_SETTINGS_KEY) + if not isinstance(custom_settings, dict) or not custom_settings: + return {} + + result, default_dict = CustomMediaSettingsParser().parse( + custom_settings, physical_port, key) + if result: + return result + + return default_dict + + def get_speed_lane_count_and_subport(port, cfg_port_tbl): port_speed, lane_count, subport_num = 0, 0, 0 found, port_info = cfg_port_tbl.get(port) @@ -402,33 +597,40 @@ def notify_media_setting(logical_port_name, transceiver_dict, ganged_member_num += 1 # If the port has a gearbox, then we need to calculate the media settings key based on the number of # lanes on the line-side of the gearbox - if logical_port_name in gearbox_lanes_dict: - key = get_media_settings_key(physical_port, transceiver_dict, port_speed, gearbox_lanes_dict[logical_port_name]) + gearbox_line_lane_count = gearbox_lanes_dict.get(logical_port_name) + if gearbox_line_lane_count is not None: + key = get_media_settings_key( + physical_port, transceiver_dict, port_speed, gearbox_line_lane_count + ) else: key = get_media_settings_key(physical_port, transceiver_dict, port_speed, lane_count) helper_logger.log_notice("Retrieving media settings for port {} speed {} num_lanes {}, using key {}".format(logical_port_name, port_speed, lane_count, key)) media_dict = get_media_settings_value(physical_port, key) + custom_media_dict = get_custom_media_settings_value(physical_port, key) - if len(media_dict) == 0: + if not media_dict and not custom_media_dict: + helper_logger.log_info("Error in obtaining media setting for {}".format(logical_port_name)) + return + + fvs_list = MediaSettingsParserBase.to_db_value( + media_dict, lane_count, subport_num, gearbox_line_lane_count + ) + + custom_db_value = CustomMediaSettingsParser.to_db_value( + custom_media_dict, lane_count, subport_num) + if custom_db_value is not None: + fvs_list.append((CustomMediaSettingsParser.CUSTOM_SERDES_ATTRS_KEY_IN_DB, custom_db_value)) + + if not fvs_list: helper_logger.log_info("Error in obtaining media setting for {}".format(logical_port_name)) return - fvs = swsscommon.FieldValuePairs(len(media_dict)) + fvs = swsscommon.FieldValuePairs(len(fvs_list)) - index = 0 helper_logger.log_notice("Publishing SI setting for port {} in APP_DB:".format(logical_port_name)) - for media_key in media_dict: - if type(media_dict[media_key]) is dict: - if "gb_line" in media_key: - lane_count_si = gearbox_lanes_dict[logical_port_name] - else: - lane_count_si = lane_count - val_str = get_serdes_si_setting_val_str(media_dict[media_key], lane_count_si, subport_num) - else: - val_str = media_dict[media_key] + for index, (media_key, val_str) in enumerate(fvs_list): helper_logger.log_notice("{}:({},{}) ".format(index, str(media_key), str(val_str))) fvs[index] = (str(media_key), str(val_str)) - index += 1 xcvr_table_helper.get_app_port_tbl(asic_index).set(port_name, fvs) xcvr_table_helper.get_state_port_tbl(asic_index).set(logical_port_name, [(NPU_SI_SETTINGS_SYNC_STATUS_KEY, NPU_SI_SETTINGS_NOTIFIED_VALUE)])