| #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 "g3/macros.h" |
| #include "grpcpp/security/auth_context.h" |
| #include "grpcpp/server_context.h" |
| #include "grpcpp/support/server_callback.h" |
| #include "grpcpp/support/status.h" |
| #include "nlohmann/json_fwd.hpp" |
| #include "json_utils.h" |
| #include "fru_component_model.pb.h" |
| #include "fru_service.pb.h" |
| #include "app.hpp" |
| #include "dbus_utility.hpp" // NOLINT |
| #include "async_resp.hpp" // NOLINT |
| #include "http_request.hpp" // NOLINT |
| #include "zatar/bmcweb_cert_provider.h" |
| |
| namespace milotic_fast_sanity { |
| |
| namespace { |
| |
| using ::grpc::AuthContext; |
| using ::grpc::CallbackServerContext; |
| using ::grpc::ServerUnaryReactor; |
| using ::milotic::authz::GetValueAsJson; |
| using ::milotic::authz::GetValueAsString; |
| |
| 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"; |
| |
| // TODO(b/422818770): Add a test case for this util function. |
| // Create Internal Request to query given `url`. |
| absl::StatusOr<crow::Request> 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; |
| } |
| |
| absl::Status IsLocationInfoValid(const nlohmann::json &fru) { |
| // TODO(b/422818770): Add a test case to cover the missing |
| // location info case. |
| const nlohmann::json *fru_location = GetValueAsJson(fru, "Location"); |
| if (fru_location == nullptr) { |
| return absl::NotFoundError("Location info not found."); |
| } |
| |
| const nlohmann::json *fru_location_part_location = |
| GetValueAsJson(*fru_location, "PartLocation"); |
| if (fru_location_part_location == nullptr) { |
| return absl::NotFoundError("Location.PartLocation info not found."); |
| } |
| |
| const nlohmann::json *fru_location_part_location_service_label = |
| GetValueAsJson(*fru_location_part_location, "ServiceLabel"); |
| if (fru_location_part_location_service_label == nullptr) { |
| return absl::NotFoundError( |
| "Location.PartLocation.ServiceLabel info not found."); |
| } |
| |
| return absl::OkStatus(); |
| } |
| |
| absl::StatusOr<std::string> ExtractRootChassisLocationCodeFromJson( |
| const nlohmann::json &json) { |
| if (!json.contains("Members")) { |
| return absl::InternalError( |
| "Redfish response does not contain `Members` field."); |
| } |
| |
| for (const auto &fru : json["Members"]) { |
| // The root node's indegree must be 0. |
| if (fru.contains("Links") && fru["Links"].contains("ContainedBy")) { |
| continue; |
| } |
| |
| // Make sure there is valid location info. |
| if (!IsLocationInfoValid(fru).ok()) { |
| continue; |
| } |
| |
| return *GetValueAsString(fru["Location"]["PartLocation"], "ServiceLabel"); |
| } |
| |
| // Certain machines don't have a Root Chassis Location Code |
| return ""; |
| } |
| |
| bool IsLocationTypeSupported(const nlohmann::json &location_type_json) { |
| static const absl::NoDestructor<absl::flat_hash_set<std::string>> |
| supported_location_types({"Slot", "Bay", "Connector"}); |
| return supported_location_types->contains(std::string(location_type_json)); |
| } |
| |
| absl::StatusOr<Devpath> ExtractDevpathFromJson( |
| const nlohmann::json &json, absl::string_view root_chassis_location_code) { |
| ECCLESIA_RETURN_IF_ERROR(IsLocationInfoValid(json)); |
| |
| // Ensure this is a valid & supported FRU. |
| const auto location_type_it = |
| json["Location"]["PartLocation"].find("LocationType"); |
| if (location_type_it == json["Location"]["PartLocation"].end() || |
| !IsLocationTypeSupported(*location_type_it)) { |
| return Devpath(); |
| } |
| |
| // This is the root chassis. Return empty devpath. |
| const auto &res_member_location = json["Location"]; |
| std::string service_label = |
| res_member_location["PartLocation"]["ServiceLabel"]; |
| if (service_label == root_chassis_location_code) { |
| return Devpath(); |
| } |
| |
| // 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 = |
| res_member_location.find("PartLocationContext"); |
| if (part_location_context_it != res_member_location.end()) { |
| part_location_context = res_member_location.at("PartLocationContext"); |
| } |
| 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()); |
| } |
| |
| return Devpath(part_location_context, service_label); |
| } |
| |
| void InsertDevpath(const nlohmann::json &json, |
| absl::string_view root_chassis_location_code, |
| std::shared_ptr<PartLocationContextToServiceLabelSet> |
| &part_location_context_to_service_label_set) { |
| absl::StatusOr<Devpath> devpath_status = |
| ExtractDevpathFromJson(json, root_chassis_location_code); |
| // TODO(b/422818770): Utilize the error status better. |
| // E.g., provide logs. |
| if (!devpath_status.ok() || devpath_status.value().service_label.empty()) { |
| return; |
| } |
| |
| absl::MutexLock lock(&part_location_context_to_service_label_set->mutex); |
| part_location_context_to_service_label_set |
| ->part_location_context_to_service_label_set[devpath_status.value() |
| .part_location_context] |
| .insert(devpath_status.value().service_label); |
| } |
| |
| void GenerateDevpathFromRedfishObject( |
| const nlohmann::json &json, absl::string_view root_chassis_location_code, |
| std::shared_ptr<PartLocationContextToServiceLabelSet> |
| part_location_context_to_service_label_set) { |
| InsertDevpath(json, root_chassis_location_code, |
| part_location_context_to_service_label_set); |
| const auto assembly_it = json.find("Assembly"); |
| if (assembly_it != json.end() && |
| assembly_it->find("Assemblies") != json["Assembly"].end()) { |
| for (const auto &fru : (*assembly_it)["Assemblies"]) { |
| GenerateDevpathFromRedfishObject( |
| fru, root_chassis_location_code, |
| part_location_context_to_service_label_set); |
| } |
| } |
| if (json.contains("Members")) { |
| for (const auto &fru : json["Members"]) { |
| GenerateDevpathFromRedfishObject( |
| fru, root_chassis_location_code, |
| part_location_context_to_service_label_set); |
| } |
| } |
| } |
| |
| void SendGetAllFruInfoResponse( |
| ServerUnaryReactor &reactor, GetAllFruInfoResponse &response, |
| const std::shared_ptr<PartLocationContextToServiceLabelSet> |
| &part_location_context_to_service_label_set) { |
| // Build the devpath list |
| // Insert the default local root chassis devpath first. |
| std::vector<std::string> devpaths({"/phys"}); |
| |
| { |
| absl::MutexLock lock(&part_location_context_to_service_label_set->mutex); |
| for (const auto &[part_location_context, service_label_set] : |
| part_location_context_to_service_label_set |
| ->part_location_context_to_service_label_set) { |
| for (const auto &service_label : service_label_set) { |
| devpaths.push_back(absl::StrCat( |
| "/", kLocalRootChassisLocationCode, "/", |
| (!part_location_context.empty() ? part_location_context + "/" : ""), |
| service_label)); |
| } |
| } |
| } |
| |
| if (devpaths.empty()) { |
| reactor.Finish(::grpc::Status(::grpc::StatusCode::INTERNAL, |
| "No matched devpath found.")); |
| } |
| |
| for (const std::string &devpath : devpaths) { |
| if (devpath.empty()) { |
| continue; |
| } |
| FruComponent fru; |
| fru.mutable_primary_identifier()->set_value(devpath); |
| fru.mutable_primary_identifier()->set_type( |
| UniqueIdentifierType::UNIQUE_IDENTIFIER_TYPE_LOCAL_DEVPATH); |
| response.mutable_fru_components()->Add(std::move(fru)); |
| } |
| reactor.Finish(::grpc::Status(::grpc::StatusCode::OK, "Devpath list sent.")); |
| } |
| |
| void DecreaseOngoingDevpathExtractionRequestCount( |
| const std::shared_ptr<OngoingDevpathExtractionRequestCount> |
| &ongoing_devpath_extraction_request_count) { |
| absl::MutexLock lock(&ongoing_devpath_extraction_request_count->mutex); |
| --ongoing_devpath_extraction_request_count |
| ->ongoing_devpath_extraction_request_count; |
| } |
| |
| } // namespace |
| |
| FruServiceImpl::FruServiceImpl(App *app, const FruServiceOptions &options) |
| : app_(app), |
| 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; |
| } |
| |
| 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 = |
| 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 = |
| 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; |
| } |
| |
| RequestRedfishAndExtractDevpaths(reactor, request, response, |
| *root_chassis_location_code_status); |
| }); |
| app_->handle(*crow_request_status, async_resp); |
| } |
| |
| void FruServiceImpl::RequestRedfishAndExtractDevpaths( |
| ServerUnaryReactor &reactor, const GetAllFruInfoRequest &request, |
| GetAllFruInfoResponse &response, |
| absl::string_view root_chassis_location_code) { |
| std::shared_ptr<PartLocationContextToServiceLabelSet> |
| part_location_context_to_service_label_set = |
| std::make_shared<PartLocationContextToServiceLabelSet>(); |
| |
| std::shared_ptr<OngoingDevpathExtractionRequestCount> |
| ongoing_devpath_extraction_request_count = |
| std::make_shared<OngoingDevpathExtractionRequestCount>( |
| kRootUrls.size()); |
| |
| for (absl::string_view url : kRootUrls) { |
| absl::StatusOr<crow::Request> devpath_request_status = |
| 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, part_location_context_to_service_label_set, |
| root_chassis_location_code = std::string(root_chassis_location_code), |
| ongoing_devpath_extraction_request_count](crow::Response &res) { |
| if (res.stringResponse.has_value()) { |
| GenerateDevpathFromRedfishObject( |
| res.jsonValue, root_chassis_location_code, |
| part_location_context_to_service_label_set); |
| } else { |
| LOG(WARNING) << "No valid response from redfish for url: `" << url |
| << "`"; |
| } |
| |
| DecreaseOngoingDevpathExtractionRequestCount( |
| ongoing_devpath_extraction_request_count); |
| |
| absl::MutexLock lock( |
| &ongoing_devpath_extraction_request_count->mutex); |
| if (ongoing_devpath_extraction_request_count |
| ->ongoing_devpath_extraction_request_count == 0) { |
| SendGetAllFruInfoResponse( |
| reactor, response, part_location_context_to_service_label_set); |
| } |
| }); |
| app_->handle(*devpath_request_status, async_devpath_resp); |
| } |
| } |
| |
| } // namespace milotic_fast_sanity |