Create `GetAllFruInfo` RPC

This RPC now returns all devpaths under ["/redfish/v1/Chassis", "/redfish/v1/Systems/system/Storage", "/redfish/v1/Systems/system/Memory", "/redfish/v1/Systems/system/Processor"].

TODO:
1. Make the root paths config-based.
2. Apply request filters to select matched devpaths instead of all devpaths.
3. [Long term?] Add more info into the response, instead of just returning the devpath list.
4. Integrate the RPC into BMCWeb.
5. Create a local server & client?

Tested:
Unit test
PiperOrigin-RevId: 769374568
Change-Id: I21289e74a81e84fabcd8e9ed782807e5944768e5
diff --git a/tlbmc/fru_component_model.proto b/tlbmc/fru_component_model.proto
new file mode 100644
index 0000000..e402c7a
--- /dev/null
+++ b/tlbmc/fru_component_model.proto
@@ -0,0 +1,156 @@
+edition = "2023";
+
+package milotic_fast_sanity;
+
+// Enum defining the standard types of unique identifiers for components.
+enum UniqueIdentifierType {
+  UNIQUE_IDENTIFIER_TYPE_UNSPECIFIED = 0;
+  UNIQUE_IDENTIFIER_TYPE_LOCAL_DEVPATH = 1;
+  UNIQUE_IDENTIFIER_TYPE_MACHINE_DEVPATH = 2;
+  UNIQUE_IDENTIFIER_TYPE_LOGICAL_NAME = 3;
+  UNIQUE_IDENTIFIER_TYPE_UUID = 4;
+  UNIQUE_IDENTIFIER_TYPE_ADDRESS = 5;
+  // Add other common identifier types as needed.
+}
+
+// Represents a unique identifier for a component, specifying its value and
+// type.
+message UniqueIdentifier {
+  string value = 1;
+  repeated string short_names = 2;
+  UniqueIdentifierType type = 3;
+}
+
+// Generic SMBus Location information.
+message I2cLocation {
+  string bus = 1;
+  string address = 2;
+}
+
+// Generic Location details for a component, allowing for different types of
+// physical locations.
+message Location {
+  oneof location_type {
+    I2cLocation i2c_location = 1;
+    // Extend with other location types as needed.
+  }
+}
+
+// Defines the type of connector.
+// This is a subset of the connector types defined in the redfish spec.
+enum ConnectorType {
+  CONNECTOR_TYPE_UNSPECIFIED = 0;
+  CONNECTOR_TYPE_AC_POWER = 1;
+  CONNECTOR_TYPE_DB9 = 2;
+  CONNECTOR_TYPE_DC_POWER = 3;
+  CONNECTOR_TYPE_DISPLAY_PORT = 4;
+  CONNECTOR_TYPE_HDMI = 5;
+  CONNECTOR_TYPE_ICI = 6;
+  CONNECTOR_TYPE_IPASS = 7;
+  CONNECTOR_TYPE_PCI_E = 8;
+  CONNECTOR_TYPE_PROPRIETARY = 9;
+  CONNECTOR_TYPE_RJ45 = 10;
+  CONNECTOR_TYPE_SATA = 11;
+  CONNECTOR_TYPE_SCSI = 12;
+  CONNECTOR_TYPE_SLIMSAS = 13;
+  CONNECTOR_TYPE_SFP = 14;
+  CONNECTOR_TYPE_SFP_PLUS = 15;
+  CONNECTOR_TYPE_USBA = 16;
+  CONNECTOR_TYPE_USBC = 17;
+  CONNECTOR_TYPE_QSFP = 18;
+  CONNECTOR_TYPE_CDFP = 19;
+  CONNECTOR_TYPE_OSFP = 20;
+  // Add more as needed
+}
+
+// Defines the type of cable per the redfish spec.
+enum CableType {
+  CABLE_TYPE_UNSPECIFIED = 0;
+  CABLE_TYPE_POWER = 1;
+  CABLE_TYPE_NETWORK = 2;
+  CABLE_TYPE_STORAGE = 3;
+  CABLE_TYPE_FAN = 4;
+  CABLE_TYPE_PCIE = 5;
+  CABLE_TYPE_USB = 6;
+  CABLE_TYPE_VIDEO = 7;
+  CABLE_TYPE_FABRIC = 8;
+  CABLE_TYPE_SERIAL = 9;
+  CABLE_TYPE_GENERAL = 10;
+
+  // Add more as needed
+}
+
+// FRI Classification
+enum ComponentClassification {
+  COMPONENT_CLASSIFICATION_UNSPECIFIED = 0;
+  COMPONENT_CLASSIFICATION_MEMORY = 1;
+  COMPONENT_CLASSIFICATION_PROCCESSOR = 2;
+  COMPONENT_CLASSIFICATION_CABLE = 3;
+  COMPONENT_CLASSIFICATION_FAN = 4;
+  COMPONENT_CLASSIFICATION_DRIVE = 5;
+  COMPONENT_CLASSIFICATION_BOARD = 6;
+}
+
+// Defines the general category or type of this component.
+enum ComponentType {
+  COMPONENT_NODE_TYPE_UNSPECIFIED = 0;
+  COMPONENT_NODE_TYPE_PLUGIN = 1;  // e.g., Motherboard, Tray, DIMM, etc.
+  COMPONENT_NODE_TYPE_DEVICE = 2;  // e.g., Embedded ROT, controller, etc.
+}
+
+// Describes various relationships this component has with others.
+message RelatedComponents {
+  // Components (devices, connectors) that are direct children or contained
+  // within this component.
+  repeated UniqueIdentifier child_components = 1;
+
+  // The parent component(s) this component is part of or plugged into.
+  UniqueIdentifier parent_component = 2;
+}
+
+// Represents a single Field Replaceable Unit (FRU) or a logical component.
+message FruComponent {
+  UniqueIdentifier primary_identifier = 1;
+  repeated UniqueIdentifier secondary_identifiers = 2;
+
+  // The type of FRU.
+  ComponentType fru_type = 3;
+
+  // The class of FRU.
+  ComponentClassification fru_class = 4;
+
+  // Location details of the component.
+  Location location = 5;
+
+  // Information specific to connector components.
+  ConnectorType connector_type = 6;
+
+  // Information specific to cable components.
+  CableType cable_type = 7;
+
+  // Structured relationships describing the component's children, parents, and
+  // plugged-in items.
+  RelatedComponents relationships = 8;
+
+  // A FRU can be in the following states:
+  // 1. OK: The FRU is present.
+  //
+  // The following states are only applicable if the intent model is available
+  // on the server either provided as part of the request or natively present on
+  // the server.
+  // 2. NOT_SCANNED: The FRU has not been scanned.
+  //    The implemenatations that don't have an
+  //    intent model will not be able to report this status per FRU. In such
+  //    cases, a FRU may be reported without an identifier
+  // 3. NOT_FOUND: The FRU has not been found.
+  //    The implementations that scan all the FRUs on the system may choose
+  //    to report this status for FRUs that are not present by comparing the
+  //    scanned FRUs with the intent model.
+  enum Status {
+    STATUS_UNKNOWN = 0;
+    STATUS_OK = 1;
+    STATUS_NOT_SCANNED = 2;
+    STATUS_NOT_FOUND = 3;
+  }
+  Status status = 9;
+}
diff --git a/tlbmc/fru_service.proto b/tlbmc/fru_service.proto
new file mode 100644
index 0000000..f4397ff
--- /dev/null
+++ b/tlbmc/fru_service.proto
@@ -0,0 +1,46 @@
+edition = "2023";
+
+package milotic_fast_sanity;
+
+import "fru_component_model.proto";
+
+message GetAllFruInfoRequest {
+  // Optional filter by the general component node type.
+  // Only FRUs matching these types will be returned.
+  // This is an OR filter.
+  repeated ComponentType filter_by_fru_type = 1;
+
+  // Optional filter by the FRU class.
+  // Only FRUs matching this FruClass will be returned.
+  repeated ComponentClassification filter_by_fru_class = 2;
+
+  // Optional filter by an identifier's value.
+  // This is an OR filter.
+  repeated UniqueIdentifier filter_by_identifier_value = 3;
+
+  // List of expected FRU components.
+  repeated UniqueIdentifier expected_fru_components = 4;
+
+  enum FruScanType {
+    FRU_SCAN_TYPE_UNSPECIFIED = 0;
+    // Performs a scan of the expected FRUs only provided in the request.
+    FRU_SCAN_TYPE_EXPECTED_ONLY = 1;
+    // Performs a scan of all FRUs on the system.
+    FRU_SCAN_TYPE_ALL = 2;
+  }
+
+  // Optional filter by the type of FRU scan to perform.
+  // If unspecified, defaults to FRU_SCAN_TYPE_ALL.
+  FruScanType fru_scan_type = 5 [default = FRU_SCAN_TYPE_ALL];
+}
+
+message GetAllFruInfoResponse {
+  repeated FruComponent fru_components = 1;
+}
+
+service FruService {
+  // Get the FRU information for a given component.
+  rpc GetAllFruInfo(GetAllFruInfoRequest) returns (GetAllFruInfoResponse) {
+    option deadline = 60.0;
+  }
+}
diff --git a/tlbmc/service/fru_service.cc b/tlbmc/service/fru_service.cc
new file mode 100644
index 0000000..bbb4446
--- /dev/null
+++ b/tlbmc/service/fru_service.cc
@@ -0,0 +1,362 @@
+#include "tlbmc/service/fru_service.h"
+
+#include <array>
+#include <memory>
+#include <string>
+#include <system_error>  // NOLINT
+#include <utility>
+#include <vector>
+
+#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/string_view.h"
+#include "absl/synchronization/mutex.h"
+#include "grpcpp/support/status.h"
+#include "nlohmann/json_fwd.hpp"
+#include "json_utils.h"
+#include "fru_component_model.proto.h"
+#include "fru_service.proto.h"
+#include "dbus_utility.hpp" // NOLINT
+#include "async_resp.hpp" // NOLINT
+#include "http_request.hpp" // NOLINT
+#include "util/task/status_macros.h"
+
+namespace milotic_fast_sanity {
+
+namespace {
+
+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);
+  // TODO(b/422818770): Add the server & client pair.
+  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");
+  }
+
+  return absl::NotFoundError("Root chassis location code not found.");
+}
+
+absl::StatusOr<Devpath> ExtractDevpathFromJson(
+    const nlohmann::json &json, absl::string_view root_chassis_location_code) {
+  // TODO(b/422818770): Add a test case to cover the missing
+  // location info case.
+  RETURN_IF_ERROR(IsLocationInfoValid(json));
+
+  // Ensure this is a FRU.
+  const auto location_type_it =
+      json["Location"]["PartLocation"].find("LocationType");
+  if (location_type_it == json["Location"]["PartLocation"].end() ||
+      (*location_type_it) != "Slot") {
+    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 =
+      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(absl::NotFoundError("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(absl::OkStatus());
+}
+
+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) {}
+
+ServerUnaryReactor *FruServiceImpl::GetAllFruInfo(
+    CallbackServerContext *context, const GetAllFruInfoRequest *request,
+    GetAllFruInfoResponse *response) {
+  ServerUnaryReactor *reactor = context->DefaultReactor();
+  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(absl::InternalError(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(absl::InternalError(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(root_chassis_location_code_status.status());
+          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,
+         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
diff --git a/tlbmc/service/fru_service.h b/tlbmc/service/fru_service.h
new file mode 100644
index 0000000..25e868a
--- /dev/null
+++ b/tlbmc/service/fru_service.h
@@ -0,0 +1,80 @@
+#ifndef THIRD_PARTY_MILOTIC_EXTERNAL_CC_FAST_SANITY_SERVICE_FRU_SERVICE_H_
+#define THIRD_PARTY_MILOTIC_EXTERNAL_CC_FAST_SANITY_SERVICE_FRU_SERVICE_H_
+
+#include <cstdint>
+#include <string>
+
+#include "absl/base/thread_annotations.h"
+#include "absl/container/flat_hash_map.h"
+#include "absl/container/flat_hash_set.h"
+#include "absl/strings/string_view.h"
+#include "absl/synchronization/mutex.h"
+#include "grpcpp/server_context.h"
+#include "grpcpp/support/server_callback.h"
+#include "fru_service.grpc.pb.h"
+#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 {
+
+using ::grpc::CallbackServerContext;
+using ::grpc::ServerUnaryReactor;
+
+struct Devpath {
+  std::string part_location_context;
+  std::string service_label;
+
+  Devpath() = default;
+
+  Devpath(absl::string_view part_location_context,
+          absl::string_view service_label)
+      : part_location_context(part_location_context),
+        service_label(service_label) {}
+};
+
+struct PartLocationContextToServiceLabelSet {
+  absl::Mutex mutex;
+  absl::flat_hash_map<std::string, absl::flat_hash_set<std::string>>
+      part_location_context_to_service_label_set ABSL_GUARDED_BY(mutex);
+};
+
+struct FruServiceOptions {
+  bool enable_authorization = true;
+  ::milotic::redfish::BmcWebCertProvider *cert_provider = nullptr;
+};
+
+struct OngoingDevpathExtractionRequestCount {
+  absl::Mutex mutex;
+  uint32_t ongoing_devpath_extraction_request_count ABSL_GUARDED_BY(mutex);
+
+  explicit OngoingDevpathExtractionRequestCount(
+      uint32_t ongoing_devpath_extraction_request_count)
+      : ongoing_devpath_extraction_request_count(
+            ongoing_devpath_extraction_request_count) {}
+};
+
+class FruServiceImpl final : public grpc::FruService::CallbackService {
+ public:
+  explicit FruServiceImpl(App *app, const FruServiceOptions &options);
+
+  ServerUnaryReactor *GetAllFruInfo(CallbackServerContext *context,
+                                    const GetAllFruInfoRequest *request,
+                                    GetAllFruInfoResponse *response) override;
+
+ protected:
+  void GenerateMatchedDevpathsFromRedfishPaths(
+      ServerUnaryReactor &reactor, const GetAllFruInfoRequest &request,
+      GetAllFruInfoResponse &response);
+  void RequestRedfishAndExtractDevpaths(
+      ServerUnaryReactor &reactor, const GetAllFruInfoRequest &request,
+      GetAllFruInfoResponse &response,
+      absl::string_view root_chassis_location_code);
+
+  App *app_;
+};
+
+}  // namespace milotic_fast_sanity
+
+#endif  // THIRD_PARTY_MILOTIC_EXTERNAL_CC_FAST_SANITY_SERVICE_FRU_SERVICE_H_