Implements FanCollection schema

The FanCollection schema is a resource in Redifsh version 2022.2 [1]
that represents the management properties for the monitoring and
management of cooling fans implemented by Redfish [2].

This commit retrieves the fan collection by obtaining the endpoints of
the `cooled_by` association. The `cooled_by` association represents the
relationship between a chassis and the fans responsible for providing
cooling to the chassis.

ref:
[1] https://www.dmtf.org/sites/default/files/standards/documents/DSP0268_2022.2.pdf
[2] http://redfish.dmtf.org/schemas/v1/Fan.v1_3_0.json

Redfish validator is currently failing. In order for the validator to
pass, it is necessary to merge this commit with
https://gerrit.openbmc.org/c/openbmc/bmcweb/+/57559

Tested:
1. doGet method to get FanCollection
```
curl -k -H "X-Auth-Token: $token" -X GET
https://${bmc}/redfish/v1/Chassis/chassis/ThermalSubsystem/Fans
{
  "@odata.id": "/redfish/v1/Chassis/chassis/ThermalSubsystem/Fans",
  "@odata.type": "#FanCollection.FanCollection",
  "Description": "The collection of Fan resource instances chassis",
  "Members": [
    {
      "@odata.id": "/redfish/v1/Chassis/chassis/ThermalSubsystem/Fans/fan5"
    },
    {
      "@odata.id": "/redfish/v1/Chassis/chassis/ThermalSubsystem/Fans/fan4"
    },
    {
      "@odata.id": "/redfish/v1/Chassis/chassis/ThermalSubsystem/Fans/fan3"
    },
    {
      "@odata.id": "/redfish/v1/Chassis/chassis/ThermalSubsystem/Fans/fan2"
    },
    {
      "@odata.id": "/redfish/v1/Chassis/chassis/ThermalSubsystem/Fans/fan1"
    },
    {
      "@odata.id": "/redfish/v1/Chassis/chassis/ThermalSubsystem/Fans/fan0"
    }
  ],
  "Members@odata.count": 6,
  "Name": "Fan Collection"
}

2. Input the wrong chassisId with the doGet method
curl -k https://${bmc}/redfish/v1/Chassis/chassis11/ThermalSubsystem/Fans
{
  "error": {
    "@Message.ExtendedInfo": [
      {
        "@odata.type": "#Message.v1_1_1.Message",
        "Message": "The requested resource of type Chassis
                    named 'chassis11' was not found.",
        "MessageArgs": [
          "Chassis",
          "chassis11"
        ],
        "MessageId": "Base.1.13.0.ResourceNotFound",
        "MessageSeverity": "Critical",
        "Resolution": "Provide a valid resource identifier and
                       resubmit the request."
      }
    ],
    "code": "Base.1.13.0.ResourceNotFound",
    "message": "The requested resource of type Chassis named
                'chassis11' was not found."
  }
}
```

Signed-off-by: George Liu <liuxiwei@inspur.com>
Change-Id: If5e9ff5655f444694c7ca1aea95d45e2c9222625
Signed-off-by: Lakshmi Yadlapati <lakshmiy@us.ibm.com>
diff --git a/Redfish.md b/Redfish.md
index f2fb141..4607589 100644
--- a/Redfish.md
+++ b/Redfish.md
@@ -312,6 +312,14 @@
 
 - Status
 
+#### /redfish/v1/Chassis/{ChassisId}/ThermalSubsystem/Fans
+
+##### FansCollection
+
+- Description
+- Members
+- Members@odata.count
+
 ### /redfish/v1/Chassis/{ChassisId}/Power#/PowerControl/{ControlName}/
 
 #### PowerControl
diff --git a/redfish-core/include/redfish.hpp b/redfish-core/include/redfish.hpp
index e97a832..dc8dc81 100644
--- a/redfish-core/include/redfish.hpp
+++ b/redfish-core/include/redfish.hpp
@@ -26,6 +26,7 @@
 #include "event_service.hpp"
 #include "eventservice_sse.hpp"
 #include "fabric_adapters.hpp"
+#include "fan.hpp"
 #include "hypervisor_system.hpp"
 #include "log_services.hpp"
 #include "manager_diagnostic_data.hpp"
@@ -95,6 +96,7 @@
         requestRoutesPowerSupply(app);
         requestRoutesPowerSupplyCollection(app);
         requestRoutesThermalSubsystem(app);
+        requestRoutesFanCollection(app);
 #endif
         requestRoutesManagerCollection(app);
         requestRoutesManager(app);
