blob: bc6b2928adc73deb73974625f0a5b0c234eae980 [file] [edit]
#include "tlbmc/collector/fru_collector.h"
#include <cctype>
#include <cstddef>
#include <cstdint>
#include <filesystem> // NOLINT
#include <fstream>
#include <ios>
#include <memory>
#include <optional>
#include <string>
#include <system_error> // NOLINT
#include <utility>
#include <vector>
#include "gbmc-hal/api/app/inventory/smbios_types.h"
#include "bus_topology.pb.h"
#include "absl/base/thread_annotations.h"
#include "absl/container/flat_hash_map.h"
#include "absl/container/flat_hash_set.h"
#include "absl/functional/any_invocable.h"
#include "absl/log/log.h"
#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "absl/strings/ascii.h"
#include "absl/strings/match.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/str_join.h"
#include "absl/strings/string_view.h"
#include "absl/synchronization/mutex.h"
#include "absl/time/time.h"
#include "g3/macros.h"
#include "nlohmann/json_fwd.hpp"
#include "json_utils.h"
#include "tlbmc/collector/resource_state_manager.h"
#include "ad_hoc_fru_config.pb.h"
#include "tlbmc/configs/entity_config.h"
#include "hal_common_config.pb.h"
#include "tlbmc/hal/fru_scanner.h"
#include "fru.pb.h"
#include "memory.pb.h"
#include "processor.pb.h"
#include "resource.pb.h"
#include "status.pb.h"
#include "tlbmc/scheduler/scheduler.h"
#include "tlbmc/time/time.h"
#include "tlbmc/utils/fram_utils.h"
#include "tlbmc/utils/fru_utils.h"
#include "google/protobuf/json/json.h"
#include "google/protobuf/util/json_util.h"
namespace milotic_tlbmc {
using AdHocFruScanContext = FruCollector::AdHocFruScanContext;
namespace {
// Extracts the entire trailing numeric digit sequence from the host ID (e.g.,
// "system12" -> "12", "system2" -> "2"). If the host ID represents a single
// system (e.g., "system") which does not contain a trailing digit suffix, it
// naturally returns an empty string "", forcing resolvers to fall back to
// single-system default file paths.
std::string ExtractHostNumber(absl::string_view host_id) {
int i = static_cast<int>(host_id.size()) - 1;
while (i >= 0 &&
(std::isdigit(static_cast<unsigned char>(host_id[i])) != 0)) {
--i;
}
return std::string(host_id.substr(i + 1));
}
} // namespace
std::string FruCollector::GetFruKey(uint64_t fru_bus, uint64_t fru_address) {
return absl::StrCat(fru_bus, ":", fru_address);
}
// These ad-hoc scanning configs will be in
// json["ProbeV2"]["AdHocFruConfig"]
std::vector<AdHocFruConfig> FruCollector::Options::ParseAdHocFruConfigs(
const std::vector<nlohmann::json>& config_list) {
std::vector<AdHocFruConfig> ad_hoc_fru_configs;
for (const auto& config : config_list) {
const nlohmann::json* tlbmc_config =
GetValueAsJson(config, kTlbmcConfigKey);
if (tlbmc_config == nullptr) {
continue;
}
const nlohmann::json* ad_hoc_fru_scanning_config =
GetValueAsJson(*tlbmc_config, kAdHocFruConfigKey);
if (ad_hoc_fru_scanning_config == nullptr) {
continue;
}
AdHocFruConfig ad_hoc_fru_config_proto;
::google::protobuf::json::ParseOptions opts;
if (!::google::protobuf::json::JsonStringToMessage(ad_hoc_fru_scanning_config->dump(),
&ad_hoc_fru_config_proto, opts)
.ok()) {
const std::string* config_name =
milotic::authz::GetValueAsString(config, "Name");
LOG(WARNING) << absl::StrCat(
"Failed to convert ad-hoc FRU scanning config to proto for ",
(config_name == nullptr ? "unnamed config"
: std::string(*config_name)));
continue;
}
ad_hoc_fru_configs.push_back(ad_hoc_fru_config_proto);
}
return ad_hoc_fru_configs;
}
nlohmann::json FruCollector::ToJson() const {
nlohmann::json response;
std::string json_string;
::google::protobuf::util::JsonPrintOptions opts;
opts.preserve_proto_field_names = true;
absl::MutexLock lock(fru_table_mutex_);
if (!::google::protobuf::json::MessageToJsonString(fru_table_, &json_string, opts)
.ok()) {
LOG(ERROR) << "Failed to convert FRU table to JSON";
return response;
}
return nlohmann::json::parse(json_string, nullptr, false);
}
// Returns the next scan info if there is a next scan period to schedule.
// Returns nullopt if there is no next scan period to schedule.
std::optional<AdHocFruScanContext> FruCollector::GetNextScanInfo(
const AdHocFruConfig& ad_hoc_fru_config,
const AdHocFruScanContext& scan_context) {
if (ad_hoc_fru_config.periodic_scan_config_size() <=
scan_context.cur_periodic_scan_config_index) {
return std::nullopt;
}
// 0 indexed so if we are at the last scan period, we need to move onto next
// period.
if (ad_hoc_fru_config
.periodic_scan_config(
static_cast<int>(scan_context.cur_periodic_scan_config_index))
.scan_count() ==
scan_context.cur_periodic_scan_config_scan_count + 1) {
// If we are at the last scan period, then no more scans.
if (scan_context.cur_periodic_scan_config_index + 1 ==
ad_hoc_fru_config.periodic_scan_config_size()) {
return std::nullopt;
}
return AdHocFruScanContext{
.cur_periodic_scan_config_index =
scan_context.cur_periodic_scan_config_index + 1,
.cur_periodic_scan_config_scan_count = 0,
.found = scan_context.found};
}
return AdHocFruScanContext{
.cur_periodic_scan_config_index =
scan_context.cur_periodic_scan_config_index,
.cur_periodic_scan_config_scan_count =
scan_context.cur_periodic_scan_config_scan_count + 1,
.found = scan_context.found};
}
void FruCollector::RescheduleAdHocFruScan(
const AdHocFruConfig& ad_hoc_fru_config, size_t ad_hoc_fru_scan_context_id,
const AdHocFruScanContext& scan_context) {
// Schedule task with new period
task_scheduler_->ScheduleOneShotAsync(
[this, ad_hoc_fru_config,
ad_hoc_fru_scan_context_id](absl::AnyInvocable<void()> on_done) {
AttemptAdHocFruScan(ad_hoc_fru_config, ad_hoc_fru_scan_context_id);
on_done();
},
DecodeGoogleApiProto(ad_hoc_fru_config
.periodic_scan_config(static_cast<int>(
scan_context.cur_periodic_scan_config_index))
.scan_interval()));
}
void FruCollector::AttemptAdHocFruScan(const AdHocFruConfig& ad_hoc_fru_config,
size_t ad_hoc_fru_scan_context_id) {
AdHocFruScanContext scan_context =
GetAdHocFruScanContext(ad_hoc_fru_scan_context_id);
LOG(INFO) << "Attempting ad-hoc FRU scan for " << ad_hoc_fru_config.name();
std::shared_ptr<EntityConfig> entity_config = GetEntityConfig();
if (entity_config == nullptr) {
LOG(WARNING) << "Entity config is not yet set. Will rescan later for "
<< ad_hoc_fru_config.name();
RescheduleAdHocFruScan(ad_hoc_fru_config, ad_hoc_fru_scan_context_id,
scan_context);
return;
}
AdHocScannerType scanner_type = ad_hoc_fru_config.scanner_type();
LOG(INFO) << "Ad-hoc config: " << ad_hoc_fru_config.name()
<< " requests scanner_type: "
<< AdHocScannerType_Name(scanner_type);
auto it = options_.fru_scanners.find(scanner_type);
if (it == options_.fru_scanners.end()) {
LOG(ERROR) << "FruScanner not found for type "
<< AdHocScannerType_Name(scanner_type);
return;
}
FruScanner* fru_scanner = it->second;
if (fru_scanner == nullptr) {
LOG(ERROR) << "FruScanner found for type "
<< AdHocScannerType_Name(scanner_type) << " but it is NULL";
return;
}
// Lock the scan_in_progress_mutex_ to ensure that only one FRU scan is
// in progress at a time.
std::unique_ptr<absl::MutexLock> lock;
if (options_.enable_deterministic_fru_scanning &&
scanner_type == SCANNER_TYPE_DEFAULT_IPMI_I2C) {
lock = std::make_unique<absl::MutexLock>(scan_in_progress_mutex_);
}
// If the FRU is already in the FRU table, then we can skip the scan to
// avoid unnecessary I2C transactions and config reloading.
std::string fru_key =
GetFruKey(ad_hoc_fru_config.hal_common_config().bus(),
ad_hoc_fru_config.hal_common_config().address());
if (ContainsFru(fru_key)) {
LOG(INFO)
<< "FRU already exists in the FRU table for ad-hoc FRU (presumbly "
"found in periodic/deterministic FRU scanning): "
<< fru_key;
scan_context.found = true;
UpdateAdHocFruScanContext(ad_hoc_fru_scan_context_id, scan_context);
return;
}
// Try to scan FRU
absl::StatusOr<std::unique_ptr<I2cFruInfo>> ad_hoc_fru_info =
fru_scanner->GetI2cFruInfoFromBus(
static_cast<int>(ad_hoc_fru_config.hal_common_config().bus()),
static_cast<int>(ad_hoc_fru_config.hal_common_config().address()),
DecodeGoogleApiProto(ad_hoc_fru_config.delay_between_reads()));
// If scan failed, next scan is already scheduled, so update scan
// context and reschedule with different period if needed
if (!ad_hoc_fru_info.ok()) {
LOG(WARNING) << "Failed to scan ad-hoc FRU: " << ad_hoc_fru_config.name()
<< " with status: " << ad_hoc_fru_info.status();
ScanNextAdHocIfNeeded(ad_hoc_fru_config, ad_hoc_fru_scan_context_id,
scan_context);
return;
}
LOG(INFO) << "Successfully scanned ad-hoc FRU: " << ad_hoc_fru_config.name();
// If scan succeeded, update FRU table
absl::StatusOr<RawFru> fru =
CreateRawFruFromScanner(scanner_type, *ad_hoc_fru_info);
if (!fru.ok()) {
LOG(WARNING) << "Failed to create raw FRU from data: " << fru.status()
<< " for FRU: " << ad_hoc_fru_config.name();
ScanNextAdHocIfNeeded(ad_hoc_fru_config, ad_hoc_fru_scan_context_id,
scan_context);
return;
}
AddFru(*fru);
scan_context.found = true;
UpdateAdHocFruScanContext(ad_hoc_fru_scan_context_id, scan_context);
LOG(WARNING) << "Updated FRU table for FRU key: " << fru->key()
<< " for ad-hoc FRU: " << ad_hoc_fru_config.name();
entity_config->UpdateFruAndTopology(GetCopyOfCurrentScannedFrus(),
std::vector<RawFru>{*fru});
}
void FruCollector::ScanNextAdHocIfNeeded(
const AdHocFruConfig& ad_hoc_fru_config, size_t ad_hoc_fru_scan_context_id,
AdHocFruScanContext& scan_context) {
std::optional<AdHocFruScanContext> next_scan_info =
GetNextScanInfo(ad_hoc_fru_config, scan_context);
// If there is no next scan info, then we are done with scanning.
if (!next_scan_info.has_value()) {
return;
}
// Sanity check periodic scan config size, fail safe.
if (ad_hoc_fru_config.periodic_scan_config_size() <
next_scan_info->cur_periodic_scan_config_index) {
LOG(ERROR) << "Error in ad-hoc FRU scanning logic. No longer scanning "
<< ad_hoc_fru_config.name();
return;
}
scan_context.cur_periodic_scan_config_index =
next_scan_info->cur_periodic_scan_config_index;
scan_context.cur_periodic_scan_config_scan_count =
next_scan_info->cur_periodic_scan_config_scan_count;
UpdateAdHocFruScanContext(ad_hoc_fru_scan_context_id, scan_context);
RescheduleAdHocFruScan(ad_hoc_fru_config, ad_hoc_fru_scan_context_id,
scan_context);
LOG(INFO) << "Rescheduled ad-hoc FRU scan for " << ad_hoc_fru_config.name();
}
void FruCollector::ScheduleAdHocFruScan(
const AdHocFruConfig& ad_hoc_fru_config) {
// Index to add to ad_hoc_fru_scan_contexts_
size_t ad_hoc_fru_scan_context_id = GetAdHocFruScanContextsSize();
AddAdHocFruScanContext(
AdHocFruScanContext{.cur_periodic_scan_config_index = 0,
.cur_periodic_scan_config_scan_count = 0,
.found = false});
// Scheduler automatically reruns task at defined periods
task_scheduler_->ScheduleOneShotAsync(
[this, ad_hoc_fru_config,
ad_hoc_fru_scan_context_id](absl::AnyInvocable<void()> on_done) {
AttemptAdHocFruScan(ad_hoc_fru_config, ad_hoc_fru_scan_context_id);
on_done();
},
DecodeGoogleApiProto(
ad_hoc_fru_config.periodic_scan_config(0).scan_interval()));
}
absl::StatusOr<const std::vector<std::string>&>
FruCollector::GetAssociatedSmbiosInventoryParts(
const std::string& host_id) const {
auto it = host_to_smbios_inventory_parts_.find(host_id);
if (it == host_to_smbios_inventory_parts_.end()) {
return absl::NotFoundError(absl::StrCat("Host ", host_id,
" not found in host to SMBIOS "
"inventory parts map."));
}
return it->second;
}
RawFru CreateAndPopulatePresentRawFru(
const std::unique_ptr<I2cFruInfo>& i2c_fru_info,
const absl::flat_hash_map<std::string, std::string>& fru_data_fields) {
RawFru raw_fru;
// Assuming we can have only one FRU per I2C bus/address pair
raw_fru.set_key(
FruCollector::GetFruKey(i2c_fru_info->bus, i2c_fru_info->address));
// Populate I2C info.
raw_fru.mutable_i2c_info()->set_bus(i2c_fru_info->bus);
raw_fru.mutable_i2c_info()->set_address(i2c_fru_info->address);
// Populate deterministic FRU scanning fields.
raw_fru.set_present(true);
raw_fru.set_expected_barepath(i2c_fru_info->expected_barepath);
raw_fru.set_expected_part_number(i2c_fru_info->expected_part_number);
raw_fru.set_expected_serial_number(i2c_fru_info->expected_serial_number);
// Populate FRU data.
FruData* fru_data = raw_fru.mutable_data();
for (const auto& fru_data_field : fru_data_fields) {
absl::string_view fru_data_field_stripped =
absl::StripAsciiWhitespace(fru_data_field.second);
(*fru_data->mutable_fields())[fru_data_field.first] =
fru_data_field_stripped;
if (fru_data_field.first == "BOARD_MANUFACTURER") {
fru_data->mutable_fru_info()->set_board_manufacturer(
fru_data_field_stripped);
} else if (fru_data_field.first == "BOARD_PRODUCT_NAME") {
fru_data->mutable_fru_info()->set_board_product_name(
fru_data_field_stripped);
} else if (fru_data_field.first == "BOARD_SERIAL_NUMBER") {
fru_data->mutable_fru_info()->set_board_serial_number(
fru_data_field_stripped);
} else if (fru_data_field.first == "BOARD_PART_NUMBER") {
fru_data->mutable_fru_info()->set_board_part_number(
fru_data_field_stripped);
} else if (fru_data_field.first == "PRODUCT_MANUFACTURER") {
fru_data->mutable_fru_info()->set_product_manufacturer(
fru_data_field_stripped);
} else if (fru_data_field.first == "PRODUCT_PRODUCT_NAME") {
fru_data->mutable_fru_info()->set_product_product_name(
fru_data_field_stripped);
} else if (fru_data_field.first == "PRODUCT_SERIAL_NUMBER") {
fru_data->mutable_fru_info()->set_product_serial_number(
fru_data_field_stripped);
} else if (fru_data_field.first == "PRODUCT_PART_NUMBER") {
fru_data->mutable_fru_info()->set_product_part_number(
fru_data_field_stripped);
} else if (fru_data_field.first == "PRODUCT_VERSION") {
fru_data->mutable_fru_info()->set_product_version(
fru_data_field_stripped);
} else if (fru_data_field.first == "PRODUCT_ASSET_TAG") {
fru_data->mutable_fru_info()->set_product_asset_tag(
fru_data_field_stripped);
} else if (fru_data_field.first == "INTERNAL_MAC_ADDRESS") {
fru_data->mutable_fru_info()->set_mac_address(fru_data_field_stripped);
}
}
return raw_fru;
}
absl::StatusOr<RawFru> FruCollector::CreateRawFruFromScanner(
AdHocScannerType scanner_type,
const std::unique_ptr<I2cFruInfo>& i2c_fru_info) {
switch (scanner_type) {
case SCANNER_TYPE_DEFAULT_IPMI_I2C:
return CreateRawFruFromIpmiFruData(i2c_fru_info);
case SCANNER_TYPE_CAVIUM_HSM_FRAM:
return CreateRawFruFromFramData(i2c_fru_info);
default:
LOG(ERROR) << "Unexpected value for AdHocScannerType: "
<< static_cast<int>(scanner_type);
return absl::InvalidArgumentError(
absl::StrCat("Unexpected scanner type: ", scanner_type));
}
}
absl::StatusOr<RawFru> FruCollector::CreateRawFruFromIpmiFruData(
const std::unique_ptr<I2cFruInfo>& i2c_fru) {
absl::flat_hash_map<std::string, std::string> fru_data_fields;
// Get Fru data fields.
size_t unknown_bus_object_count = 0;
std::optional<std::string> product_name =
getProductName(i2c_fru->data, fru_data_fields, i2c_fru->bus,
i2c_fru->address, unknown_bus_object_count);
if (!product_name.has_value()) {
return absl::InternalError(absl::StrCat(
"Failed to get product name for FRU at bus: ", i2c_fru->bus,
" address: ", i2c_fru->address));
}
return CreateAndPopulatePresentRawFru(i2c_fru, fru_data_fields);
}
std::optional<RawFruTable> FruCollector::DetectsAndReadsCachedFru(
const std::string& cached_fru_table_path) {
std::ifstream file(cached_fru_table_path, std::ios::binary);
RawFruTable fru_table;
if (!fru_table.ParseFromIstream(&file)) {
LOG(WARNING) << "Error parsing cached FRU table file at path: "
<< cached_fru_table_path;
LOG(WARNING) << "Check the file exists and is in the correct format. Treat "
"this as no cached FRU table";
return std::nullopt;
}
LOG(WARNING) << "Successfully read cached FRU table from file at path: "
<< cached_fru_table_path;
return fru_table;
}
void FruCollector::WriteCachedFru(const RawFruTable& fru_table,
const std::string& cached_fru_table_path) {
std::filesystem::path path(cached_fru_table_path);
std::filesystem::path parent_path = path.parent_path();
std::error_code ec;
bool exists_result = std::filesystem::exists(parent_path, ec);
if (ec) {
LOG(ERROR) << "Error checking exists for path '" << parent_path
<< "': " << ec.message();
return;
}
if (!exists_result) {
std::filesystem::create_directories(parent_path, ec);
if (ec) {
LOG(WARNING) << "Error creating directories for cached FRU table file at "
"path: "
<< parent_path;
return;
}
}
std::ofstream file(cached_fru_table_path, std::ios::binary);
if (!fru_table.SerializeToOstream(&file)) {
LOG(WARNING) << "Error serializing cached FRU table to file at path: "
<< cached_fru_table_path;
return;
}
LOG(WARNING) << "Successfully wrote cached FRU table to file at path: "
<< cached_fru_table_path;
}
absl::StatusOr<std::unique_ptr<FruCollector>> FruCollector::Create(
Options options) {
if (options.fru_scanners.empty()) {
return absl::InvalidArgumentError("FruScanners is empty");
}
// Validate AdHocFruConfigs against available scanners
for (const auto& ad_hoc_config : options.ad_hoc_fru_scanning_configs) {
AdHocScannerType scanner_type = ad_hoc_config.scanner_type();
if (!options.fru_scanners.contains(scanner_type)) {
return absl::InvalidArgumentError(
absl::StrCat("Scanner type ", AdHocScannerType_Name(scanner_type),
" required by ad-hoc config '", ad_hoc_config.name(),
"' not found in provided fru_scanners map."));
}
}
// Initialize the resource state manager.
absl::flat_hash_map<RelatedState, std::vector<RelatedStateConfig>>
state_to_configs;
absl::flat_hash_map<std::string, RelatedState> fru_barepath_to_related_state;
for (const auto& host_power_related_state_config :
options.host_power_related_state_configs) {
if (host_power_related_state_config.related_state() ==
milotic_tlbmc::RELATED_STATE_NONE) {
// Skip the resource state manager initialization if the related state is
// NONE.
LOG(WARNING) << "Illegal host power related state configuration: Related "
"state is NONE: "
<< host_power_related_state_config;
continue;
}
if (host_power_related_state_config.resource_identifiers().empty()) {
return absl::InvalidArgumentError(absl::StrCat(
"Resource identifiers is empty for host power related state config: ",
host_power_related_state_config));
}
if (host_power_related_state_config.host_id().empty()) {
return absl::InvalidArgumentError(
absl::StrCat("Host id is empty for host power related state config: ",
host_power_related_state_config));
}
state_to_configs[host_power_related_state_config.related_state()].push_back(
host_power_related_state_config);
// Store the FRU barepath for each related state for quick lookup later.
for (const std::string& resource_identifier :
host_power_related_state_config.resource_identifiers()) {
if (resource_identifier.empty()) {
return absl::InvalidArgumentError(
absl::StrCat("Resource identifier is empty for host power related "
"state config: ",
host_power_related_state_config));
}
fru_barepath_to_related_state[resource_identifier] =
host_power_related_state_config.related_state();
}
}
options.fru_barepath_to_related_state =
std::move(fru_barepath_to_related_state);
absl::flat_hash_map<RelatedState, std::unique_ptr<ResourceStateManager>>
resource_state_managers;
for (const auto& [state, configs] : state_to_configs) {
ECCLESIA_ASSIGN_OR_RETURN(
resource_state_managers[state],
ResourceStateManager::Create(
{.target_state = state, .related_state_configs = configs}));
}
// Initialize the host to smbios inventory parts map.
absl::flat_hash_map<std::string, std::vector<std::string>>
host_to_smbios_inventory_parts;
for (const auto& os_state_related_state_config :
options.os_state_related_state_configs) {
if (os_state_related_state_config.related_state() ==
milotic_tlbmc::RELATED_STATE_NONE) {
// Skip the host to smbios inventory parts initialization if the related
// state is NONE.
LOG(WARNING) << "Illegal host OS state related state configuration: "
"Related state is NONE: "
<< os_state_related_state_config;
continue;
}
if (os_state_related_state_config.resource_identifiers().empty()) {
return absl::InvalidArgumentError(absl::StrCat(
"Resource identifiers is empty for os state related state config: ",
os_state_related_state_config));
}
if (os_state_related_state_config.host_id().empty()) {
return absl::InvalidArgumentError(
absl::StrCat("Host id is empty for os state related state config: ",
os_state_related_state_config));
}
for (const std::string& resource_identifier :
os_state_related_state_config.resource_identifiers()) {
if (resource_identifier.empty()) {
return absl::InvalidArgumentError(
absl::StrCat("Resource identifier is empty for os state related "
"state config: ",
os_state_related_state_config));
}
host_to_smbios_inventory_parts[os_state_related_state_config.host_id()]
.push_back(resource_identifier);
}
}
options.host_to_smbios_inventory_parts =
std::move(host_to_smbios_inventory_parts);
std::optional<RawFruTable> cached_fru_table =
DetectsAndReadsCachedFru(options.cached_fru_table_path);
// If cached FRU table is present, use it, and will not scan any fixed FRUs,
// nor write cached FRU to file.
if (cached_fru_table.has_value()) {
return std::unique_ptr<FruCollector>(new FruCollector(
std::move(options), std::move(*cached_fru_table),
std::make_shared<TaskScheduler>(), std::move(resource_state_managers)));
}
RawFruTable fru_table;
for (const auto& [scanner_type, scanner] : options.fru_scanners) {
if (scanner == nullptr) {
return absl::InvalidArgumentError(
absl::StrCat("Scanner for type ", AdHocScannerType_Name(scanner_type),
" is null."));
}
LOG(INFO) << "Scanning FRUs with scanner type: "
<< AdHocScannerType_Name(scanner_type);
if (scanner_type == SCANNER_TYPE_GPIO || scanner_type == SCANNER_TYPE_USB) {
// Skip I2C scanning for GPIO FRUs.
continue;
}
ECCLESIA_ASSIGN_OR_RETURN(std::vector<std::unique_ptr<I2cFruInfo>> frus,
scanner->ScanAllI2cFrus());
for (const auto& fru_info : frus) {
if (options.enable_deterministic_fru_scanning) {
// Support I2C IPMI scanning for now. HSM FRAM to be supported later.
if (scanner_type == SCANNER_TYPE_DEFAULT_IPMI_I2C) {
// create RawFru for absent FRUs.
if (!fru_info->present) {
RawFru raw_fru;
raw_fru.set_present(fru_info->present);
if (fru_info->parsing_error) {
raw_fru.set_parsing_error(true);
}
raw_fru.set_expected_barepath(fru_info->expected_barepath);
raw_fru.set_expected_part_number(fru_info->expected_part_number);
raw_fru.set_expected_serial_number(
fru_info->expected_serial_number);
raw_fru.set_key(GetFruKey(fru_info->bus, fru_info->address));
raw_fru.mutable_i2c_info()->set_bus(fru_info->bus);
raw_fru.mutable_i2c_info()->set_address(fru_info->address);
LOG(WARNING) << "Updated FRU table for FRU key [FRU Absent]: "
<< raw_fru.key() << " from scanner "
<< AdHocScannerType_Name(scanner_type)
<< ". Barepath: " << raw_fru.expected_barepath();
fru_table.mutable_key_to_raw_fru()->insert(
{raw_fru.key(), raw_fru});
continue;
}
}
}
// Create RawFru from eeprom data for present FRUs. Not necessarily
// means the FRU is valid (i.e., could be illogical).
ECCLESIA_ASSIGN_OR_RETURN(
RawFru fru, CreateRawFruFromScanner(scanner_type, fru_info));
fru_table.mutable_key_to_raw_fru()->insert({fru.key(), fru});
LOG(WARNING) << "Updated FRU table for FRU key [FRU Detected]: "
<< fru.key() << " from scanner "
<< AdHocScannerType_Name(scanner_type) << ". Barepath: "
<< (fru.has_expected_barepath()
? std::string(fru.expected_barepath())
: "N/A");
}
}
WriteCachedFru(fru_table, options.cached_fru_table_path);
return std::unique_ptr<FruCollector>(new FruCollector(
std::move(options), std::move(fru_table),
std::make_shared<TaskScheduler>(), std::move(resource_state_managers)));
}
absl::StatusOr<std::pair<std::string, std::string>>
FruCollector::GetPartAndSerialNumberFromFields(
const absl::flat_hash_map<std::string, std::string>& fru_data_fields) {
auto board_part_number_iter = fru_data_fields.find("BOARD_PART_NUMBER");
auto board_serial_number_iter = fru_data_fields.find("BOARD_SERIAL_NUMBER");
if (board_part_number_iter != fru_data_fields.end() &&
!board_part_number_iter->second.empty() &&
board_serial_number_iter != fru_data_fields.end() &&
!board_serial_number_iter->second.empty()) {
return std::make_pair(board_part_number_iter->second,
board_serial_number_iter->second);
}
LOG(WARNING) << "Failed to get board part number and serial number from FRU "
"data fields: at least one of them is missing. Trying to get "
"product part number and serial number instead.";
auto product_part_number_iter = fru_data_fields.find("PRODUCT_PART_NUMBER");
auto product_serial_number_iter =
fru_data_fields.find("PRODUCT_SERIAL_NUMBER");
if (product_part_number_iter != fru_data_fields.end() &&
!product_part_number_iter->second.empty() &&
product_serial_number_iter != fru_data_fields.end() &&
!product_serial_number_iter->second.empty()) {
return std::make_pair(product_part_number_iter->second,
product_serial_number_iter->second);
}
return absl::InternalError(absl::StrCat(
"Failed to get part number and serial number from FRU data "
"fields: ",
absl::StrJoin(fru_data_fields, ",", absl::PairFormatter("="))));
}
absl::StatusOr<bool> FruCollector::IsFruMissingOk(
const std::string& fru_barepath) {
if (fru_barepath.empty()) {
return absl::InvalidArgumentError("Fru barepath is empty.");
}
if (!fru_barepath_to_related_state_.contains(fru_barepath)) {
// If the FRU is not in the map, it means that the FRU is not configured
// with a related state, so it is not a related resource. Therefore, it is
// not ok for it to be missing.
DLOG(WARNING) << "Fru barepath: " << fru_barepath
<< " is not found in fru_barepath_to_related_state_ map. "
<< "Therefore, it is not ok for it to be missing.";
return false;
}
RelatedState related_state = fru_barepath_to_related_state_[fru_barepath];
if (!resource_state_managers_.contains(related_state)) {
// If the resource state manager is not found, it means that the resource
// state manager is not initialized or the FRU is in
// fru_barepath_to_related_state_ map but not in the resource state manager.
// Any of those cases should not happen in normal operation and should be
// considered an internal error.
return absl::InternalError(
absl::StrCat("Resource state manager is not found for related state: ",
RelatedState_Name(related_state)));
}
return resource_state_managers_[related_state]
->IsStateRelatedResourceMissingOk(fru_barepath);
}
absl::StatusOr<FruTable> FruCollector::ScanAllFrusDeterministically() {
DLOG(INFO) << "Entering ScanAllFrusDeterministically";
if (!options_.enable_deterministic_fru_scanning) {
return absl::FailedPreconditionError(
"Deterministic FRU scanning is disabled. Cannot scan FRUs "
"deterministically.");
}
// In the long term, we are supposed to support all types of FRUs, including
// both I2C IPMI and FRAM based protocols with a all-in-one deterministic
// scanner. In the short term, we will only support I2C IPMI with
// SCANNER_TYPE_DEFAULT_IPMI_I2C.
if (!options_.fru_scanners.contains(SCANNER_TYPE_DEFAULT_IPMI_I2C)) {
return absl::InvalidArgumentError(
"SCANNER_TYPE_DEFAULT_IPMI_I2C is required for deterministic FRU "
"scanning.");
}
// Lock the scan_in_progress_mutex_ to ensure that only one FRU scan is
// in progress at a time.
absl::MutexLock lock(scan_in_progress_mutex_);
FruTable fru_table;
DLOG(INFO)
<< "Deterministic BMC: Scanning FRUs with deterministic FRU scanners. "
"Number of deterministic FRU scanners: "
<< options_.deterministic_fru_scanners.size();
// Scan all FRUs with the deterministic FRU scanners.
for (const auto& [probe_path, scanner] :
options_.deterministic_fru_scanners) {
DLOG(INFO) << "Deterministic BMC: Scanning FRUs with deterministic FRU "
"scanner for probe path: "
<< uhmm::BmcMonitoredComponentsResponse::MonitoredComponent::
ProbePath_Name(probe_path);
switch (probe_path) {
case uhmm::BmcMonitoredComponentsResponse::MonitoredComponent::
PROBE_PATH_NCSI: {
ECCLESIA_ASSIGN_OR_RETURN(
std::vector<std::unique_ptr<CableFruInfo>> frus,
scanner->ScanAllCableFrus());
for (const auto& fru_info : frus) {
Fru fru;
Attributes* attributes = fru.mutable_attributes();
attributes->mutable_expected_hardware_info()->set_expected_barepath(
fru_info->expected_barepath);
if (fru_info->present) {
attributes->set_presence_status(
PresenceStatus::PRESENCE_STATUS_PRESENT);
} else {
attributes->set_presence_status(
PresenceStatus::PRESENCE_STATUS_NOT_FOUND);
}
fru_table.mutable_key_to_fru()->insert(
{fru_info->expected_barepath, fru});
}
break;
}
case uhmm::BmcMonitoredComponentsResponse::MonitoredComponent::
PROBE_PATH_ADC: {
ECCLESIA_ASSIGN_OR_RETURN(std::vector<std::unique_ptr<AdcFruInfo>> frus,
scanner->ScanAllAdcFrus());
for (const auto& fru_info : frus) {
Fru fru;
Attributes* attributes = fru.mutable_attributes();
attributes->mutable_expected_hardware_info()->set_expected_barepath(
fru_info->expected_barepath);
attributes->set_presence_status(fru_info->presence_status);
fru_table.mutable_key_to_fru()->insert(
{fru_info->expected_barepath, fru});
}
break;
}
default:
LOG(WARNING) << "Unsupported probe path: " << probe_path;
}
}
// Scan all I2C Sensor Assembly FRUs with the default IPMI I2C scanner.
auto& fru_scanner_ipmi_i2c =
options_.fru_scanners[SCANNER_TYPE_DEFAULT_IPMI_I2C];
ECCLESIA_ASSIGN_OR_RETURN(
std::vector<std::unique_ptr<I2cSensorAssemblyFruInfo>>
sensor_assembly_frus,
fru_scanner_ipmi_i2c->ScanAllI2cSensorAssemblyFru());
DLOG(INFO) << "Deterministic BMC: Scanning FRUs with deterministic FRU "
"scanner for sensor assembly FRU: "
<< sensor_assembly_frus.size();
for (const auto& fru_info : sensor_assembly_frus) {
DLOG(INFO) << "Deterministic BMC: Scanning FRUs with deterministic FRU "
"scanner for sensor assembly FRU: "
<< fru_info->expected_barepath;
Fru fru;
Attributes* attributes = fru.mutable_attributes();
attributes->mutable_expected_hardware_info()->set_expected_barepath(
fru_info->expected_barepath);
if (fru_info->present) {
attributes->set_presence_status(PresenceStatus::PRESENCE_STATUS_PRESENT);
} else {
attributes->set_presence_status(
PresenceStatus::PRESENCE_STATUS_NOT_FOUND);
}
fru_table.mutable_key_to_fru()->insert({fru_info->expected_barepath, fru});
}
// TODO: b/509637698 - Migrate after the new model is ready.
if (options_.fru_scanners.contains(SCANNER_TYPE_GPIO)) {
auto& fru_scanner_gpio = options_.fru_scanners[SCANNER_TYPE_GPIO];
ECCLESIA_ASSIGN_OR_RETURN(
std::vector<std::unique_ptr<GpioFruInfo>> gpio_frus,
fru_scanner_gpio->ScanAllGpioFrus());
for (const auto& fru_info : gpio_frus) {
Fru fru;
Attributes* attributes = fru.mutable_attributes();
attributes->mutable_expected_hardware_info()->set_expected_barepath(
fru_info->expected_barepath);
attributes->set_presence_status(fru_info->presence_status);
fru_table.mutable_key_to_fru()->insert(
{fru_info->expected_barepath, fru});
}
}
// TODO: b/526494991 - Migrate after the new model is ready.
if (options_.fru_scanners.contains(SCANNER_TYPE_USB)) {
auto& fru_scanner_usb = options_.fru_scanners[SCANNER_TYPE_USB];
ECCLESIA_ASSIGN_OR_RETURN(std::vector<std::unique_ptr<UsbFruInfo>> usb_frus,
fru_scanner_usb->ScanAllUsbFrus());
for (const auto& fru_info : usb_frus) {
Fru fru;
Attributes* attributes = fru.mutable_attributes();
attributes->mutable_expected_hardware_info()->set_expected_barepath(
fru_info->expected_barepath);
attributes->set_presence_status(fru_info->presence_status);
fru_table.mutable_key_to_fru()->insert(
{fru_info->expected_barepath, fru});
}
}
// Scan all I2C eeprom FRUs with the default IPMI I2C scanner.
ECCLESIA_ASSIGN_OR_RETURN(std::vector<std::unique_ptr<I2cFruInfo>> frus,
fru_scanner_ipmi_i2c->ScanAllI2cFrus());
// construct RawFruTable and FruTable by iterating through all FRUs.
RawFruTable raw_fru_table;
for (const auto& fru_info : frus) {
// Construct Fru from I2cFruInfo (Deterministic BMC)
Fru fru;
Attributes* attributes = fru.mutable_attributes();
attributes->set_key(GetFruKey(fru_info->bus, fru_info->address));
attributes->mutable_expected_hardware_info()->set_expected_barepath(
fru_info->expected_barepath);
attributes->mutable_expected_hardware_info()->set_expected_part_number(
fru_info->expected_part_number);
attributes->mutable_expected_hardware_info()->set_expected_serial_number(
fru_info->expected_serial_number);
fru.mutable_i2c_info()->set_bus(fru_info->bus);
fru.mutable_i2c_info()->set_address(fru_info->address);
if (fru_info->parsing_error) {
LOG(WARNING) << "Parsing error detected for FRU: "
<< fru_info->expected_barepath
<< " at bus: " << fru_info->bus
<< " address: " << fru_info->address
<< ". Setting presence status to NOT_SCANNED.";
attributes->set_presence_status(
PresenceStatus::PRESENCE_STATUS_NOT_SCANNED);
fru_table.mutable_key_to_fru()->insert({attributes->key(), fru});
continue;
}
if (!fru_info->present) {
// Nothing to be done for RawFru.
absl::StatusOr<bool> is_missing_ok_status =
IsFruMissingOk(fru_info->expected_barepath);
bool is_missing_ok = false;
if (is_missing_ok_status.ok()) {
is_missing_ok = *is_missing_ok_status;
} else {
LOG(WARNING) << "Failed to check if FRU is missing ok: "
<< is_missing_ok_status.status();
}
if (is_missing_ok) {
attributes->set_presence_status(
PresenceStatus::PRESENCE_STATUS_NOT_SCANNED);
} else {
// Mark it FRU as not found.
attributes->set_presence_status(
PresenceStatus::PRESENCE_STATUS_NOT_FOUND);
}
fru_table.mutable_key_to_fru()->insert({attributes->key(), fru});
continue;
}
// Parse FRU data to get actual part number and serial number for present
// FRUs.
absl::flat_hash_map<std::string, std::string> fru_data_fields;
resCodes res = formatIPMIFRU(fru_info->data, fru_data_fields);
if (res == resCodes::resErr) {
return absl::InternalError(
absl::StrCat("Failed to parse FRU for device at bus: ", fru_info->bus,
" address: ", fru_info->address,
" with error: ", resCodesToString(res)));
}
if (res == resCodes::resWarn) {
LOG(WARNING) << "Warnings while parsing FRU for device at bus "
<< fru_info->bus << " address " << fru_info->address
<< ". You may want to check log for details.";
}
// Construct RawFru for present FRUs only. Present FRU is not necessarily
// valid. So RawFruTable will have both expected and illogical present FRUs.
// This is to align with old non-deterministic logic.
RawFru raw_fru = CreateAndPopulatePresentRawFru(fru_info, fru_data_fields);
raw_fru_table.mutable_key_to_raw_fru()->insert({raw_fru.key(), raw_fru});
if (options_.strict_part_info_check) {
// Continue to complete the construction of Fru
auto part_and_serial_number =
GetPartAndSerialNumberFromFields(fru_data_fields);
if (!part_and_serial_number.ok()) {
return absl::InternalError(absl::StrCat(
"Failed to get part and serial number for device at bus: ",
fru_info->bus, " address: ", fru_info->address,
" with error: ", part_and_serial_number.status()));
}
std::string actual_part_number = part_and_serial_number->first;
std::string actual_serial_number = part_and_serial_number->second;
if (actual_part_number != fru_info->expected_part_number ||
actual_serial_number != fru_info->expected_serial_number) {
attributes->set_presence_status(
PresenceStatus::PRESENCE_STATUS_PRESENT_ILLOGICAL);
} else {
attributes->set_presence_status(
PresenceStatus::PRESENCE_STATUS_PRESENT);
}
} else {
// Mark it FRU as present regardless of the part info.
attributes->set_presence_status(PresenceStatus::PRESENCE_STATUS_PRESENT);
}
fru_table.mutable_key_to_fru()->insert({attributes->key(), fru});
}
std::vector<RawFru> new_raw_frus;
for (const auto& [key, _] : raw_fru_table.key_to_raw_fru()) {
if (!ContainsFru(key)) {
new_raw_frus.push_back(raw_fru_table.key_to_raw_fru().at(key));
}
}
SetFruTable(std::move(raw_fru_table));
// TODO(b/484338308): Update FruTable and TopologyConfig in EntityConfig for
// dBMC redfish conformity.
// GetEntityConfig()->UpdateFruAndTopology(GetCopyOfCurrentScannedFrus(),
// new_raw_frus);
return fru_table;
}
absl::Status FruCollector::ScanDeterministicallyAndUpdateFruTable() {
ECCLESIA_ASSIGN_OR_RETURN(FruTable fru_table, ScanAllFrusDeterministically());
SetDeterministicFruTable(std::move(fru_table));
return absl::OkStatus();
}
void FruCollector::SetUpAdHocFruScanning() {
if (options_.fru_scanners.empty()) {
LOG(WARNING) << "FruScanners is empty";
return;
}
LOG(INFO) << "Number of available fru_scanners: "
<< options_.fru_scanners.size();
for (const auto& ad_hoc_fru_config : options_.ad_hoc_fru_scanning_configs) {
std::string fru_key =
GetFruKey(ad_hoc_fru_config.hal_common_config().bus(),
ad_hoc_fru_config.hal_common_config().address());
// If the frutable already has the ad-hoc fru, skip it.
if (ContainsFru(fru_key)) {
LOG(WARNING) << "FRU key " << fru_key
<< " already exists, skipping ad-hoc setup for "
<< ad_hoc_fru_config.name();
continue;
}
// If Ad-hoc FRU config has no periodic scan config, skip it.
if (ad_hoc_fru_config.periodic_scan_config_size() == 0) {
LOG(WARNING) << "No periodic_scan_config for " << ad_hoc_fru_config.name()
<< ", skipping.";
continue;
}
LOG(INFO) << "Scheduling AdHocFruScan for " << ad_hoc_fru_config.name();
ScheduleAdHocFruScan(ad_hoc_fru_config);
}
}
void FruCollector::SetUpPeriodicDeterministicFruScanning() {
if (!options_.enable_deterministic_fru_scanning) {
LOG(WARNING) << "Deterministic FRU scanning is disabled, skipping periodic "
"deterministic FRU scan.";
return;
}
if (!options_.enable_deterministic_periodic_fru_scanning) {
LOG(WARNING) << "Deterministic BMC is enabled, but Periodic deterministic "
"FRU scanning is not enabled, "
"skipping.";
return;
}
if (options_.periodic_deterministic_fru_scan_interval_ms <= 0) {
LOG(WARNING) << "Deterministic BMC periodic scan interval is not positive: "
<< options_.periodic_deterministic_fru_scan_interval_ms
<< " ms";
return;
}
LOG(INFO) << "Setting up periodic deterministic FRU scan with interval "
<< options_.periodic_deterministic_fru_scan_interval_ms << " ms";
absl::MutexLock lock(deterministic_scan_task_id_mutex_);
if (deterministic_scan_task_id_.has_value()) {
LOG(WARNING) << "Periodic deterministic FRU scan already scheduled.";
return;
}
deterministic_scan_task_id_ = task_scheduler_->RunAndScheduleAsync(
[this](absl::AnyInvocable<void()> on_done) {
if (absl::Status status =
this->ScanDeterministicallyAndUpdateFruTable();
!status.ok()) {
LOG(WARNING) << "Periodic Deterministic FRU scan failed: " << status;
}
on_done();
},
absl::Milliseconds(options_.periodic_deterministic_fru_scan_interval_ms));
}
void FruCollector::HandleHostOsInactiveToStandby(const std::string& host_id,
bool setup_run) {
if (!options_.smbios_inventory) {
LOG(ERROR) << "HandleHostOsInactiveToStandby: SMBIOS inventory options not "
"available. Exiting.";
return;
}
absl::StatusOr<std::vector<std::string>> associated_parts =
GetAssociatedSmbiosInventoryParts(host_id);
if (!associated_parts.ok()) {
LOG(WARNING)
<< "HandleHostOsInactiveToStandby: No configured SMBIOS inventory "
"parts for host '"
<< host_id
<< "'. Ignoring OS transition from inactive to standby (NO-OP).";
return;
}
HostPollingState* state = GetHostPollingState(host_id);
if (state == nullptr) {
LOG(ERROR) << "HandleHostOsInactiveToStandby: HostPollingState not found "
"for host '"
<< host_id << "'. Exiting OS transition.";
return;
}
absl::MutexLock lock(state->host_polling_mutex);
if (state->is_polling) {
task_scheduler_->Cancel(state->polling_task_id);
}
state->is_polling = true;
state->polling_start_time = options_.clock->Now();
state->polling_task_id = task_scheduler_->ScheduleOneShotAsync(
[this, host_id, parts = *associated_parts,
setup_run](absl::AnyInvocable<void()> on_done) {
this->PollSmbiosStatus(host_id, parts, setup_run);
on_done();
},
absl::Milliseconds(1));
}
void FruCollector::HandleHostOsStandbyToInactive(const std::string& host_id,
bool setup_run) {
if (!options_.smbios_inventory) {
LOG(ERROR) << "HandleHostOsStandbyToInactive: SMBIOS inventory options not "
"available. Exiting.";
return;
}
absl::StatusOr<std::vector<std::string>> associated_parts =
GetAssociatedSmbiosInventoryParts(host_id);
if (!associated_parts.ok()) {
LOG(WARNING)
<< "HandleHostOsStandbyToInactive: No configured SMBIOS inventory "
"parts for host '"
<< host_id
<< "'. Ignoring OS transition from standby to inactive (NO-OP).";
return;
}
HostPollingState* state = GetHostPollingState(host_id);
if (state == nullptr) {
LOG(ERROR) << "HandleHostOsStandbyToInactive: HostPollingState not found "
"for host '"
<< host_id << "'. Exiting OS transition.";
return;
}
{
absl::MutexLock lock(state->host_polling_mutex);
state->is_polling = false;
if (state->polling_task_id != -1) {
task_scheduler_->Cancel(state->polling_task_id);
state->polling_task_id = -1;
}
// Mark the SMBIOS status as consumed. This ensures that any pending "NEW"
// status is cleared when polling is cancelled. This is important to avoid
// the possibility of the next boot thinking there is new SMBIOS data when
// there isn't.
WriteSmbiosStatusConsumed(host_id);
}
{
absl::MutexLock table_lock(smbios_fru_table_mutex_);
SetSmbiosInventoryStatus(smbios_fru_table_, *associated_parts,
PresenceStatus::PRESENCE_STATUS_NOT_SCANNED);
}
}
void FruCollector::SetSmbiosInventoryStatus(
FruTable& table, const std::vector<std::string>& associated_parts,
PresenceStatus target_status) {
auto& key_to_fru = *table.mutable_key_to_fru();
for (const std::string& barepath : associated_parts) {
auto it = key_to_fru.find(barepath);
if (it != key_to_fru.end()) {
it->second.mutable_attributes()->set_presence_status(target_status);
}
}
}
void FruCollector::PollSmbiosStatus(
const std::string& host_id,
const std::vector<std::string>& associated_parts, bool setup_run) {
HostPollingState* state = GetHostPollingState(host_id);
if (state == nullptr) {
return;
}
absl::MutexLock lock(state->host_polling_mutex);
if (!state->is_polling) {
return;
}
if (options_.clock->Now() - state->polling_start_time > absl::Minutes(2)) {
LOG(WARNING) << "PollSmbiosStatus: SMBIOS 'NEW' status timeout after 2 "
"minutes for host: '"
<< host_id << "'";
state->is_polling = false;
state->polling_task_id = -1;
// If 2 minutes lapsed after transitioning to standby but we don't see a new
// blob come, we assume the current blob is the up-to-date one. This is
// inefficient as it will fail to detect issues where the SMBIOS blob indeed
// didn't arrive.
if (setup_run) {
ExecuteSmbiosUpdate(host_id, associated_parts,
/*update_status_file=*/false);
} else {
absl::MutexLock table_lock(smbios_fru_table_mutex_);
SetSmbiosInventoryStatus(smbios_fru_table_, associated_parts,
PresenceStatus::PRESENCE_STATUS_NOT_FOUND);
}
return;
}
bool status_is_new = (ReadSmbiosStatus(host_id) == "NEW");
if (status_is_new) {
ExecuteSmbiosUpdate(host_id, associated_parts,
/*update_status_file=*/true);
state->is_polling = false;
state->polling_task_id = -1;
} else {
state->polling_task_id = task_scheduler_->ScheduleOneShotAsync(
[this, host_id, associated_parts,
setup_run](absl::AnyInvocable<void()> on_done) {
this->PollSmbiosStatus(host_id, associated_parts, setup_run);
on_done();
},
absl::Seconds(5));
}
}
// Returns the isolated SMBIOS binary file path for a specific host ID.
std::string FruCollector::GetSmbiosFilePathForHost(
const std::string& host_id) const {
std::string suffix = ExtractHostNumber(host_id);
if (!suffix.empty()) {
// Multi-host systems use the host-indexed path.
return absl::StrCat(options_.smbios_multihost_host_file_path_prefix,
suffix);
}
// Single-host systems use the default path.
return options_.smbios_single_host_file_path;
}
void FruCollector::ExecuteSmbiosUpdate(
const std::string& host_id,
const std::vector<std::string>& associated_parts, bool update_status_file) {
absl::StatusOr<platforms::gbmc::hal::SmbiosData> smbios_data;
if (host_id == "system") {
smbios_data = options_.smbios_inventory->ReadAndParse();
} else {
// multi-host systems use the host specific path.
std::string target_path = GetSmbiosFilePathForHost(host_id);
smbios_data = options_.smbios_inventory->ReadAndParse(target_path);
}
if (!smbios_data.ok()) {
LOG(ERROR) << "Failed to read and parse SMBIOS data for host '" << host_id
<< "': " << smbios_data.status();
// Only mark the components as NOT_FOUND if the SMBIOS blob is explicitly
// missing. If there is a parsing error (e.g., DataLoss), we leave the
// status as NOT_SCANNED to avoid falsely reporting absence when the issue
// might be a transient firmware bug or corrupt data.
if (absl::IsNotFound(smbios_data.status())) {
absl::MutexLock table_lock(&smbios_fru_table_mutex_);
SetSmbiosInventoryStatus(smbios_fru_table_, associated_parts,
PresenceStatus::PRESENCE_STATUS_NOT_FOUND);
}
return;
}
FruTable local_table = GetCopyOfSmbiosFruTable();
SetSmbiosInventoryStatus(local_table, associated_parts,
PresenceStatus::PRESENCE_STATUS_NOT_FOUND);
ProcessSmbiosCpu(associated_parts, smbios_data->cpus, local_table);
ProcessSmbiosDimm(associated_parts, smbios_data->dimms, local_table);
{
absl::MutexLock table_lock(smbios_fru_table_mutex_);
for (const std::string& barepath : associated_parts) {
auto it = local_table.key_to_fru().find(barepath);
if (it != local_table.key_to_fru().end()) {
(*smbios_fru_table_.mutable_key_to_fru())[barepath] = it->second;
}
}
}
if (update_status_file) {
WriteSmbiosStatusConsumed(host_id);
}
}
void FruCollector::ProcessSmbiosCpu(
const std::vector<std::string>& associated_parts,
const std::vector<platforms::gbmc::hal::SmbiosCpuInfo>& cpus,
FruTable& fru_table) {
for (const auto& info : cpus) {
if (!info.socket_designation.has_value()) {
continue;
}
// Check bit 6 (0x40) of the status field to determine CPU presence.
// Spec: google3/gbmc-hal/docs/specs/dsp0134-smbios-v3-9-0-spec.md
// Note for future reference: Bits 0-2 (0x07) indicate the CPU Status
// (1 = Enabled, other values = Disabled/Unknown/etc). Currently, we
// only need to collect presence, so we don't evaluate the functional state.
if (!info.status.has_value() || (*info.status & 0x40) == 0) {
continue;
}
std::string designation = *info.socket_designation;
// The key is the barepath of the FRU, e.g., `/phys/CPU0` or
// `/phys/PE0/CPU1`. The designation/locator from SMBIOS might be just
// `CPU0` or `CPU1`. We use `EndsWith` to match the FRU.
// Multi-host & Single-host: Only search and match keys associated with this
// host
auto& key_to_fru = *fru_table.mutable_key_to_fru();
for (const std::string& key : associated_parts) {
auto it = key_to_fru.find(key);
if (it != key_to_fru.end() && absl::EndsWith(key, designation)) {
Fru& fru = it->second;
fru.mutable_attributes()->set_presence_status(
PresenceStatus::PRESENCE_STATUS_PRESENT);
auto* cpu = fru.mutable_data()->mutable_processor_info();
if (info.cpuid_x86_stepping) {
cpu->set_stepping(std::to_string(*info.cpuid_x86_stepping));
}
break;
}
}
}
}
void FruCollector::ProcessSmbiosDimm(
const std::vector<std::string>& associated_parts,
const std::vector<platforms::gbmc::hal::SmbiosDimmInfo>& dimms,
FruTable& fru_table) {
for (const auto& info : dimms) {
if (!info.device_locator.has_value()) {
continue;
}
// An absent, unpopulated, or empty DIMM will yield size_bytes = 0.
// Spec: google3/gbmc-hal/docs/specs/dsp0134-smbios-v3-9-0-spec.md
if (!info.size_bytes.has_value() || *info.size_bytes <= 0) {
continue;
}
// Additionally, check for "NO DIMM" in manufacturer or part number.
if (info.manufacturer &&
absl::StrContains(absl::AsciiStrToUpper(*info.manufacturer),
"NO DIMM")) {
continue;
}
if (info.part_number &&
absl::StrContains(absl::AsciiStrToUpper(*info.part_number),
"NO DIMM")) {
continue;
}
std::string locator = *info.device_locator;
// The key is the barepath of the FRU, e.g., `/phys/DIMM0` or
// `/phys/PE0/DIMM1`. The designation/locator from SMBIOS might be just
// `DIMM0` or `DIMM1`. We use `EndsWith` to match the FRU.
// Multi-host & Single-host: Only search and match keys associated with this
// host
auto& key_to_fru = *fru_table.mutable_key_to_fru();
for (const std::string& key : associated_parts) {
auto it = key_to_fru.find(key);
if (it != key_to_fru.end() && absl::EndsWith(key, locator)) {
Fru& fru = it->second;
fru.mutable_attributes()->set_presence_status(
PresenceStatus::PRESENCE_STATUS_PRESENT);
auto* mem = fru.mutable_data()->mutable_memory_info();
if (info.manufacturer) {
mem->set_manufacturer(*info.manufacturer);
}
if (info.serial_number) {
mem->set_serial_number(*info.serial_number);
}
if (info.part_number) {
mem->set_part_number(*info.part_number);
}
if (info.size_bytes) {
mem->set_capacity_mib(
static_cast<int64_t>(*info.size_bytes / (1024 * 1024)));
}
break;
}
}
}
}
std::string FruCollector::GetSmbiosStatusFilePathForHost(
const std::string& host_id) const {
std::string suffix = ExtractHostNumber(host_id);
if (!suffix.empty()) {
return absl::StrCat(options_.smbios_multihost_host_status_file_path_prefix,
suffix);
}
return options_.smbios_single_host_status_file_path;
}
std::string FruCollector::ReadSmbiosStatus(const std::string& host_id) const {
std::string final_path = GetSmbiosStatusFilePathForHost(host_id);
std::ifstream status_file(final_path);
if (status_file) {
std::string status;
status_file >> status;
return status;
}
return "";
}
void FruCollector::WriteSmbiosStatusConsumed(const std::string& host_id) const {
// Update the status file to "Consumed" to indicate that the SMBIOS data has
// been consumed. We write to a temporary file and rename it to ensure
// atomicity.
std::string final_path = GetSmbiosStatusFilePathForHost(host_id);
std::string tmp_status_file_path = final_path + ".bmcweb.tmp";
std::ofstream status_file(tmp_status_file_path, std::ios::trunc);
if (status_file) {
status_file << "Consumed";
status_file.close();
std::error_code ec;
std::filesystem::rename(tmp_status_file_path, final_path, ec);
if (ec) {
LOG(ERROR) << "Failed to atomically rename status file: " << ec.message();
}
}
}
FruCollector::FruCollector(
Options options, RawFruTable fru_table,
std::shared_ptr<TaskScheduler> task_scheduler,
absl::flat_hash_map<RelatedState, std::unique_ptr<ResourceStateManager>>
resource_state_managers)
: task_scheduler_(std::move(task_scheduler)),
options_(std::move(options)),
fru_table_(std::move(fru_table)),
fru_barepath_to_related_state_(
std::move(options_.fru_barepath_to_related_state)),
host_to_smbios_inventory_parts_(
std::move(options_.host_to_smbios_inventory_parts)),
resource_state_managers_(std::move(resource_state_managers)) {
InitializeSmbiosPollingStates();
InitializeExpectedSmbiosComponents();
}
void FruCollector::InitializeSmbiosPollingStates() {
for (const auto& [host_id, _] : host_to_smbios_inventory_parts_) {
host_polling_states_[host_id] = std::make_unique<HostPollingState>();
}
}
void FruCollector::InitializeExpectedSmbiosComponents() {
absl::MutexLock lock(smbios_fru_table_mutex_);
for (const auto& component : options_.expected_smbios_components) {
Fru fru;
fru.mutable_attributes()
->mutable_expected_hardware_info()
->set_expected_barepath(component.barepath());
fru.mutable_attributes()->set_presence_status(
PresenceStatus::PRESENCE_STATUS_NOT_SCANNED);
smbios_fru_table_.mutable_key_to_fru()->insert({component.barepath(), fru});
}
}
FruCollector::HostPollingState* FruCollector::GetHostPollingState(
const std::string& host_id) const {
auto it = host_polling_states_.find(host_id);
if (it != host_polling_states_.end()) {
return it->second.get();
}
return nullptr;
}
std::unique_ptr<FruCollector> EmptyFruCollector::Create() {
return std::make_unique<EmptyFruCollector>();
}
absl::StatusOr<FruTable> EmptyFruCollector::ScanAllFrusDeterministically() {
return absl::UnimplementedError(
"EmptyFruCollector::ScanAllFrusDeterministically is called. This is "
"unimplemented.");
}
absl::Status EmptyFruCollector::ScanDeterministicallyAndUpdateFruTable() {
return absl::UnimplementedError(
"EmptyFruCollector::ScanDeterministicallyAndUpdateFruTable is called. "
"This is unimplemented.");
}
RawFruTable EmptyFruCollector::GetCopyOfCurrentScannedFrus() const
ABSL_LOCKS_EXCLUDED(fru_table_mutex_) {
LOG(WARNING) << "EmptyFruCollector::GetCopyOfCurrentScannedFrus is called. "
"returning empty RawFruTable.";
return RawFruTable();
}
FruTable EmptyFruCollector::GetCopyOfSmbiosFruTable() const
ABSL_LOCKS_EXCLUDED(smbios_fru_table_mutex_) {
LOG(WARNING) << "EmptyFruCollector::GetCopyOfSmbiosFruTable is called. "
"returning empty FruTable.";
return FruTable();
}
FruTable EmptyFruCollector::GetCopyOfDeterministicFruTable() const
ABSL_LOCKS_EXCLUDED(deterministic_fru_table_mutex_) {
LOG(WARNING)
<< "EmptyFruCollector::GetCopyOfDeterministicFruTable is called. "
"returning empty FruTable.";
return FruTable();
}
nlohmann::json EmptyFruCollector::ToJson() const {
return nlohmann::json::parse("{\"Warning\": \"EmptyFruCollector used.\"}");
}
void EmptyFruCollector::SetEntityConfig(
const std::shared_ptr<EntityConfig>& entity_config)
ABSL_LOCKS_EXCLUDED(entity_config_mutex_) {
LOG(WARNING) << "EmptyFruCollector::SetEntityConfig is called. This is a "
"no-op.";
}
std::shared_ptr<EntityConfig> EmptyFruCollector::GetEntityConfig() const {
return nullptr;
}
absl::Status EmptyFruCollector::ConfigureDeterministicCollection(
const Config& config) {
return absl::UnimplementedError(
"EmptyFruCollector::ConfigureDeterministicCollection is called. This is "
"not "
"implemented.");
}
absl::Status FruCollector::ConfigureDeterministicCollection(
const Config& config) {
if (config.sampling_interval_ms < 0) {
return absl::InvalidArgumentError("Invalid sampling interval");
}
if (!options_.enable_deterministic_fru_scanning) {
LOG(WARNING) << "Deterministic FRU scanning is disabled, "
"ConfigureCollection has no effect.";
return absl::OkStatus();
}
// Eagerly trigger a deterministic scan so HFT clients receive an initial
// frame with fresh data right at subscription time without waiting 30
// seconds.
if (absl::Status status = ScanDeterministicallyAndUpdateFruTable();
!status.ok()) {
LOG(WARNING) << "Initial deterministic FRU scan failed: " << status;
}
// on change is what we want, so this method will not be called eventually.
LOG(WARNING) << "Deterministic FRU scanning is enabled, but dynamic "
"configuration of the periodic scan interval is not "
"supported. The provided sampling interval of "
<< config.sampling_interval_ms << " ms will be ignored.";
return absl::OkStatus();
}
const absl::flat_hash_map<RelatedState, std::unique_ptr<ResourceStateManager>>&
EmptyFruCollector::GetResourceStateManagers() const {
return resource_state_managers_;
}
nlohmann::json EmptyFruCollector::GetSchedulerStats() const {
return nlohmann::json::parse("{\"Warning\": \"EmptyFruCollector used.\"}");
}
std::size_t EmptyFruCollector::GetAllUserTaskCount() const { return 0; }
} // namespace milotic_tlbmc