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