Aggregation: Add satellite only links

Allows the aggregator to insert links to top level collections which are
only supported by the satellite bmc.

Tested:
Links were added to responses for collections which are not currently
supported by BMCWeb.  Also verified that top level collections and
satellite resources were still aggregated correctly.

curl localhost/redfish/v1
{
  ...
  "Fabrics": {
    "@odata.id": "/redfish/v1/Fabrics"
  },
  ...
}

curl localhost/redfish/v1/UpdateService
{
  ...
  "SoftwareInventory": {
    "@odata.id": "/redfish/v1/UpdateService/SoftwareInventory"
  }
}

The following $expand queries also returned as expected
curl -s 'localhost/redfish/v1?$expand=.($levels=1)'
curl -s 'localhost/redfish/v1/UpdateService?$expand=.($levels=1)'

Signed-off-by: Carson Labrado <clabrado@google.com>
Change-Id: Ie755f67bd28f81f6677670c09c9a210935ae0af9
diff --git a/redfish-core/include/redfish_aggregator.hpp b/redfish-core/include/redfish_aggregator.hpp
index a6920e8..7cae89a 100644
--- a/redfish-core/include/redfish_aggregator.hpp
+++ b/redfish-core/include/redfish_aggregator.hpp
@@ -527,20 +527,29 @@
     enum AggregationType
     {
         Collection,
+        ContainsSubordinate,
         Resource,
     };
 
     static void
-        startAggregation(AggregationType isCollection,
-                         const crow::Request& thisReq,
+        startAggregation(AggregationType aggType, const crow::Request& thisReq,
                          const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
     {
-        if ((isCollection == AggregationType::Collection) &&
-            (thisReq.method() != boost::beast::http::verb::get))
+        if (thisReq.method() != boost::beast::http::verb::get)
         {
-            BMCWEB_LOG_DEBUG
-                << "Only aggregate GET requests to top level collections";
-            return;
+            if (aggType == AggregationType::Collection)
+            {
+                BMCWEB_LOG_DEBUG
+                    << "Only aggregate GET requests to top level collections";
+                return;
+            }
+
+            if (aggType == AggregationType::ContainsSubordinate)
+            {
+                BMCWEB_LOG_DEBUG << "Only aggregate GET requests when uptree of"
+                                 << " a top level collection";
+                return;
+            }
         }
 
         // Create a copy of thisReq so we we can still locally process the req
@@ -549,15 +558,15 @@
         if (ec)
         {
             BMCWEB_LOG_ERROR << "Failed to create copy of request";
-            if (isCollection != AggregationType::Collection)
+            if (aggType == AggregationType::Resource)
             {
                 messages::internalError(asyncResp->res);
             }
             return;
         }
 
-        getSatelliteConfigs(std::bind_front(aggregateAndHandle, isCollection,
-                                            localReq, asyncResp));
+        getSatelliteConfigs(
+            std::bind_front(aggregateAndHandle, aggType, localReq, asyncResp));
     }
 
     static void findSatellite(
@@ -592,7 +601,7 @@
     // Intended to handle an incoming request based on if Redfish Aggregation
     // is enabled.  Forwards request to satellite BMC if it exists.
     static void aggregateAndHandle(
-        AggregationType isCollection,
+        AggregationType aggType,
         const std::shared_ptr<crow::Request>& sharedReq,
         const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
         const boost::system::error_code& ec,
@@ -613,9 +622,10 @@
         // aggregate
         if (satelliteInfo.empty())
         {
-            // For collections we'll also handle the request locally so we
+            // For collections or resources that can contain a subordinate
+            // top level collection we'll also handle the request locally so we
             // don't need to write an error code
-            if (isCollection == AggregationType::Resource)
+            if (aggType == AggregationType::Resource)
             {
                 std::string nameStr = sharedReq->url().segments().back();
                 messages::resourceNotFound(asyncResp->res, "", nameStr);
@@ -629,7 +639,7 @@
 
         // We previously determined the request is for a collection.  No need to
         // check again
-        if (isCollection == AggregationType::Collection)
+        if (aggType == AggregationType::Collection)
         {
             BMCWEB_LOG_DEBUG << "Aggregating a collection";
             // We need to use a specific response handler and send the
@@ -639,6 +649,19 @@
             return;
         }
 
+        // We previously determined the request may contain a subordinate
+        // collection.  No need to check again
+        if (aggType == AggregationType::ContainsSubordinate)
+        {
+            BMCWEB_LOG_DEBUG
+                << "Aggregating what may have a subordinate collection";
+            // We need to use a specific response handler and send the
+            // request to all known satellites
+            getInstance().forwardContainsSubordinateRequests(thisReq, asyncResp,
+                                                             satelliteInfo);
+            return;
+        }
+
         const boost::urls::segments_view urlSegments = thisReq.url().segments();
         boost::urls::url currentUrl("/");
         boost::urls::segments_view::iterator it = urlSegments.begin();
@@ -728,6 +751,31 @@
         }
     }
 
+    // Forward request for a URI that is uptree of a top level collection to
+    // each known satellite BMC
+    void forwardContainsSubordinateRequests(
+        const crow::Request& thisReq,
+        const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+        const std::unordered_map<std::string, boost::urls::url>& satelliteInfo)
+    {
+        for (const auto& sat : satelliteInfo)
+        {
+            std::function<void(crow::Response&)> cb = std::bind_front(
+                processContainsSubordinateResponse, sat.first, asyncResp);
+
+            // will ignore an expanded resource in the response if that resource
+            // is not already supported by the aggregating BMC
+            // TODO: Improve the processing so that we don't have to strip query
+            // params in this specific case
+            std::string targetURI(thisReq.url().path());
+            std::string data = thisReq.req.body();
+            client.sendDataWithCallback(
+                std::move(data), std::string(sat.second.host()),
+                sat.second.port_number(), targetURI, false /*useSSL*/,
+                thisReq.fields(), thisReq.method(), cb);
+        }
+    }
+
   public:
     explicit RedfishAggregator(boost::asio::io_context& ioc) :
         client(ioc,
@@ -1060,12 +1108,12 @@
                     continue;
                 }
 
-                BMCWEB_LOG_DEBUG << "Adding link for " << *strValue
-                                 << " from BMC " << prefix;
                 addedLinks = true;
                 if (!asyncResp->res.jsonValue.contains(prop.first))
                 {
                     // Only add the property if it did not already exist
+                    BMCWEB_LOG_DEBUG << "Adding link for " << *strValue
+                                     << " from BMC " << prefix;
                     asyncResp->res.jsonValue[prop.first]["@odata.id"] =
                         *strValue;
                     continue;
@@ -1214,7 +1262,16 @@
             return Result::LocalHandle;
         }
 
-        BMCWEB_LOG_DEBUG << "Aggregation not required";
+        // If nothing else then the request could be for a resource which has a
+        // top level collection as a subordinate
+        if (searchCollectionsArray(url.path(), SearchType::ContainsSubordinate))
+        {
+            startAggregation(AggregationType::ContainsSubordinate, thisReq,
+                             asyncResp);
+            return Result::LocalHandle;
+        }
+
+        BMCWEB_LOG_DEBUG << "Aggregation not required for " << url.buffer();
         return Result::LocalHandle;
     }
 };