Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
21 changes: 13 additions & 8 deletions sonic_platform_base/bmc_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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):
"""
Expand All @@ -416,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)
49 changes: 49 additions & 0 deletions sonic_platform_base/bmc_fw_update.py
Original file line number Diff line number Diff line change
@@ -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()
11 changes: 8 additions & 3 deletions sonic_platform_base/redfish_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, '')

'''
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand Down
9 changes: 5 additions & 4 deletions tests/bmc_base_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, [], {}],
]

Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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')
Expand Down
165 changes: 165 additions & 0 deletions tests/bmc_fw_update_test.py
Original file line number Diff line number Diff line change
@@ -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)
4 changes: 2 additions & 2 deletions tests/redfish_client_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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')
Expand Down
Loading