xref: /openbmc/bmcweb/features/redfish/include/redfish_aggregator.hpp (revision 11987af6e58a2d341c97f752e90c929e0a1189e5)
1 #pragma once
2 
3 #include <aggregation_utils.hpp>
4 #include <boost/algorithm/string/predicate.hpp>
5 #include <dbus_utility.hpp>
6 #include <error_messages.hpp>
7 #include <http_client.hpp>
8 #include <http_connection.hpp>
9 
10 #include <array>
11 
12 namespace redfish
13 {
14 
15 enum class Result
16 {
17     LocalHandle,
18     NoLocalHandle
19 };
20 
21 // clang-format off
22 // These are all of the properties as of version 2022.2 of the Redfish Resource
23 // and Schema Guide whose Type is "string (URI)" and the name does not end in a
24 // case-insensitive form of "uri".  That version of the schema is associated
25 // with version 1.16.0 of the Redfish Specification.  Going forward, new URI
26 // properties should end in URI so this list should not need to be maintained as
27 // the spec is updated.  NOTE: These have been pre-sorted in order to be
28 // compatible with binary search
29 constexpr std::array nonUriProperties{
30     "@Redfish.ActionInfo",
31     // "@odata.context", // We can't fix /redfish/v1/$metadata URIs
32     "@odata.id",
33     // "Destination", // Only used by EventService and won't be a Redfish URI
34     // "HostName", // Isn't actually a Redfish URI
35     "Image",
36     "MetricProperty",
37     "OriginOfCondition",
38     "TaskMonitor",
39     "target", // normal string, but target URI for POST to invoke an action
40 };
41 // clang-format on
42 
43 // Determines if the passed property contains a URI.  Those property names
44 // either end with a case-insensitive version of "uri" or are specifically
45 // defined in the above array.
46 inline bool isPropertyUri(const std::string_view propertyName)
47 {
48     return boost::iends_with(propertyName, "uri") ||
49            std::binary_search(nonUriProperties.begin(), nonUriProperties.end(),
50                               propertyName);
51 }
52 
53 static void addPrefixToItem(nlohmann::json& item, std::string_view prefix)
54 {
55     std::string* strValue = item.get_ptr<std::string*>();
56     if (strValue == nullptr)
57     {
58         BMCWEB_LOG_CRITICAL << "Field wasn't a string????";
59         return;
60     }
61     // Make sure the value is a properly formatted URI
62     auto parsed = boost::urls::parse_relative_ref(*strValue);
63     if (!parsed)
64     {
65         BMCWEB_LOG_CRITICAL << "Couldn't parse URI from resource " << *strValue;
66         return;
67     }
68 
69     boost::urls::url_view thisUrl = *parsed;
70 
71     // We don't need to aggregate JsonSchemas due to potential issues such as
72     // version mismatches between aggregator and satellite BMCs.  For now
73     // assume that the aggregator has all the schemas and versions that the
74     // aggregated server has.
75     if (crow::utility::readUrlSegments(thisUrl, "redfish", "v1", "JsonSchemas",
76                                        crow::utility::OrMorePaths()))
77     {
78         BMCWEB_LOG_DEBUG << "Skipping JsonSchemas URI prefix fixing";
79         return;
80     }
81 
82     // The first two segments should be "/redfish/v1".  We need to check that
83     // before we can search topCollections
84     if (!crow::utility::readUrlSegments(thisUrl, "redfish", "v1",
85                                         crow::utility::OrMorePaths()))
86     {
87         return;
88     }
89 
90     // Check array adding a segment each time until collection is identified
91     // Add prefix to segment after the collection
92     const boost::urls::segments_view urlSegments = thisUrl.segments();
93     bool addedPrefix = false;
94     boost::urls::url url("/");
95     boost::urls::segments_view::iterator it = urlSegments.begin();
96     const boost::urls::segments_view::const_iterator end = urlSegments.end();
97 
98     // Skip past the leading "/redfish/v1"
99     it++;
100     it++;
101     for (; it != end; it++)
102     {
103         // Trailing "/" will result in an empty segment.  In that case we need
104         // to return so we don't apply a prefix to top level collections such
105         // as "/redfish/v1/Chassis/"
106         if ((*it).empty())
107         {
108             return;
109         }
110 
111         if (std::binary_search(topCollections.begin(), topCollections.end(),
112                                url.buffer()))
113         {
114             std::string collectionItem(prefix);
115             collectionItem += "_" + (*it);
116             url.segments().push_back(collectionItem);
117             it++;
118             addedPrefix = true;
119             break;
120         }
121 
122         url.segments().push_back(*it);
123     }
124 
125     // Finish constructing the URL here (if needed) to avoid additional checks
126     for (; it != end; it++)
127     {
128         url.segments().push_back(*it);
129     }
130 
131     if (addedPrefix)
132     {
133         url.segments().insert(url.segments().begin(), {"redfish", "v1"});
134         item = url;
135     }
136 }
137 
138 // Search the json for all URIs and add the supplied prefix if the URI is for
139 // an aggregated resource.
140 static void addPrefixes(nlohmann::json& json, std::string_view prefix)
141 {
142     nlohmann::json::object_t* object =
143         json.get_ptr<nlohmann::json::object_t*>();
144     if (object != nullptr)
145     {
146         for (std::pair<const std::string, nlohmann::json>& item : *object)
147         {
148             if (isPropertyUri(item.first))
149             {
150                 addPrefixToItem(item.second, prefix);
151                 continue;
152             }
153 
154             // Recusively parse the rest of the json
155             addPrefixes(item.second, prefix);
156         }
157         return;
158     }
159     nlohmann::json::array_t* array = json.get_ptr<nlohmann::json::array_t*>();
160     if (array != nullptr)
161     {
162         for (nlohmann::json& item : *array)
163         {
164             addPrefixes(item, prefix);
165         }
166     }
167 }
168 
169 class RedfishAggregator
170 {
171   private:
172     const std::string retryPolicyName = "RedfishAggregation";
173     const std::string retryPolicyAction = "TerminateAfterRetries";
174     const uint32_t retryAttempts = 1;
175     const uint32_t retryTimeoutInterval = 0;
176     const std::string id = "Aggregator";
177 
178     RedfishAggregator()
179     {
180         getSatelliteConfigs(constructorCallback);
181 
182         // Setup the retry policy to be used by Redfish Aggregation
183         crow::HttpClient::getInstance().setRetryConfig(
184             retryAttempts, retryTimeoutInterval, aggregationRetryHandler,
185             retryPolicyName);
186         crow::HttpClient::getInstance().setRetryPolicy(retryPolicyAction,
187                                                        retryPolicyName);
188     }
189 
190     static inline boost::system::error_code
191         aggregationRetryHandler(unsigned int respCode)
192     {
193         // As a default, assume 200X is alright.
194         // We don't need to retry on a 404
195         if ((respCode < 200) || ((respCode >= 300) && (respCode != 404)))
196         {
197             return boost::system::errc::make_error_code(
198                 boost::system::errc::result_out_of_range);
199         }
200 
201         // Return 0 if the response code is valid
202         return boost::system::errc::make_error_code(
203             boost::system::errc::success);
204     }
205 
206     // Dummy callback used by the Constructor so that it can report the number
207     // of satellite configs when the class is first created
208     static void constructorCallback(
209         const std::unordered_map<std::string, boost::urls::url>& satelliteInfo)
210     {
211         BMCWEB_LOG_DEBUG << "There were "
212                          << std::to_string(satelliteInfo.size())
213                          << " satellite configs found at startup";
214     }
215 
216     // Polls D-Bus to get all available satellite config information
217     // Expects a handler which interacts with the returned configs
218     static void getSatelliteConfigs(
219         const std::function<void(
220             const std::unordered_map<std::string, boost::urls::url>&)>& handler)
221     {
222         BMCWEB_LOG_DEBUG << "Gathering satellite configs";
223         crow::connections::systemBus->async_method_call(
224             [handler](const boost::system::error_code ec,
225                       const dbus::utility::ManagedObjectType& objects) {
226             if (ec)
227             {
228                 BMCWEB_LOG_ERROR << "DBUS response error " << ec.value() << ", "
229                                  << ec.message();
230                 return;
231             }
232 
233             // Maps a chosen alias representing a satellite BMC to a url
234             // containing the information required to create a http
235             // connection to the satellite
236             std::unordered_map<std::string, boost::urls::url> satelliteInfo;
237 
238             findSatelliteConfigs(objects, satelliteInfo);
239 
240             if (!satelliteInfo.empty())
241             {
242                 BMCWEB_LOG_DEBUG << "Redfish Aggregation enabled with "
243                                  << std::to_string(satelliteInfo.size())
244                                  << " satellite BMCs";
245             }
246             else
247             {
248                 BMCWEB_LOG_DEBUG
249                     << "No satellite BMCs detected.  Redfish Aggregation not enabled";
250             }
251             handler(satelliteInfo);
252             },
253             "xyz.openbmc_project.EntityManager",
254             "/xyz/openbmc_project/inventory",
255             "org.freedesktop.DBus.ObjectManager", "GetManagedObjects");
256     }
257 
258     // Search D-Bus objects for satellite config objects and add their
259     // information if valid
260     static void findSatelliteConfigs(
261         const dbus::utility::ManagedObjectType& objects,
262         std::unordered_map<std::string, boost::urls::url>& satelliteInfo)
263     {
264         for (const auto& objectPath : objects)
265         {
266             for (const auto& interface : objectPath.second)
267             {
268                 if (interface.first ==
269                     "xyz.openbmc_project.Configuration.SatelliteController")
270                 {
271                     BMCWEB_LOG_DEBUG << "Found Satellite Controller at "
272                                      << objectPath.first.str;
273 
274                     if (!satelliteInfo.empty())
275                     {
276                         BMCWEB_LOG_ERROR
277                             << "Redfish Aggregation only supports one satellite!";
278                         BMCWEB_LOG_DEBUG << "Clearing all satellite data";
279                         satelliteInfo.clear();
280                         return;
281                     }
282 
283                     // For now assume there will only be one satellite config.
284                     // Assign it the name/prefix "5B247A"
285                     addSatelliteConfig("5B247A", interface.second,
286                                        satelliteInfo);
287                 }
288             }
289         }
290     }
291 
292     // Parse the properties of a satellite config object and add the
293     // configuration if the properties are valid
294     static void addSatelliteConfig(
295         const std::string& name,
296         const dbus::utility::DBusPropertiesMap& properties,
297         std::unordered_map<std::string, boost::urls::url>& satelliteInfo)
298     {
299         boost::urls::url url;
300 
301         for (const auto& prop : properties)
302         {
303             if (prop.first == "Hostname")
304             {
305                 const std::string* propVal =
306                     std::get_if<std::string>(&prop.second);
307                 if (propVal == nullptr)
308                 {
309                     BMCWEB_LOG_ERROR << "Invalid Hostname value";
310                     return;
311                 }
312                 url.set_host(*propVal);
313             }
314 
315             else if (prop.first == "Port")
316             {
317                 const uint64_t* propVal = std::get_if<uint64_t>(&prop.second);
318                 if (propVal == nullptr)
319                 {
320                     BMCWEB_LOG_ERROR << "Invalid Port value";
321                     return;
322                 }
323 
324                 if (*propVal > std::numeric_limits<uint16_t>::max())
325                 {
326                     BMCWEB_LOG_ERROR << "Port value out of range";
327                     return;
328                 }
329                 url.set_port(std::to_string(static_cast<uint16_t>(*propVal)));
330             }
331 
332             else if (prop.first == "AuthType")
333             {
334                 const std::string* propVal =
335                     std::get_if<std::string>(&prop.second);
336                 if (propVal == nullptr)
337                 {
338                     BMCWEB_LOG_ERROR << "Invalid AuthType value";
339                     return;
340                 }
341 
342                 // For now assume authentication not required to communicate
343                 // with the satellite BMC
344                 if (*propVal != "None")
345                 {
346                     BMCWEB_LOG_ERROR
347                         << "Unsupported AuthType value: " << *propVal
348                         << ", only \"none\" is supported";
349                     return;
350                 }
351                 url.set_scheme("http");
352             }
353         } // Finished reading properties
354 
355         // Make sure all required config information was made available
356         if (url.host().empty())
357         {
358             BMCWEB_LOG_ERROR << "Satellite config " << name << " missing Host";
359             return;
360         }
361 
362         if (!url.has_port())
363         {
364             BMCWEB_LOG_ERROR << "Satellite config " << name << " missing Port";
365             return;
366         }
367 
368         if (!url.has_scheme())
369         {
370             BMCWEB_LOG_ERROR << "Satellite config " << name
371                              << " missing AuthType";
372             return;
373         }
374 
375         std::string resultString;
376         auto result = satelliteInfo.insert_or_assign(name, std::move(url));
377         if (result.second)
378         {
379             resultString = "Added new satellite config ";
380         }
381         else
382         {
383             resultString = "Updated existing satellite config ";
384         }
385 
386         BMCWEB_LOG_DEBUG << resultString << name << " at "
387                          << result.first->second.scheme() << "://"
388                          << result.first->second.encoded_host_and_port();
389     }
390 
391     enum AggregationType
392     {
393         Collection,
394         Resource,
395     };
396 
397     static void
398         startAggregation(AggregationType isCollection,
399                          const crow::Request& thisReq,
400                          const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
401     {
402         if ((isCollection == AggregationType::Collection) &&
403             (thisReq.method() != boost::beast::http::verb::get))
404         {
405             BMCWEB_LOG_DEBUG
406                 << "Only aggregate GET requests to top level collections";
407             return;
408         }
409 
410         // Create a copy of thisReq so we we can still locally process the req
411         std::error_code ec;
412         auto localReq = std::make_shared<crow::Request>(thisReq.req, ec);
413         if (ec)
414         {
415             BMCWEB_LOG_ERROR << "Failed to create copy of request";
416             if (isCollection != AggregationType::Collection)
417             {
418                 messages::internalError(asyncResp->res);
419             }
420             return;
421         }
422 
423         getSatelliteConfigs(std::bind_front(aggregateAndHandle, isCollection,
424                                             localReq, asyncResp));
425     }
426 
427     static void findSatellite(
428         const crow::Request& req,
429         const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
430         const std::unordered_map<std::string, boost::urls::url>& satelliteInfo,
431         std::string_view memberName)
432     {
433         // Determine if the resource ID begins with a known prefix
434         for (const auto& satellite : satelliteInfo)
435         {
436             std::string targetPrefix = satellite.first;
437             targetPrefix += "_";
438             if (memberName.starts_with(targetPrefix))
439             {
440                 BMCWEB_LOG_DEBUG << "\"" << satellite.first
441                                  << "\" is a known prefix";
442 
443                 // Remove the known prefix from the request's URI and
444                 // then forward to the associated satellite BMC
445                 getInstance().forwardRequest(req, asyncResp, satellite.first,
446                                              satelliteInfo);
447                 return;
448             }
449         }
450 
451         // We didn't recognize the prefix and need to return a 404
452         std::string nameStr = req.urlView.segments().back();
453         messages::resourceNotFound(asyncResp->res, "", nameStr);
454     }
455 
456     // Intended to handle an incoming request based on if Redfish Aggregation
457     // is enabled.  Forwards request to satellite BMC if it exists.
458     static void aggregateAndHandle(
459         AggregationType isCollection,
460         const std::shared_ptr<crow::Request>& sharedReq,
461         const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
462         const std::unordered_map<std::string, boost::urls::url>& satelliteInfo)
463     {
464         if (sharedReq == nullptr)
465         {
466             return;
467         }
468 
469         // No satellite configs means we don't need to keep attempting to
470         // aggregate
471         if (satelliteInfo.empty())
472         {
473             // For collections we'll also handle the request locally so we
474             // don't need to write an error code
475             if (isCollection == AggregationType::Resource)
476             {
477                 std::string nameStr = sharedReq->urlView.segments().back();
478                 messages::resourceNotFound(asyncResp->res, "", nameStr);
479             }
480             return;
481         }
482 
483         const crow::Request& thisReq = *sharedReq;
484         BMCWEB_LOG_DEBUG << "Aggregation is enabled, begin processing of "
485                          << thisReq.target();
486 
487         // We previously determined the request is for a collection.  No need to
488         // check again
489         if (isCollection == AggregationType::Collection)
490         {
491             BMCWEB_LOG_DEBUG << "Aggregating a collection";
492             // We need to use a specific response handler and send the
493             // request to all known satellites
494             getInstance().forwardCollectionRequests(thisReq, asyncResp,
495                                                     satelliteInfo);
496             return;
497         }
498 
499         std::string updateServiceName;
500         std::string memberName;
501         if (crow::utility::readUrlSegments(
502                 thisReq.urlView, "redfish", "v1", "UpdateService",
503                 std::ref(updateServiceName), std::ref(memberName),
504                 crow::utility::OrMorePaths()))
505         {
506             // Must be FirmwareInventory or SoftwareInventory
507             findSatellite(thisReq, asyncResp, satelliteInfo, memberName);
508             return;
509         }
510 
511         std::string collectionName;
512         if (crow::utility::readUrlSegments(
513                 thisReq.urlView, "redfish", "v1", std::ref(collectionName),
514                 std::ref(memberName), crow::utility::OrMorePaths()))
515         {
516             findSatellite(thisReq, asyncResp, satelliteInfo, memberName);
517             return;
518         }
519 
520         // We shouldn't reach this point since we should've hit one of the
521         // previous exits
522         messages::internalError(asyncResp->res);
523     }
524 
525     // Attempt to forward a request to the satellite BMC associated with the
526     // prefix.
527     void forwardRequest(
528         const crow::Request& thisReq,
529         const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
530         const std::string& prefix,
531         const std::unordered_map<std::string, boost::urls::url>& satelliteInfo)
532     {
533         const auto& sat = satelliteInfo.find(prefix);
534         if (sat == satelliteInfo.end())
535         {
536             // Realistically this shouldn't get called since we perform an
537             // earlier check to make sure the prefix exists
538             BMCWEB_LOG_ERROR << "Unrecognized satellite prefix \"" << prefix
539                              << "\"";
540             return;
541         }
542 
543         // We need to strip the prefix from the request's path
544         std::string targetURI(thisReq.target());
545         size_t pos = targetURI.find(prefix + "_");
546         if (pos == std::string::npos)
547         {
548             // If this fails then something went wrong
549             BMCWEB_LOG_ERROR << "Error removing prefix \"" << prefix
550                              << "_\" from request URI";
551             messages::internalError(asyncResp->res);
552             return;
553         }
554         targetURI.erase(pos, prefix.size() + 1);
555 
556         std::function<void(crow::Response&)> cb =
557             std::bind_front(processResponse, prefix, asyncResp);
558 
559         std::string data = thisReq.req.body();
560         crow::HttpClient::getInstance().sendDataWithCallback(
561             data, id, std::string(sat->second.host()),
562             sat->second.port_number(), targetURI, false /*useSSL*/,
563             thisReq.fields, thisReq.method(), retryPolicyName, cb);
564     }
565 
566     // Forward a request for a collection URI to each known satellite BMC
567     void forwardCollectionRequests(
568         const crow::Request& thisReq,
569         const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
570         const std::unordered_map<std::string, boost::urls::url>& satelliteInfo)
571     {
572         for (const auto& sat : satelliteInfo)
573         {
574             std::function<void(crow::Response&)> cb = std::bind_front(
575                 processCollectionResponse, sat.first, asyncResp);
576 
577             std::string targetURI(thisReq.target());
578             std::string data = thisReq.req.body();
579             crow::HttpClient::getInstance().sendDataWithCallback(
580                 data, id, std::string(sat.second.host()),
581                 sat.second.port_number(), targetURI, false /*useSSL*/,
582                 thisReq.fields, thisReq.method(), retryPolicyName, cb);
583         }
584     }
585 
586     // Processes the response returned by a satellite BMC and loads its
587     // contents into asyncResp
588     static void
589         processResponse(std::string_view prefix,
590                         const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
591                         crow::Response& resp)
592     {
593         // No processing needed if the request wasn't successful
594         if (resp.resultInt() != 200)
595         {
596             BMCWEB_LOG_DEBUG << "No need to parse satellite response";
597             asyncResp->res.stringResponse = std::move(resp.stringResponse);
598             return;
599         }
600 
601         // The resp will not have a json component
602         // We need to create a json from resp's stringResponse
603         if (resp.getHeaderValue("Content-Type") == "application/json")
604         {
605             nlohmann::json jsonVal =
606                 nlohmann::json::parse(resp.body(), nullptr, false);
607             if (jsonVal.is_discarded())
608             {
609                 BMCWEB_LOG_ERROR << "Error parsing satellite response as JSON";
610                 messages::operationFailed(asyncResp->res);
611                 return;
612             }
613 
614             BMCWEB_LOG_DEBUG << "Successfully parsed satellite response";
615 
616             // TODO: For collections we  want to add the satellite responses to
617             // our response rather than just straight overwriting them if our
618             // local handling was successful (i.e. would return a 200).
619             addPrefixes(jsonVal, prefix);
620 
621             BMCWEB_LOG_DEBUG << "Added prefix to parsed satellite response";
622 
623             asyncResp->res.result(resp.result());
624             asyncResp->res.jsonValue = std::move(jsonVal);
625 
626             BMCWEB_LOG_DEBUG << "Finished writing asyncResp";
627         }
628         else
629         {
630             if (!resp.body().empty())
631             {
632                 // We received a 200 response without the correct Content-Type
633                 // so return an Operation Failed error
634                 BMCWEB_LOG_ERROR
635                     << "Satellite response must be of type \"application/json\"";
636                 messages::operationFailed(asyncResp->res);
637             }
638         }
639     }
640 
641     // Processes the collection response returned by a satellite BMC and merges
642     // its "@odata.id" values
643     static void processCollectionResponse(
644         const std::string& prefix,
645         const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
646         crow::Response& resp)
647     {
648         if (resp.resultInt() != 200)
649         {
650             BMCWEB_LOG_DEBUG
651                 << "Collection resource does not exist in satellite BMC \""
652                 << prefix << "\"";
653             // Return the error if we haven't had any successes
654             if (asyncResp->res.resultInt() != 200)
655             {
656                 asyncResp->res.stringResponse = std::move(resp.stringResponse);
657             }
658             return;
659         }
660 
661         // The resp will not have a json component
662         // We need to create a json from resp's stringResponse
663         if (resp.getHeaderValue("Content-Type") == "application/json")
664         {
665             nlohmann::json jsonVal =
666                 nlohmann::json::parse(resp.body(), nullptr, false);
667             if (jsonVal.is_discarded())
668             {
669                 BMCWEB_LOG_ERROR << "Error parsing satellite response as JSON";
670 
671                 // Notify the user if doing so won't overwrite a valid response
672                 if ((asyncResp->res.resultInt() != 200) &&
673                     (asyncResp->res.resultInt() != 502))
674                 {
675                     messages::operationFailed(asyncResp->res);
676                 }
677                 return;
678             }
679 
680             BMCWEB_LOG_DEBUG << "Successfully parsed satellite response";
681 
682             // Now we need to add the prefix to the URIs contained in the
683             // response.
684             addPrefixes(jsonVal, prefix);
685 
686             BMCWEB_LOG_DEBUG << "Added prefix to parsed satellite response";
687 
688             // If this resource collection does not exist on the aggregating bmc
689             // and has not already been added from processing the response from
690             // a different satellite then we need to completely overwrite
691             // asyncResp
692             if (asyncResp->res.resultInt() != 200)
693             {
694                 // We only want to aggregate collections that contain a
695                 // "Members" array
696                 if ((!jsonVal.contains("Members")) &&
697                     (!jsonVal["Members"].is_array()))
698                 {
699                     BMCWEB_LOG_DEBUG
700                         << "Skipping aggregating unsupported resource";
701                     return;
702                 }
703 
704                 BMCWEB_LOG_DEBUG
705                     << "Collection does not exist, overwriting asyncResp";
706                 asyncResp->res.result(resp.result());
707                 asyncResp->res.jsonValue = std::move(jsonVal);
708 
709                 BMCWEB_LOG_DEBUG << "Finished overwriting asyncResp";
710             }
711             else
712             {
713                 // We only want to aggregate collections that contain a
714                 // "Members" array
715                 if ((!asyncResp->res.jsonValue.contains("Members")) &&
716                     (!asyncResp->res.jsonValue["Members"].is_array()))
717 
718                 {
719                     BMCWEB_LOG_DEBUG
720                         << "Skipping aggregating unsupported resource";
721                     return;
722                 }
723 
724                 BMCWEB_LOG_DEBUG << "Adding aggregated resources from \""
725                                  << prefix << "\" to collection";
726 
727                 // TODO: This is a potential race condition with multiple
728                 // satellites and the aggregating bmc attempting to write to
729                 // update this array.  May need to cascade calls to the next
730                 // satellite at the end of this function.
731                 // This is presumably not a concern when there is only a single
732                 // satellite since the aggregating bmc should have completed
733                 // before the response is received from the satellite.
734 
735                 auto& members = asyncResp->res.jsonValue["Members"];
736                 auto& satMembers = jsonVal["Members"];
737                 for (auto& satMem : satMembers)
738                 {
739                     members.push_back(std::move(satMem));
740                 }
741                 asyncResp->res.jsonValue["Members@odata.count"] =
742                     members.size();
743 
744                 // TODO: Do we need to sort() after updating the array?
745             }
746         }
747         else
748         {
749             BMCWEB_LOG_ERROR << "Received unparsable response from \"" << prefix
750                              << "\"";
751             // We received as response that was not a json
752             // Notify the user only if we did not receive any valid responses,
753             // if the resource collection does not already exist on the
754             // aggregating BMC, and if we did not already set this warning due
755             // to a failure from a different satellite
756             if ((asyncResp->res.resultInt() != 200) &&
757                 (asyncResp->res.resultInt() != 502))
758             {
759                 messages::operationFailed(asyncResp->res);
760             }
761         }
762     } // End processCollectionResponse()
763 
764   public:
765     RedfishAggregator(const RedfishAggregator&) = delete;
766     RedfishAggregator& operator=(const RedfishAggregator&) = delete;
767     RedfishAggregator(RedfishAggregator&&) = delete;
768     RedfishAggregator& operator=(RedfishAggregator&&) = delete;
769     ~RedfishAggregator() = default;
770 
771     static RedfishAggregator& getInstance()
772     {
773         static RedfishAggregator handler;
774         return handler;
775     }
776 
777     // Entry point to Redfish Aggregation
778     // Returns Result stating whether or not we still need to locally handle the
779     // request
780     static Result
781         beginAggregation(const crow::Request& thisReq,
782                          const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
783     {
784         using crow::utility::OrMorePaths;
785         using crow::utility::readUrlSegments;
786         const boost::urls::url_view url = thisReq.urlView;
787         // UpdateService is the only top level resource that is not a Collection
788         if (readUrlSegments(url, "redfish", "v1", "UpdateService"))
789         {
790             return Result::LocalHandle;
791         }
792 
793         // We don't need to aggregate JsonSchemas due to potential issues such
794         // as version mismatches between aggregator and satellite BMCs.  For
795         // now assume that the aggregator has all the schemas and versions that
796         // the aggregated server has.
797         if (crow::utility::readUrlSegments(url, "redfish", "v1", "JsonSchemas",
798                                            crow::utility::OrMorePaths()))
799         {
800             return Result::LocalHandle;
801         }
802 
803         if (readUrlSegments(url, "redfish", "v1", "UpdateService",
804                             "SoftwareInventory") ||
805             readUrlSegments(url, "redfish", "v1", "UpdateService",
806                             "FirmwareInventory"))
807         {
808             startAggregation(AggregationType::Collection, thisReq, asyncResp);
809             return Result::LocalHandle;
810         }
811 
812         // Is the request for a resource collection?:
813         // /redfish/v1/<resource>
814         // e.g. /redfish/v1/Chassis
815         std::string collectionName;
816         if (readUrlSegments(url, "redfish", "v1", std::ref(collectionName)))
817         {
818             startAggregation(AggregationType::Collection, thisReq, asyncResp);
819             return Result::LocalHandle;
820         }
821 
822         // We know that the ID of an aggregated resource will begin with
823         // "5B247A".  For the most part the URI will begin like this:
824         // /redfish/v1/<resource>/<resource ID>
825         // Note, FirmwareInventory and SoftwareInventory are "special" because
826         // they are two levels deep, but still need aggregated
827         // /redfish/v1/UpdateService/FirmwareInventory/<FirmwareInventory ID>
828         // /redfish/v1/UpdateService/SoftwareInventory/<SoftwareInventory ID>
829         std::string memberName;
830         if (readUrlSegments(url, "redfish", "v1", "UpdateService",
831                             "SoftwareInventory", std::ref(memberName),
832                             OrMorePaths()) ||
833             readUrlSegments(url, "redfish", "v1", "UpdateService",
834                             "FirmwareInventory", std::ref(memberName),
835                             OrMorePaths()) ||
836             readUrlSegments(url, "redfish", "v1", std::ref(collectionName),
837                             std::ref(memberName), OrMorePaths()))
838         {
839             if (memberName.starts_with("5B247A"))
840             {
841                 BMCWEB_LOG_DEBUG << "Need to forward a request";
842 
843                 // Extract the prefix from the request's URI, retrieve the
844                 // associated satellite config information, and then forward the
845                 // request to that satellite.
846                 startAggregation(AggregationType::Resource, thisReq, asyncResp);
847                 return Result::NoLocalHandle;
848             }
849             return Result::LocalHandle;
850         }
851 
852         BMCWEB_LOG_DEBUG << "Aggregation not required";
853         return Result::LocalHandle;
854     }
855 };
856 
857 } // namespace redfish
858