| #include "tlbmc/service/fru_service.h" |
| |
| #include <array> |
| #include <memory> |
| #include <string> |
| #include <system_error> // NOLINT |
| #include <utility> |
| #include <vector> |
| |
| #include "absl/base/no_destructor.h" |
| #include "absl/container/flat_hash_map.h" |
| #include "absl/container/flat_hash_set.h" |
| #include "absl/log/log.h" |
| #include "absl/status/status.h" |
| #include "absl/status/statusor.h" |
| #include "absl/strings/match.h" |
| #include "absl/strings/str_cat.h" |
| #include "absl/strings/str_format.h" |
| #include "absl/strings/string_view.h" |
| #include "absl/synchronization/mutex.h" |
| #include "absl/types/span.h" |
| #include "g3/macros.h" |
| #include "app.hpp" |
| #include "http_request.hpp" |
| #include "http_response.hpp" |
| #include "async_resp.hpp" |
| #include <nlohmann/json_fwd.hpp> |
| #include "json_utils.h" |
| #include "fru_component_model.pb.h" |
| #include "fru_service.pb.h" |
| #include "fru.pb.h" |
| #include "g3/grpc_headers.h" |
| #include "zatar/bmcweb_cert_provider.h" |
| |
| namespace milotic_fast_sanity { |
| |
| namespace { |
| |
| using ::grpc::AuthContext; |
| using ::grpc::CallbackServerContext; |
| using ::grpc::ServerUnaryReactor; |
| using ::milotic::authz::GetValueAsString; |
| |
| using FruComponentStatus = FruComponent::Status; |
| |
| constexpr absl::string_view kRootChassisUrl = "/redfish/v1/Chassis?$expand=."; |
| // TODO(b/415893570): Make this config-based. |
| // Be ware: BB has `"/redfish/v1/Systems/system1/Memory"` as the memory |
| // Redfish path. |
| constexpr std::array<absl::string_view, 5> kRootUrls = { |
| "/redfish/v1/Chassis?$expand=.($levels=2)", |
| "/redfish/v1/Cables?$expand=.($levels=1)", |
| "/redfish/v1/Systems/system/Storage?$expand=.($levels=1)", |
| "/redfish/v1/Systems/system/Memory?$expand=.($levels=1)", |
| "/redfish/v1/Systems/system/Processors?$expand=.($levels=2)"}; |
| // local_devpath3 = "/" + LocalRootChassisLocationCode + PartLocationContext + |
| // ServiceLabel |
| constexpr absl::string_view kLocalRootChassisLocationCode = "phys"; |
| |
| const auto kPluginLocationTypes = |
| absl::NoDestructor<absl::flat_hash_map<absl::string_view, ComponentType>>( |
| {{"Slot", COMPONENT_NODE_TYPE_PLUGIN}, |
| {"Bay", COMPONENT_NODE_TYPE_PLUGIN}, |
| {"Connector", COMPONENT_NODE_TYPE_PLUGIN}}); |
| |
| } // namespace |
| |
| absl::StatusOr<crow::Request> FruServiceImpl::CreateRedfishRequest( |
| absl::string_view url, boost::beast::http::verb method) { |
| boost::beast::http::request<boost::beast::http::string_body> boost_request; |
| boost_request.target(url); |
| boost_request.method(method); |
| std::error_code error; |
| crow::Request crow_request(boost_request, error); |
| if (error) { |
| return absl::InternalError("Error creating crow_request."); |
| } |
| |
| crow_request.fromGrpc = false; |
| return crow_request; |
| } |
| |
| void FruServiceImpl::SendGetAllFruInfoResponse( |
| ServerUnaryReactor& reactor, GetAllFruInfoResponse& response, |
| const std::shared_ptr<FruComponents>& fru_components) { |
| absl::MutexLock lock(fru_components->mutex); |
| for (auto& [devpath, fru] : fru_components->devpath_to_fru_component) { |
| response.mutable_fru_components()->Add(std::move(fru)); |
| } |
| reactor.Finish(::grpc::Status(::grpc::StatusCode::OK, "Devpath list sent.")); |
| } |
| |
| bool FruServiceImpl::IsLocationTypeSupported( |
| const nlohmann::json& location_type_json) { |
| return kPluginLocationTypes->contains(std::string(location_type_json)); |
| } |
| |
| absl::Status FruServiceImpl::IsLocationInfoValid(const nlohmann::json& fru) { |
| const auto fru_location_it = fru.find("Location"); |
| if (fru_location_it == fru.end()) { |
| return absl::NotFoundError("Location info not found."); |
| } |
| |
| const auto fru_location_part_location_it = |
| fru_location_it->find("PartLocation"); |
| if (fru_location_part_location_it == fru_location_it->end()) { |
| return absl::NotFoundError("Location.PartLocation info not found."); |
| } |
| |
| // Ensure this is a valid & supported FRU. |
| const auto location_type_it = |
| fru_location_part_location_it->find("LocationType"); |
| if (location_type_it == fru_location_part_location_it->end()) { |
| return absl::NotFoundError( |
| "Location.PartLocation.LocationType info not found."); |
| } |
| if (!IsLocationTypeSupported(*location_type_it)) { |
| return absl::InvalidArgumentError("Unsupported location type."); |
| } |
| |
| const auto fru_location_part_location_service_label_it = |
| fru_location_part_location_it->find("ServiceLabel"); |
| if (fru_location_part_location_service_label_it == |
| fru_location_part_location_it->end()) { |
| return absl::NotFoundError( |
| "Location.PartLocation.ServiceLabel info not found."); |
| } |
| |
| return absl::OkStatus(); |
| } |
| |
| absl::StatusOr<std::string> |
| FruServiceImpl::ExtractRootChassisLocationCodeFromJson( |
| const nlohmann::json& json) { |
| const auto fru_members_it = json.find("Members"); |
| if (fru_members_it == json.end()) { |
| return absl::InternalError( |
| "Redfish response does not contain `Members` field."); |
| } |
| |
| for (const nlohmann::json& fru : *fru_members_it) { |
| // The root node's indegree must be 0. |
| const auto fru_links_it = fru.find("Links"); |
| if (fru_links_it != fru.end() && fru_links_it->contains("ContainedBy")) { |
| continue; |
| } |
| |
| // Make sure there is valid location info. |
| if (!IsLocationInfoValid(fru).ok()) { |
| continue; |
| } |
| |
| // With the previous check of `IsLocationInfoValid(fru).ok()`, we can |
| // directly access `Location.PartLocation.ServiceLabel` here. |
| return *GetValueAsString(fru["Location"]["PartLocation"], "ServiceLabel"); |
| } |
| |
| // Certain machines don't have a Root Chassis Location Code |
| return ""; |
| } |
| |
| FruComponentStatus FruServiceImpl::ExtractFruStatus(const nlohmann::json& fru) { |
| const auto fru_status_it = fru.find("Status"); |
| if (fru_status_it == fru.end()) { |
| return FruComponent::STATUS_OK; |
| } |
| const auto fru_status_state_it = fru_status_it->find("State"); |
| if (fru_status_state_it == fru_status_it->end()) { |
| return FruComponent::STATUS_OK; |
| } |
| if (*fru_status_state_it == "Absent") { |
| return FruComponent::STATUS_NOT_FOUND; |
| } |
| return FruComponent::STATUS_OK; |
| } |
| |
| // Returning empty string here is intentional, which will be used as the |
| // `short_name` when there is no model name. |
| std::string FruServiceImpl::ExtractFruModelName(const nlohmann::json& fru) { |
| const auto fru_model_it = fru.find("Model"); |
| if (fru_model_it == fru.end()) { |
| return ""; |
| } |
| return *fru_model_it; |
| } |
| |
| FruComponent FruServiceImpl::BuildFruComponent(absl::string_view devpath, |
| absl::string_view model_name, |
| FruComponentStatus status, |
| ComponentType component_type) { |
| FruComponent fru; |
| fru.mutable_primary_identifier()->set_value(devpath); |
| fru.mutable_primary_identifier()->set_type( |
| UniqueIdentifierType::UNIQUE_IDENTIFIER_TYPE_LOCAL_DEVPATH); |
| fru.mutable_primary_identifier()->mutable_short_names()->Add( |
| std::string(model_name)); |
| fru.set_status(status); |
| fru.set_fru_type(component_type); |
| return fru; |
| } |
| |
| absl::StatusOr<FruComponent> FruServiceImpl::ExtractFruComponentFromJson( |
| const nlohmann::json& json, absl::string_view root_chassis_location_code) { |
| // Make sure `json["Location"]["PartLocation"]["ServiceLabel"]` exists, so |
| // that we can access the JSON without checking the existence. |
| ECCLESIA_RETURN_IF_ERROR(IsLocationInfoValid(json)); |
| |
| const auto fru_location_it = json.find("Location"); |
| const auto fru_location_part_location_it = |
| fru_location_it->find("PartLocation"); |
| |
| // We can directly access the value here because we have already checked the |
| // existence of this element by the previous call of |
| // `ECCLESIA_RETURN_IF_ERROR(IsLocationInfoValid(json))`. |
| std::string service_label = |
| *GetValueAsString(*fru_location_part_location_it, "ServiceLabel"); |
| // This is the root chassis. |
| if (service_label == root_chassis_location_code || |
| service_label == kLocalRootChassisLocationCode) { |
| return absl::InternalError("Root chassis FRU will be built separately."); |
| } |
| |
| // PartLocationContext is optional, and the prefix should be removed if it |
| // is equal to the root chassis location code. |
| std::string part_location_context; |
| const auto part_location_context_it = |
| fru_location_it->find("PartLocationContext"); |
| if (part_location_context_it != fru_location_it->end()) { |
| part_location_context = *part_location_context_it; |
| } |
| bool start_with_root_chassis_location_code = |
| !root_chassis_location_code.empty() && |
| absl::StartsWith(part_location_context, root_chassis_location_code); |
| bool start_with_local_root_chassis_location_code = |
| absl::StartsWith(part_location_context, kLocalRootChassisLocationCode); |
| if (start_with_root_chassis_location_code || |
| start_with_local_root_chassis_location_code) { |
| // Note that RootChassisLocationCode should be eliminated if it is the |
| // prefix of either `PartLocationContext` or `ServiceLabel`. |
| part_location_context = |
| start_with_root_chassis_location_code |
| ? part_location_context.substr(root_chassis_location_code.length()) |
| : part_location_context.substr( |
| kLocalRootChassisLocationCode.length()); |
| } |
| |
| FruComponentStatus status = ExtractFruStatus(json); |
| std::string model_name = ExtractFruModelName(json); |
| |
| ComponentType component_type = COMPONENT_NODE_TYPE_UNSPECIFIED; |
| if (auto it = kPluginLocationTypes->find( |
| *GetValueAsString(*fru_location_part_location_it, "LocationType")); |
| it != kPluginLocationTypes->end()) { |
| component_type = it->second; |
| } |
| |
| return BuildFruComponent( |
| absl::StrFormat( |
| "/%s/%s%s", kLocalRootChassisLocationCode, |
| (!part_location_context.empty() ? part_location_context + "/" : ""), |
| service_label), |
| model_name, status, component_type); |
| } |
| |
| void FruServiceImpl::AppendGeneratedFruComponentsFromRedfishObject( |
| const nlohmann::json& json, absl::string_view root_chassis_location_code, |
| const std::shared_ptr<FruComponents>& fru_components) { |
| absl::StatusOr<FruComponent> fru_info_status = |
| ExtractFruComponentFromJson(json, root_chassis_location_code); |
| DLOG(INFO) << "Redfish response is : " << json.dump(2); |
| // TODO(b/422818770): Utilize the error status better. |
| // E.g., provide logs. |
| if (fru_info_status.ok()) { |
| fru_components->AddFruComponent(std::move(fru_info_status.value())); |
| } |
| const auto assembly_it = json.find("Assembly"); |
| if (assembly_it != json.end()) { |
| const auto assemblies_it = assembly_it->find("Assemblies"); |
| if (assemblies_it != assembly_it->end()) { |
| for (const auto& fru : *assemblies_it) { |
| AppendGeneratedFruComponentsFromRedfishObject( |
| fru, root_chassis_location_code, fru_components); |
| } |
| } |
| } |
| const auto members_it = json.find("Members"); |
| if (members_it != json.end()) { |
| for (const auto& fru : *members_it) { |
| AppendGeneratedFruComponentsFromRedfishObject( |
| fru, root_chassis_location_code, fru_components); |
| } |
| } |
| } |
| |
| FruServiceImpl::FruServiceImpl(App* app, const FruServiceOptions& options) |
| : app_(app), |
| enable_deterministic_bmc_(options.enable_deterministic_bmc), |
| fru_collector_(options.fru_collector), |
| has_trust_bundle_(options.cert_provider->GetServerStatus() == |
| ::milotic::redfish::BmcWebCertProvider:: |
| ServerStatus::kWithRootCertsAndProdSignedCert || |
| options.cert_provider->GetServerStatus() == |
| ::milotic::redfish::BmcWebCertProvider:: |
| ServerStatus::kWithRootCertsAndSelfSignedCert) { |
| } |
| |
| ::grpc::Status FruServiceImpl::RequestIsAuthenticated( |
| const AuthContext* auth_context) const { |
| // Perform Authentication |
| // If the server has no trust bundle, then fail fast sanity. |
| if (!has_trust_bundle_) { |
| return ::grpc::Status(::grpc::StatusCode::UNAUTHENTICATED, |
| "Server has no trust bundle, failing fast sanity."); |
| } |
| |
| // If peer is no authenticated, then return an error. |
| if (!auth_context->IsPeerAuthenticated()) { |
| return ::grpc::Status( |
| ::grpc::StatusCode::UNAUTHENTICATED, |
| absl::StrFormat("Peer %s is not authenticated.", |
| auth_context->GetPeerIdentityPropertyName())); |
| } |
| return ::grpc::Status(::grpc::StatusCode::OK, "Peer is authenticated."); |
| } |
| |
| ServerUnaryReactor* FruServiceImpl::GetAllFruInfo( |
| CallbackServerContext* context, const GetAllFruInfoRequest* request, |
| GetAllFruInfoResponse* response) { |
| ServerUnaryReactor* reactor = context->DefaultReactor(); |
| |
| if (::grpc::Status status = |
| RequestIsAuthenticated(context->auth_context().get()); |
| !status.ok()) { |
| reactor->Finish(status); |
| return reactor; |
| } |
| |
| GenerateMatchedDevpathsFromRedfishPaths(*reactor, *request, *response); |
| return reactor; |
| } |
| |
| ServerUnaryReactor* FruServiceImpl::GetFruInfoDeterministicallyOnDemand( |
| CallbackServerContext* context, |
| const GetFruInfoDeterministicallyRequest* request, |
| GetFruInfoDeterministicallyResponse* response) { |
| ServerUnaryReactor* reactor = context->DefaultReactor(); |
| |
| if (::grpc::Status status = |
| RequestIsAuthenticated(context->auth_context().get()); |
| !status.ok()) { |
| reactor->Finish(status); |
| return reactor; |
| } |
| |
| if (!enable_deterministic_bmc_) { |
| reactor->Finish(::grpc::Status(::grpc::StatusCode::FAILED_PRECONDITION, |
| "Deterministic BMC is not enabled. Cannot " |
| "scan FRUs deterministically.")); |
| return reactor; |
| } |
| |
| if (fru_collector_ == nullptr) { |
| reactor->Finish( |
| ::grpc::Status(::grpc::StatusCode::FAILED_PRECONDITION, |
| "FRU collector is not initialized. Cannot scan FRUs " |
| "deterministically.")); |
| return reactor; |
| } |
| |
| absl::StatusOr<milotic_tlbmc::FruTable> frus = |
| fru_collector_->ScanAllFrusDeterministically(); |
| if (!frus.ok()) { |
| reactor->Finish( |
| ::grpc::Status(::grpc::StatusCode::INTERNAL, |
| absl::StrCat("Failed to scan FRUs deterministically: ", |
| frus.status().message()))); |
| return reactor; |
| } |
| |
| for (const auto& [key, fru] : frus->key_to_fru()) { |
| if (!fru.attributes().expected_hardware_info().has_expected_barepath()) { |
| // 1. it's a general good practice to check existence before use. This is |
| // to avoid silently ignore the unexpected case in the future and give |
| // empty string as the default value in the response. |
| // 2. for those FRUs we don't have expected barepath from UHMM (which |
| // means not supported yet), we will not return them in the response. |
| // 3. Although there is no way to reach this case with the current |
| // implementation, when extra part discovery is added in the future, we |
| // will need to revisit this logic since the FRU will not have |
| // expected_barepath. |
| LOG(WARNING) << "FRU key: " << key << " does not have expected barepath."; |
| continue; |
| } |
| auto* fru_proto = response->add_frus(); |
| fru_proto->set_barepath( |
| fru.attributes().expected_hardware_info().expected_barepath()); |
| fru_proto->set_presence_status(fru.attributes().presence_status()); |
| } |
| |
| milotic_tlbmc::FruTable smbios_fru_table = |
| fru_collector_->GetCopyOfSmbiosFruTable(); |
| for (auto& [key, fru] : *smbios_fru_table.mutable_key_to_fru()) { |
| if (!fru.attributes().expected_hardware_info().has_expected_barepath()) { |
| LOG(WARNING) << "FRU key: " << key << " does not have expected barepath."; |
| continue; |
| } |
| auto* fru_proto = response->add_frus(); |
| fru_proto->set_barepath( |
| fru.attributes().expected_hardware_info().expected_barepath()); |
| fru_proto->set_presence_status(fru.attributes().presence_status()); |
| if (fru.data().has_processor_info()) { |
| fru_proto->mutable_processor_info()->Swap( |
| fru.mutable_data()->mutable_processor_info()); |
| } else if (fru.data().has_memory_info()) { |
| fru_proto->mutable_memory_info()->Swap( |
| fru.mutable_data()->mutable_memory_info()); |
| } |
| } |
| |
| reactor->Finish(grpc::Status::OK); |
| return reactor; |
| } |
| |
| absl::StatusOr<std::vector<std::string>> FruServiceImpl::GetAllChassisUrls( |
| const nlohmann::json& json) { |
| auto members = json.find("Members"); |
| if (members == json.end()) { |
| return absl::InternalError( |
| "Redfish response does not contain `Members` field."); |
| } |
| |
| std::vector<std::string> chassis_urls; |
| |
| for (const auto& fru : *members) { |
| auto odata_id = fru.find("@odata.id"); |
| if (odata_id == fru.end()) { |
| LOG(WARNING) << "No `@odata.id` field found for FRU: " << fru; |
| continue; |
| } |
| chassis_urls.push_back(*odata_id); |
| } |
| |
| return chassis_urls; |
| } |
| |
| void FruServiceImpl::GenerateMatchedDevpathsFromRedfishPaths( |
| ServerUnaryReactor& reactor, const GetAllFruInfoRequest& request, |
| GetAllFruInfoResponse& response) { |
| // Extract the root chassis location code from the first url. |
| absl::StatusOr<crow::Request> crow_request_status = |
| FruServiceImpl::CreateRedfishRequest(kRootChassisUrl, |
| boost::beast::http::verb::get); |
| if (!crow_request_status.ok()) { |
| LOG(WARNING) << "Error creating redfish request for url: `" |
| << kRootChassisUrl << "`; with status: `" |
| << crow_request_status.status() << "`"; |
| reactor.Finish( |
| ::grpc::Status(::grpc::StatusCode::INTERNAL, |
| absl::StrCat("Error creating redfish request for " |
| "root chassis location code url: `", |
| kRootChassisUrl, "`."))); |
| return; |
| } |
| |
| auto async_resp = std::make_shared<bmcweb::AsyncResp>(nullptr); |
| async_resp->response_type = |
| bmcweb::AsyncResp::ResponseType::kOriginOfCondition; |
| async_resp->res.setCompleteRequestHandler([this, &reactor, &request, |
| &response](crow::Response& res) { |
| if (!res.stringResponse.has_value()) { |
| reactor.Finish( |
| ::grpc::Status(::grpc::StatusCode::INTERNAL, |
| absl::StrCat("No valid response from redfish " |
| "for root chassis url: `", |
| kRootChassisUrl, "`."))); |
| return; |
| } |
| absl::StatusOr<std::string> root_chassis_location_code_status = |
| FruServiceImpl::ExtractRootChassisLocationCodeFromJson(res.jsonValue); |
| if (!root_chassis_location_code_status.ok()) { |
| reactor.Finish(::grpc::Status( |
| ::grpc::StatusCode::INTERNAL, |
| std::string(root_chassis_location_code_status.status().message()))); |
| return; |
| } |
| |
| absl::StatusOr<std::vector<std::string>> chassis_urls = |
| GetAllChassisUrls(res.jsonValue); |
| if (!chassis_urls.ok()) { |
| reactor.Finish( |
| ::grpc::Status(::grpc::StatusCode::INTERNAL, |
| std::string(chassis_urls.status().message()))); |
| return; |
| } |
| |
| RequestRedfishAndExtractDevpaths(reactor, request, response, |
| *root_chassis_location_code_status, |
| *chassis_urls); |
| }); |
| app_->handle(*crow_request_status, async_resp); |
| } |
| |
| void FruServiceImpl::RequestRedfishAndExtractDevpaths( |
| ServerUnaryReactor& reactor, const GetAllFruInfoRequest& request, |
| GetAllFruInfoResponse& response, |
| absl::string_view root_chassis_location_code, |
| absl::Span<const std::string> chassis_urls) { |
| auto fru_components = std::make_shared<FruComponents>(); |
| |
| // The root chassis is successfully extracted. Need to explicitly add its |
| // corresponding FRU into the list, as some platforms do not have a root |
| // chassis location code to be identified as a devpath. |
| FruComponent root_fru_component = FruServiceImpl::BuildFruComponent( |
| absl::StrCat("/", kLocalRootChassisLocationCode), "", |
| FruComponent::STATUS_OK, COMPONENT_NODE_TYPE_PLUGIN); |
| fru_components->AddFruComponent(std::move(root_fru_component)); |
| |
| // Combine all urls into one vector. |
| std::vector<std::string> all_urls(kRootUrls.begin(), kRootUrls.end()); |
| |
| for (const auto& url : chassis_urls) { |
| all_urls.push_back( |
| absl::StrFormat("%s/ThermalSubsystem/Fans?$expand=.", url)); |
| } |
| |
| std::shared_ptr<OngoingDevpathExtractionRequestCount> |
| ongoing_devpath_extraction_request_count = |
| std::make_shared<OngoingDevpathExtractionRequestCount>( |
| all_urls.size()); |
| |
| for (absl::string_view url : all_urls) { |
| absl::StatusOr<crow::Request> devpath_request_status = |
| FruServiceImpl::CreateRedfishRequest(url, |
| boost::beast::http::verb::get); |
| if (!devpath_request_status.ok()) { |
| LOG(WARNING) << "Error creating redfish request for url: `" << url |
| << "`; with status: `" << devpath_request_status.status() |
| << "`"; |
| continue; |
| } |
| auto async_devpath_resp = std::make_shared<bmcweb::AsyncResp>(nullptr); |
| async_devpath_resp->response_type = |
| bmcweb::AsyncResp::ResponseType::kOriginOfCondition; |
| |
| // Extract devpaths from the rest of the requests. |
| async_devpath_resp->res.setCompleteRequestHandler( |
| [&reactor, &response, url, fru_components, |
| root_chassis_location_code = std::string(root_chassis_location_code), |
| ongoing_devpath_extraction_request_count](crow::Response& res) { |
| if (res.stringResponse.has_value()) { |
| FruServiceImpl::AppendGeneratedFruComponentsFromRedfishObject( |
| res.jsonValue, root_chassis_location_code, fru_components); |
| } else { |
| LOG(WARNING) << "No valid response from redfish for url: `" << url |
| << "`"; |
| } |
| |
| ongoing_devpath_extraction_request_count |
| ->DecreaseOngoingDevpathExtractionRequestCount(); |
| |
| absl::MutexLock lock(ongoing_devpath_extraction_request_count->mutex); |
| if (ongoing_devpath_extraction_request_count |
| ->ongoing_devpath_extraction_request_count == 0) { |
| FruServiceImpl::SendGetAllFruInfoResponse(reactor, response, |
| fru_components); |
| } |
| }); |
| app_->handle(*devpath_request_status, async_devpath_resp); |
| } |
| } |
| |
| } // namespace milotic_fast_sanity |