Query ThermalSubsystem/Fans for every Chassis

Fans can have machine level devpaths as well.

Querying them requires the prerequisite that we figure out all the chassis on Redfish.

Created a helper and unit tested separately.

#tlbmc-fast-sanity

Shows on machine correctly
https://paste.googleplex.com/6562953008250880

PiperOrigin-RevId: 783407804
Change-Id: I122d4b3c66d52756272d0a343ea516869291397e
diff --git a/tlbmc/service/fru_service.cc b/tlbmc/service/fru_service.cc
index 30af4f7..f0529c8 100644
--- a/tlbmc/service/fru_service.cc
+++ b/tlbmc/service/fru_service.cc
@@ -17,6 +17,7 @@
 #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"
@@ -318,6 +319,28 @@
   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) {
@@ -358,8 +381,18 @@
       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);
+                                     *root_chassis_location_code_status,
+                                     *chassis_urls);
   });
   app_->handle(*crow_request_status, async_resp);
 }
@@ -367,7 +400,8 @@
 void FruServiceImpl::RequestRedfishAndExtractDevpaths(
     ServerUnaryReactor &reactor, const GetAllFruInfoRequest &request,
     GetAllFruInfoResponse &response,
-    absl::string_view root_chassis_location_code) {
+    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
@@ -378,12 +412,20 @@
                         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>(
-              kRootUrls.size());
+              all_urls.size());
 
-  for (absl::string_view url : kRootUrls) {
+  for (absl::string_view url : all_urls) {
     absl::StatusOr<crow::Request> devpath_request_status =
         CreateRedfishRequest(url, boost::beast::http::verb::get);
     if (!devpath_request_status.ok()) {
diff --git a/tlbmc/service/fru_service.h b/tlbmc/service/fru_service.h
index 4a2bdc5..95c3acd 100644
--- a/tlbmc/service/fru_service.h
+++ b/tlbmc/service/fru_service.h
@@ -6,12 +6,15 @@
 #include <vector>
 
 #include "absl/base/thread_annotations.h"
+#include "absl/status/statusor.h"
 #include "absl/strings/string_view.h"
 #include "absl/synchronization/mutex.h"
+#include "absl/types/span.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 "fru_component_model.pb.h"
 #include "fru_service.grpc.pb.h"
 #include "app.hpp"
@@ -32,7 +35,7 @@
   absl::Mutex mutex;
   std::vector<FruComponent> fru_components ABSL_GUARDED_BY(mutex);
 
-  void AddFruComponent(FruComponent&& fru_component) {
+  void AddFruComponent(FruComponent &&fru_component) {
     absl::MutexLock lock(&mutex);
     fru_components.push_back(std::move(fru_component));
   }
@@ -69,11 +72,15 @@
   ::grpc::Status RequestIsAuthenticated(
       const ::grpc::AuthContext *auth_context) const;
 
+  static absl::StatusOr<std::vector<std::string>> GetAllChassisUrls(
+      const nlohmann::json &json);
+
  protected:
   void RequestRedfishAndExtractDevpaths(
       ::grpc::ServerUnaryReactor &reactor, const GetAllFruInfoRequest &request,
       GetAllFruInfoResponse &response,
-      absl::string_view root_chassis_location_code);
+      absl::string_view root_chassis_location_code,
+      absl::Span<const std::string> chassis_urls);
 
   App *app_;
   const bool has_trust_bundle_ = false;