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