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 boost::system::error_code& ec,
243         const std::unordered_map<std::string, boost::urls::url>& satelliteInfo)
244     {
245         if (ec)
246         {
247             BMCWEB_LOG_ERROR << "Something went wrong while querying dbus!";
248             return;
249         }
250 
251         BMCWEB_LOG_DEBUG << "There were "
252                          << std::to_string(satelliteInfo.size())
253                          << " satellite configs found at startup";
254     }
255 
256     // Search D-Bus objects for satellite config objects and add their
257     // information if valid
258     static void findSatelliteConfigs(
259         const dbus::utility::ManagedObjectType& objects,
260         std::unordered_map<std::string, boost::urls::url>& satelliteInfo)
261     {
262         for (const auto& objectPath : objects)
263         {
264             for (const auto& interface : objectPath.second)
265             {
266                 if (interface.first ==
267                     "xyz.openbmc_project.Configuration.SatelliteController")
268                 {
269                     BMCWEB_LOG_DEBUG << "Found Satellite Controller at "
270                                      << objectPath.first.str;
271 
272                     if (!satelliteInfo.empty())
273                     {
274                         BMCWEB_LOG_ERROR
275                             << "Redfish Aggregation only supports one satellite!";
276                         BMCWEB_LOG_DEBUG << "Clearing all satellite data";
277                         satelliteInfo.clear();
278                         return;
279                     }
280 
281                     // For now assume there will only be one satellite config.
282                     // Assign it the name/prefix "5B247A"
283                     addSatelliteConfig("5B247A", interface.second,
284                                        satelliteInfo);
285                 }
286             }
287         }
288     }
289 
290     // Parse the properties of a satellite config object and add the
291     // configuration if the properties are valid
292     static void addSatelliteConfig(
293         const std::string& name,
294         const dbus::utility::DBusPropertiesMap& properties,
295         std::unordered_map<std::string, boost::urls::url>& satelliteInfo)
296     {
297         boost::urls::url url;
298 
299         for (const auto& prop : properties)
300         {
301             if (prop.first == "Hostname")
302             {
303                 const std::string* propVal =
304                     std::get_if<std::string>(&prop.second);
305                 if (propVal == nullptr)
306                 {
307                     BMCWEB_LOG_ERROR << "Invalid Hostname value";
308                     return;
309                 }
310                 url.set_host(*propVal);
311             }
312 
313             else if (prop.first == "Port")
314             {
315                 const uint64_t* propVal = std::get_if<uint64_t>(&prop.second);
316                 if (propVal == nullptr)
317                 {
318                     BMCWEB_LOG_ERROR << "Invalid Port value";
319                     return;
320                 }
321 
322                 if (*propVal > std::numeric_limits<uint16_t>::max())
323                 {
324                     BMCWEB_LOG_ERROR << "Port value out of range";
325                     return;
326                 }
327                 url.set_port(std::to_string(static_cast<uint16_t>(*propVal)));
328             }
329 
330             else if (prop.first == "AuthType")
331             {
332                 const std::string* propVal =
333                     std::get_if<std::string>(&prop.second);
334                 if (propVal == nullptr)
335                 {
336                     BMCWEB_LOG_ERROR << "Invalid AuthType value";
337                     return;
338                 }
339 
340                 // For now assume authentication not required to communicate
341                 // with the satellite BMC
342                 if (*propVal != "None")
343                 {
344                     BMCWEB_LOG_ERROR
345                         << "Unsupported AuthType value: " << *propVal
346                         << ", only \"none\" is supported";
347                     return;
348                 }
349                 url.set_scheme("http");
350             }
351         } // Finished reading properties
352 
353         // Make sure all required config information was made available
354         if (url.host().empty())
355         {
356             BMCWEB_LOG_ERROR << "Satellite config " << name << " missing Host";
357             return;
358         }
359 
360         if (!url.has_port())
361         {
362             BMCWEB_LOG_ERROR << "Satellite config " << name << " missing Port";
363             return;
364         }
365 
366         if (!url.has_scheme())
367         {
368             BMCWEB_LOG_ERROR << "Satellite config " << name
369                              << " missing AuthType";
370             return;
371         }
372 
373         std::string resultString;
374         auto result = satelliteInfo.insert_or_assign(name, std::move(url));
375         if (result.second)
376         {
377             resultString = "Added new satellite config ";
378         }
379         else
380         {
381             resultString = "Updated existing satellite config ";
382         }
383 
384         BMCWEB_LOG_DEBUG << resultString << name << " at "
385                          << result.first->second.scheme() << "://"
386                          << result.first->second.encoded_host_and_port();
387     }
388 
389     enum AggregationType
390     {
391         Collection,
392         Resource,
393     };
394 
395     static void
396         startAggregation(AggregationType isCollection,
397                          const crow::Request& thisReq,
398                          const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
399     {
400         if ((isCollection == AggregationType::Collection) &&
401             (thisReq.method() != boost::beast::http::verb::get))
402         {
403             BMCWEB_LOG_DEBUG
404                 << "Only aggregate GET requests to top level collections";
405             return;
406         }
407 
408         // Create a copy of thisReq so we we can still locally process the req
409         std::error_code ec;
410         auto localReq = std::make_shared<crow::Request>(thisReq.req, ec);
411         if (ec)
412         {
413             BMCWEB_LOG_ERROR << "Failed to create copy of request";
414             if (isCollection != AggregationType::Collection)
415             {
416                 messages::internalError(asyncResp->res);
417             }
418             return;
419         }
420 
421         getSatelliteConfigs(std::bind_front(aggregateAndHandle, isCollection,
422                                             localReq, asyncResp));
423     }
424 
425     static void findSatellite(
426         const crow::Request& req,
427         const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
428         const std::unordered_map<std::string, boost::urls::url>& satelliteInfo,
429         std::string_view memberName)
430     {
431         // Determine if the resource ID begins with a known prefix
432         for (const auto& satellite : satelliteInfo)
433         {
434             std::string targetPrefix = satellite.first;
435             targetPrefix += "_";
436             if (memberName.starts_with(targetPrefix))
437             {
438                 BMCWEB_LOG_DEBUG << "\"" << satellite.first
439                                  << "\" is a known prefix";
440 
441                 // Remove the known prefix from the request's URI and
442                 // then forward to the associated satellite BMC
443                 getInstance().forwardRequest(req, asyncResp, satellite.first,
444                                              satelliteInfo);
445                 return;
446             }
447         }
448 
449         // We didn't recognize the prefix and need to return a 404
450         std::string nameStr = req.url().segments().back();
451         messages::resourceNotFound(asyncResp->res, "", nameStr);
452     }
453 
454     // Intended to handle an incoming request based on if Redfish Aggregation
455     // is enabled.  Forwards request to satellite BMC if it exists.
456     static void aggregateAndHandle(
457         AggregationType isCollection,
458         const std::shared_ptr<crow::Request>& sharedReq,
459         const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
460         const boost::system::error_code& ec,
461         const std::unordered_map<std::string, boost::urls::url>& satelliteInfo)
462     {
463         if (sharedReq == nullptr)
464         {
465             return;
466         }
467         // Something went wrong while querying dbus
468         if (ec)
469         {
470             messages::internalError(asyncResp->res);
471             return;
472         }
473 
474         // No satellite configs means we don't need to keep attempting to
475         // aggregate
476         if (satelliteInfo.empty())
477         {
478             // For collections we'll also handle the request locally so we
479             // don't need to write an error code
480             if (isCollection == AggregationType::Resource)
481             {
482                 std::string nameStr = sharedReq->url().segments().back();
483                 messages::resourceNotFound(asyncResp->res, "", nameStr);
484             }
485             return;
486         }
487 
488         const crow::Request& thisReq = *sharedReq;
489         BMCWEB_LOG_DEBUG << "Aggregation is enabled, begin processing of "
490                          << thisReq.target();
491 
492         // We previously determined the request is for a collection.  No need to
493         // check again
494         if (isCollection == AggregationType::Collection)
495         {
496             BMCWEB_LOG_DEBUG << "Aggregating a collection";
497             // We need to use a specific response handler and send the
498             // request to all known satellites
499             getInstance().forwardCollectionRequests(thisReq, asyncResp,
500                                                     satelliteInfo);
501             return;
502         }
503 
504         const boost::urls::segments_view urlSegments = thisReq.url().segments();
505         boost::urls::url currentUrl("/");
506         boost::urls::segments_view::iterator it = urlSegments.begin();
507         const boost::urls::segments_view::const_iterator end =
508             urlSegments.end();
509 
510         // Skip past the leading "/redfish/v1"
511         it++;
512         it++;
513         for (; it != end; it++)
514         {
515             if (std::binary_search(topCollections.begin(), topCollections.end(),
516                                    currentUrl.buffer()))
517             {
518                 // We've matched a resource collection so this current segment
519                 // must contain an aggregation prefix
520                 findSatellite(thisReq, asyncResp, satelliteInfo, *it);
521                 return;
522             }
523 
524             currentUrl.segments().push_back(*it);
525         }
526 
527         // We shouldn't reach this point since we should've hit one of the
528         // previous exits
529         messages::internalError(asyncResp->res);
530     }
531 
532     // Attempt to forward a request to the satellite BMC associated with the
533     // prefix.
534     void forwardRequest(
535         const crow::Request& thisReq,
536         const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
537         const std::string& prefix,
538         const std::unordered_map<std::string, boost::urls::url>& satelliteInfo)
539     {
540         const auto& sat = satelliteInfo.find(prefix);
541         if (sat == satelliteInfo.end())
542         {
543             // Realistically this shouldn't get called since we perform an
544             // earlier check to make sure the prefix exists
545             BMCWEB_LOG_ERROR << "Unrecognized satellite prefix \"" << prefix
546                              << "\"";
547             return;
548         }
549 
550         // We need to strip the prefix from the request's path
551         std::string targetURI(thisReq.target());
552         size_t pos = targetURI.find(prefix + "_");
553         if (pos == std::string::npos)
554         {
555             // If this fails then something went wrong
556             BMCWEB_LOG_ERROR << "Error removing prefix \"" << prefix
557                              << "_\" from request URI";
558             messages::internalError(asyncResp->res);
559             return;
560         }
561         targetURI.erase(pos, prefix.size() + 1);
562 
563         std::function<void(crow::Response&)> cb =
564             std::bind_front(processResponse, prefix, asyncResp);
565 
566         std::string data = thisReq.req.body();
567         crow::HttpClient::getInstance().sendDataWithCallback(
568             data, id, std::string(sat->second.host()),
569             sat->second.port_number(), targetURI, false /*useSSL*/,
570             thisReq.fields(), thisReq.method(), retryPolicyName, cb);
571     }
572 
573     // Forward a request for a collection URI to each known satellite BMC
574     void forwardCollectionRequests(
575         const crow::Request& thisReq,
576         const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
577         const std::unordered_map<std::string, boost::urls::url>& satelliteInfo)
578     {
579         for (const auto& sat : satelliteInfo)
580         {
581             std::function<void(crow::Response&)> cb = std::bind_front(
582                 processCollectionResponse, sat.first, asyncResp);
583 
584             std::string targetURI(thisReq.target());
585             std::string data = thisReq.req.body();
586             crow::HttpClient::getInstance().sendDataWithCallback(
587                 data, id, std::string(sat.second.host()),
588                 sat.second.port_number(), targetURI, false /*useSSL*/,
589                 thisReq.fields(), thisReq.method(), retryPolicyName, cb);
590         }
591     }
592 
593   public:
594     RedfishAggregator(const RedfishAggregator&) = delete;
595     RedfishAggregator& operator=(const RedfishAggregator&) = delete;
596     RedfishAggregator(RedfishAggregator&&) = delete;
597     RedfishAggregator& operator=(RedfishAggregator&&) = delete;
598     ~RedfishAggregator() = default;
599 
600     static RedfishAggregator& getInstance()
601     {
602         static RedfishAggregator handler;
603         return handler;
604     }
605 
606     // Polls D-Bus to get all available satellite config information
607     // Expects a handler which interacts with the returned configs
608     static void getSatelliteConfigs(
609         std::function<
610             void(const boost::system::error_code&,
611                  const std::unordered_map<std::string, boost::urls::url>&)>
612             handler)
613     {
614         BMCWEB_LOG_DEBUG << "Gathering satellite configs";
615         crow::connections::systemBus->async_method_call(
616             [handler{std::move(handler)}](
617                 const boost::system::error_code& ec,
618                 const dbus::utility::ManagedObjectType& objects) {
619             std::unordered_map<std::string, boost::urls::url> satelliteInfo;
620             if (ec)
621             {
622                 BMCWEB_LOG_ERROR << "DBUS response error " << ec.value() << ", "
623                                  << ec.message();
624                 handler(ec, satelliteInfo);
625                 return;
626             }
627 
628             // Maps a chosen alias representing a satellite BMC to a url
629             // containing the information required to create a http
630             // connection to the satellite
631             findSatelliteConfigs(objects, satelliteInfo);
632 
633             if (!satelliteInfo.empty())
634             {
635                 BMCWEB_LOG_DEBUG << "Redfish Aggregation enabled with "
636                                  << std::to_string(satelliteInfo.size())
637                                  << " satellite BMCs";
638             }
639             else
640             {
641                 BMCWEB_LOG_DEBUG
642                     << "No satellite BMCs detected.  Redfish Aggregation not enabled";
643             }
644             handler(ec, satelliteInfo);
645             },
646             "xyz.openbmc_project.EntityManager",
647             "/xyz/openbmc_project/inventory",
648             "org.freedesktop.DBus.ObjectManager", "GetManagedObjects");
649     }
650 
651     // Processes the response returned by a satellite BMC and loads its
652     // contents into asyncResp
653     static void
654         processResponse(std::string_view prefix,
655                         const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
656                         crow::Response& resp)
657     {
658         // 429 and 502 mean we didn't actually send the request so don't
659         // overwrite the response headers in that case
660         if ((resp.resultInt() == 429) || (resp.resultInt() == 502))
661         {
662             asyncResp->res.result(resp.result());
663             return;
664         }
665 
666         // We want to attempt prefix fixing regardless of response code
667         // The resp will not have a json component
668         // We need to create a json from resp's stringResponse
669         if (resp.getHeaderValue("Content-Type") == "application/json")
670         {
671             nlohmann::json jsonVal =
672                 nlohmann::json::parse(resp.body(), nullptr, false);
673             if (jsonVal.is_discarded())
674             {
675                 BMCWEB_LOG_ERROR << "Error parsing satellite response as JSON";
676                 messages::operationFailed(asyncResp->res);
677                 return;
678             }
679 
680             BMCWEB_LOG_DEBUG << "Successfully parsed satellite response";
681 
682             addPrefixes(jsonVal, prefix);
683 
684             BMCWEB_LOG_DEBUG << "Added prefix to parsed satellite response";
685 
686             asyncResp->res.result(resp.result());
687             asyncResp->res.jsonValue = std::move(jsonVal);
688 
689             BMCWEB_LOG_DEBUG << "Finished writing asyncResp";
690         }
691         else
692         {
693             // We allow any Content-Type that is not "application/json" now
694             asyncResp->res.result(resp.result());
695             asyncResp->res.write(resp.body());
696         }
697         addAggregatedHeaders(asyncResp->res, resp, prefix);
698     }
699 
700     // Processes the collection response returned by a satellite BMC and merges
701     // its "@odata.id" values
702     static void processCollectionResponse(
703         const std::string& prefix,
704         const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
705         crow::Response& resp)
706     {
707         // 429 and 502 mean we didn't actually send the request so don't
708         // overwrite the response headers in that case
709         if ((resp.resultInt() == 429) || (resp.resultInt() == 502))
710         {
711             return;
712         }
713 
714         if (resp.resultInt() != 200)
715         {
716             BMCWEB_LOG_DEBUG
717                 << "Collection resource does not exist in satellite BMC \""
718                 << prefix << "\"";
719             // Return the error if we haven't had any successes
720             if (asyncResp->res.resultInt() != 200)
721             {
722                 asyncResp->res.stringResponse = std::move(resp.stringResponse);
723             }
724             return;
725         }
726 
727         // The resp will not have a json component
728         // We need to create a json from resp's stringResponse
729         if (resp.getHeaderValue("Content-Type") == "application/json")
730         {
731             nlohmann::json jsonVal =
732                 nlohmann::json::parse(resp.body(), nullptr, false);
733             if (jsonVal.is_discarded())
734             {
735                 BMCWEB_LOG_ERROR << "Error parsing satellite response as JSON";
736 
737                 // Notify the user if doing so won't overwrite a valid response
738                 if ((asyncResp->res.resultInt() != 200) &&
739                     (asyncResp->res.resultInt() != 429) &&
740                     (asyncResp->res.resultInt() != 502))
741                 {
742                     messages::operationFailed(asyncResp->res);
743                 }
744                 return;
745             }
746 
747             BMCWEB_LOG_DEBUG << "Successfully parsed satellite response";
748 
749             // Now we need to add the prefix to the URIs contained in the
750             // response.
751             addPrefixes(jsonVal, prefix);
752 
753             BMCWEB_LOG_DEBUG << "Added prefix to parsed satellite response";
754 
755             // If this resource collection does not exist on the aggregating bmc
756             // and has not already been added from processing the response from
757             // a different satellite then we need to completely overwrite
758             // asyncResp
759             if (asyncResp->res.resultInt() != 200)
760             {
761                 // We only want to aggregate collections that contain a
762                 // "Members" array
763                 if ((!jsonVal.contains("Members")) &&
764                     (!jsonVal["Members"].is_array()))
765                 {
766                     BMCWEB_LOG_DEBUG
767                         << "Skipping aggregating unsupported resource";
768                     return;
769                 }
770 
771                 BMCWEB_LOG_DEBUG
772                     << "Collection does not exist, overwriting asyncResp";
773                 asyncResp->res.result(resp.result());
774                 asyncResp->res.jsonValue = std::move(jsonVal);
775                 asyncResp->res.addHeader("Content-Type", "application/json");
776 
777                 BMCWEB_LOG_DEBUG << "Finished overwriting asyncResp";
778             }
779             else
780             {
781                 // We only want to aggregate collections that contain a
782                 // "Members" array
783                 if ((!asyncResp->res.jsonValue.contains("Members")) &&
784                     (!asyncResp->res.jsonValue["Members"].is_array()))
785 
786                 {
787                     BMCWEB_LOG_DEBUG
788                         << "Skipping aggregating unsupported resource";
789                     return;
790                 }
791 
792                 BMCWEB_LOG_DEBUG << "Adding aggregated resources from \""
793                                  << prefix << "\" to collection";
794 
795                 // TODO: This is a potential race condition with multiple
796                 // satellites and the aggregating bmc attempting to write to
797                 // update this array.  May need to cascade calls to the next
798                 // satellite at the end of this function.
799                 // This is presumably not a concern when there is only a single
800                 // satellite since the aggregating bmc should have completed
801                 // before the response is received from the satellite.
802 
803                 auto& members = asyncResp->res.jsonValue["Members"];
804                 auto& satMembers = jsonVal["Members"];
805                 for (auto& satMem : satMembers)
806                 {
807                     members.push_back(std::move(satMem));
808                 }
809                 asyncResp->res.jsonValue["Members@odata.count"] =
810                     members.size();
811 
812                 // TODO: Do we need to sort() after updating the array?
813             }
814         }
815         else
816         {
817             BMCWEB_LOG_ERROR << "Received unparsable response from \"" << prefix
818                              << "\"";
819             // We received a response that was not a json.
820             // Notify the user only if we did not receive any valid responses,
821             // if the resource collection does not already exist on the
822             // aggregating BMC, and if we did not already set this warning due
823             // to a failure from a different satellite
824             if ((asyncResp->res.resultInt() != 200) &&
825                 (asyncResp->res.resultInt() != 429) &&
826                 (asyncResp->res.resultInt() != 502))
827             {
828                 messages::operationFailed(asyncResp->res);
829             }
830         }
831     } // End processCollectionResponse()
832 
833     // Entry point to Redfish Aggregation
834     // Returns Result stating whether or not we still need to locally handle the
835     // request
836     static Result
837         beginAggregation(const crow::Request& thisReq,
838                          const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
839     {
840         using crow::utility::OrMorePaths;
841         using crow::utility::readUrlSegments;
842         const boost::urls::url_view url = thisReq.url();
843 
844         // We don't need to aggregate JsonSchemas due to potential issues such
845         // as version mismatches between aggregator and satellite BMCs.  For
846         // now assume that the aggregator has all the schemas and versions that
847         // the aggregated server has.
848         if (crow::utility::readUrlSegments(url, "redfish", "v1", "JsonSchemas",
849                                            crow::utility::OrMorePaths()))
850         {
851             return Result::LocalHandle;
852         }
853 
854         // The first two segments should be "/redfish/v1".  We need to check
855         // that before we can search topCollections
856         if (!crow::utility::readUrlSegments(url, "redfish", "v1",
857                                             crow::utility::OrMorePaths()))
858         {
859             return Result::LocalHandle;
860         }
861 
862         // Parse the URI to see if it begins with a known top level collection
863         // such as:
864         // /redfish/v1/Chassis
865         // /redfish/v1/UpdateService/FirmwareInventory
866         const boost::urls::segments_view urlSegments = url.segments();
867         boost::urls::url currentUrl("/");
868         boost::urls::segments_view::iterator it = urlSegments.begin();
869         const boost::urls::segments_view::const_iterator end =
870             urlSegments.end();
871 
872         // Skip past the leading "/redfish/v1"
873         it++;
874         it++;
875         for (; it != end; it++)
876         {
877             const std::string& collectionItem = *it;
878             if (std::binary_search(topCollections.begin(), topCollections.end(),
879                                    currentUrl.buffer()))
880             {
881                 // We've matched a resource collection so this current segment
882                 // might contain an aggregation prefix
883                 // TODO: This needs to be rethought when we can support multiple
884                 // satellites due to
885                 // /redfish/v1/AggregationService/AggregationSources/5B247A
886                 // being a local resource describing the satellite
887                 if (collectionItem.starts_with("5B247A_"))
888                 {
889                     BMCWEB_LOG_DEBUG << "Need to forward a request";
890 
891                     // Extract the prefix from the request's URI, retrieve the
892                     // associated satellite config information, and then forward
893                     // the request to that satellite.
894                     startAggregation(AggregationType::Resource, thisReq,
895                                      asyncResp);
896                     return Result::NoLocalHandle;
897                 }
898 
899                 // Handle collection URI with a trailing backslash
900                 // e.g. /redfish/v1/Chassis/
901                 it++;
902                 if ((it == end) && collectionItem.empty())
903                 {
904                     startAggregation(AggregationType::Collection, thisReq,
905                                      asyncResp);
906                 }
907 
908                 // We didn't recognize the prefix or it's a collection with a
909                 // trailing "/".  In both cases we still want to locally handle
910                 // the request
911                 return Result::LocalHandle;
912             }
913 
914             currentUrl.segments().push_back(collectionItem);
915         }
916 
917         // If we made it here then currentUrl could contain a top level
918         // collection URI without a trailing "/", e.g. /redfish/v1/Chassis
919         if (std::binary_search(topCollections.begin(), topCollections.end(),
920                                currentUrl.buffer()))
921         {
922             startAggregation(AggregationType::Collection, thisReq, asyncResp);
923             return Result::LocalHandle;
924         }
925 
926         BMCWEB_LOG_DEBUG << "Aggregation not required";
927         return Result::LocalHandle;
928     }
929 };
930 
931 } // namespace redfish
932