Implement AggregationSource

Adds an AggregationSource resource for each satellite config present on
dbus.

Adds the AggregationSource schema which we had previously ignored.

Tested:
Querying an AggregationSource returned the expected information.

curl localhost/redfish/v1/AggregationService/AggregationSources/5B247A
{
  "@odata.id": "/redfish/v1/AggregationService/AggregationSources/5B247A",
  "@odata.type": "#AggregationSource.v1_3_1.AggregationSource",
  "HostName": "http://122.111.11.1:80",
  "Id": "5B247A",
  "Name": "Aggregation source",
  "Password": null,
}

Service Validator passed.  The Service Validator also passed after
removing the satellite config from the system such that
/redfish/v1/AggregationService/AggregationSources returns an empty
Members array.

Signed-off-by: Carson Labrado <clabrado@google.com>
Change-Id: I88b5fbc15f27cddd330ec22a25427fd8b18cf766
diff --git a/redfish-core/include/redfish.hpp b/redfish-core/include/redfish.hpp
index 954dfc7..cf41824 100644
--- a/redfish-core/include/redfish.hpp
+++ b/redfish-core/include/redfish.hpp
@@ -73,8 +73,9 @@
     {
         requestAccountServiceRoutes(app);
 #ifdef BMCWEB_ENABLE_REDFISH_AGGREGATION
-        requestAggregationServiceRoutes(app);
-        requestAggregationSourcesRoutes(app);
+        requestRoutesAggregationService(app);
+        requestRoutesAggregationSourceCollection(app);
+        requestRoutesAggregationSource(app);
 #endif
         requestRoutesRoles(app);
         requestRoutesRoleCollection(app);
diff --git a/redfish-core/include/redfish_aggregator.hpp b/redfish-core/include/redfish_aggregator.hpp
index de51ca6..85c1c15 100644
--- a/redfish-core/include/redfish_aggregator.hpp
+++ b/redfish-core/include/redfish_aggregator.hpp
@@ -239,55 +239,20 @@
     // Dummy callback used by the Constructor so that it can report the number
     // of satellite configs when the class is first created
     static void constructorCallback(
+        const boost::system::error_code& ec,
         const std::unordered_map<std::string, boost::urls::url>& satelliteInfo)
     {
+        if (ec)
+        {
+            BMCWEB_LOG_ERROR << "Something went wrong while querying dbus!";
+            return;
+        }
+
         BMCWEB_LOG_DEBUG << "There were "
                          << std::to_string(satelliteInfo.size())
                          << " satellite configs found at startup";
     }
 
-    // Polls D-Bus to get all available satellite config information
-    // Expects a handler which interacts with the returned configs
-    static void getSatelliteConfigs(
-        const std::function<void(
-            const std::unordered_map<std::string, boost::urls::url>&)>& handler)
-    {
-        BMCWEB_LOG_DEBUG << "Gathering satellite configs";
-        crow::connections::systemBus->async_method_call(
-            [handler](const boost::system::error_code& ec,
-                      const dbus::utility::ManagedObjectType& objects) {
-            if (ec)
-            {
-                BMCWEB_LOG_ERROR << "DBUS response error " << ec.value() << ", "
-                                 << ec.message();
-                return;
-            }
-
-            // Maps a chosen alias representing a satellite BMC to a url
-            // containing the information required to create a http
-            // connection to the satellite
-            std::unordered_map<std::string, boost::urls::url> satelliteInfo;
-
-            findSatelliteConfigs(objects, satelliteInfo);
-
-            if (!satelliteInfo.empty())
-            {
-                BMCWEB_LOG_DEBUG << "Redfish Aggregation enabled with "
-                                 << std::to_string(satelliteInfo.size())
-                                 << " satellite BMCs";
-            }
-            else
-            {
-                BMCWEB_LOG_DEBUG
-                    << "No satellite BMCs detected.  Redfish Aggregation not enabled";
-            }
-            handler(satelliteInfo);
-            },
-            "xyz.openbmc_project.EntityManager",
-            "/xyz/openbmc_project/inventory",
-            "org.freedesktop.DBus.ObjectManager", "GetManagedObjects");
-    }
-
     // Search D-Bus objects for satellite config objects and add their
     // information if valid
     static void findSatelliteConfigs(
@@ -492,12 +457,19 @@
         AggregationType isCollection,
         const std::shared_ptr<crow::Request>& sharedReq,
         const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+        const boost::system::error_code& ec,
         const std::unordered_map<std::string, boost::urls::url>& satelliteInfo)
     {
         if (sharedReq == nullptr)
         {
             return;
         }
+        // Something went wrong while querying dbus
+        if (ec)
+        {
+            messages::internalError(asyncResp->res);
+            return;
+        }
 
         // No satellite configs means we don't need to keep attempting to
         // aggregate
@@ -631,6 +603,51 @@
         return handler;
     }
 
+    // Polls D-Bus to get all available satellite config information
+    // Expects a handler which interacts with the returned configs
+    static void getSatelliteConfigs(
+        std::function<
+            void(const boost::system::error_code&,
+                 const std::unordered_map<std::string, boost::urls::url>&)>
+            handler)
+    {
+        BMCWEB_LOG_DEBUG << "Gathering satellite configs";
+        crow::connections::systemBus->async_method_call(
+            [handler{std::move(handler)}](
+                const boost::system::error_code& ec,
+                const dbus::utility::ManagedObjectType& objects) {
+            std::unordered_map<std::string, boost::urls::url> satelliteInfo;
+            if (ec)
+            {
+                BMCWEB_LOG_ERROR << "DBUS response error " << ec.value() << ", "
+                                 << ec.message();
+                handler(ec, satelliteInfo);
+                return;
+            }
+
+            // Maps a chosen alias representing a satellite BMC to a url
+            // containing the information required to create a http
+            // connection to the satellite
+            findSatelliteConfigs(objects, satelliteInfo);
+
+            if (!satelliteInfo.empty())
+            {
+                BMCWEB_LOG_DEBUG << "Redfish Aggregation enabled with "
+                                 << std::to_string(satelliteInfo.size())
+                                 << " satellite BMCs";
+            }
+            else
+            {
+                BMCWEB_LOG_DEBUG
+                    << "No satellite BMCs detected.  Redfish Aggregation not enabled";
+            }
+            handler(ec, satelliteInfo);
+            },
+            "xyz.openbmc_project.EntityManager",
+            "/xyz/openbmc_project/inventory",
+            "org.freedesktop.DBus.ObjectManager", "GetManagedObjects");
+    }
+
     // Processes the response returned by a satellite BMC and loads its
     // contents into asyncResp
     static void
@@ -863,7 +880,11 @@
             {
                 // We've matched a resource collection so this current segment
                 // might contain an aggregation prefix
-                if (collectionItem.starts_with("5B247A"))
+                // TODO: This needs to be rethought when we can support multiple
+                // satellites due to
+                // /redfish/v1/AggregationService/AggregationSources/5B247A
+                // being a local resource describing the satellite
+                if (collectionItem.starts_with("5B247A_"))
                 {
                     BMCWEB_LOG_DEBUG << "Need to forward a request";
 
diff --git a/redfish-core/include/schemas.hpp b/redfish-core/include/schemas.hpp
index c2cbb8b..8dd2d7b 100644
--- a/redfish-core/include/schemas.hpp
+++ b/redfish-core/include/schemas.hpp
@@ -18,6 +18,7 @@
         "AccountService",
         "ActionInfo",
         "AggregationService",
+        "AggregationSource",
         "AggregationSourceCollection",
         "Assembly",
         "AttributeRegistry",
diff --git a/redfish-core/lib/aggregation_service.hpp b/redfish-core/lib/aggregation_service.hpp
index f0eb651..1cd2e68 100644
--- a/redfish-core/lib/aggregation_service.hpp
+++ b/redfish-core/lib/aggregation_service.hpp
@@ -5,6 +5,7 @@
 #include "http_request.hpp"
 #include "http_response.hpp"
 #include "query.hpp"
+#include "redfish_aggregator.hpp"
 #include "registries/privilege_registry.hpp"
 
 #include <nlohmann/json.hpp>
@@ -50,7 +51,7 @@
         "/redfish/v1/AggregationService/AggregationSources";
 }
 
-inline void requestAggregationServiceRoutes(App& app)
+inline void requestRoutesAggregationService(App& app)
 {
     BMCWEB_ROUTE(app, "/redfish/v1/AggregationService/")
         .privileges(redfish::privileges::headAggregationService)
@@ -62,7 +63,31 @@
             std::bind_front(handleAggregationServiceGet, std::ref(app)));
 }
 
-inline void handleAggregationSourcesGet(
+inline void populateAggregationSourceCollection(
+    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+    const boost::system::error_code ec,
+    const std::unordered_map<std::string, boost::urls::url>& satelliteInfo)
+{
+    // Something went wrong while querying dbus
+    if (ec)
+    {
+        messages::internalError(asyncResp->res);
+        return;
+    }
+    nlohmann::json::array_t members = nlohmann::json::array();
+    for (const auto& sat : satelliteInfo)
+    {
+        nlohmann::json::object_t member;
+        member["@odata.id"] =
+            crow::utility::urlFromPieces("redfish", "v1", "AggregationService",
+                                         "AggregationSources", sat.first);
+        members.push_back(std::move(member));
+    }
+    asyncResp->res.jsonValue["Members@odata.count"] = members.size();
+    asyncResp->res.jsonValue["Members"] = std::move(members);
+}
+
+inline void handleAggregationSourceCollectionGet(
     App& app, const crow::Request& req,
     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
 {
@@ -78,18 +103,124 @@
     json["@odata.type"] =
         "#AggregationSourceCollection.AggregationSourceCollection";
     json["Name"] = "Aggregation Source Collection";
-    json["Members"] = nlohmann::json::array();
-    json["Members@odata.count"] = 0;
 
-    // TODO: Query D-Bus for satellite configs and add them to the Members array
+    // Query D-Bus for satellite configs and add them to the Members array
+    RedfishAggregator::getSatelliteConfigs(
+        std::bind_front(populateAggregationSourceCollection, asyncResp));
 }
 
-inline void requestAggregationSourcesRoutes(App& app)
+inline void handleAggregationSourceCollectionHead(
+    App& app, const crow::Request& req,
+    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
+{
+    if (!redfish::setUpRedfishRoute(app, req, asyncResp))
+    {
+        return;
+    }
+    asyncResp->res.addHeader(
+        boost::beast::http::field::link,
+        "</redfish/v1/JsonSchemas/AggregationService/AggregationSourceCollection.json>; rel=describedby");
+}
+
+inline void requestRoutesAggregationSourceCollection(App& app)
 {
     BMCWEB_ROUTE(app, "/redfish/v1/AggregationService/AggregationSources/")
-        .privileges(redfish::privileges::getAggregationService)
+        .privileges(redfish::privileges::getAggregationSourceCollection)
+        .methods(boost::beast::http::verb::get)(std::bind_front(
+            handleAggregationSourceCollectionGet, std::ref(app)));
+
+    BMCWEB_ROUTE(app, "/redfish/v1/AggregationService/AggregationSources/")
+        .privileges(redfish::privileges::getAggregationSourceCollection)
+        .methods(boost::beast::http::verb::head)(std::bind_front(
+            handleAggregationSourceCollectionHead, std::ref(app)));
+}
+
+inline void populateAggregationSource(
+    const std::string& aggregationSourceId,
+    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+    const boost::system::error_code ec,
+    const std::unordered_map<std::string, boost::urls::url>& satelliteInfo)
+{
+    asyncResp->res.addHeader(
+        boost::beast::http::field::link,
+        "</redfish/v1/JsonSchemas/AggregationSource/AggregationSource.json>; rel=describedby");
+
+    // Something went wrong while querying dbus
+    if (ec)
+    {
+        messages::internalError(asyncResp->res);
+        return;
+    }
+
+    const auto& sat = satelliteInfo.find(aggregationSourceId);
+    if (sat == satelliteInfo.end())
+    {
+        messages::resourceNotFound(asyncResp->res, "AggregationSource",
+                                   aggregationSourceId);
+        return;
+    }
+
+    asyncResp->res.jsonValue["@odata.id"] =
+        crow::utility::urlFromPieces("redfish", "v1", "AggregationService",
+                                     "AggregationSources", aggregationSourceId);
+    asyncResp->res.jsonValue["@odata.type"] =
+        "#AggregationSource.v1_3_1.AggregationSource";
+    asyncResp->res.jsonValue["Id"] = aggregationSourceId;
+
+    // TODO: We may want to change this whenever we support aggregating multiple
+    // satellite BMCs.  Otherwise all AggregationSource resources will have the
+    // same "Name".
+    // TODO: We should use the "Name" from the satellite config whenever we add
+    // support for including it in the data returned in satelliteInfo.
+    asyncResp->res.jsonValue["Name"] = "Aggregation source";
+    std::string hostName(sat->second.encoded_origin());
+    asyncResp->res.jsonValue["HostName"] = std::move(hostName);
+
+    // The Redfish spec requires Password to be null in responses
+    asyncResp->res.jsonValue["Password"] = nullptr;
+}
+
+inline void handleAggregationSourceGet(
+    App& app, const crow::Request& req,
+    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+    const std::string& aggregationSourceId)
+{
+    if (!redfish::setUpRedfishRoute(app, req, asyncResp))
+    {
+        return;
+    }
+
+    // Query D-Bus for satellite config corresponding to the specified
+    // AggregationSource
+    RedfishAggregator::getSatelliteConfigs(std::bind_front(
+        populateAggregationSource, aggregationSourceId, asyncResp));
+}
+
+inline void handleAggregationSourceHead(
+    App& app, const crow::Request& req,
+    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+    const std::string& aggregationSourceId)
+{
+    if (!redfish::setUpRedfishRoute(app, req, asyncResp))
+    {
+        return;
+    }
+    asyncResp->res.addHeader(
+        boost::beast::http::field::link,
+        "</redfish/v1/JsonSchemas/AggregationService/AggregationSource.json>; rel=describedby");
+
+    // Needed to prevent unused variable error
+    BMCWEB_LOG_DEBUG << "Added link header to response from "
+                     << aggregationSourceId;
+}
+
+inline void requestRoutesAggregationSource(App& app)
+{
+    BMCWEB_ROUTE(app,
+                 "/redfish/v1/AggregationService/AggregationSources/<str>/")
+        .privileges(redfish::privileges::getAggregationSource)
         .methods(boost::beast::http::verb::get)(
-            std::bind_front(handleAggregationSourcesGet, std::ref(app)));
+            std::bind_front(handleAggregationSourceGet, std::ref(app)));
 }
 
 } // namespace redfish