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