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