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