Redfish Aggregation: Router to satellite resources

Adds ability to route requests to either native resources or
resources that belong to a satellite BMC as part of Redfish
Aggregation.  A prefix in the URI denotes if the resource is actually
from a satellite BMC.  Prefixes are only used to denote satellite
resources.  The URI of resources on the local/aggregating BMC will
remain unchanged.

Prefixes are separated from the resource ID by an underscore.  This
means that underscores cannot be used in the prefix name itself.
The prefixes used by satellite BMCs are revealed via D-Bus as well as
the config information needed to connect to that BMC.

Requests for satellite resources will not be handled locally.

Care should be taken to not name any local resources in a way that
could cause a collision (e.g. having a Chassis object named
"aggregated0_1U" on the aggregating BMC).

The patch only covers routing requests.  Requests to collection URIs
like /redfish/v1/Chassis will only return resources on the local BMC.
A future patch will cover adding satellite resources to collections.

Also note that URIs returned in the responses will not have the proper
prefix included.  Fixing these URIs will be addressed in future
patches.

A number of TODO comments are included in the code to indicate that
this functionality (collections and URI fixup) still needs to be
implemented.

Example URIs w/o Redfish Aggregation:
/redfish/v1/Chassis/1U/
/redfish/v1/Systems/system/
/redfish/v1/Managers/bmc/

Example URIs after enabling Redfish Aggregation if the associated
resources are located on the local/aggregating BMC:
/redfish/v1/Chassis/1U/
/redfish/v1/Systems/system/
/redfish/v1/Managers/bmc/

Example URIs if resources are instead located on a satellite BMC
named "aggregated0":
/redfish/v1/Chassis/aggregated0_1U/
/redfish/v1/Systems/aggregated0_system/
/redfish/v1/Managers/aggregated0_bmc/

Tested:
I was able to query supported resources located on the local BMC
as well as on a satellite BMC.  Requests with unknown prefixes return
a 404.  Requests to resource collections only return the resources
that are located on the aggregating BMC.