diff --git a/redfish-core/lib/fan.hpp b/redfish-core/lib/fan.hpp
new file mode 100644
index 0000000..3a9572f
--- /dev/null
+++ b/redfish-core/lib/fan.hpp
@@ -0,0 +1,159 @@
+#pragma once
+
+#include "app.hpp"
+#include "dbus_utility.hpp"
+#include "error_messages.hpp"
+#include "query.hpp"
+#include "registries/privilege_registry.hpp"
+#include "utils/chassis_utils.hpp"
+
+#include <boost/url/format.hpp>
+#include <sdbusplus/message/types.hpp>
+
+#include <functional>
+#include <memory>
+#include <optional>
+#include <string>
+#include <string_view>
+
+namespace redfish
+{
+constexpr std::array<std::string_view, 1> fanInterface = {
+    "xyz.openbmc_project.Inventory.Item.Fan"};
+
+inline void
+    updateFanList(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+                  const std::string& chassisId,
+                  const dbus::utility::MapperGetSubTreePathsResponse& fanPaths)
+{
+    nlohmann::json& fanList = asyncResp->res.jsonValue["Members"];
+    for (const std::string& fanPath : fanPaths)
+    {
+        std::string fanName =
+            sdbusplus::message::object_path(fanPath).filename();
+        if (fanName.empty())
+        {
+            continue;
+        }
+
+        nlohmann::json item = nlohmann::json::object();
+        item["@odata.id"] = boost::urls::format(
+            "/redfish/v1/Chassis/{}/ThermalSubsystem/Fans/{}", chassisId,
+            fanName);
+
+        fanList.emplace_back(std::move(item));
+    }
+    asyncResp->res.jsonValue["Members@odata.count"] = fanList.size();
+}
+
+inline void getFanPaths(
+    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+    const std::optional<std::string>& validChassisPath,
+    const std::function<void(const dbus::utility::MapperGetSubTreePathsResponse&
+                                 fanPaths)>& callback)
+{
+    sdbusplus::message::object_path endpointPath{*validChassisPath};
+    endpointPath /= "cooled_by";
+
+    dbus::utility::getAssociatedSubTreePaths(
+        endpointPath,
+        sdbusplus::message::object_path("/xyz/openbmc_project/inventory"), 0,
+        fanInterface,
+        [asyncResp, callback](
+            const boost::system::error_code& ec,
+            const dbus::utility::MapperGetSubTreePathsResponse& subtreePaths) {
+        if (ec)
+        {
+            if (ec.value() != EBADR)
+            {
+                BMCWEB_LOG_ERROR
+                    << "DBUS response error for getAssociatedSubTreePaths "
+                    << ec.value();
+                messages::internalError(asyncResp->res);
+            }
+            return;
+        }
+        callback(subtreePaths);
+        });
+}
+
+inline void doFanCollection(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+                            const std::string& chassisId,
+                            const std::optional<std::string>& validChassisPath)
+{
+    if (!validChassisPath)
+    {
+        messages::resourceNotFound(asyncResp->res, "Chassis", chassisId);
+        return;
+    }
+
+    asyncResp->res.addHeader(
+        boost::beast::http::field::link,
+        "</redfish/v1/JsonSchemas/FanCollection/FanCollection.json>; rel=describedby");
+    asyncResp->res.jsonValue["@odata.type"] = "#FanCollection.FanCollection";
+    asyncResp->res.jsonValue["@odata.id"] = boost::urls::format(
+        "/redfish/v1/Chassis/{}/ThermalSubsystem/Fans", chassisId);
+    asyncResp->res.jsonValue["Name"] = "Fan Collection";
+    asyncResp->res.jsonValue["Description"] =
+        "The collection of Fan resource instances " + chassisId;
+    asyncResp->res.jsonValue["Members"] = nlohmann::json::array();
+    asyncResp->res.jsonValue["Members@odata.count"] = 0;
+
+    getFanPaths(asyncResp, validChassisPath,
+                std::bind_front(updateFanList, asyncResp, chassisId));
+}
+
+inline void
+    handleFanCollectionHead(App& app, const crow::Request& req,
+                            const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+                            const std::string& chassisId)
+{
+    if (!redfish::setUpRedfishRoute(app, req, asyncResp))
+    {
+        return;
+    }
+
+    redfish::chassis_utils::getValidChassisPath(
+        asyncResp, chassisId,
+        [asyncResp,
+         chassisId](const std::optional<std::string>& validChassisPath) {
+        if (!validChassisPath)
+        {
+            messages::resourceNotFound(asyncResp->res, "Chassis", chassisId);
+            return;
+        }
+        asyncResp->res.addHeader(
+            boost::beast::http::field::link,
+            "</redfish/v1/JsonSchemas/FanCollection/FanCollection.json>; rel=describedby");
+        });
+}
+
+inline void
+    handleFanCollectionGet(App& app, const crow::Request& req,
+                           const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+                           const std::string& chassisId)
+{
+    if (!redfish::setUpRedfishRoute(app, req, asyncResp))
+    {
+        return;
+    }
+
+    redfish::chassis_utils::getValidChassisPath(
+        asyncResp, chassisId,
+        std::bind_front(doFanCollection, asyncResp, chassisId));
+}
+
+inline void requestRoutesFanCollection(App& app)
+{
+    BMCWEB_ROUTE(app, "/redfish/v1/Chassis/<str>/ThermalSubsystem/Fans/")
+        .privileges(redfish::privileges::headFanCollection)
+        .methods(boost::beast::http::verb::head)(
+            std::bind_front(handleFanCollectionHead, std::ref(app)));
+
+    BMCWEB_ROUTE(app, "/redfish/v1/Chassis/<str>/ThermalSubsystem/Fans/")
+        .privileges(redfish::privileges::getFanCollection)
+        .methods(boost::beast::http::verb::get)(
+            std::bind_front(handleFanCollectionGet, std::ref(app)));
+}
+
+} // namespace redfish
diff --git a/redfish-core/lib/thermal_subsystem.hpp b/redfish-core/lib/thermal_subsystem.hpp
index 6528a55..804b849 100644
--- a/redfish-core/lib/thermal_subsystem.hpp
+++ b/redfish-core/lib/thermal_subsystem.hpp
@@ -38,6 +38,9 @@
     asyncResp->res.jsonValue["@odata.id"] = boost::urls::format(
         "/redfish/v1/Chassis/{}/ThermalSubsystem", chassisId);
 
+    asyncResp->res.jsonValue["Fans"]["@odata.id"] = boost::urls::format(
+        "/redfish/v1/Chassis/{}/ThermalSubsystem/Fans", chassisId);
+
     asyncResp->res.jsonValue["Status"]["State"] = "Enabled";
     asyncResp->res.jsonValue["Status"]["Health"] = "OK";
 }