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         // We want to attempt prefix fixing regardless of response code
606         // The resp will not have a json component
607         // We need to create a json from resp's stringResponse
608         if (resp.getHeaderValue("Content-Type") == "application/json")
609         {
610             nlohmann::json jsonVal =
611                 nlohmann::json::parse(resp.body(), nullptr, false);
612             if (jsonVal.is_discarded())
613             {
614                 BMCWEB_LOG_ERROR << "Error parsing satellite response as JSON";
615                 messages::operationFailed(asyncResp->res);
616                 return;
617             }
618 
619             BMCWEB_LOG_DEBUG << "Successfully parsed satellite response";
620 
621             addPrefixes(jsonVal, prefix);
622 
623             BMCWEB_LOG_DEBUG << "Added prefix to parsed satellite response";
624 
625             asyncResp->res.result(resp.result());
626             asyncResp->res.jsonValue = std::move(jsonVal);
627 
628             BMCWEB_LOG_DEBUG << "Finished writing asyncResp";
629         }
630         else
631         {
632             if (!resp.body().empty())
633             {
634                 // We received a valid response without the correct
635                 // Content-Type so return an Operation Failed error
636                 BMCWEB_LOG_ERROR
637                     << "Satellite response must be of type \"application/json\"";
638                 messages::operationFailed(asyncResp->res);
639             }
640         }
641     }
642 
643     // Processes the collection response returned by a satellite BMC and merges
644     // its "@odata.id" values
645     static void processCollectionResponse(
646         const std::string& prefix,
647         const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
648         crow::Response& resp)
649     {
650         if (resp.resultInt() != 200)
651         {
652             BMCWEB_LOG_DEBUG
653                 << "Collection resource does not exist in satellite BMC \""
654                 << prefix << "\"";
655             // Return the error if we haven't had any successes
656             if (asyncResp->res.resultInt() != 200)
657             {
658                 asyncResp->res.stringResponse = std::move(resp.stringResponse);
659             }
660             return;
661         }
662 
663         // The resp will not have a json component
664         // We need to create a json from resp's stringResponse
665         if (resp.getHeaderValue("Content-Type") == "application/json")
666         {
667             nlohmann::json jsonVal =
668                 nlohmann::json::parse(resp.body(), nullptr, false);
669             if (jsonVal.is_discarded())
670             {
671                 BMCWEB_LOG_ERROR << "Error parsing satellite response as JSON";
672 
673                 // Notify the user if doing so won't overwrite a valid response
674                 if ((asyncResp->res.resultInt() != 200) &&
675                     (asyncResp->res.resultInt() != 502))
676                 {
677                     messages::operationFailed(asyncResp->res);
678                 }
679                 return;
680             }
681 
682             BMCWEB_LOG_DEBUG << "Successfully parsed satellite response";
683 
684             // Now we need to add the prefix to the URIs contained in the
685             // response.
686             addPrefixes(jsonVal, prefix);
687 
688             BMCWEB_LOG_DEBUG << "Added prefix to parsed satellite response";
689 
690             // If this resource collection does not exist on the aggregating bmc
691             // and has not already been added from processing the response from
692             // a different satellite then we need to completely overwrite
693             // asyncResp
694             if (asyncResp->res.resultInt() != 200)
695             {
696                 // We only want to aggregate collections that contain a
697                 // "Members" array
698                 if ((!jsonVal.contains("Members")) &&
699                     (!jsonVal["Members"].is_array()))
700                 {
701                     BMCWEB_LOG_DEBUG
702                         << "Skipping aggregating unsupported resource";
703                     return;
704                 }
705 
706                 BMCWEB_LOG_DEBUG
707                     << "Collection does not exist, overwriting asyncResp";
708                 asyncResp->res.result(resp.result());
709                 asyncResp->res.jsonValue = std::move(jsonVal);
710 
711                 BMCWEB_LOG_DEBUG << "Finished overwriting asyncResp";
712             }
713             else
714             {
715                 // We only want to aggregate collections that contain a
716                 // "Members" array
717                 if ((!asyncResp->res.jsonValue.contains("Members")) &&
718                     (!asyncResp->res.jsonValue["Members"].is_array()))
719 
720                 {
721                     BMCWEB_LOG_DEBUG
722                         << "Skipping aggregating unsupported resource";
723                     return;
724                 }
725 
726                 BMCWEB_LOG_DEBUG << "Adding aggregated resources from \""
727                                  << prefix << "\" to collection";
728 
729                 // TODO: This is a potential race condition with multiple
730                 // satellites and the aggregating bmc attempting to write to
731                 // update this array.  May need to cascade calls to the next
732                 // satellite at the end of this function.
733                 // This is presumably not a concern when there is only a single
734                 // satellite since the aggregating bmc should have completed
735                 // before the response is received from the satellite.
736 
737                 auto& members = asyncResp->res.jsonValue["Members"];
738                 auto& satMembers = jsonVal["Members"];
739                 for (auto& satMem : satMembers)
740                 {
741                     members.push_back(std::move(satMem));
742                 }
743                 asyncResp->res.jsonValue["Members@odata.count"] =
744                     members.size();
745 
746                 // TODO: Do we need to sort() after updating the array?
747             }
748         }
749         else
750         {
751             BMCWEB_LOG_ERROR << "Received unparsable response from \"" << prefix
752                              << "\"";
753             // We received as response that was not a json
754             // Notify the user only if we did not receive any valid responses,
755             // if the resource collection does not already exist on the
756             // aggregating BMC, and if we did not already set this warning due
757             // to a failure from a different satellite
758             if ((asyncResp->res.resultInt() != 200) &&
759                 (asyncResp->res.resultInt() != 502))
760             {
761                 messages::operationFailed(asyncResp->res);
762             }
763         }
764     } // End processCollectionResponse()
765 
766     // Entry point to Redfish Aggregation
767     // Returns Result stating whether or not we still need to locally handle the
768     // request
769     static Result
770         beginAggregation(const crow::Request& thisReq,
771                          const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
772     {
773         using crow::utility::OrMorePaths;
774         using crow::utility::readUrlSegments;
775         const boost::urls::url_view url = thisReq.urlView;
776 
777         // We don't need to aggregate JsonSchemas due to potential issues such
778         // as version mismatches between aggregator and satellite BMCs.  For
779         // now assume that the aggregator has all the schemas and versions that
780         // the aggregated server has.
781         if (crow::utility::readUrlSegments(url, "redfish", "v1", "JsonSchemas",
782                                            crow::utility::OrMorePaths()))
783         {
784             return Result::LocalHandle;
785         }
786 
787         // The first two segments should be "/redfish/v1".  We need to check
788         // that before we can search topCollections
789         if (!crow::utility::readUrlSegments(url, "redfish", "v1",
790                                             crow::utility::OrMorePaths()))
791         {
792             return Result::LocalHandle;
793         }
794 
795         // Parse the URI to see if it begins with a known top level collection
796         // such as:
797         // /redfish/v1/Chassis
798         // /redfish/v1/UpdateService/FirmwareInventory
799         const boost::urls::segments_view urlSegments = url.segments();
800         std::string collectionItem;
801         boost::urls::url currentUrl("/");
802         boost::urls::segments_view::iterator it = urlSegments.begin();
803         const boost::urls::segments_view::const_iterator end =
804             urlSegments.end();
805 
806         // Skip past the leading "/redfish/v1"
807         it++;
808         it++;
809         for (; it != end; it++)
810         {
811             collectionItem = *it;
812             if (std::binary_search(topCollections.begin(), topCollections.end(),
813                                    currentUrl.buffer()))
814             {
815                 // We've matched a resource collection so this current segment
816                 // might contain an aggregation prefix
817                 if (collectionItem.starts_with("5B247A"))
818                 {
819                     BMCWEB_LOG_DEBUG << "Need to forward a request";
820 
821                     // Extract the prefix from the request's URI, retrieve the
822                     // associated satellite config information, and then forward
823                     // the request to that satellite.
824                     startAggregation(AggregationType::Resource, thisReq,
825                                      asyncResp);
826                     return Result::NoLocalHandle;
827                 }
828 
829                 // Handle collection URI with a trailing backslash
830                 // e.g. /redfish/v1/Chassis/
831                 it++;
832                 if ((it == end) && collectionItem.empty())
833                 {
834                     startAggregation(AggregationType::Collection, thisReq,
835                                      asyncResp);
836                 }
837 
838                 // We didn't recognize the prefix or it's a collection with a
839                 // trailing "/".  In both cases we still want to locally handle
840                 // the request
841                 return Result::LocalHandle;
842             }
843 
844             currentUrl.segments().push_back(collectionItem);
845         }
846 
847         // If we made it here then currentUrl could contain a top level
848         // collection URI without a trailing "/", e.g. /redfish/v1/Chassis
849         if (std::binary_search(topCollections.begin(), topCollections.end(),
850                                currentUrl.buffer()))
851         {
852             startAggregation(AggregationType::Collection, thisReq, asyncResp);
853             return Result::LocalHandle;
854         }
855 
856         BMCWEB_LOG_DEBUG << "Aggregation not required";
857         return Result::LocalHandle;
858     }
859 };
860 
861 } // namespace redfish
862