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