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