diff --git a/scripts/reboot b/scripts/reboot index 044334af3e..349c841058 100755 --- a/scripts/reboot +++ b/scripts/reboot @@ -5,6 +5,8 @@ declare -r EXIT_ERROR=1 declare -r WATCHDOG_UTIL="/usr/local/bin/watchdogutil" declare -r PRE_REBOOT_HOOK="pre_reboot_hook" +source reboot_smartswitch_helper + DEVPATH="/usr/share/sonic/device" PLAT_REBOOT="platform_reboot" PLATFORM_UPDATE_REBOOT_CAUSE="platform_update_reboot_cause" @@ -37,10 +39,17 @@ EXIT_NEXT_IMAGE_NOT_EXISTS=4 EXIT_SONIC_INSTALLER_VERIFY_REBOOT=21 EXIT_PLATFORM_FW_AU_FAILURE=22 PLATFORM_FWUTIL_AU_REBOOT_HANDLE="platform_fw_au_reboot_handle" +PLATFORM_JSON_FILE="platform.json" +PLATFORM_JSON_PATH="${DEVPATH}/${PLATFORM}/${PLATFORM_JSON_FILE}" REBOOT_SCRIPT_NAME=$(basename $0) REBOOT_TYPE="${REBOOT_SCRIPT_NAME}" TAG_LATEST=no REBOOT_FLAGS="" +FORCE_REBOOT="no" +SMART_SWITCH="no" +DPU_MODULE_NAME="" +REBOOT_DPU="no" +PRE_SHUTDOWN="no" function debug() { @@ -128,6 +137,8 @@ function show_help_and_exit() echo " " echo " Available options:" echo " -h, -? : getting this help" + echo " -d : DPU module name on a smart switch, option is invalid when on DPU" + echo " -p : Pre-shutdown steps on DPU, invalid on NPU" exit ${EXIT_SUCCESS} } @@ -154,7 +165,7 @@ function reboot_pre_check() ${DEVPATH}/${PLATFORM}/${PLATFORM_REBOOT_PRE_CHECK} [[ $? -ne 0 ]] && exit $? fi - + # Verify the next image by sonic-installer local message=$(sonic-installer verify-next-image 2>&1) if [ $? -ne 0 ]; then @@ -178,7 +189,7 @@ function check_conflict_boot_in_fw_update() function parse_options() { - while getopts "h?vf" opt; do + while getopts "h?vfpd:" opt; do case ${opt} in h|\? ) show_help_and_exit @@ -192,6 +203,13 @@ function parse_options() f ) REBOOT_FLAGS+=" -f" ;; + d ) + REBOOT_DPU="yes" + DPU_MODULE_NAME="$OPTARG" + ;; + p ) + PRE_SHUTDOWN="yes" + ;; esac done } @@ -225,6 +243,18 @@ fi debug "User requested rebooting device ..." +handle_smart_switch "$REBOOT_DPU" "$PRE_SHUTDOWN" "$DPU_MODULE_NAME" +smart_switch_result=$? +if [[ $smart_switch_result -ne 0 ]]; then + exit $smart_switch_result +fi + +# On a smartswitch, complete the DPU reboot and exit +is_smartswitch=$(is_smartswitch) +if [ "$is_smartswitch" == "True" ] && [ "$REBOOT_DPU" == "yes" ]; then + exit $smart_switch_result +fi + check_conflict_boot_in_fw_update setup_reboot_variables @@ -287,6 +317,11 @@ if [ -x ${WATCHDOG_UTIL} ]; then ${WATCHDOG_UTIL} arm fi +if [[ "${PRE_SHUTDOWN}" == "yes" ]]; then + echo "${DPU_MODULE_NAME} pre-shutdown steps are completed" + exit ${EXIT_SUCCESS} +fi + if [ -x ${DEVPATH}/${PLATFORM}/${PLAT_REBOOT} ]; then VERBOSE=yes debug "Rebooting with platform ${PLATFORM} specific tool ..." ${DEVPATH}/${PLATFORM}/${PLAT_REBOOT} $@ diff --git a/scripts/reboot_smartswitch_helper b/scripts/reboot_smartswitch_helper new file mode 100644 index 0000000000..5ddff01cce --- /dev/null +++ b/scripts/reboot_smartswitch_helper @@ -0,0 +1,291 @@ +#!/bin/bash + +declare -r GNMI_PORT=50052 +declare -r MODULE_REBOOT_DPU="DPU" +declare -r MODULE_REBOOT_SMARTSWITCH="SMARTSWITCH" + +# Function to print debug message +function log_message() { + local message=$1 + echo "$(date '+%Y-%m-%d %H:%M:%S') - $message" >&2 +} + +# Function to check if running on smart switch +function is_smartswitch() +{ + python3 -c "from utilities_common.chassis import is_smartswitch; print(is_smartswitch())" | grep -q "True" +} + +# Function to check if running on DPU +function is_dpu() +{ + python3 -c "from utilities_common.chassis import is_dpu; print(is_dpu())" | grep -q "True" +} + +# Function to retrieve number of DPUs +function get_num_dpus() +{ + python3 -c "from utilities_common.chassis import get_num_dpus; print(get_num_dpus())" +} + +# Function to retrieve DPU IP from CONFIG_DB +function get_dpu_ip() +{ + local DPU_NAME=$1 + sonic-db-cli CONFIG_DB HGET "DHCP_SERVER_IPV4_PORT|bridge-midplane|${DPU_NAME}" "ips@" +} + +# Function to retrieve GNMI port from CONFIG_DB +function get_gnmi_port() +{ + local DPU_NAME=$1 + sonic-db-cli CONFIG_DB HGET "DPU_PORT|$DPU_NAME" "gnmi" +} + +# Function to get reboot status from DPU +function get_reboot_status() +{ + local dpu_ip=$1 + local port=$2 + local reboot_status + reboot_status=$(docker exec -i gnmi gnoi_client -target "${dpu_ip}:${port}" -logtostderr -insecure -rpc RebootStatus 2>/dev/null) + if [ $? -ne 0 ] || [ -z "$reboot_status" ]; then + log_message "Error: Failed to send reboot status command to DPU ${DPU_NAME}" + return ${EXIT_ERROR} + fi + local is_reboot_active + is_reboot_active=$(echo "$reboot_status" | grep "active" | awk '{print $2}') + if [ "$is_reboot_active" == "false" ]; then + log_message "DPU ${DPU_NAME} has finished rebooting" + return ${EXIT_SUCCESS} + fi + return ${EXIT_ERROR} +} + +# Function to detach PCI module +function pci_detach_module() +{ + local DPU_NAME=$1 + local DPU_BUS_INFO=$2 + python3 -c "from utilities_common.module import ModuleHelper; helper = ModuleHelper(); helper.pci_detach_module('${DPU_NAME}')" + if [ $? -ne 0 ]; then + log_message "Error: PCI detach vendor API is not available" + echo 1 > /sys/bus/pci/devices/${DPU_BUS_INFO}/remove + fi +} + +# Function to rescan PCI module +function pci_reattach_module() +{ + local DPU_NAME=$1 + local DPU_BUS_INFO=$2 + python3 -c "from utilities_common.module import ModuleHelper; helper = ModuleHelper(); helper.pci_reattach_module('${DPU_NAME}')" + if [ $? -ne 0 ]; then + log_message "Error: PCI reattach vendor API is not available" + echo 1 > /sys/bus/pci/devices/${DPU_BUS_INFO}/rescan + fi +} + +# Function to reboot DPU +function reboot_dpu_platform() +{ + local DPU_NAME=$1 + local REBOOT_TYPE=$2 + python3 -c "from utilities_common.module import ModuleHelper; helper = ModuleHelper(); helper.reboot_module('${DPU_NAME}', '${REBOOT_TYPE}')" +} + +# Function to wait for DPU reboot status +function wait_for_dpu_reboot_status() +{ + local dpu_ip=$1 + local port=$2 + + if [[ -z "$PLATFORM_JSON_PATH" ]]; then + log_message "Error: PLATFORM_JSON_PATH is not defined" + exit $EXIT_ERROR + fi + + local dpu_halt_services_timeout=$(jq -r '.dpu_halt_services_timeout' "$PLATFORM_JSON_PATH" 2>/dev/null) + if [ $? -ne 0 ]; then + log_message "Error: Failed to retrieve dpu_halt_services_timeout from ${PLATFORM_JSON_PATH}" + return ${EXIT_ERROR} + fi + + local poll_interval=5 + local waited_time=0 + while true; do + local reboot_status + get_reboot_status "${dpu_ip}" "${port}" + reboot_status=$? + if [ $reboot_status -eq ${EXIT_SUCCESS} ]; then + break + fi + + sleep "$poll_interval" + waited_time=$((waited_time + poll_interval)) + if [ $waited_time -ge $dpu_halt_services_timeout ]; then + log_message "Error: Timeout waiting for DPU ${DPU_NAME} to finish rebooting" + return ${EXIT_ERROR} + fi + done +} + +# Function to send reboot command to DPU +function gnmi_reboot_dpu() +{ + # Retrieve DPU IP and GNMI port + dpu_ip=$(get_dpu_ip "${DPU_NAME}") + log_message "DPU IP ${DPU_NAME}: $dpu_ip" + port=$(get_gnmi_port "${DPU_NAME}") + if [ -z "$port" ]; then + port=$GNMI_PORT # Default GNMI port + fi + log_message "GNMI port ${DPU_NAME}: $port" + + if [ -z "$dpu_ip" ]; then + log_message "Error: Failed to retrieve DPU IP for ${DPU_NAME}" + return ${EXIT_ERROR} + fi + + docker exec -i gnmi gnoi_client -target ${dpu_ip}:${port} -logtostderr -insecure -rpc Reboot -jsonin '{"method":3}' + if [ $? -ne 0 ]; then + log_message "Error: Failed to send reboot command to DPU ${DPU_NAME}" + return ${EXIT_ERROR} + fi + + wait_for_dpu_reboot_status "${dpu_ip}" "${port}" +} + +function reboot_dpu() +{ + local DPU_NAME=$1 + local REBOOT_TYPE=$2 + local DPU_INDEX=${DPU_NAME//[!0-9]/} + + debug "User requested rebooting device ${DPU_NAME} ..." + + # Send reboot command to DPU + gnmi_reboot_dpu "${DPU_NAME}" + if [ $? -ne 0 ]; then + log_message "Error: Failed to send gnoi command to reboot DPU ${DPU_NAME}" + fi + + local DPU_BUS_INFO=$(jq -r --arg DPU_NAME "$DPU_NAME" '.DPUS[$DPU_NAME].bus_info' "$PLATFORM_JSON_PATH") + if [ -z "$DPU_BUS_INFO" ] || [ "$DPU_BUS_INFO" = "null" ]; then + log_message "Error: Failed to retrieve bus info for DPU ${DPU_NAME}" + return ${EXIT_ERROR} + fi + + # Update STATE_DB and handle PCIe removal and rescan + sonic-db-cli STATE_DB set "PCIE_DETACH_INFO|${DPU_NAME}" '{"dpu_id": "'${DPU_INDEX}'", "dpu_state": "detaching", "bus_info": "'${DPU_BUS_INFO}'"}' + + pci_detach_module ${DPU_NAME} ${DPU_BUS_INFO} + if [ $? -ne 0 ]; then + log_message "Error: Failed to detach PCI module for DPU ${DPU_NAME}" + return ${EXIT_ERROR} + fi + + reboot_dpu_platform ${DPU_NAME} ${REBOOT_TYPE} + if [ $? -ne 0 ]; then + log_message "Error: Failed to send platform command to reboot DPU ${DPU_NAME}" + return ${EXIT_ERROR} + fi + + pci_reattach_module ${DPU_NAME} ${DPU_BUS_INFO} + + sonic-db-cli STATE_DB del "PCIE_DETACH_INFO|${DPU_NAME}" +} + +# Function to reboot all DPUs in parallel +function reboot_all_dpus() { + local NUM_DPU=$1 + + if [[ -z $NUM_DPU ]]; then + log_message "Error: Failed to retrieve number of DPUs or no DPUs found" + return + fi + + local failures=0 + for (( i=0; i<"$NUM_DPU"; i++ )); do + log_message "Rebooting DPU module dpu$i" + reboot_dpu "dpu$i" "$MODULE_REBOOT_SMARTSWITCH" & + if [ $? -ne 0 ]; then + ((failures++)) + fi + done + wait + return $failures +} + +# Function to verify DPU module name +function verify_dpu_module_name() { + local DPU_MODULE_NAME=$1 + local NUM_DPU=$2 + + if [[ -z "$DPU_MODULE_NAME" ]]; then + log_message "Error: DPU module name not provided" + return $EXIT_ERROR + fi + + NUM_DPU=$((NUM_DPU - 1)) + if [[ ! "$DPU_MODULE_NAME" =~ ^dpu[0-$NUM_DPU]$ ]]; then + log_message "Error: Invalid DPU module name provided" + return $EXIT_ERROR + fi +} + +# Function to handle scenarios on smart switch +function handle_smart_switch() { + local REBOOT_DPU=$1 + local PRE_SHUTDOWN=$2 + local DPU_NAME=$3 + + NUM_DPU=$(get_num_dpus) + + if is_dpu; then + if [[ "$PRE_SHUTDOWN" != "yes" ]]; then + log_message "Error: '-p' option not specified for a DPU" + return $EXIT_ERROR + elif [[ "$REBOOT_DPU" == "yes" ]]; then + log_message "Error: '-d' option specified for a DPU" + return $EXIT_ERROR + fi + return $EXIT_SUCCESS + fi + + if [[ "$PRE_SHUTDOWN" == "yes" ]]; then + log_message "Error: '-p' option specified for a non-DPU" + return $EXIT_ERROR + fi + + if [[ "$REBOOT_DPU" == "yes" ]]; then + if is_smartswitch; then + if [[ -z $NUM_DPU ]]; then + log_message "Error: Failed to retrieve number of DPUs or no DPUs found" + return $EXIT_ERROR + fi + + DPU_MODULE_NAME="${DPU_NAME,,}" + verify_dpu_module_name "$DPU_MODULE_NAME" "$NUM_DPU" + result=$? + if [[ $result -ne $EXIT_SUCCESS ]]; then + return $result + fi + + log_message "Rebooting device ${DPU_MODULE_NAME}" + reboot_dpu "$DPU_MODULE_NAME" "$MODULE_REBOOT_DPU" + result=$? + return $result + else + log_message "Error: '-d' option specified for a non-smart-switch" + return $EXIT_ERROR + fi + fi + + # If the system is a smart switch, reboot all DPUs in parallel + if is_smartswitch; then + reboot_all_dpus "$NUM_DPU" "$MODULE_REBOOT_SMARTSWITCH" + result=$? + return $result + fi +} diff --git a/setup.py b/setup.py index 4a11624a87..071da4b652 100644 --- a/setup.py +++ b/setup.py @@ -163,6 +163,7 @@ 'scripts/psushow', 'scripts/queuestat', 'scripts/reboot', + 'scripts/reboot_smartswitch_helper', 'scripts/route_check.py', 'scripts/route_check_test.sh', 'scripts/vnet_route_check.py', diff --git a/tests/test_chassis.py b/tests/test_chassis.py new file mode 100644 index 0000000000..57fa418018 --- /dev/null +++ b/tests/test_chassis.py @@ -0,0 +1,38 @@ +import pytest +from unittest import mock +from utilities_common import chassis + + +class TestChassis: + @pytest.fixture + def mock_device_info(self): + with mock.patch('utilities_common.chassis.device_info') as mock_device_info: + yield mock_device_info + + def test_is_smartswitch(self, mock_device_info): + mock_device_info.is_smartswitch = mock.Mock(return_value=True) + assert chassis.is_smartswitch() == True # noqa: E712 + + mock_device_info.is_smartswitch = mock.Mock(return_value=False) + assert chassis.is_smartswitch() == False # noqa: E712 + + def test_is_dpu(self, mock_device_info): + mock_device_info.is_dpu = mock.Mock(return_value=True) + assert chassis.is_dpu() == True # noqa: E712 + + mock_device_info.is_dpu = mock.Mock(return_value=False) + assert chassis.is_dpu() == False # noqa: E712 + + def test_get_num_dpus(self, mock_device_info): + mock_device_info.get_num_dpus = mock.Mock(return_value=4) + assert chassis.get_num_dpus() == 4 + + del mock_device_info.get_num_dpus + assert chassis.get_num_dpus() == 0 # noqa: E712 + + def test_get_dpu_list(self, mock_device_info): + mock_device_info.get_dpu_list = mock.Mock(return_value=['dpu1', 'dpu2']) + assert chassis.get_dpu_list() == ['dpu1', 'dpu2'] + + del mock_device_info.get_dpu_list + assert chassis.get_dpu_list() == [] # noqa: E712 diff --git a/tests/test_module.py b/tests/test_module.py new file mode 100644 index 0000000000..71ba28be2e --- /dev/null +++ b/tests/test_module.py @@ -0,0 +1,111 @@ +import sys +import pytest +from unittest import mock +from utilities_common.util_base import UtilHelper +from utilities_common.module import ModuleHelper, INVALID_MODULE_INDEX + +sys.modules['sonic_platform'] = mock.MagicMock() + +util = UtilHelper() +module_helper = ModuleHelper() + + +class TestModuleHelper: + @pytest.fixture + def mock_load_platform_chassis(self): + with mock.patch('utilities_common.module.util.load_platform_chassis') as mock_load_platform_chassis: + yield mock_load_platform_chassis + + @pytest.fixture + def mock_try_get(self): + with mock.patch('utilities_common.module.util.try_get') as mock_try_get: + yield mock_try_get + + @pytest.fixture + def mock_try_get_args(self): + with mock.patch.object(ModuleHelper, 'try_get_args') as mock_try_get_args: + yield mock_try_get_args + + @pytest.fixture + def mock_log_error(self): + with mock.patch('utilities_common.module.log.log_error') as mock_log_error: + yield mock_log_error + + def test_try_get_args_success(self): + def mock_callback(arg): + return arg + + result = module_helper.try_get_args(mock_callback, "test_arg") + assert result == "test_arg" + + def test_try_get_args_none_return(self): + def mock_callback(arg): + return None + + result = module_helper.try_get_args(mock_callback, "test_arg", default="default_value") + assert result == "default_value" + + def test_try_get_args_not_implemented_error(self): + def mock_callback(arg): + raise NotImplementedError + + result = module_helper.try_get_args(mock_callback, "test_arg", default="default_value") + assert result == "default_value" + + def test_init_success(self, mock_load_platform_chassis): + mock_load_platform_chassis.return_value = mock.MagicMock() + module_helper = ModuleHelper() + assert module_helper.platform_chassis is not None + + def test_init_failure(self, mock_load_platform_chassis, mock_log_error): + mock_load_platform_chassis.return_value = None + module_helper = ModuleHelper() + mock_log_error.assert_called_once_with("Failed to load platform chassis") + assert module_helper.platform_chassis is None + + def test_reboot_module_success(self, mock_load_platform_chassis, mock_try_get_args): + mock_try_get_args.return_value = True + mock_load_platform_chassis.return_value.get_module_index.return_value = 1 + mock_load_platform_chassis.return_value.get_module.return_value.reboot.return_value = True + + result = module_helper.reboot_module("DPU1", "cold") + assert result is True + + def test_reboot_module_invalid_index(self, mock_load_platform_chassis, mock_try_get_args): + mock_try_get_args.return_value = INVALID_MODULE_INDEX + mock_load_platform_chassis.return_value.get_module_index.return_value = INVALID_MODULE_INDEX + + result = module_helper.reboot_module("DPU1", "cold") + assert result is False + + def test_pci_detach_module_success(self, mock_load_platform_chassis, mock_try_get_args, mock_try_get): + mock_try_get_args.return_value = True + mock_try_get.return_value = True + mock_load_platform_chassis.return_value.get_module_index.return_value = 1 + mock_load_platform_chassis.return_value.get_module.return_value.pci_detach.return_value = True + + result = module_helper.pci_detach_module("DPU1") + assert result is True + + def test_pci_detach_module_invalid_index(self, mock_load_platform_chassis, mock_try_get_args): + mock_try_get_args.return_value = INVALID_MODULE_INDEX + mock_load_platform_chassis.return_value.get_module_index.return_value = INVALID_MODULE_INDEX + + result = module_helper.pci_detach_module("DPU1") + assert result is False + + def test_pci_reattach_module_success(self, mock_load_platform_chassis, mock_try_get_args, mock_try_get): + mock_try_get_args.return_value = True + mock_try_get.return_value = True + mock_load_platform_chassis.return_value.get_module_index.return_value = 1 + mock_load_platform_chassis.return_value.get_module.return_value.pci_reattach.return_value = True + + result = module_helper.pci_reattach_module("DPU1") + assert result is True + + def test_pci_reattach_module_invalid_index(self, mock_load_platform_chassis, mock_try_get_args): + mock_try_get_args.return_value = INVALID_MODULE_INDEX + mock_load_platform_chassis.return_value.get_module_index.return_value = INVALID_MODULE_INDEX + + result = module_helper.pci_reattach_module("DPU1") + assert result is False diff --git a/utilities_common/chassis.py b/utilities_common/chassis.py index 1283bca580..264e5cf661 100644 --- a/utilities_common/chassis.py +++ b/utilities_common/chassis.py @@ -16,3 +16,23 @@ def get_chassis_local_interfaces(): lst = data[1].split(",") return lst return lst + + +def is_smartswitch(): + return hasattr(device_info, 'is_smartswitch') and device_info.is_smartswitch() + + +def is_dpu(): + return hasattr(device_info, 'is_dpu') and device_info.is_dpu() + + +def get_num_dpus(): + if hasattr(device_info, 'get_num_dpus'): + return device_info.get_num_dpus() + return 0 + + +def get_dpu_list(): + if hasattr(device_info, 'get_dpu_list'): + return device_info.get_dpu_list() + return [] diff --git a/utilities_common/module.py b/utilities_common/module.py new file mode 100644 index 0000000000..119719b0a0 --- /dev/null +++ b/utilities_common/module.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 +# +# module.py +# +# Utility helper for modules within SONiC + +from sonic_py_common import syslogger +from utilities_common.util_base import UtilHelper + +SYSLOG_IDENTIFIER = "module" +NOT_AVAILABLE = 'N/A' +INVALID_MODULE_INDEX = -1 + +# Load the UtilHelper class +util = UtilHelper() + +# Global logger instance +log = syslogger.SysLogger(SYSLOG_IDENTIFIER) + + +class ModuleHelper: + def __init__(self): + # Global variable for platform chassis + self.platform_chassis = util.load_platform_chassis() + if not self.platform_chassis: + log.log_error("Failed to load platform chassis") + + def try_get_args(self, callback, *args, **kwargs): + """ + Handy function to invoke the callback and catch NotImplementedError + :param callback: Callback to be invoked + :param args: Arguments to be passed to callback + :param kwargs: Default return value if exception occur + :return: Default return value if exception occur else return value of the callback + """ + default = kwargs.get('default', NOT_AVAILABLE) + try: + ret = callback(*args) + if ret is None: + ret = default + except NotImplementedError: + ret = default + + return ret + + def reboot_module(self, module_name, reboot_type): + """ + Reboot the specified module by invoking the platform API. + + Args: + module_name (str): The name of the module to reboot. + reboot_type (str): The type of reboot requested for the module. + + Returns: + bool: True if the reboot command was successfully sent, False otherwise. + """ + module_index = self.try_get_args(self.platform_chassis.get_module_index, module_name, + default=INVALID_MODULE_INDEX) + if module_index < 0: + log.log_error("Unable to get module-index for {}". format(module_name)) + return False + + if not hasattr(self.platform_chassis.get_module(module_index), 'reboot'): + log.log_error("Reboot method not found in platform chassis") + return False + + log.log_info("Rebooting module {} with reboot_type {}...".format(module_name, reboot_type)) + status = self.try_get_args(self.platform_chassis.get_module(module_index).reboot, reboot_type, default=False) + if not status: + log.log_error("Reboot status for module {}: {}".format(module_name, status)) + return False + + return True + + def pci_detach_module(self, module_name): + """ + Detach the specified module by invoking the platform API. + Note: Caller to make sure this method is not invoked concurrently with + pci_reattach_module for the same module. + + Args: + module_name (str): The name of the module to detach. + + Returns: + bool: True if the detach command was successfully sent, False otherwise. + """ + module_index = self.try_get_args(self.platform_chassis.get_module_index, module_name, + default=INVALID_MODULE_INDEX) + if module_index < 0: + log.log_error("Unable to get module-index for {}". format(module_name)) + return False + + if not hasattr(self.platform_chassis.get_module(module_index), 'pci_detach'): + log.log_error("PCI detach method not found in platform chassis") + return False + + log.log_info("Detaching module {}...".format(module_name)) + status = util.try_get(self.platform_chassis.get_module(module_index).pci_detach, default=False) + if not status: + log.log_error("PCI detach status for module {}: {}".format(module_name, status)) + return False + + return True + + def pci_reattach_module(self, module_name): + """ + Rescan the specified module by invoking the platform API. + Note: Caller to make sure this method is not invoked concurrently with + pci_detach_module for the same module. + + Args: + module_name (str): The name of the module to rescan. + + Returns: + bool: True if the rescan command was successfully sent, False otherwise. + """ + module_index = self.try_get_args(self.platform_chassis.get_module_index, module_name, + default=INVALID_MODULE_INDEX) + if module_index < 0: + log.log_error("Unable to get module-index for {}". format(module_name)) + return False + + if not hasattr(self.platform_chassis.get_module(module_index), 'pci_reattach'): + log.log_error("PCI reattach method not found in platform chassis") + return False + + log.log_info("Rescanning module {}...".format(module_name)) + status = util.try_get(self.platform_chassis.get_module(module_index).pci_reattach, default=False) + if not status: + log.log_error("PCI rescan status for module {}: {}".format(module_name, status)) + return False + + return True