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