From a1f8373db090df7d7c91d916833c88034fcddbc7 Mon Sep 17 00:00:00 2001 From: Junchao-Mellanox Date: Wed, 11 Oct 2023 09:31:05 +0300 Subject: [PATCH] [Mellanox] Implement low power mode for cmis host management --- .../sonic_platform/device_data.py | 9 +++ .../mlnx-platform-api/sonic_platform/sfp.py | 32 +++++++++++ .../mlnx-platform-api/sonic_platform/utils.py | 28 ++++++++-- .../tests/test_device_data.py | 9 +++ .../mlnx-platform-api/tests/test_sfp.py | 55 ++++++++++++++++++- .../mlnx-platform-api/tests/test_utils.py | 3 + 6 files changed, 129 insertions(+), 7 deletions(-) diff --git a/platform/mellanox/mlnx-platform-api/sonic_platform/device_data.py b/platform/mellanox/mlnx-platform-api/sonic_platform/device_data.py index 9f681709ccd2..c1fdd5af06c9 100644 --- a/platform/mellanox/mlnx-platform-api/sonic_platform/device_data.py +++ b/platform/mellanox/mlnx-platform-api/sonic_platform/device_data.py @@ -227,3 +227,12 @@ def get_cpld_component_list(cls): # Currently, only fetching BIOS version is supported return ComponentCPLDSN2201.get_component_list() return ComponentCPLD.get_component_list() + + @classmethod + @utils.read_only_cache() + def is_independent_mode(cls): + from sonic_py_common import device_info + _, hwsku_dir = device_info.get_paths_to_platform_and_hwsku_dirs() + sai_profile_file = os.path.join(hwsku_dir, 'sai.profile') + data = utils.read_key_value_file(sai_profile_file, delimeter='=') + return data.get('SAI_INDEPENDENT_MODULE_MODE') == '1' diff --git a/platform/mellanox/mlnx-platform-api/sonic_platform/sfp.py b/platform/mellanox/mlnx-platform-api/sonic_platform/sfp.py index 76c841713f80..0afd0d7617a1 100644 --- a/platform/mellanox/mlnx-platform-api/sonic_platform/sfp.py +++ b/platform/mellanox/mlnx-platform-api/sonic_platform/sfp.py @@ -120,6 +120,7 @@ SFP_SYSFS_STATUS_ERROR = 'statuserror' SFP_SYSFS_PRESENT = 'present' SFP_SYSFS_RESET = 'reset' +SFP_SYSFS_HWRESET = 'hw_reset' SFP_SYSFS_POWER_MODE = 'power_mode' SFP_SYSFS_POWER_MODE_POLICY = 'power_mode_policy' POWER_MODE_POLICY_HIGH = 1 @@ -318,6 +319,13 @@ def get_lpmode(self): Returns: A Boolean, True if lpmode is enabled, False if disabled """ + try: + if self.is_sw_control(): + api = self.get_xcvr_api() + return api.get_lpmode() if api else False + except Exception as e: + print(e) + return False file_path = SFP_SDK_MODULE_SYSFS_ROOT_TEMPLATE.format(self.sdk_index) + SFP_SYSFS_POWER_MODE power_mode = utils.read_int_from_file(file_path) return power_mode == POWER_MODE_LOW @@ -345,6 +353,19 @@ def set_lpmode(self, lpmode): Returns: A boolean, True if lpmode is set successfully, False if not """ + try: + if self.is_sw_control(): + api = self.get_xcvr_api() + if not api: + return False + if api.get_lpmode() == lpmode: + return True + api.set_lpmode(lpmode) + return api.get_lpmode() == lpmode + except Exception as e: + print(e) + return False + print('\nNotice: please set port admin status to down before setting power mode, ignore this message if already set') file_path = SFP_SDK_MODULE_SYSFS_ROOT_TEMPLATE.format(self.sdk_index) + SFP_SYSFS_POWER_MODE_POLICY target_admin_mode = POWER_MODE_POLICY_AUTO if lpmode else POWER_MODE_POLICY_HIGH @@ -541,6 +562,17 @@ def get_xcvr_api(self): self._xcvr_api.get_tx_fault = self.get_tx_fault return self._xcvr_api + def is_sw_control(self): + if not DeviceDataManager.is_independent_mode(): + return False + + db = utils.DbUtils.get_db_instance('STATE_DB') + control_type = db.get('STATE_DB', f'TRANSCEIVER_MODULES_MGMT|{self.sdk_index}', 'control_type') + if not control_type: + raise Exception(f'Module {self.sdk_index} is in initialization, please retry later') + + return control_type == 'SW_CONTROL' + class RJ45Port(NvidiaSFPCommon): """class derived from SFP, representing RJ45 ports""" diff --git a/platform/mellanox/mlnx-platform-api/sonic_platform/utils.py b/platform/mellanox/mlnx-platform-api/sonic_platform/utils.py index 83063b5c368e..84b6d7a19509 100644 --- a/platform/mellanox/mlnx-platform-api/sonic_platform/utils.py +++ b/platform/mellanox/mlnx-platform-api/sonic_platform/utils.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2020-2021 NVIDIA CORPORATION & AFFILIATES. +# Copyright (c) 2020-2023 NVIDIA CORPORATION & AFFILIATES. # Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -100,15 +100,15 @@ def read_float_from_file(file_path, default=0.0, raise_exception=False, log_func return read_from_file(file_path=file_path, target_type=float, default=default, raise_exception=raise_exception, log_func=log_func) -def _key_value_converter(content): +def _key_value_converter(content, delimeter): ret = {} for line in content.splitlines(): - k,v = line.split(':') + k,v = line.split(delimeter) ret[k.strip()] = v.strip() return ret -def read_key_value_file(file_path, default={}, raise_exception=False, log_func=logger.log_error): +def read_key_value_file(file_path, default={}, raise_exception=False, log_func=logger.log_error, delimeter=':'): """Read file content and parse the content to a dict. The file content should like: key1:value1 key2:value2 @@ -119,7 +119,8 @@ def read_key_value_file(file_path, default={}, raise_exception=False, log_func=l raise_exception (bool, optional): If exception should be raised or hiden. Defaults to False. log_func (optional): logger function.. Defaults to logger.log_error. """ - return read_from_file(file_path=file_path, target_type=_key_value_converter, default=default, raise_exception=raise_exception, log_func=log_func) + converter = lambda content: _key_value_converter(content, delimeter) + return read_from_file(file_path=file_path, target_type=converter, default=default, raise_exception=raise_exception, log_func=log_func) def write_file(file_path, content, raise_exception=False, log_func=logger.log_error): @@ -285,3 +286,20 @@ def wait_until(predict, timeout, interval=1, *args, **kwargs): time.sleep(interval) timeout -= interval return False + + +class DbUtils: + db_instances = {} + + @classmethod + def get_db_instance(cls, db_name, **kargs): + try: + if db_name not in cls.db_instances: + from swsscommon.swsscommon import SonicV2Connector + db = SonicV2Connector(use_unix_socket_path=True) + db.connect(db_name) + cls.db_instances[db_name] = db + return cls.db_instances[db_name] + except Exception as e: + logger.log_error(f'Failed to get DB instance for DB {db_name} - {e}') + raise e diff --git a/platform/mellanox/mlnx-platform-api/tests/test_device_data.py b/platform/mellanox/mlnx-platform-api/tests/test_device_data.py index d99591d513c8..866f01c3e7e3 100644 --- a/platform/mellanox/mlnx-platform-api/tests/test_device_data.py +++ b/platform/mellanox/mlnx-platform-api/tests/test_device_data.py @@ -52,5 +52,14 @@ def test_get_linecard_max_port_count(self): def test_get_bios_component(self): assert DeviceDataManager.get_bios_component() is not None + @mock.patch('sonic_py_common.device_info.get_paths_to_platform_and_hwsku_dirs', mock.MagicMock(return_value=('', '/tmp'))) + @mock.patch('sonic_platform.device_data.utils.read_key_value_file') + def test_is_independent_mode(self, mock_read): + mock_read.return_value = {} + assert not DeviceDataManager.is_independent_mode() + mock_read.return_value = {'SAI_INDEPENDENT_MODULE_MODE': '1'} + assert DeviceDataManager.is_independent_mode() + + diff --git a/platform/mellanox/mlnx-platform-api/tests/test_sfp.py b/platform/mellanox/mlnx-platform-api/tests/test_sfp.py index 4f5d4f160008..b54fa3e5b5ce 100644 --- a/platform/mellanox/mlnx-platform-api/tests/test_sfp.py +++ b/platform/mellanox/mlnx-platform-api/tests/test_sfp.py @@ -247,9 +247,11 @@ def test_reset(self, mock_write): assert sfp.reset() mock_write.assert_called_with('/sys/module/sx_core/asic0/module0/reset', '1') + @mock.patch('sonic_platform.sfp.SFP.is_sw_control') @mock.patch('sonic_platform.utils.read_int_from_file') - def test_get_lpmode(self, mock_read_int): + def test_get_lpmode(self, mock_read_int, mock_control): sfp = SFP(0) + mock_control.return_value = False mock_read_int.return_value = 1 assert sfp.get_lpmode() mock_read_int.assert_called_with('/sys/module/sx_core/asic0/module0/power_mode') @@ -257,10 +259,23 @@ def test_get_lpmode(self, mock_read_int): mock_read_int.return_value = 2 assert not sfp.get_lpmode() + mock_control.return_value = True + sfp.get_xcvr_api = mock.MagicMock() + sfp.get_xcvr_api.return_value = None + assert not sfp.get_lpmode() + mock_api = mock.MagicMock() + mock_api.get_lpmode = mock.MagicMock(return_value=True) + sfp.get_xcvr_api.return_value = mock_api + assert sfp.get_lpmode() + mock_control.side_effect = Exception('') + assert not sfp.get_lpmode() + + @mock.patch('sonic_platform.sfp.SFP.is_sw_control') @mock.patch('sonic_platform.utils.write_file') @mock.patch('sonic_platform.utils.read_int_from_file') - def test_set_lpmode(self, mock_read_int, mock_write): + def test_set_lpmode(self, mock_read_int, mock_write, mock_control): sfp = SFP(0) + mock_control.return_value = False mock_read_int.return_value = 1 assert sfp.set_lpmode(False) assert mock_write.call_count == 0 @@ -268,6 +283,23 @@ def test_set_lpmode(self, mock_read_int, mock_write): assert sfp.set_lpmode(True) mock_write.assert_called_with('/sys/module/sx_core/asic0/module0/power_mode_policy', '2') + mock_control.return_value = True + sfp.get_xcvr_api = mock.MagicMock() + sfp.get_xcvr_api.return_value = None + assert not sfp.set_lpmode(True) + + mock_api = mock.MagicMock() + mock_api.get_lpmode = mock.MagicMock(return_value=True) + sfp.get_xcvr_api.return_value = mock_api + assert sfp.set_lpmode(True) + + mock_api.get_lpmode.return_value = False + mock_api.set_lpmode = mock.MagicMock(return_value=True) + assert not sfp.set_lpmode(True) + + mock_control.side_effect = Exception('') + assert not sfp.set_lpmode(False) + @mock.patch('sonic_platform.sfp.SFP.read_eeprom') def test_get_xcvr_api(self, mock_read): sfp = SFP(0) @@ -289,3 +321,22 @@ def test_rj45_basic(self): assert sfp.get_transceiver_bulk_status() assert sfp.get_transceiver_threshold_info() sfp.reinit() + + @mock.patch('sonic_platform.device_data.DeviceDataManager.is_independent_mode') + @mock.patch('sonic_platform.utils.DbUtils.get_db_instance') + def test_is_sw_control(self, mock_get_db, mock_mode): + sfp = SFP(0) + mock_mode.return_value = False + assert not sfp.is_sw_control() + mock_mode.return_value = True + + mock_db = mock.MagicMock() + mock_get_db.return_value = mock_db + mock_db.get = mock.MagicMock(return_value=None) + with pytest.raises(Exception): + sfp.is_sw_control() + + mock_db.get.return_value = 'FW_CONTROL' + assert not sfp.is_sw_control() + mock_db.get.return_value = 'SW_CONTROL' + assert sfp.is_sw_control() diff --git a/platform/mellanox/mlnx-platform-api/tests/test_utils.py b/platform/mellanox/mlnx-platform-api/tests/test_utils.py index ad474433bfe8..04b00f82f4f9 100644 --- a/platform/mellanox/mlnx-platform-api/tests/test_utils.py +++ b/platform/mellanox/mlnx-platform-api/tests/test_utils.py @@ -191,3 +191,6 @@ def test_read_key_value_file(self): mock_os_open = mock.mock_open(read_data='a:b') with mock.patch('sonic_platform.utils.open', mock_os_open): assert utils.read_key_value_file('some_file') == {'a':'b'} + mock_os_open = mock.mock_open(read_data='a=b') + with mock.patch('sonic_platform.utils.open', mock_os_open): + assert utils.read_key_value_file('some_file', delimeter='=') == {'a':'b'}