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