xref: /openbmc/bmcweb/features/redfish/include/redfish_aggregator.hpp (revision 39662a3be1877afd893b70d78a449f91ba36c260)
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.url().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->url().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 = thisReq.url().segments();
533         boost::urls::url currentUrl("/");
534         boost::urls::segments_view::iterator it = urlSegments.begin();
535         const boost::urls::segments_view::const_iterator end =
536             urlSegments.end();
537 
538         // Skip past the leading "/redfish/v1"
539         it++;
540         it++;
541         for (; it != end; it++)
542         {
543             if (std::binary_search(topCollections.begin(), topCollections.end(),
544                                    currentUrl.buffer()))
545             {
546                 // We've matched a resource collection so this current segment
547                 // must contain an aggregation prefix
548                 findSatellite(thisReq, asyncResp, satelliteInfo, *it);
549                 return;
550             }
551 
552             currentUrl.segments().push_back(*it);
553         }
554 
555         // We shouldn't reach this point since we should've hit one of the
556         // previous exits
557         messages::internalError(asyncResp->res);
558     }
559 
560     // Attempt to forward a request to the satellite BMC associated with the
561     // prefix.
562     void forwardRequest(
563         const crow::Request& thisReq,
564         const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
565         const std::string& prefix,
566         const std::unordered_map<std::string, boost::urls::url>& satelliteInfo)
567     {
568         const auto& sat = satelliteInfo.find(prefix);
569         if (sat == satelliteInfo.end())
570         {
571             // Realistically this shouldn't get called since we perform an
572             // earlier check to make sure the prefix exists
573             BMCWEB_LOG_ERROR << "Unrecognized satellite prefix \"" << prefix
574                              << "\"";
575             return;
576         }
577 
578         // We need to strip the prefix from the request's path
579         std::string targetURI(thisReq.target());
580         size_t pos = targetURI.find(prefix + "_");
581         if (pos == std::string::npos)
582         {
583             // If this fails then something went wrong
584             BMCWEB_LOG_ERROR << "Error removing prefix \"" << prefix
585                              << "_\" from request URI";
586             messages::internalError(asyncResp->res);
587             return;
588         }
589         targetURI.erase(pos, prefix.size() + 1);
590 
591         std::function<void(crow::Response&)> cb =
592             std::bind_front(processResponse, prefix, asyncResp);
593 
594         std::string data = thisReq.req.body();
595         crow::HttpClient::getInstance().sendDataWithCallback(
596             data, id, std::string(sat->second.host()),
597             sat->second.port_number(), targetURI, false /*useSSL*/,
598             thisReq.fields(), thisReq.method(), retryPolicyName, cb);
599     }
600 
601     // Forward a request for a collection URI to each known satellite BMC
602     void forwardCollectionRequests(
603         const crow::Request& thisReq,
604         const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
605         const std::unordered_map<std::string, boost::urls::url>& satelliteInfo)
606     {
607         for (const auto& sat : satelliteInfo)
608         {
609             std::function<void(crow::Response&)> cb = std::bind_front(
610                 processCollectionResponse, sat.first, asyncResp);
611 
612             std::string targetURI(thisReq.target());
613             std::string data = thisReq.req.body();
614             crow::HttpClient::getInstance().sendDataWithCallback(
615                 data, id, std::string(sat.second.host()),
616                 sat.second.port_number(), targetURI, false /*useSSL*/,
617                 thisReq.fields(), thisReq.method(), retryPolicyName, cb);
618         }
619     }
620 
621   public:
622     RedfishAggregator(const RedfishAggregator&) = delete;
623     RedfishAggregator& operator=(const RedfishAggregator&) = delete;
624     RedfishAggregator(RedfishAggregator&&) = delete;
625     RedfishAggregator& operator=(RedfishAggregator&&) = delete;
626     ~RedfishAggregator() = default;
627 
628     static RedfishAggregator& getInstance()
629     {
630         static RedfishAggregator handler;
631         return handler;
632     }
633 
634     // Processes the response returned by a satellite BMC and loads its
635     // contents into asyncResp
636     static void
637         processResponse(std::string_view prefix,
638                         const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
639                         crow::Response& resp)
640     {
641         // 429 and 502 mean we didn't actually send the request so don't
642         // overwrite the response headers in that case
643         if ((resp.resultInt() == 429) || (resp.resultInt() == 502))
644         {
645             asyncResp->res.result(resp.result());
646             return;
647         }
648 
649         // We want to attempt prefix fixing regardless of response code
650         // The resp will not have a json component
651         // We need to create a json from resp's stringResponse
652         if (resp.getHeaderValue("Content-Type") == "application/json")
653         {
654             nlohmann::json jsonVal =
655                 nlohmann::json::parse(resp.body(), nullptr, false);
656             if (jsonVal.is_discarded())
657             {
658                 BMCWEB_LOG_ERROR << "Error parsing satellite response as JSON";
659                 messages::operationFailed(asyncResp->res);
660                 return;
661             }
662 
663             BMCWEB_LOG_DEBUG << "Successfully parsed satellite response";
664 
665             addPrefixes(jsonVal, prefix);
666 
667             BMCWEB_LOG_DEBUG << "Added prefix to parsed satellite response";
668 
669             asyncResp->res.result(resp.result());
670             asyncResp->res.jsonValue = std::move(jsonVal);
671 
672             BMCWEB_LOG_DEBUG << "Finished writing asyncResp";
673         }
674         else
675         {
676             // We allow any Content-Type that is not "application/json" now
677             asyncResp->res.result(resp.result());
678             asyncResp->res.write(resp.body());
679         }
680         addAggregatedHeaders(asyncResp->res, resp, prefix);
681     }
682 
683     // Processes the collection response returned by a satellite BMC and merges
684     // its "@odata.id" values
685     static void processCollectionResponse(
686         const std::string& prefix,
687         const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
688         crow::Response& resp)
689     {
690         // 429 and 502 mean we didn't actually send the request so don't
691         // overwrite the response headers in that case
692         if ((resp.resultInt() == 429) || (resp.resultInt() == 502))
693         {
694             return;
695         }
696 
697         if (resp.resultInt() != 200)
698         {
699             BMCWEB_LOG_DEBUG
700                 << "Collection resource does not exist in satellite BMC \""
701                 << prefix << "\"";
702             // Return the error if we haven't had any successes
703             if (asyncResp->res.resultInt() != 200)
704             {
705                 asyncResp->res.stringResponse = std::move(resp.stringResponse);
706             }
707             return;
708         }
709 
710         // The resp will not have a json component
711         // We need to create a json from resp's stringResponse
712         if (resp.getHeaderValue("Content-Type") == "application/json")
713         {
714             nlohmann::json jsonVal =
715                 nlohmann::json::parse(resp.body(), nullptr, false);
716             if (jsonVal.is_discarded())
717             {
718                 BMCWEB_LOG_ERROR << "Error parsing satellite response as JSON";
719 
720                 // Notify the user if doing so won't overwrite a valid response
721                 if ((asyncResp->res.resultInt() != 200) &&
722                     (asyncResp->res.resultInt() != 429) &&
723                     (asyncResp->res.resultInt() != 502))
724                 {
725                     messages::operationFailed(asyncResp->res);
726                 }
727                 return;
728             }
729 
730             BMCWEB_LOG_DEBUG << "Successfully parsed satellite response";
731 
732             // Now we need to add the prefix to the URIs contained in the
733             // response.
734             addPrefixes(jsonVal, prefix);
735 
736             BMCWEB_LOG_DEBUG << "Added prefix to parsed satellite response";
737 
738             // If this resource collection does not exist on the aggregating bmc
739             // and has not already been added from processing the response from
740             // a different satellite then we need to completely overwrite
741             // asyncResp
742             if (asyncResp->res.resultInt() != 200)
743             {
744                 // We only want to aggregate collections that contain a
745                 // "Members" array
746                 if ((!jsonVal.contains("Members")) &&
747                     (!jsonVal["Members"].is_array()))
748                 {
749                     BMCWEB_LOG_DEBUG
750                         << "Skipping aggregating unsupported resource";
751                     return;
752                 }
753 
754                 BMCWEB_LOG_DEBUG
755                     << "Collection does not exist, overwriting asyncResp";
756                 asyncResp->res.result(resp.result());
757                 asyncResp->res.jsonValue = std::move(jsonVal);
758                 asyncResp->res.addHeader("Content-Type", "application/json");
759 
760                 BMCWEB_LOG_DEBUG << "Finished overwriting asyncResp";
761             }
762             else
763             {
764                 // We only want to aggregate collections that contain a
765                 // "Members" array
766                 if ((!asyncResp->res.jsonValue.contains("Members")) &&
767                     (!asyncResp->res.jsonValue["Members"].is_array()))
768 
769                 {
770                     BMCWEB_LOG_DEBUG
771                         << "Skipping aggregating unsupported resource";
772                     return;
773                 }
774 
775                 BMCWEB_LOG_DEBUG << "Adding aggregated resources from \""
776                                  << prefix << "\" to collection";
777 
778                 // TODO: This is a potential race condition with multiple
779                 // satellites and the aggregating bmc attempting to write to
780                 // update this array.  May need to cascade calls to the next
781                 // satellite at the end of this function.
782                 // This is presumably not a concern when there is only a single
783                 // satellite since the aggregating bmc should have completed
784                 // before the response is received from the satellite.
785 
786                 auto& members = asyncResp->res.jsonValue["Members"];
787                 auto& satMembers = jsonVal["Members"];
788                 for (auto& satMem : satMembers)
789                 {
790                     members.push_back(std::move(satMem));
791                 }
792                 asyncResp->res.jsonValue["Members@odata.count"] =
793                     members.size();
794 
795                 // TODO: Do we need to sort() after updating the array?
796             }
797         }
798         else
799         {
800             BMCWEB_LOG_ERROR << "Received unparsable response from \"" << prefix
801                              << "\"";
802             // We received a response that was not a json.
803             // Notify the user only if we did not receive any valid responses,
804             // if the resource collection does not already exist on the
805             // aggregating BMC, and if we did not already set this warning due
806             // to a failure from a different satellite
807             if ((asyncResp->res.resultInt() != 200) &&
808                 (asyncResp->res.resultInt() != 429) &&
809                 (asyncResp->res.resultInt() != 502))
810             {
811                 messages::operationFailed(asyncResp->res);
812             }
813         }
814     } // End processCollectionResponse()
815 
816     // Entry point to Redfish Aggregation
817     // Returns Result stating whether or not we still need to locally handle the
818     // request
819     static Result
820         beginAggregation(const crow::Request& thisReq,
821                          const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
822     {
823         using crow::utility::OrMorePaths;
824         using crow::utility::readUrlSegments;
825         const boost::urls::url_view url = thisReq.url();
826 
827         // We don't need to aggregate JsonSchemas due to potential issues such
828         // as version mismatches between aggregator and satellite BMCs.  For
829         // now assume that the aggregator has all the schemas and versions that
830         // the aggregated server has.
831         if (crow::utility::readUrlSegments(url, "redfish", "v1", "JsonSchemas",
832                                            crow::utility::OrMorePaths()))
833         {
834             return Result::LocalHandle;
835         }
836 
837         // The first two segments should be "/redfish/v1".  We need to check
838         // that before we can search topCollections
839         if (!crow::utility::readUrlSegments(url, "redfish", "v1",
840                                             crow::utility::OrMorePaths()))
841         {
842             return Result::LocalHandle;
843         }
844 
845         // Parse the URI to see if it begins with a known top level collection
846         // such as:
847         // /redfish/v1/Chassis
848         // /redfish/v1/UpdateService/FirmwareInventory
849         const boost::urls::segments_view urlSegments = url.segments();
850         boost::urls::url currentUrl("/");
851         boost::urls::segments_view::iterator it = urlSegments.begin();
852         const boost::urls::segments_view::const_iterator end =
853             urlSegments.end();
854 
855         // Skip past the leading "/redfish/v1"
856         it++;
857         it++;
858         for (; it != end; it++)
859         {
860             const std::string& collectionItem = *it;
861             if (std::binary_search(topCollections.begin(), topCollections.end(),
862                                    currentUrl.buffer()))
863             {
864                 // We've matched a resource collection so this current segment
865                 // might contain an aggregation prefix
866                 if (collectionItem.starts_with("5B247A"))
867                 {
868                     BMCWEB_LOG_DEBUG << "Need to forward a request";
869 
870                     // Extract the prefix from the request's URI, retrieve the
871                     // associated satellite config information, and then forward
872                     // the request to that satellite.
873                     startAggregation(AggregationType::Resource, thisReq,
874                                      asyncResp);
875                     return Result::NoLocalHandle;
876                 }
877 
878                 // Handle collection URI with a trailing backslash
879                 // e.g. /redfish/v1/Chassis/
880                 it++;
881                 if ((it == end) && collectionItem.empty())
882                 {
883                     startAggregation(AggregationType::Collection, thisReq,
884                                      asyncResp);
885                 }
886 
887                 // We didn't recognize the prefix or it's a collection with a
888                 // trailing "/".  In both cases we still want to locally handle
889                 // the request
890                 return Result::LocalHandle;
891             }
892 
893             currentUrl.segments().push_back(collectionItem);
894         }
895 
896         // If we made it here then currentUrl could contain a top level
897         // collection URI without a trailing "/", e.g. /redfish/v1/Chassis
898         if (std::binary_search(topCollections.begin(), topCollections.end(),
899                                currentUrl.buffer()))
900         {
901             startAggregation(AggregationType::Collection, thisReq, asyncResp);
902             return Result::LocalHandle;
903         }
904 
905         BMCWEB_LOG_DEBUG << "Aggregation not required";
906         return Result::LocalHandle;
907     }
908 };
909 
910 } // namespace redfish
911