| #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 |