blob: 30af4f796f06393ee4b209c3b2dd959c6ec039c0 [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 "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;
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";
// 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));
}
FruComponentStatus ExtractFruStatus(const nlohmann::json &fru) {
if (!fru.contains("Status") || !fru["Status"].contains("State") ||
fru["Status"]["State"] == "Enabled") {
return FruComponent::STATUS_OK;
}
if (fru["Status"]["State"] == "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 ExtractFruModelName(const nlohmann::json &fru) {
if (!fru.contains("Model")) {
return "";
}
return fru["Model"];
}
FruComponent 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> ExtractFruComponentFromJson(
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 absl::InvalidArgumentError(
"Invalid location info in the Redfish response.");
}
const auto &res_member_location = json["Location"];
std::string service_label =
res_member_location["PartLocation"]["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 =
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());
}
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 GenerateFruComponentsFromRedfishObject(
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() &&
assembly_it->find("Assemblies") != json["Assembly"].end()) {
for (const auto &fru : (*assembly_it)["Assemblies"]) {
GenerateFruComponentsFromRedfishObject(fru, root_chassis_location_code,
fru_components);
}
}
if (json.contains("Members")) {
for (const auto &fru : json["Members"]) {
GenerateFruComponentsFromRedfishObject(fru, root_chassis_location_code,
fru_components);
}
}
}
void SendGetAllFruInfoResponse(
ServerUnaryReactor &reactor, GetAllFruInfoResponse &response,
const std::shared_ptr<FruComponents> &fru_components) {
absl::MutexLock lock(&fru_components->mutex);
if (fru_components->fru_components.empty()) {
reactor.Finish(::grpc::Status(::grpc::StatusCode::INTERNAL,
"No matched devpath found."));
}
for (FruComponent &fru : fru_components->fru_components) {
if (fru.primary_identifier().value().empty()) {
continue;
}
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) {
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 =
BuildFruComponent(absl::StrCat("/", kLocalRootChassisLocationCode), "",
FruComponent::STATUS_OK);
fru_components->AddFruComponent(std::move(root_fru_component));
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, 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()) {
GenerateFruComponentsFromRedfishObject(
res.jsonValue, root_chassis_location_code, fru_components);
} 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, fru_components);
}
});
app_->handle(*devpath_request_status, async_devpath_resp);
}
}
} // namespace milotic_fast_sanity