xref: /openbmc/bmcweb/features/redfish/include/redfish_aggregator.hpp (revision 7e8890c5c2b8d69c104f2db4e82fb06c40374c08)
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(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         boost::urls::string_value name = req.urlView.segments().back();
439         std::string_view nameStr(name.data(), name.size());
440         messages::resourceNotFound(asyncResp->res, "", nameStr);
441     }
442 
443     // Intended to handle an incoming request based on if Redfish Aggregation
444     // is enabled.  Forwards request to satellite BMC if it exists.
445     static void aggregateAndHandle(
446         AggregationType isCollection,
447         const std::shared_ptr<crow::Request>& sharedReq,
448         const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
449         const std::unordered_map<std::string, boost::urls::url>& satelliteInfo)
450     {
451         if (sharedReq == nullptr)
452         {
453             return;
454         }
455 
456         // No satellite configs means we don't need to keep attempting to
457         // aggregate
458         if (satelliteInfo.empty())
459         {
460             // For collections we'll also handle the request locally so we
461             // don't need to write an error code
462             if (isCollection == AggregationType::Resource)
463             {
464                 boost::urls::string_value name =
465                     sharedReq->urlView.segments().back();
466                 std::string_view nameStr(name.data(), name.size());
467                 messages::resourceNotFound(asyncResp->res, "", nameStr);
468             }
469             return;
470         }
471 
472         const crow::Request& thisReq = *sharedReq;
473         BMCWEB_LOG_DEBUG << "Aggregation is enabled, begin processing of "
474                          << thisReq.target();
475 
476         // We previously determined the request is for a collection.  No need to
477         // check again
478         if (isCollection == AggregationType::Collection)
479         {
480             BMCWEB_LOG_DEBUG << "Aggregating a collection";
481             // We need to use a specific response handler and send the
482             // request to all known satellites
483             getInstance().forwardCollectionRequests(thisReq, asyncResp,
484                                                     satelliteInfo);
485             return;
486         }
487 
488         std::string updateServiceName;
489         std::string memberName;
490         if (crow::utility::readUrlSegments(
491                 thisReq.urlView, "redfish", "v1", "UpdateService",
492                 std::ref(updateServiceName), std::ref(memberName),
493                 crow::utility::OrMorePaths()))
494         {
495             // Must be FirmwareInventory or SoftwareInventory
496             findSatellite(thisReq, asyncResp, satelliteInfo, memberName);
497             return;
498         }
499 
500         std::string collectionName;
501         if (crow::utility::readUrlSegments(
502                 thisReq.urlView, "redfish", "v1", std::ref(collectionName),
503                 std::ref(memberName), crow::utility::OrMorePaths()))
504         {
505             findSatellite(thisReq, asyncResp, satelliteInfo, memberName);
506             return;
507         }
508 
509         // We shouldn't reach this point since we should've hit one of the
510         // previous exits
511         messages::internalError(asyncResp->res);
512     }
513 
514     // Attempt to forward a request to the satellite BMC associated with the
515     // prefix.
516     void forwardRequest(
517         const crow::Request& thisReq,
518         const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
519         const std::string& prefix,
520         const std::unordered_map<std::string, boost::urls::url>& satelliteInfo)
521     {
522         const auto& sat = satelliteInfo.find(prefix);
523         if (sat == satelliteInfo.end())
524         {
525             // Realistically this shouldn't get called since we perform an
526             // earlier check to make sure the prefix exists
527             BMCWEB_LOG_ERROR << "Unrecognized satellite prefix \"" << prefix
528                              << "\"";
529             return;
530         }
531 
532         // We need to strip the prefix from the request's path
533         std::string targetURI(thisReq.target());
534         size_t pos = targetURI.find(prefix + "_");
535         if (pos == std::string::npos)
536         {
537             // If this fails then something went wrong
538             BMCWEB_LOG_ERROR << "Error removing prefix \"" << prefix
539                              << "_\" from request URI";
540             messages::internalError(asyncResp->res);
541             return;
542         }
543         targetURI.erase(pos, prefix.size() + 1);
544 
545         std::function<void(crow::Response&)> cb =
546             std::bind_front(processResponse, prefix, asyncResp);
547 
548         std::string data = thisReq.req.body();
549         crow::HttpClient::getInstance().sendDataWithCallback(
550             data, id, std::string(sat->second.host()),
551             sat->second.port_number(), targetURI, false /*useSSL*/,
552             thisReq.fields, thisReq.method(), retryPolicyName, cb);
553     }
554 
555     // Forward a request for a collection URI to each known satellite BMC
556     void forwardCollectionRequests(
557         const crow::Request& thisReq,
558         const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
559         const std::unordered_map<std::string, boost::urls::url>& satelliteInfo)
560     {
561         for (const auto& sat : satelliteInfo)
562         {
563             std::function<void(crow::Response&)> cb = std::bind_front(
564                 processCollectionResponse, sat.first, asyncResp);
565 
566             std::string targetURI(thisReq.target());
567             std::string data = thisReq.req.body();
568             crow::HttpClient::getInstance().sendDataWithCallback(
569                 data, id, std::string(sat.second.host()),
570                 sat.second.port_number(), targetURI, false /*useSSL*/,
571                 thisReq.fields, thisReq.method(), retryPolicyName, cb);
572         }
573     }
574 
575     // Processes the response returned by a satellite BMC and loads its
576     // contents into asyncResp
577     static void
578         processResponse(std::string_view prefix,
579                         const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
580                         crow::Response& resp)
581     {
582         // No processing needed if the request wasn't successful
583         if (resp.resultInt() != 200)
584         {
585             BMCWEB_LOG_DEBUG << "No need to parse satellite response";
586             asyncResp->res.stringResponse = std::move(resp.stringResponse);
587             return;
588         }
589 
590         // The resp will not have a json component
591         // We need to create a json from resp's stringResponse
592         if (resp.getHeaderValue("Content-Type") == "application/json")
593         {
594             nlohmann::json jsonVal =
595                 nlohmann::json::parse(resp.body(), nullptr, false);
596             if (jsonVal.is_discarded())
597             {
598                 BMCWEB_LOG_ERROR << "Error parsing satellite response as JSON";
599                 messages::operationFailed(asyncResp->res);
600                 return;
601             }
602 
603             BMCWEB_LOG_DEBUG << "Successfully parsed satellite response";
604 
605             // TODO: For collections we  want to add the satellite responses to
606             // our response rather than just straight overwriting them if our
607             // local handling was successful (i.e. would return a 200).
608             addPrefixes(jsonVal, prefix);
609 
610             BMCWEB_LOG_DEBUG << "Added prefix to parsed satellite response";
611 
612             asyncResp->res.result(resp.result());
613             asyncResp->res.jsonValue = std::move(jsonVal);
614 
615             BMCWEB_LOG_DEBUG << "Finished writing asyncResp";
616         }
617         else
618         {
619             if (!resp.body().empty())
620             {
621                 // We received a 200 response without the correct Content-Type
622                 // so return an Operation Failed error
623                 BMCWEB_LOG_ERROR
624                     << "Satellite response must be of type \"application/json\"";
625                 messages::operationFailed(asyncResp->res);
626             }
627         }
628     }
629 
630     // Processes the collection response returned by a satellite BMC and merges
631     // its "@odata.id" values
632     static void processCollectionResponse(
633         const std::string& prefix,
634         const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
635         crow::Response& resp)
636     {
637         if (resp.resultInt() != 200)
638         {
639             BMCWEB_LOG_DEBUG
640                 << "Collection resource does not exist in satellite BMC \""
641                 << prefix << "\"";
642             // Return the error if we haven't had any successes
643             if (asyncResp->res.resultInt() != 200)
644             {
645                 asyncResp->res.stringResponse = std::move(resp.stringResponse);
646             }
647             return;
648         }
649 
650         // The resp will not have a json component
651         // We need to create a json from resp's stringResponse
652         if (resp.getHeaderValue("Content-Type") == "application/json")
653         {
654             nlohmann::json jsonVal =
655                 nlohmann::json::parse(resp.body(), nullptr, false);
656             if (jsonVal.is_discarded())
657             {
658                 BMCWEB_LOG_ERROR << "Error parsing satellite response as JSON";
659 
660                 // Notify the user if doing so won't overwrite a valid response
661                 if ((asyncResp->res.resultInt() != 200) &&
662                     (asyncResp->res.resultInt() != 502))
663                 {
664                     messages::operationFailed(asyncResp->res);
665                 }
666                 return;
667             }
668 
669             BMCWEB_LOG_DEBUG << "Successfully parsed satellite response";
670 
671             // Now we need to add the prefix to the URIs contained in the
672             // response.
673             addPrefixes(jsonVal, prefix);
674 
675             BMCWEB_LOG_DEBUG << "Added prefix to parsed satellite response";
676 
677             // If this resource collection does not exist on the aggregating bmc
678             // and has not already been added from processing the response from
679             // a different satellite then we need to completely overwrite
680             // asyncResp
681             if (asyncResp->res.resultInt() != 200)
682             {
683                 // We only want to aggregate collections that contain a
684                 // "Members" array
685                 if ((!jsonVal.contains("Members")) &&
686                     (!jsonVal["Members"].is_array()))
687                 {
688                     BMCWEB_LOG_DEBUG
689                         << "Skipping aggregating unsupported resource";
690                     return;
691                 }
692 
693                 BMCWEB_LOG_DEBUG
694                     << "Collection does not exist, overwriting asyncResp";
695                 asyncResp->res.result(resp.result());
696                 asyncResp->res.jsonValue = std::move(jsonVal);
697 
698                 BMCWEB_LOG_DEBUG << "Finished overwriting asyncResp";
699             }
700             else
701             {
702                 // We only want to aggregate collections that contain a
703                 // "Members" array
704                 if ((!asyncResp->res.jsonValue.contains("Members")) &&
705                     (!asyncResp->res.jsonValue["Members"].is_array()))
706 
707                 {
708                     BMCWEB_LOG_DEBUG
709                         << "Skipping aggregating unsupported resource";
710                     return;
711                 }
712 
713                 BMCWEB_LOG_DEBUG << "Adding aggregated resources from \""
714                                  << prefix << "\" to collection";
715 
716                 // TODO: This is a potential race condition with multiple
717                 // satellites and the aggregating bmc attempting to write to
718                 // update this array.  May need to cascade calls to the next
719                 // satellite at the end of this function.
720                 // This is presumably not a concern when there is only a single
721                 // satellite since the aggregating bmc should have completed
722                 // before the response is received from the satellite.
723 
724                 auto& members = asyncResp->res.jsonValue["Members"];
725                 auto& satMembers = jsonVal["Members"];
726                 for (auto& satMem : satMembers)
727                 {
728                     members.push_back(std::move(satMem));
729                 }
730                 asyncResp->res.jsonValue["Members@odata.count"] =
731                     members.size();
732 
733                 // TODO: Do we need to sort() after updating the array?
734             }
735         }
736         else
737         {
738             BMCWEB_LOG_ERROR << "Received unparsable response from \"" << prefix
739                              << "\"";
740             // We received as response that was not a json
741             // Notify the user only if we did not receive any valid responses,
742             // if the resource collection does not already exist on the
743             // aggregating BMC, and if we did not already set this warning due
744             // to a failure from a different satellite
745             if ((asyncResp->res.resultInt() != 200) &&
746                 (asyncResp->res.resultInt() != 502))
747             {
748                 messages::operationFailed(asyncResp->res);
749             }
750         }
751     } // End processCollectionResponse()
752 
753   public:
754     RedfishAggregator(const RedfishAggregator&) = delete;
755     RedfishAggregator& operator=(const RedfishAggregator&) = delete;
756     RedfishAggregator(RedfishAggregator&&) = delete;
757     RedfishAggregator& operator=(RedfishAggregator&&) = delete;
758     ~RedfishAggregator() = default;
759 
760     static RedfishAggregator& getInstance()
761     {
762         static RedfishAggregator handler;
763         return handler;
764     }
765 
766     // Entry point to Redfish Aggregation
767     // Returns Result stating whether or not we still need to locally handle the
768     // request
769     static Result
770         beginAggregation(const crow::Request& thisReq,
771                          const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
772     {
773         using crow::utility::OrMorePaths;
774         using crow::utility::readUrlSegments;
775         const boost::urls::url_view& url = thisReq.urlView;
776         // UpdateService is the only top level resource that is not a Collection
777         if (readUrlSegments(url, "redfish", "v1", "UpdateService"))
778         {
779             return Result::LocalHandle;
780         }
781 
782         // We don't need to aggregate JsonSchemas due to potential issues such
783         // as version mismatches between aggregator and satellite BMCs.  For
784         // now assume that the aggregator has all the schemas and versions that
785         // the aggregated server has.
786         if (crow::utility::readUrlSegments(url, "redfish", "v1", "JsonSchemas",
787                                            crow::utility::OrMorePaths()))
788         {
789             return Result::LocalHandle;
790         }
791 
792         if (readUrlSegments(url, "redfish", "v1", "UpdateService",
793                             "SoftwareInventory") ||
794             readUrlSegments(url, "redfish", "v1", "UpdateService",
795                             "FirmwareInventory"))
796         {
797             startAggregation(AggregationType::Collection, thisReq, asyncResp);
798             return Result::LocalHandle;
799         }
800 
801         // Is the request for a resource collection?:
802         // /redfish/v1/<resource>
803         // e.g. /redfish/v1/Chassis
804         std::string collectionName;
805         if (readUrlSegments(url, "redfish", "v1", std::ref(collectionName)))
806         {
807             startAggregation(AggregationType::Collection, thisReq, asyncResp);
808             return Result::LocalHandle;
809         }
810 
811         // We know that the ID of an aggregated resource will begin with
812         // "5B247A".  For the most part the URI will begin like this:
813         // /redfish/v1/<resource>/<resource ID>
814         // Note, FirmwareInventory and SoftwareInventory are "special" because
815         // they are two levels deep, but still need aggregated
816         // /redfish/v1/UpdateService/FirmwareInventory/<FirmwareInventory ID>
817         // /redfish/v1/UpdateService/SoftwareInventory/<SoftwareInventory ID>
818         std::string memberName;
819         if (readUrlSegments(url, "redfish", "v1", "UpdateService",
820                             "SoftwareInventory", std::ref(memberName),
821                             OrMorePaths()) ||
822             readUrlSegments(url, "redfish", "v1", "UpdateService",
823                             "FirmwareInventory", std::ref(memberName),
824                             OrMorePaths()) ||
825             readUrlSegments(url, "redfish", "v1", std::ref(collectionName),
826                             std::ref(memberName), OrMorePaths()))
827         {
828             if (memberName.starts_with("5B247A"))
829             {
830                 BMCWEB_LOG_DEBUG << "Need to forward a request";
831 
832                 // Extract the prefix from the request's URI, retrieve the
833                 // associated satellite config information, and then forward the
834                 // request to that satellite.
835                 startAggregation(AggregationType::Resource, thisReq, asyncResp);
836                 return Result::NoLocalHandle;
837             }
838             return Result::LocalHandle;
839         }
840 
841         BMCWEB_LOG_DEBUG << "Aggregation not required";
842         return Result::LocalHandle;
843     }
844 };
845 
846 } // namespace redfish
847