blob: c7c6c6b6d415392176e065f3719c4791efd4601f [file] [log] [blame]
#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_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 "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 "http_response.hpp"
#include "async_resp.hpp"
#include "app.hpp"
#include "http_request.hpp"
#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";
} // 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) {
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::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) {
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);
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);
return BuildFruComponent(
absl::StrFormat(
"/%s/%s%s", kLocalRootChassisLocationCode,
(!part_location_context.empty() ? part_location_context + "/" : ""),
service_label),
model_name, status);
}
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);
// 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),
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;
}
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);
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