| #include "tlbmc/collector/fru_collector.h" |
| |
| #include <cstddef> |
| #include <filesystem> |
| #include <fstream> |
| #include <ios> |
| #include <memory> |
| #include <optional> |
| #include <string> |
| #include <system_error> |
| #include <utility> |
| #include <vector> |
| |
| #include "absl/base/thread_annotations.h" |
| #include "absl/container/flat_hash_map.h" |
| #include "absl/functional/any_invocable.h" |
| #include "absl/log/log.h" |
| #include "absl/memory/memory.h" |
| #include "absl/status/status.h" |
| #include "absl/status/statusor.h" |
| #include "absl/strings/str_cat.h" |
| #include "absl/strings/string_view.h" |
| #include "absl/synchronization/mutex.h" |
| #include "g3/macros.h" |
| #include "nlohmann/json_fwd.hpp" |
| #include "json_utils.h" |
| #include "ad_hoc_fru_config.pb.h" |
| #include "tlbmc/configs/entity_config.h" |
| #include "i2c_common_config.pb.h" |
| #include "tlbmc/hal/fru_scanner.h" |
| #include "fru.pb.h" |
| #include "resource.pb.h" |
| #include "tlbmc/scheduler/scheduler.h" |
| #include "tlbmc/time/time.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; |
| |
| // 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, FruScanner* fru_scanner, |
| 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, fru_scanner, |
| ad_hoc_fru_scan_context_id](absl::AnyInvocable<void()> on_done) { |
| AttemptAdHocFruScan(ad_hoc_fru_config, fru_scanner, |
| 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, |
| FruScanner* fru_scanner, |
| 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."; |
| RescheduleAdHocFruScan(ad_hoc_fru_config, fru_scanner, |
| 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.i2c_common_config().bus()), |
| static_cast<int>(ad_hoc_fru_config.i2c_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()) { |
| 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, fru_scanner, |
| ad_hoc_fru_scan_context_id, scan_context); |
| return; |
| } |
| |
| // If scan succeeded, update FRU table |
| absl::StatusOr<RawFru> fru = CreateRawFruFromI2cFruInfo(*ad_hoc_fru_info); |
| if (!fru.ok()) { |
| LOG(WARNING) << "Failed to create raw FRU from I2C FRU info: " |
| << fru.status() << " for FRU: " << ad_hoc_fru_config.name(); |
| LOG(WARNING) << "Will attempt rescan"; |
| RescheduleAdHocFruScan(ad_hoc_fru_config, fru_scanner, |
| ad_hoc_fru_scan_context_id, scan_context); |
| return; |
| } |
| AddFru(*fru); |
| scan_context.found = true; |
| UpdateAdHocFruScanContext(ad_hoc_fru_scan_context_id, scan_context); |
| entity_config->UpdateFruAndTopology(GetCopyOfCurrentScannedFrus()); |
| |
| LOG(WARNING) << "Updated FRU table for FRU key: " << fru->key(); |
| } |
| |
| void FruCollector::ScheduleAdHocFruScan(const AdHocFruConfig& ad_hoc_fru_config, |
| FruScanner* fru_scanner) { |
| // 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, fru_scanner, |
| ad_hoc_fru_scan_context_id](absl::AnyInvocable<void()> on_done) { |
| LOG(INFO) << "Attempting ad-hoc FRU scan for " |
| << ad_hoc_fru_config.name(); |
| AttemptAdHocFruScan(ad_hoc_fru_config, fru_scanner, |
| ad_hoc_fru_scan_context_id); |
| on_done(); |
| }, |
| DecodeGoogleApiProto( |
| ad_hoc_fru_config.periodic_scan_config(0).scan_interval())); |
| } |
| |
| absl::StatusOr<RawFru> FruCollector::CreateRawFruFromI2cFruInfo( |
| 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)); |
| } |
| |
| // Create FRU resource. |
| RawFru fru; |
| |
| // Assuming we can have only one FRU per I2C bus/address pair |
| fru.set_key(absl::StrCat(i2c_fru->bus, ":", i2c_fru->address)); |
| |
| // Populate I2C info. |
| fru.mutable_i2c_info()->set_bus(i2c_fru->bus); |
| fru.mutable_i2c_info()->set_address(i2c_fru->address); |
| |
| // Populate FRU data. |
| FruData* fru_data = fru.mutable_data(); |
| for (const auto& fru_data_field : fru_data_fields) { |
| (*fru_data->mutable_fields())[fru_data_field.first] = fru_data_field.second; |
| if (fru_data_field.first == "BOARD_MANUFACTURER") { |
| fru_data->mutable_fru_info()->set_board_manufacturer( |
| fru_data_field.second); |
| } else if (fru_data_field.first == "BOARD_PRODUCT_NAME") { |
| fru_data->mutable_fru_info()->set_board_product_name( |
| fru_data_field.second); |
| } else if (fru_data_field.first == "BOARD_SERIAL_NUMBER") { |
| fru_data->mutable_fru_info()->set_board_serial_number( |
| fru_data_field.second); |
| } else if (fru_data_field.first == "BOARD_PART_NUMBER") { |
| fru_data->mutable_fru_info()->set_board_part_number( |
| fru_data_field.second); |
| } else if (fru_data_field.first == "PRODUCT_MANUFACTURER") { |
| fru_data->mutable_fru_info()->set_product_manufacturer( |
| fru_data_field.second); |
| } else if (fru_data_field.first == "PRODUCT_PRODUCT_NAME") { |
| fru_data->mutable_fru_info()->set_product_product_name( |
| fru_data_field.second); |
| } else if (fru_data_field.first == "PRODUCT_SERIAL_NUMBER") { |
| fru_data->mutable_fru_info()->set_product_serial_number( |
| fru_data_field.second); |
| } else if (fru_data_field.first == "PRODUCT_PART_NUMBER") { |
| fru_data->mutable_fru_info()->set_product_part_number( |
| fru_data_field.second); |
| } |
| } |
| |
| return fru; |
| } |
| |
| 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::error_code error_code; |
| std::filesystem::path parent_path = path.parent_path(); |
| if (!std::filesystem::exists(parent_path)) { |
| std::filesystem::create_directories(path.parent_path(), error_code); |
| if (error_code) { |
| LOG(WARNING) << "Error creating directories for cached FRU table file at " |
| "path: " |
| << 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) { |
| 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 absl::WrapUnique( |
| new FruCollector(std::move(options), std::move(*cached_fru_table), |
| std::make_shared<TaskScheduler>())); |
| } |
| |
| if (options.fru_scanners.empty()) { |
| return absl::InvalidArgumentError("FruScanners is empty"); |
| } |
| |
| // TODO(rahulkpr): Add support for multiple FRU scanners. |
| FruScanner* fru_scanner = options.fru_scanners[0]; |
| |
| if (fru_scanner == nullptr) { |
| return absl::InvalidArgumentError("FruScanner is null"); |
| } |
| |
| // TODO(rahulkpr): Move this logic to i2c fru specific function |
| ECCLESIA_ASSIGN_OR_RETURN(std::vector<std::unique_ptr<I2cFruInfo>> i2c_frus, |
| fru_scanner->ScanAllI2cFrus()); |
| RawFruTable fru_table; |
| for (const auto& i2c_fru : i2c_frus) { |
| ECCLESIA_ASSIGN_OR_RETURN(RawFru fru, CreateRawFruFromI2cFruInfo(i2c_fru)); |
| |
| fru_table.mutable_key_to_raw_fru()->insert({fru.key(), fru}); |
| DLOG(INFO) << "Updated FRU table for FRU key: " << fru.key(); |
| } |
| |
| WriteCachedFru(fru_table, options.cached_fru_table_path); |
| |
| return absl::WrapUnique(new FruCollector(std::move(options), |
| std::move(fru_table), |
| std::make_shared<TaskScheduler>())); |
| } |
| |
| void FruCollector::SetUpAdHocFruScanning() { |
| if (options_.fru_scanners.empty()) { |
| return; |
| } |
| |
| // TODO(rahulkpr): Add support for multiple FRU scanners. |
| FruScanner* fru_scanner = options_.fru_scanners[0]; |
| |
| for (const auto& ad_hoc_fru_config : options_.ad_hoc_fru_scanning_configs) { |
| std::string fru_key = |
| absl::StrCat(ad_hoc_fru_config.i2c_common_config().bus(), ":", |
| ad_hoc_fru_config.i2c_common_config().address()); |
| // If the frutable already has the ad-hoc fru, skip it. |
| if (ContainsFru(fru_key)) { |
| continue; |
| } |
| |
| // If Ad-hoc FRU config has no periodic scan config, skip it. |
| if (ad_hoc_fru_config.periodic_scan_config_size() == 0) { |
| continue; |
| } |
| |
| ScheduleAdHocFruScan(ad_hoc_fru_config, fru_scanner); |
| } |
| } |
| |
| FruCollector::FruCollector(Options options, RawFruTable fru_table, |
| std::shared_ptr<TaskScheduler> task_scheduler) |
| : task_scheduler_(std::move(task_scheduler)), |
| options_(std::move(options)), |
| fru_table_(std::move(fru_table)) { |
| SetUpAdHocFruScanning(); |
| } |
| |
| std::unique_ptr<FruCollector> EmptyFruCollector::Create() { |
| return std::make_unique<EmptyFruCollector>(); |
| } |
| |
| RawFruTable EmptyFruCollector::GetCopyOfCurrentScannedFrus() const |
| ABSL_LOCKS_EXCLUDED(fru_table_mutex_) { |
| LOG(WARNING) << "EmptyFruCollector::GetCopyOfCurrentScannedFrus is called. " |
| "This is a no-op."; |
| return RawFruTable(); |
| } |
| |
| 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; |
| } |
| |
| nlohmann::json EmptyFruCollector::GetSchedulerStats() const { |
| return nlohmann::json::parse("{\"Warning\": \"EmptyFruCollector used.\"}"); |
| } |
| |
| std::size_t EmptyFruCollector::GetAllUserTaskCount() const { return 0; } |
| |
| } // namespace milotic_tlbmc |