Add topology links for Chassis

Implement the physical topology design[0] for Chassis resources to
report which Chassis contain or are contained by other Chassis.

Query this information from the object mapper using associations
defined in phosphor-dbus-interfaces[1] and make it available in the
Links section of the Redfish result.

[0] https://gerrit.openbmc.org/c/openbmc/docs/+/54205
[1] https://gerrit.openbmc.org/c/openbmc/phosphor-dbus-interfaces/+/58441

Tested:
Built for evb-ast2600 with example JSON configs and ran in QEMU

Ran Redfish Service Validator with no new warnings

Sample output:
'''
curl -k -u root:0penBmc -X GET https://127.0.0.1:60443/redfish/v1/Chassis/Subchassis
{
  "@odata.id": "/redfish/v1/Chassis/Subchassis",
  "@odata.type": "#Chassis.v1_16_0.Chassis",
...
  "Id": "Subchassis",
  "Links": {
    "ComputerSystems": [
      {
        "@odata.id": "/redfish/v1/Systems/system"
      }
    ],
    "ContainedBy": {
      "@odata.id": "/redfish/v1/Chassis/Superchassis"
    },
    "ManagedBy": [
      {
        "@odata.id": "/redfish/v1/Managers/bmc"
      }
    ]
  },
...
}

curl -k -u root:0penBmc -X GET https://127.0.0.1:60443/redfish/v1/Chassis/Superchassis
{
  "@odata.id": "/redfish/v1/Chassis/Superchassis",
  "@odata.type": "#Chassis.v1_16_0.Chassis",
...
  "Id": "Superchassis",
  "Links": {
    "ComputerSystems": [
      {
        "@odata.id": "/redfish/v1/Systems/system"
      }
    ],
    "Contains": [
      {
        "@odata.id": "/redfish/v1/Chassis/Subchassis"
      }
    ],
    "Contains@odata.count": 1,
    "ManagedBy": [
      {
        "@odata.id": "/redfish/v1/Managers/bmc"
      }
    ]
  },
...
}
'''

Signed-off-by: Jie Yang <jjy@google.com>
Signed-off-by: Zhenwei Chen <zhenweichen0207@gmail.com>
Signed-off-by: Benjamin Fair <benjaminfair@google.com>
Change-Id: Idc4c3e99b8269bcc5f94112e977a89970abd0bf3
diff --git a/redfish-core/lib/chassis.hpp b/redfish-core/lib/chassis.hpp
index 30ba4bc..fc89d03 100644
--- a/redfish-core/lib/chassis.hpp
+++ b/redfish-core/lib/chassis.hpp
@@ -211,6 +211,104 @@
         asyncResp, boost::urls::url("/redfish/v1/Chassis"), interfaces);
 }
 
+inline void getChassisContainedBy(
+    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+    const std::string& chassisId, const boost::system::error_code& ec,
+    const dbus::utility::MapperEndPoints& upstreamChassisPaths)
+{
+    if (ec)
+    {
+        if (ec.value() != EBADR)
+        {
+            BMCWEB_LOG_ERROR << "DBUS response error " << ec;
+            messages::internalError(asyncResp->res);
+        }
+        return;
+    }
+    if (upstreamChassisPaths.empty())
+    {
+        return;
+    }
+    if (upstreamChassisPaths.size() > 1)
+    {
+        BMCWEB_LOG_ERROR << chassisId << " is contained by mutliple chassis";
+        messages::internalError(asyncResp->res);
+        return;
+    }
+
+    sdbusplus::message::object_path upstreamChassisPath(
+        upstreamChassisPaths[0]);
+    std::string upstreamChassis = upstreamChassisPath.filename();
+    if (upstreamChassis.empty())
+    {
+        BMCWEB_LOG_WARNING << "Malformed upstream Chassis path "
+                           << upstreamChassisPath.str << " on " << chassisId;
+        return;
+    }
+
+    asyncResp->res.jsonValue["Links"]["ContainedBy"]["@odata.id"] =
+        boost::urls::format("/redfish/v1/Chassis/{}", upstreamChassis);
+}
+
+inline void getChassisContains(
+    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+    const std::string& chassisId, const boost::system::error_code& ec,
+    const dbus::utility::MapperEndPoints& downstreamChassisPaths)
+{
+    if (ec)
+    {
+        if (ec.value() != EBADR)
+        {
+            BMCWEB_LOG_ERROR << "DBUS response error " << ec;
+            messages::internalError(asyncResp->res);
+        }
+        return;
+    }
+    if (downstreamChassisPaths.empty())
+    {
+        return;
+    }
+    nlohmann::json& jValue = asyncResp->res.jsonValue["Links"]["Contains"];
+    if (!jValue.is_array())
+    {
+        // Create the array if it was empty
+        jValue = nlohmann::json::array();
+    }
+    for (const auto& p : downstreamChassisPaths)
+    {
+        sdbusplus::message::object_path downstreamChassisPath(p);
+        std::string downstreamChassis = downstreamChassisPath.filename();
+        if (downstreamChassis.empty())
+        {
+            BMCWEB_LOG_WARNING << "Malformed downstream Chassis path "
+                               << downstreamChassisPath.str << " on "
+                               << chassisId;
+            continue;
+        }
+        nlohmann::json link;
+        link["@odata.id"] = boost::urls::format("/redfish/v1/Chassis/{}",
+                                                downstreamChassis);
+        jValue.push_back(std::move(link));
+    }
+    asyncResp->res.jsonValue["Links"]["Contains@odata.count"] = jValue.size();
+}
+
+inline void
+    getChassisConnectivity(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+                           const std::string& chassisId,
+                           const std::string& chassisPath)
+{
+    BMCWEB_LOG_DEBUG << "Get chassis connectivity";
+
+    dbus::utility::getAssociationEndPoints(
+        chassisPath + "/contained_by",
+        std::bind_front(getChassisContainedBy, asyncResp, chassisId));
+
+    dbus::utility::getAssociationEndPoints(
+        chassisPath + "/containing",
+        std::bind_front(getChassisContains, asyncResp, chassisId));
+}
+
 /**
  * ChassisCollection derived class for delivering Chassis Collection Schema
  *  Functions triggers appropriate requests on DBus
@@ -304,6 +402,8 @@
                 continue;
             }
 
+            getChassisConnectivity(asyncResp, chassisId, path);
+
             auto health = std::make_shared<HealthPopulate>(asyncResp);
 
             if constexpr (bmcwebEnableHealthPopulate)