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