blob: 1e7c75dc06bd8312720b505a3e74ebc767feb0a3 [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_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