From f9119cc9cbcb43c021bfa04059871b89eb36a78b Mon Sep 17 00:00:00 2001 From: Ben Levi Date: Thu, 18 Dec 2025 12:30:00 +0200 Subject: [PATCH 1/3] make the bmc fw update script common with updated_components Signed-off-by: Ben Levi --- sonic_platform_base/bmc-fw-update.py | 49 +++++++++++++++++++++++++++ sonic_platform_base/bmc_base.py | 15 ++++---- sonic_platform_base/redfish_client.py | 11 ++++-- tests/bmc_base_test.py | 9 ++--- tests/redfish_client_test.py | 4 +-- 5 files changed, 72 insertions(+), 16 deletions(-) create mode 100644 sonic_platform_base/bmc-fw-update.py diff --git a/sonic_platform_base/bmc-fw-update.py b/sonic_platform_base/bmc-fw-update.py new file mode 100644 index 000000000..ae7e05c7e --- /dev/null +++ b/sonic_platform_base/bmc-fw-update.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 + +""" +BMC firmware update utility +Handles BMC firmware update process +""" + +import sys + +def main(): + try: + import sonic_platform + from sonic_py_common.logger import Logger + + logger = Logger('bmc-fw-update') + + if len(sys.argv) != 2: + logger.log_error("Missing firmware image path argument") + sys.exit(1) + image_path = sys.argv[1] + + chassis = sonic_platform.platform.Platform().get_chassis() + bmc = chassis.get_bmc() + if bmc is None: + logger.log_error("Failed to get BMC instance from chassis") + sys.exit(1) + + logger.log_notice(f"Starting BMC firmware update with {image_path}") + ret, (error_msg, updated_components) = bmc.update_firmware(image_path) + if ret != 0: + logger.log_error(f'Failed to update BMC firmware. Error {ret}: {error_msg}') + sys.exit(1) + + logger.log_notice(f"Firmware updated successfully via the BMC. Updated components: {updated_components}") + + if bmc.get_firmware_id() in updated_components: + logger.log_notice("BMC firmware updated successfully, restarting BMC...") + ret, error_msg = bmc.request_bmc_reset() + if ret != 0: + logger.log_error(f'Failed to restart BMC. Error {ret}: {error_msg}') + sys.exit(1) + logger.log_notice("BMC firmware update completed successfully") + + except Exception as e: + logger.log_error(f'BMC firmware update exception: {e}') + sys.exit(1) + +if __name__ == "__main__": + main() diff --git a/sonic_platform_base/bmc_base.py b/sonic_platform_base/bmc_base.py index 5ea887a21..4d7c5d85e 100644 --- a/sonic_platform_base/bmc_base.py +++ b/sonic_platform_base/bmc_base.py @@ -104,7 +104,7 @@ def _get_default_root_password(self): """ raise NotImplementedError - def _get_firmware_id(self): + def get_firmware_id(self): """ Get the BMC firmware ID. Should be implemented by vendor-specific BMC class. @@ -123,7 +123,7 @@ def _get_eeprom_id(self): A string containing the BMC EEPROM ID """ raise NotImplementedError - + def _get_ip_addr(self): """ Get BMC IP address @@ -328,7 +328,7 @@ def get_version(self): """ ret = 0 try: - ret, version = self._get_firmware_version(self._get_firmware_id()) + ret, version = self._get_firmware_version(self.get_firmware_id()) if ret != RedfishClient.ERR_CODE_OK: logger.log_error(f'Failed to get BMC firmware version: {ret}') return 'N/A' @@ -380,17 +380,18 @@ def update_firmware(self, fw_image): fw_image: A string containing the path to the firmware image file Returns: - A tuple (ret, msg) where: + A tuple (ret, (msg, updated_components)) where: ret: An integer return code indicating success (0) or failure msg: A string containing status message about the firmware update + updated_components: A list of component IDs that were updated """ logger.log_notice(f'Installing BMC firmware image {fw_image}') - ret, msg = self.rf_client.redfish_api_update_firmware(fw_image, fw_ids=[self._get_firmware_id()]) + ret, msg, updated_components = self.rf_client.redfish_api_update_firmware(fw_image, fw_ids=[self.get_firmware_id()]) logger.log_notice(f'Firmware update result: {ret}') if msg: logger.log_notice(f'{msg}') - return (ret, msg) - + return (ret, (msg, updated_components)) + @with_session_management def request_bmc_reset(self, graceful=True): """ diff --git a/sonic_platform_base/redfish_client.py b/sonic_platform_base/redfish_client.py index 14624ceea..85a90b4c6 100644 --- a/sonic_platform_base/redfish_client.py +++ b/sonic_platform_base/redfish_client.py @@ -716,6 +716,11 @@ def __update_successful_handler(self, event_msg, context): valid, err_msg = self.__validate_message_args(event_msg) if not valid: return (RedfishClient.ERR_CODE_INVALID_JSON_FORMAT, err_msg) + comp_id = event_msg['MessageArgs'][0] + if 'updated_components' in context: + context['updated_components'].append(comp_id) + else: + context['updated_components'] = [comp_id] return (RedfishClient.ERR_CODE_OK, '') ''' @@ -1123,7 +1128,7 @@ def redfish_api_get_firmware_version(self, fw_id): progress_callback: A callback function to report progress Returns: - A tuple of (ret, error_msg) + A tuple of (ret, error_msg, updated_components) ''' def redfish_api_update_firmware(self, fw_image, fw_ids = None, \ force_update=True, timeout=1800, progress_callback=None): @@ -1166,7 +1171,7 @@ def redfish_api_update_firmware(self, fw_image, fw_ids = None, \ lower_version = result.get('lower_version', False) identical_version = result.get('identical_version', False) err_detected = result.get('err_detected', False) - + updated_components = result.get('updated_components', []) if lower_version: result['ret_code'] = RedfishClient.ERR_CODE_LOWER_VERSION elif identical_version and not err_detected: @@ -1177,7 +1182,7 @@ def redfish_api_update_firmware(self, fw_image, fw_ids = None, \ ret = result['ret_code'] error_msg = result['ret_msg'] - return (ret, error_msg) + return (ret, error_msg, updated_components) ''' Trigger BMC debug log dump file diff --git a/tests/bmc_base_test.py b/tests/bmc_base_test.py index 81c171a03..4d7195a98 100644 --- a/tests/bmc_base_test.py +++ b/tests/bmc_base_test.py @@ -32,7 +32,7 @@ def test_abstract_methods(self): [bmc._get_login_user_callback, [], {}], [bmc._get_login_password_callback, [], {}], [bmc._get_default_root_password, [], {}], - [bmc._get_firmware_id, [], {}], + [bmc.get_firmware_id, [], {}], [bmc._get_eeprom_id, [], {}], ] @@ -186,7 +186,7 @@ def _get_login_password_callback(self): def _get_default_root_password(self): return 'rootpass' - def _get_firmware_id(self): + def get_firmware_id(self): return 'BMC_FW_0' def _get_eeprom_id(self): @@ -389,13 +389,14 @@ def test_update_firmware_success(self, mock_has_login, mock_logout, mock_login, mock_has_login.return_value = False mock_login.return_value = RedfishClient.ERR_CODE_OK mock_logout.return_value = RedfishClient.ERR_CODE_OK - mock_update_fw.return_value = (RedfishClient.ERR_CODE_OK, 'Update successful') + mock_update_fw.return_value = (RedfishClient.ERR_CODE_OK, 'Update successful', ['BMC_FW_0']) bmc = ConcreteBMC('169.254.0.1') - ret, msg = bmc.update_firmware('test_image.bin') + ret, (msg, updated_components) = bmc.update_firmware('test_image.bin') assert ret == RedfishClient.ERR_CODE_OK assert msg == 'Update successful' + assert updated_components == ['BMC_FW_0'] mock_update_fw.assert_called_once_with('test_image.bin', fw_ids=['BMC_FW_0']) @mock.patch.object(RedfishClient, 'redfish_api_trigger_bmc_debug_log_dump') diff --git a/tests/redfish_client_test.py b/tests/redfish_client_test.py index f41458342..723400274 100644 --- a/tests/redfish_client_test.py +++ b/tests/redfish_client_test.py @@ -434,7 +434,7 @@ def test_redfish_api_update_firmware(self, mock_popen): ret = rf.login() assert ret == RedfishClient.ERR_CODE_OK - ret, msg = rf.redfish_api_update_firmware('/tmp/test.bin', fw_ids=['BMC_FW_0'], force_update=True) + ret, msg, updated_components = rf.redfish_api_update_firmware('/tmp/test.bin', fw_ids=['BMC_FW_0'], force_update=True) assert ret == RedfishClient.ERR_CODE_OK @mock.patch('subprocess.Popen') @@ -468,7 +468,7 @@ def test_redfish_api_update_firmware_no_fw_ids(self, mock_popen): ret = rf.login() assert ret == RedfishClient.ERR_CODE_OK - ret, msg = rf.redfish_api_update_firmware('/tmp/test.bin', fw_ids=None, force_update=False) + ret, msg, updated_components = rf.redfish_api_update_firmware('/tmp/test.bin', fw_ids=None, force_update=False) assert ret == RedfishClient.ERR_CODE_OK @mock.patch('subprocess.Popen') From f32bdd17f83ff873ca0fac236db290bd0c2fdbe0 Mon Sep 17 00:00:00 2001 From: Ben Levi Date: Sun, 28 Dec 2025 16:13:32 +0200 Subject: [PATCH 2/3] BMC FW update UT Signed-off-by: Ben Levi --- .../{bmc-fw-update.py => bmc_fw_update.py} | 2 +- tests/bmc_fw_update_test.py | 165 ++++++++++++++++++ 2 files changed, 166 insertions(+), 1 deletion(-) rename sonic_platform_base/{bmc-fw-update.py => bmc_fw_update.py} (97%) create mode 100644 tests/bmc_fw_update_test.py diff --git a/sonic_platform_base/bmc-fw-update.py b/sonic_platform_base/bmc_fw_update.py similarity index 97% rename from sonic_platform_base/bmc-fw-update.py rename to sonic_platform_base/bmc_fw_update.py index ae7e05c7e..211254739 100644 --- a/sonic_platform_base/bmc-fw-update.py +++ b/sonic_platform_base/bmc_fw_update.py @@ -12,7 +12,7 @@ def main(): import sonic_platform from sonic_py_common.logger import Logger - logger = Logger('bmc-fw-update') + logger = Logger('bmc_fw_update') if len(sys.argv) != 2: logger.log_error("Missing firmware image path argument") diff --git a/tests/bmc_fw_update_test.py b/tests/bmc_fw_update_test.py new file mode 100644 index 000000000..d1e6f5e26 --- /dev/null +++ b/tests/bmc_fw_update_test.py @@ -0,0 +1,165 @@ +""" + bmc_fw_update_test.py + + Unit tests for bmc_fw_update module +""" + +import sys +import pytest + +if sys.version_info.major == 3: + from unittest import mock +else: + import mock + +try: + from sonic_py_common import logger +except ImportError: + sys.modules['sonic_py_common'] = mock.MagicMock() + sys.modules['sonic_py_common.logger'] = mock.MagicMock() + +from sonic_platform_base import bmc_fw_update + + +class TestBMCFWUpdate: + """Test class for bmc_fw_update module""" + + @mock.patch('sys.exit') + @mock.patch('sonic_py_common.logger.Logger') + def test_main_success_bmc_firmware_updated(self, mock_logger_class, mock_exit): + """Test successful firmware update with BMC firmware component updated""" + mock_logger = mock.MagicMock() + mock_bmc = mock.MagicMock() + mock_chassis = mock.MagicMock() + mock_platform = mock.MagicMock() + mock_chassis.get_bmc.return_value = mock_bmc + mock_platform.get_chassis.return_value = mock_chassis + mock_sonic_platform = mock.MagicMock() + mock_sonic_platform.platform.Platform.return_value = mock_platform + mock_logger_class.return_value = mock_logger + mock_bmc.update_firmware.return_value = (0, ('Success', ['BMC_FW_0', 'OTHER_FW'])) + mock_bmc.get_firmware_id.return_value = 'BMC_FW_0' + mock_bmc.request_bmc_reset.return_value = (0, 'BMC reset successful') + + test_args = ['bmc_fw_update.py', '/path/to/firmware.bin'] + with mock.patch.dict(sys.modules, {'sonic_platform': mock_sonic_platform}): + with mock.patch.object(sys, 'argv', test_args): + bmc_fw_update.main() + + mock_logger_class.assert_called_once_with('bmc_fw_update') + mock_chassis.get_bmc.assert_called_once() + mock_bmc.update_firmware.assert_called_once_with('/path/to/firmware.bin') + mock_bmc.get_firmware_id.assert_called_once() + mock_bmc.request_bmc_reset.assert_called_once() + mock_exit.assert_not_called() + + @mock.patch('sys.exit') + @mock.patch('sonic_py_common.logger.Logger') + def test_main_success_no_bmc_reset_needed(self, mock_logger_class, mock_exit): + """Test successful firmware update without BMC reset""" + mock_logger = mock.MagicMock() + mock_bmc = mock.MagicMock() + mock_chassis = mock.MagicMock() + mock_platform = mock.MagicMock() + mock_chassis.get_bmc.return_value = mock_bmc + mock_platform.get_chassis.return_value = mock_chassis + mock_sonic_platform = mock.MagicMock() + mock_sonic_platform.platform.Platform.return_value = mock_platform + mock_logger_class.return_value = mock_logger + mock_bmc.update_firmware.return_value = (0, ('Success', ['OTHER_FW_1', 'OTHER_FW_2'])) + mock_bmc.get_firmware_id.return_value = 'BMC_FW_0' + + test_args = ['bmc_fw_update.py', '/path/to/firmware.bin'] + with mock.patch.dict(sys.modules, {'sonic_platform': mock_sonic_platform}): + with mock.patch.object(sys, 'argv', test_args): + bmc_fw_update.main() + + mock_bmc.update_firmware.assert_called_once_with('/path/to/firmware.bin') + mock_bmc.get_firmware_id.assert_called_once() + mock_bmc.request_bmc_reset.assert_not_called() + mock_exit.assert_not_called() + + @mock.patch('sys.exit') + @mock.patch('sonic_py_common.logger.Logger') + def test_main_missing_arguments(self, mock_logger_class, mock_exit): + """Test main with missing arguments""" + mock_logger = mock.MagicMock() + mock_sonic_platform = mock.MagicMock() + mock_logger_class.return_value = mock_logger + + test_args = ['bmc_fw_update.py'] + with mock.patch.dict(sys.modules, {'sonic_platform': mock_sonic_platform}): + with mock.patch.object(sys, 'argv', test_args): + bmc_fw_update.main() + + mock_logger.log_error.assert_any_call("Missing firmware image path argument") + mock_exit.assert_called_with(1) + + @mock.patch('sys.exit') + @mock.patch('sonic_py_common.logger.Logger') + def test_main_no_bmc_instance(self, mock_logger_class, mock_exit): + """Test main when BMC instance is None""" + mock_logger = mock.MagicMock() + mock_chassis = mock.MagicMock() + mock_platform = mock.MagicMock() + mock_chassis.get_bmc.return_value = None + mock_platform.get_chassis.return_value = mock_chassis + mock_sonic_platform = mock.MagicMock() + mock_sonic_platform.platform.Platform.return_value = mock_platform + mock_logger_class.return_value = mock_logger + + test_args = ['bmc_fw_update.py', '/path/to/firmware.bin'] + with mock.patch.dict(sys.modules, {'sonic_platform': mock_sonic_platform}): + with mock.patch.object(sys, 'argv', test_args): + bmc_fw_update.main() + + mock_logger.log_error.assert_any_call("Failed to get BMC instance from chassis") + mock_exit.assert_called_with(1) + + @mock.patch('sys.exit') + @mock.patch('sonic_py_common.logger.Logger') + def test_main_update_firmware_failure(self, mock_logger_class, mock_exit): + """Test main when firmware update fails""" + mock_logger = mock.MagicMock() + mock_bmc = mock.MagicMock() + mock_chassis = mock.MagicMock() + mock_platform = mock.MagicMock() + mock_chassis.get_bmc.return_value = mock_bmc + mock_platform.get_chassis.return_value = mock_chassis + mock_sonic_platform = mock.MagicMock() + mock_sonic_platform.platform.Platform.return_value = mock_platform + mock_logger_class.return_value = mock_logger + mock_bmc.update_firmware.return_value = (1, ('Update failed', [])) + + test_args = ['bmc_fw_update.py', '/path/to/firmware.bin'] + with mock.patch.dict(sys.modules, {'sonic_platform': mock_sonic_platform}): + with mock.patch.object(sys, 'argv', test_args): + bmc_fw_update.main() + + mock_logger.log_error.assert_called_once_with('Failed to update BMC firmware. Error 1: Update failed') + mock_exit.assert_called_once_with(1) + + @mock.patch('sys.exit') + @mock.patch('sonic_py_common.logger.Logger') + def test_main_bmc_reset_failure(self, mock_logger_class, mock_exit): + """Test main when BMC reset fails""" + mock_logger = mock.MagicMock() + mock_bmc = mock.MagicMock() + mock_chassis = mock.MagicMock() + mock_platform = mock.MagicMock() + mock_chassis.get_bmc.return_value = mock_bmc + mock_platform.get_chassis.return_value = mock_chassis + mock_sonic_platform = mock.MagicMock() + mock_sonic_platform.platform.Platform.return_value = mock_platform + mock_logger_class.return_value = mock_logger + mock_bmc.update_firmware.return_value = (0, ('Success', ['BMC_FW_0'])) + mock_bmc.get_firmware_id.return_value = 'BMC_FW_0' + mock_bmc.request_bmc_reset.return_value = (1, 'Reset failed') + + test_args = ['bmc_fw_update.py', '/path/to/firmware.bin'] + with mock.patch.dict(sys.modules, {'sonic_platform': mock_sonic_platform}): + with mock.patch.object(sys, 'argv', test_args): + bmc_fw_update.main() + + mock_logger.log_error.assert_called_once_with('Failed to restart BMC. Error 1: Reset failed') + mock_exit.assert_called_once_with(1) From 35fded915ae178412f83b53e6502e78c5f456b2e Mon Sep 17 00:00:00 2001 From: Ben Levi Date: Thu, 1 Jan 2026 19:22:52 +0200 Subject: [PATCH 3/3] Fix bmc default root pass Signed-off-by: Ben Levi --- sonic_platform_base/bmc_base.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/sonic_platform_base/bmc_base.py b/sonic_platform_base/bmc_base.py index 4d7c5d85e..ce2676ade 100644 --- a/sonic_platform_base/bmc_base.py +++ b/sonic_platform_base/bmc_base.py @@ -417,4 +417,8 @@ def reset_root_password(self): ret: An integer return code indicating success (0) or failure msg: A string containing success message or error description """ - return self._change_login_password(self._get_default_root_password(), BMCBase.ROOT_ACCOUNT) + default_root_password = self._get_default_root_password() + if not default_root_password: + logger.log_error("BMC root account default password not found") + return (RedfishClient.ERR_CODE_GENERIC_ERROR, "BMC root account default password not found") + return self._change_login_password(default_root_password, BMCBase.ROOT_ACCOUNT)