Signed-off-by: Carson Labrado <clabrado@google.com>
Signed-off-by: Ed Tanous <edtanous@google.com>
Change-Id: I87a3deb730bda95e72ecd3144ea40b0e5ee7d491
diff --git a/http/http_client.hpp b/http/http_client.hpp
index 905700e..491030c 100644
--- a/http/http_client.hpp
+++ b/http/http_client.hpp
@@ -462,6 +462,12 @@
     void sendNext(bool keepAlive, uint32_t connId)
     {
         auto conn = connections[connId];
+
+        // Allow the connection's handler to be deleted
+        // This is needed because of Redfish Aggregation passing an
+        // AsyncResponse shared_ptr to this callback
+        conn->callback = nullptr;
+
         // Reuse the connection to send the next request in the queue
         if (!requestQueue.empty())
         {
diff --git a/http/http_response.hpp b/http/http_response.hpp
index 38bebb5..dd1f37a 100644
--- a/http/http_response.hpp
+++ b/http/http_response.hpp
@@ -122,6 +122,11 @@
         return stringResponse->body();
     }
 
+    std::string_view getHeaderValue(std::string_view key) const
+    {
+        return stringResponse->base()[key];
+    }
+
     void keepAlive(bool k)
     {
         stringResponse->keep_alive(k);
diff --git a/redfish-core/include/redfish_aggregator.hpp b/redfish-core/include/redfish_aggregator.hpp
index 0a5ff57..e0e3521 100644
--- a/redfish-core/include/redfish_aggregator.hpp
+++ b/redfish-core/include/redfish_aggregator.hpp
@@ -1,6 +1,9 @@
 #pragma once
 
+#include <dbus_utility.hpp>
+#include <error_messages.hpp>
 #include <http_client.hpp>
+#include <http_connection.hpp>
 
 namespace redfish
 {
@@ -11,27 +14,13 @@
     NoLocalHandle
 };
 
-// Checks if the provided path is related to one of the resource collections
-// under UpdateService
-static inline bool
-    isUpdateServiceCollection(const std::vector<std::string>& segments)
-{
-    if (segments.size() < 4)
-    {
-        return false;
-    }
-
-    return (segments[2] == "UpdateService") &&
-           ((segments[3] == "FirmwareInventory") ||
-            (segments[3] == "SoftwareInventory"));
-}
-
 class RedfishAggregator
 {
   private:
     const std::string retryPolicyName = "RedfishAggregation";
     const uint32_t retryAttempts = 5;
     const uint32_t retryTimeoutInterval = 0;
+    const std::string id = "Aggregator";
 
     RedfishAggregator()
     {
@@ -243,6 +232,203 @@
                          << result.first->second.encoded_host_and_port();
     }
 
+    enum AggregationType
+    {
+        Collection,
+        Resource,
+    };
+
+    static void
+        startAggregation(AggregationType isCollection,
+                         const crow::Request& thisReq,
+                         const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
+    {
+        // Create a copy of thisReq so we we can still locally process the req
+        std::error_code ec;
+        auto localReq = std::make_shared<crow::Request>(thisReq.req, ec);
+        if (ec)
+        {
+            BMCWEB_LOG_ERROR << "Failed to create copy of request";
+            if (isCollection != AggregationType::Collection)
+            {
+                messages::internalError(asyncResp->res);
+            }
+            return;
+        }
+
+        getSatelliteConfigs(std::bind_front(aggregateAndHandle, isCollection,
+                                            localReq, asyncResp));
+    }
+
+    static void findSatelite(
+        const crow::Request& req,
+        const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+        const std::unordered_map<std::string, boost::urls::url>& satelliteInfo,
+        std::string_view memberName)
+    {
+        // Determine if the resource ID begins with a known prefix
+        for (const auto& satellite : satelliteInfo)
+        {
+            std::string targetPrefix = satellite.first;
+            targetPrefix += "_";
+            if (memberName.starts_with(targetPrefix))
+            {
+                BMCWEB_LOG_DEBUG << "\"" << satellite.first
+                                 << "\" is a known prefix";
+
+                // Remove the known prefix from the request's URI and
+                // then forward to the associated satellite BMC
+                getInstance().forwardRequest(req, asyncResp, satellite.first,
+                                             satelliteInfo);
+                return;
+            }
+        }
+    }
+
+    // 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,
+        const std::shared_ptr<crow::Request>& sharedReq,
+        const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+        const std::unordered_map<std::string, boost::urls::url>& satelliteInfo)
+    {
+        if (sharedReq == nullptr)
+        {
+            return;
+        }
+        const crow::Request& thisReq = *sharedReq;
+        BMCWEB_LOG_DEBUG << "Aggregation is enabled, begin processing of "
+                         << thisReq.target();
+
+        // We previously determined the request is for a collection.  No need to
+        // check again
+        if (isCollection == AggregationType::Collection)
+        {
+            // TODO: This should instead be handled so that we can
+            // aggregate the satellite resource collections
+            BMCWEB_LOG_DEBUG << "Aggregating a collection";
+            return;
+        }
+
+        std::string updateServiceName;
+        std::string memberName;
+        if (crow::utility::readUrlSegments(
+                thisReq.urlView, "redfish", "v1", "UpdateService",
+                std::ref(updateServiceName), std::ref(memberName),
+                crow::utility::OrMorePaths()))
+        {
+            // Must be FirmwareInventory or SoftwareInventory
+            findSatelite(thisReq, asyncResp, satelliteInfo, memberName);
+            return;
+        }
+
+        std::string collectionName;
+        if (crow::utility::readUrlSegments(
+                thisReq.urlView, "redfish", "v1", std::ref(collectionName),
+                std::ref(memberName), crow::utility::OrMorePaths()))
+        {
+            findSatelite(thisReq, asyncResp, satelliteInfo, memberName);
+        }
+    }
+
+    // Attempt to forward a request to the satellite BMC associated with the
+    // prefix.
+    void forwardRequest(
+        const crow::Request& thisReq,
+        const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+        const std::string& prefix,
+        const std::unordered_map<std::string, boost::urls::url>& satelliteInfo)
+    {
+        const auto& sat = satelliteInfo.find(prefix);
+        if (sat == satelliteInfo.end())
+        {
+            // Realistically this shouldn't get called since we perform an
+            // earlier check to make sure the prefix exists
+            BMCWEB_LOG_ERROR << "Unrecognized satellite prefix \"" << prefix
+                             << "\"";
+            return;
+        }
+
+        // We need to strip the prefix from the request's path
+        std::string targetURI(thisReq.target());
+        size_t pos = targetURI.find(prefix + "_");
+        if (pos == std::string::npos)
+        {
+            // If this fails then something went wrong
+            BMCWEB_LOG_ERROR << "Error removing prefix \"" << prefix
+                             << "_\" from request URI";
+            messages::internalError(asyncResp->res);
+            return;
+        }
+        targetURI.erase(pos, prefix.size() + 1);
+
+        std::function<void(crow::Response&)> cb =
+            std::bind_front(processResponse, asyncResp);
+
+        std::string data = thisReq.req.body();
+        crow::HttpClient::getInstance().sendDataWithCallback(
+            data, id, std::string(sat->second.host()),
+            sat->second.port_number(), targetURI, thisReq.fields,
+            thisReq.method(), retryPolicyName, cb);
+    }
+
+    // Processes the response returned by a satellite BMC and loads its
+    // contents into asyncResp
+    static void
+        processResponse(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+                        crow::Response& resp)
+    {
+        // No processing needed if the request wasn't successful
+        if (resp.resultInt() != 200)
+        {
+            BMCWEB_LOG_DEBUG << "No need to parse satellite response";
+            asyncResp->res.stringResponse = std::move(resp.stringResponse);
+            return;
+        }
+
+        // The resp will not have a json component
+        // We need to create a json from resp's stringResponse
+        if (resp.getHeaderValue("Content-Type") == "application/json")
+        {
+            nlohmann::json jsonVal =
+                nlohmann::json::parse(resp.body(), nullptr, false);
+            if (jsonVal.is_discarded())
+            {
+                BMCWEB_LOG_ERROR << "Error parsing satellite response as JSON";
+                messages::operationFailed(asyncResp->res);
+                return;
+            }
+
+            BMCWEB_LOG_DEBUG << "Successfully parsed satellite response";
+
+            // TODO: For collections we  want to add the satellite responses to
+            // our response rather than just straight overwriting them if our
+            // local handling was successful (i.e. would return a 200).
+
+            asyncResp->res.stringResponse.emplace(
+                boost::beast::http::response<
+                    boost::beast::http::string_body>{});
+            asyncResp->res.result(resp.result());
+            asyncResp->res.jsonValue = std::move(jsonVal);
+
+            BMCWEB_LOG_DEBUG << "Finished writing asyncResp";
+            // TODO: Need to fix the URIs in the response so that they include
+            // the prefix
+        }
+        else
+        {
+            if (!resp.body().empty())
+            {
+                // We received a 200 response without the correct Content-Type
+                // so return an Operation Failed error
+                BMCWEB_LOG_ERROR
+                    << "Satellite response must be of type \"application/json\"";
+                messages::operationFailed(asyncResp->res);
+            }
+        }
+    }
+
   public:
     RedfishAggregator(const RedfishAggregator&) = delete;
     RedfishAggregator& operator=(const RedfishAggregator&) = delete;
@@ -271,6 +457,14 @@
         {
             return Result::LocalHandle;
         }
+        if (readUrlSegments(url, "redfish", "v1", "UpdateService",
+                            "SoftwareInventory") ||
+            readUrlSegments(url, "redfish", "v1", "UpdateService",
+                            "FirmwareInventory"))
+        {
+            startAggregation(AggregationType::Collection, thisReq, asyncResp);
+            return Result::LocalHandle;
+        }
 
         // Is the request for a resource collection?:
         // /redfish/v1/<resource>
@@ -278,6 +472,7 @@
         std::string collectionName;
         if (readUrlSegments(url, "redfish", "v1", std::ref(collectionName)))
         {
+            startAggregation(AggregationType::Collection, thisReq, asyncResp);
             return Result::LocalHandle;
         }
 
@@ -302,10 +497,10 @@
             {
                 BMCWEB_LOG_DEBUG << "Need to forward a request";
 
-                // TODO: Extract the prefix from the request's URI, retrieve
-                // the associated satellite config information, and then
-                // forward the request to that satellite.
-                redfish::messages::internalError(asyncResp->res);
+                // Extract the prefix from the request's URI, retrieve the
+                // associated satellite config information, and then forward the
+                // request to that satellite.
+                startAggregation(AggregationType::Resource, thisReq, asyncResp);
                 return Result::NoLocalHandle;
             }
             return Result::LocalHandle;