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