xref: /openbmc/bmcweb/redfish-core/include/redfish_aggregator.hpp (revision 2c6ffdb08b2207ff7c31041f77cc3755508d45c4)
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 constexpr unsigned int aggregatorReadBodyLimit = 50 * 1024 * 1024; // 50MB
17 
18 enum class Result
19 {
20     LocalHandle,
21     NoLocalHandle
22 };
23 
24 enum class SearchType
25 {
26     Collection,
27     CollOrCon,
28     ContainsSubordinate,
29     Resource
30 };
31 
32 // clang-format off
33 // These are all of the properties as of version 2022.2 of the Redfish Resource
34 // and Schema Guide whose Type is "string (URI)" and the name does not end in a
35 // case-insensitive form of "uri".  That version of the schema is associated
36 // with version 1.16.0 of the Redfish Specification.  Going forward, new URI
37 // properties should end in URI so this list should not need to be maintained as
38 // the spec is updated.  NOTE: These have been pre-sorted in order to be
39 // compatible with binary search
40 constexpr std::array nonUriProperties{
41     "@Redfish.ActionInfo",
42     // "@odata.context", // We can't fix /redfish/v1/$metadata URIs
43     "@odata.id",
44     // "Destination", // Only used by EventService and won't be a Redfish URI
45     // "HostName", // Isn't actually a Redfish URI
46     "Image",
47     "MetricProperty",
48     // "OriginOfCondition", // Is URI when in request, but is object in response
49     "TaskMonitor",
50     "target", // normal string, but target URI for POST to invoke an action
51 };
52 // clang-format on
53 
54 // Search the top collection array to determine if the passed URI is of a
55 // desired type
56 inline bool searchCollectionsArray(std::string_view uri,
57                                    const SearchType searchType)
58 {
59     constexpr std::string_view serviceRootUri = "/redfish/v1";
60 
61     // The passed URI must begin with "/redfish/v1", but we have to strip it
62     // from the URI since topCollections does not include it in its URIs
63     if (!uri.starts_with(serviceRootUri))
64     {
65         return false;
66     }
67 
68     // Catch empty final segments such as "/redfish/v1/Chassis//"
69     if (uri.ends_with("//"))
70     {
71         return false;
72     }
73 
74     std::size_t parseCount = uri.size() - serviceRootUri.size();
75     // Don't include the trailing "/" if it exists such as in "/redfish/v1/"
76     if (uri.ends_with("/"))
77     {
78         parseCount--;
79     }
80 
81     boost::urls::result<boost::urls::url_view> parsedUrl =
82         boost::urls::parse_relative_ref(
83             uri.substr(serviceRootUri.size(), parseCount));
84     if (!parsedUrl)
85     {
86         BMCWEB_LOG_ERROR << "Failed to get target URI from "
87                          << uri.substr(serviceRootUri.size());
88         return false;
89     }
90 
91     if (!parsedUrl->segments().is_absolute() && !parsedUrl->segments().empty())
92     {
93         return false;
94     }
95 
96     // If no segments() then the passed URI was either "/redfish/v1" or
97     // "/redfish/v1/".
98     if (parsedUrl->segments().empty())
99     {
100         return (searchType == SearchType::ContainsSubordinate) ||
101                (searchType == SearchType::CollOrCon);
102     }
103 
104     const auto* it = std::lower_bound(
105         topCollections.begin(), topCollections.end(), parsedUrl->buffer());
106     if (it == topCollections.end())
107     {
108         // parsedUrl is alphabetically after the last entry in the array so it
109         // can't be a top collection or up tree from a top collection
110         return false;
111     }
112 
113     boost::urls::url collectionUrl(*it);
114     boost::urls::segments_view collectionSegments = collectionUrl.segments();
115     boost::urls::segments_view::iterator itCollection =
116         collectionSegments.begin();
117     const boost::urls::segments_view::const_iterator endCollection =
118         collectionSegments.end();
119 
120     // Each segment in the passed URI should match the found collection
121     for (const auto& segment : parsedUrl->segments())
122     {
123         if (itCollection == endCollection)
124         {
125             // Leftover segments means the target is for an aggregation
126             // supported resource
127             return searchType == SearchType::Resource;
128         }
129 
130         if (segment != (*itCollection))
131         {
132             return false;
133         }
134         itCollection++;
135     }
136 
137     // No remaining segments means the passed URI was a top level collection
138     if (searchType == SearchType::Collection)
139     {
140         return itCollection == endCollection;
141     }
142     if (searchType == SearchType::ContainsSubordinate)
143     {
144         return itCollection != endCollection;
145     }
146 
147     // Return this check instead of "true" in case other SearchTypes get added
148     return searchType == SearchType::CollOrCon;
149 }
150 
151 // Determines if the passed property contains a URI.  Those property names
152 // either end with a case-insensitive version of "uri" or are specifically
153 // defined in the above array.
154 inline bool isPropertyUri(std::string_view propertyName)
155 {
156     return boost::iends_with(propertyName, "uri") ||
157            std::binary_search(nonUriProperties.begin(), nonUriProperties.end(),
158                               propertyName);
159 }
160 
161 static inline void addPrefixToStringItem(std::string& strValue,
162                                          std::string_view prefix)
163 {
164     // Make sure the value is a properly formatted URI
165     auto parsed = boost::urls::parse_relative_ref(strValue);
166     if (!parsed)
167     {
168         BMCWEB_LOG_CRITICAL << "Couldn't parse URI from resource " << strValue;
169         return;
170     }
171 
172     boost::urls::url_view thisUrl = *parsed;
173 
174     // We don't need to aggregate JsonSchemas due to potential issues such as
175     // version mismatches between aggregator and satellite BMCs.  For now
176     // assume that the aggregator has all the schemas and versions that the
177     // aggregated server has.
178     if (crow::utility::readUrlSegments(thisUrl, "redfish", "v1", "JsonSchemas",
179                                        crow::utility::OrMorePaths()))
180     {
181         BMCWEB_LOG_DEBUG << "Skipping JsonSchemas URI prefix fixing";
182         return;
183     }
184 
185     // The first two segments should be "/redfish/v1".  We need to check that
186     // before we can search topCollections
187     if (!crow::utility::readUrlSegments(thisUrl, "redfish", "v1",
188                                         crow::utility::OrMorePaths()))
189     {
190         return;
191     }
192 
193     // Check array adding a segment each time until collection is identified
194     // Add prefix to segment after the collection
195     const boost::urls::segments_view urlSegments = thisUrl.segments();
196     bool addedPrefix = false;
197     boost::urls::url url("/");
198     boost::urls::segments_view::iterator it = urlSegments.begin();
199     const boost::urls::segments_view::const_iterator end = urlSegments.end();
200 
201     // Skip past the leading "/redfish/v1"
202     it++;
203     it++;
204     for (; it != end; it++)
205     {
206         // Trailing "/" will result in an empty segment.  In that case we need
207         // to return so we don't apply a prefix to top level collections such
208         // as "/redfish/v1/Chassis/"
209         if ((*it).empty())
210         {
211             return;
212         }
213 
214         if (std::binary_search(topCollections.begin(), topCollections.end(),
215                                url.buffer()))
216         {
217             std::string collectionItem(prefix);
218             collectionItem += "_" + (*it);
219             url.segments().push_back(collectionItem);
220             it++;
221             addedPrefix = true;
222             break;
223         }
224 
225         url.segments().push_back(*it);
226     }
227 
228     // Finish constructing the URL here (if needed) to avoid additional checks
229     for (; it != end; it++)
230     {
231         url.segments().push_back(*it);
232     }
233 
234     if (addedPrefix)
235     {
236         url.segments().insert(url.segments().begin(), {"redfish", "v1"});
237         strValue = url.buffer();
238     }
239 }
240 
241 static inline void addPrefixToItem(nlohmann::json& item,
242                                    std::string_view prefix)
243 {
244     std::string* strValue = item.get_ptr<std::string*>();
245     if (strValue == nullptr)
246     {
247         BMCWEB_LOG_CRITICAL << "Field wasn't a string????";
248         return;
249     }
250     addPrefixToStringItem(*strValue, prefix);
251     item = *strValue;
252 }
253 
254 static inline void addAggregatedHeaders(crow::Response& asyncResp,
255                                         const crow::Response& resp,
256                                         std::string_view prefix)
257 {
258     if (!resp.getHeaderValue("Content-Type").empty())
259     {
260         asyncResp.addHeader(boost::beast::http::field::content_type,
261                             resp.getHeaderValue("Content-Type"));
262     }
263     if (!resp.getHeaderValue("Allow").empty())
264     {
265         asyncResp.addHeader(boost::beast::http::field::allow,
266                             resp.getHeaderValue("Allow"));
267     }
268     std::string_view header = resp.getHeaderValue("Location");
269     if (!header.empty())
270     {
271         std::string location(header);
272         addPrefixToStringItem(location, prefix);
273         asyncResp.addHeader(boost::beast::http::field::location, location);
274     }
275     if (!resp.getHeaderValue("Retry-After").empty())
276     {
277         asyncResp.addHeader(boost::beast::http::field::retry_after,
278                             resp.getHeaderValue("Retry-After"));
279     }
280     // TODO: we need special handling for Link Header Value
281 }
282 
283 // Fix HTTP headers which appear in responses from Task resources among others
284 static inline void addPrefixToHeadersInResp(nlohmann::json& json,
285                                             std::string_view prefix)
286 {
287     // The passed in "HttpHeaders" should be an array of headers
288     nlohmann::json::array_t* array = json.get_ptr<nlohmann::json::array_t*>();
289     if (array == nullptr)
290     {
291         BMCWEB_LOG_ERROR << "Field wasn't an array_t????";
292         return;
293     }
294 
295     for (nlohmann::json& item : *array)
296     {
297         // Each header is a single string with the form "<Field>: <Value>"
298         std::string* strHeader = item.get_ptr<std::string*>();
299         if (strHeader == nullptr)
300         {
301             BMCWEB_LOG_CRITICAL << "Field wasn't a string????";
302             continue;
303         }
304 
305         constexpr std::string_view location = "Location: ";
306         if (strHeader->starts_with(location))
307         {
308             std::string header = strHeader->substr(location.size());
309             addPrefixToStringItem(header, prefix);
310             *strHeader = std::string(location) + header;
311         }
312     }
313 }
314 
315 // Search the json for all URIs and add the supplied prefix if the URI is for
316 // an aggregated resource.
317 static inline void addPrefixes(nlohmann::json& json, std::string_view prefix)
318 {
319     nlohmann::json::object_t* object =
320         json.get_ptr<nlohmann::json::object_t*>();
321     if (object != nullptr)
322     {
323         for (std::pair<const std::string, nlohmann::json>& item : *object)
324         {
325             if (isPropertyUri(item.first))
326             {
327                 addPrefixToItem(item.second, prefix);
328                 continue;
329             }
330 
331             // "HttpHeaders" contains HTTP headers.  Among those we need to
332             // attempt to fix the "Location" header
333             if (item.first == "HttpHeaders")
334             {
335                 addPrefixToHeadersInResp(item.second, prefix);
336                 continue;
337             }
338 
339             // Recusively parse the rest of the json
340             addPrefixes(item.second, prefix);
341         }
342         return;
343     }
344     nlohmann::json::array_t* array = json.get_ptr<nlohmann::json::array_t*>();
345     if (array != nullptr)
346     {
347         for (nlohmann::json& item : *array)
348         {
349             addPrefixes(item, prefix);
350         }
351     }
352 }
353 
354 inline boost::system::error_code aggregationRetryHandler(unsigned int respCode)
355 {
356     // Allow all response codes because we want to surface any satellite
357     // issue to the client
358     BMCWEB_LOG_DEBUG << "Received " << respCode << " response from satellite";
359     return boost::system::errc::make_error_code(boost::system::errc::success);
360 }
361 
362 inline crow::ConnectionPolicy getAggregationPolicy()
363 {
364     return {.maxRetryAttempts = 1,
365             .requestByteLimit = aggregatorReadBodyLimit,
366             .maxConnections = 20,
367             .retryPolicyAction = "TerminateAfterRetries",
368             .retryIntervalSecs = std::chrono::seconds(0),
369             .invalidResp = aggregationRetryHandler};
370 }
371 
372 class RedfishAggregator
373 {
374   private:
375     crow::HttpClient client;
376 
377     // Dummy callback used by the Constructor so that it can report the number
378     // of satellite configs when the class is first created
379     static void constructorCallback(
380         const boost::system::error_code& ec,
381         const std::unordered_map<std::string, boost::urls::url>& satelliteInfo)
382     {
383         if (ec)
384         {
385             BMCWEB_LOG_ERROR << "Something went wrong while querying dbus!";
386             return;
387         }
388 
389         BMCWEB_LOG_DEBUG << "There were "
390                          << std::to_string(satelliteInfo.size())
391                          << " satellite configs found at startup";
392     }
393 
394     // Search D-Bus objects for satellite config objects and add their
395     // information if valid
396     static void findSatelliteConfigs(
397         const dbus::utility::ManagedObjectType& objects,
398         std::unordered_map<std::string, boost::urls::url>& satelliteInfo)
399     {
400         for (const auto& objectPath : objects)
401         {
402             for (const auto& interface : objectPath.second)
403             {
404                 if (interface.first ==
405                     "xyz.openbmc_project.Configuration.SatelliteController")
406                 {
407                     BMCWEB_LOG_DEBUG << "Found Satellite Controller at "
408                                      << objectPath.first.str;
409 
410                     if (!satelliteInfo.empty())
411                     {
412                         BMCWEB_LOG_ERROR
413                             << "Redfish Aggregation only supports one satellite!";
414                         BMCWEB_LOG_DEBUG << "Clearing all satellite data";
415                         satelliteInfo.clear();
416                         return;
417                     }
418 
419                     // For now assume there will only be one satellite config.
420                     // Assign it the name/prefix "5B247A"
421                     addSatelliteConfig("5B247A", interface.second,
422                                        satelliteInfo);
423                 }
424             }
425         }
426     }
427 
428     // Parse the properties of a satellite config object and add the
429     // configuration if the properties are valid
430     static void addSatelliteConfig(
431         const std::string& name,
432         const dbus::utility::DBusPropertiesMap& properties,
433         std::unordered_map<std::string, boost::urls::url>& satelliteInfo)
434     {
435         boost::urls::url url;
436 
437         for (const auto& prop : properties)
438         {
439             if (prop.first == "Hostname")
440             {
441                 const std::string* propVal =
442                     std::get_if<std::string>(&prop.second);
443                 if (propVal == nullptr)
444                 {
445                     BMCWEB_LOG_ERROR << "Invalid Hostname value";
446                     return;
447                 }
448                 url.set_host(*propVal);
449             }
450 
451             else if (prop.first == "Port")
452             {
453                 const uint64_t* propVal = std::get_if<uint64_t>(&prop.second);
454                 if (propVal == nullptr)
455                 {
456                     BMCWEB_LOG_ERROR << "Invalid Port value";
457                     return;
458                 }
459 
460                 if (*propVal > std::numeric_limits<uint16_t>::max())
461                 {
462                     BMCWEB_LOG_ERROR << "Port value out of range";
463                     return;
464                 }
465                 url.set_port(std::to_string(static_cast<uint16_t>(*propVal)));
466             }
467 
468             else if (prop.first == "AuthType")
469             {
470                 const std::string* propVal =
471                     std::get_if<std::string>(&prop.second);
472                 if (propVal == nullptr)
473                 {
474                     BMCWEB_LOG_ERROR << "Invalid AuthType value";
475                     return;
476                 }
477 
478                 // For now assume authentication not required to communicate
479                 // with the satellite BMC
480                 if (*propVal != "None")
481                 {
482                     BMCWEB_LOG_ERROR
483                         << "Unsupported AuthType value: " << *propVal
484                         << ", only \"none\" is supported";
485                     return;
486                 }
487                 url.set_scheme("http");
488             }
489         } // Finished reading properties
490 
491         // Make sure all required config information was made available
492         if (url.host().empty())
493         {
494             BMCWEB_LOG_ERROR << "Satellite config " << name << " missing Host";
495             return;
496         }
497 
498         if (!url.has_port())
499         {
500             BMCWEB_LOG_ERROR << "Satellite config " << name << " missing Port";
501             return;
502         }
503 
504         if (!url.has_scheme())
505         {
506             BMCWEB_LOG_ERROR << "Satellite config " << name
507                              << " missing AuthType";
508             return;
509         }
510 
511         std::string resultString;
512         auto result = satelliteInfo.insert_or_assign(name, std::move(url));
513         if (result.second)
514         {
515             resultString = "Added new satellite config ";
516         }
517         else
518         {
519             resultString = "Updated existing satellite config ";
520         }
521 
522         BMCWEB_LOG_DEBUG << resultString << name << " at "
523                          << result.first->second.scheme() << "://"
524                          << result.first->second.encoded_host_and_port();
525     }
526 
527     enum AggregationType
528     {
529         Collection,
530         ContainsSubordinate,
531         Resource,
532     };
533 
534     static void
535         startAggregation(AggregationType aggType, const crow::Request& thisReq,
536                          const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
537     {
538         if (thisReq.method() != boost::beast::http::verb::get)
539         {
540             if (aggType == AggregationType::Collection)
541             {
542                 BMCWEB_LOG_DEBUG
543                     << "Only aggregate GET requests to top level collections";
544                 return;
545             }
546 
547             if (aggType == AggregationType::ContainsSubordinate)
548             {
549                 BMCWEB_LOG_DEBUG << "Only aggregate GET requests when uptree of"
550                                  << " a top level collection";
551                 return;
552             }
553         }
554 
555         // Create a copy of thisReq so we we can still locally process the req
556         std::error_code ec;
557         auto localReq = std::make_shared<crow::Request>(thisReq.req, ec);
558         if (ec)
559         {
560             BMCWEB_LOG_ERROR << "Failed to create copy of request";
561             if (aggType == AggregationType::Resource)
562             {
563                 messages::internalError(asyncResp->res);
564             }
565             return;
566         }
567 
568         getSatelliteConfigs(
569             std::bind_front(aggregateAndHandle, aggType, localReq, asyncResp));
570     }
571 
572     static void findSatellite(
573         const crow::Request& req,
574         const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
575         const std::unordered_map<std::string, boost::urls::url>& satelliteInfo,
576         std::string_view memberName)
577     {
578         // Determine if the resource ID begins with a known prefix
579         for (const auto& satellite : satelliteInfo)
580         {
581             std::string targetPrefix = satellite.first;
582             targetPrefix += "_";
583             if (memberName.starts_with(targetPrefix))
584             {
585                 BMCWEB_LOG_DEBUG << "\"" << satellite.first
586                                  << "\" is a known prefix";
587 
588                 // Remove the known prefix from the request's URI and
589                 // then forward to the associated satellite BMC
590                 getInstance().forwardRequest(req, asyncResp, satellite.first,
591                                              satelliteInfo);
592                 return;
593             }
594         }
595 
596         // We didn't recognize the prefix and need to return a 404
597         std::string nameStr = req.url().segments().back();
598         messages::resourceNotFound(asyncResp->res, "", nameStr);
599     }
600 
601     // Intended to handle an incoming request based on if Redfish Aggregation
602     // is enabled.  Forwards request to satellite BMC if it exists.
603     static void aggregateAndHandle(
604         AggregationType aggType,
605         const std::shared_ptr<crow::Request>& sharedReq,
606         const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
607         const boost::system::error_code& ec,
608         const std::unordered_map<std::string, boost::urls::url>& satelliteInfo)
609     {
610         if (sharedReq == nullptr)
611         {
612             return;
613         }
614         // Something went wrong while querying dbus
615         if (ec)
616         {
617             messages::internalError(asyncResp->res);
618             return;
619         }
620 
621         // No satellite configs means we don't need to keep attempting to
622         // aggregate
623         if (satelliteInfo.empty())
624         {
625             // For collections or resources that can contain a subordinate
626             // top level collection we'll also handle the request locally so we
627             // don't need to write an error code
628             if (aggType == AggregationType::Resource)
629             {
630                 std::string nameStr = sharedReq->url().segments().back();
631                 messages::resourceNotFound(asyncResp->res, "", nameStr);
632             }
633             return;
634         }
635 
636         const crow::Request& thisReq = *sharedReq;
637         BMCWEB_LOG_DEBUG << "Aggregation is enabled, begin processing of "
638                          << thisReq.target();
639 
640         // We previously determined the request is for a collection.  No need to
641         // check again
642         if (aggType == AggregationType::Collection)
643         {
644             BMCWEB_LOG_DEBUG << "Aggregating a collection";
645             // We need to use a specific response handler and send the
646             // request to all known satellites
647             getInstance().forwardCollectionRequests(thisReq, asyncResp,
648                                                     satelliteInfo);
649             return;
650         }
651 
652         // We previously determined the request may contain a subordinate
653         // collection.  No need to check again
654         if (aggType == AggregationType::ContainsSubordinate)
655         {
656             BMCWEB_LOG_DEBUG
657                 << "Aggregating what may have a subordinate collection";
658             // We need to use a specific response handler and send the
659             // request to all known satellites
660             getInstance().forwardContainsSubordinateRequests(thisReq, asyncResp,
661                                                              satelliteInfo);
662             return;
663         }
664 
665         const boost::urls::segments_view urlSegments = thisReq.url().segments();
666         boost::urls::url currentUrl("/");
667         boost::urls::segments_view::iterator it = urlSegments.begin();
668         const boost::urls::segments_view::const_iterator end =
669             urlSegments.end();
670 
671         // Skip past the leading "/redfish/v1"
672         it++;
673         it++;
674         for (; it != end; it++)
675         {
676             if (std::binary_search(topCollections.begin(), topCollections.end(),
677                                    currentUrl.buffer()))
678             {
679                 // We've matched a resource collection so this current segment
680                 // must contain an aggregation prefix
681                 findSatellite(thisReq, asyncResp, satelliteInfo, *it);
682                 return;
683             }
684 
685             currentUrl.segments().push_back(*it);
686         }
687 
688         // We shouldn't reach this point since we should've hit one of the
689         // previous exits
690         messages::internalError(asyncResp->res);
691     }
692 
693     // Attempt to forward a request to the satellite BMC associated with the
694     // prefix.
695     void forwardRequest(
696         const crow::Request& thisReq,
697         const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
698         const std::string& prefix,
699         const std::unordered_map<std::string, boost::urls::url>& satelliteInfo)
700     {
701         const auto& sat = satelliteInfo.find(prefix);
702         if (sat == satelliteInfo.end())
703         {
704             // Realistically this shouldn't get called since we perform an
705             // earlier check to make sure the prefix exists
706             BMCWEB_LOG_ERROR << "Unrecognized satellite prefix \"" << prefix
707                              << "\"";
708             return;
709         }
710 
711         // We need to strip the prefix from the request's path
712         std::string targetURI(thisReq.target());
713         size_t pos = targetURI.find(prefix + "_");
714         if (pos == std::string::npos)
715         {
716             // If this fails then something went wrong
717             BMCWEB_LOG_ERROR << "Error removing prefix \"" << prefix
718                              << "_\" from request URI";
719             messages::internalError(asyncResp->res);
720             return;
721         }
722         targetURI.erase(pos, prefix.size() + 1);
723 
724         std::function<void(crow::Response&)> cb =
725             std::bind_front(processResponse, prefix, asyncResp);
726 
727         std::string data = thisReq.req.body();
728         client.sendDataWithCallback(
729             std::move(data), std::string(sat->second.host()),
730             sat->second.port_number(), targetURI, false /*useSSL*/,
731             thisReq.fields(), thisReq.method(), cb);
732     }
733 
734     // Forward a request for a collection URI to each known satellite BMC
735     void forwardCollectionRequests(
736         const crow::Request& thisReq,
737         const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
738         const std::unordered_map<std::string, boost::urls::url>& satelliteInfo)
739     {
740         for (const auto& sat : satelliteInfo)
741         {
742             std::function<void(crow::Response&)> cb = std::bind_front(
743                 processCollectionResponse, sat.first, asyncResp);
744 
745             std::string targetURI(thisReq.target());
746             std::string data = thisReq.req.body();
747             client.sendDataWithCallback(
748                 std::move(data), std::string(sat.second.host()),
749                 sat.second.port_number(), targetURI, false /*useSSL*/,
750                 thisReq.fields(), thisReq.method(), cb);
751         }
752     }
753 
754     // Forward request for a URI that is uptree of a top level collection to
755     // each known satellite BMC
756     void forwardContainsSubordinateRequests(
757         const crow::Request& thisReq,
758         const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
759         const std::unordered_map<std::string, boost::urls::url>& satelliteInfo)
760     {
761         for (const auto& sat : satelliteInfo)
762         {
763             std::function<void(crow::Response&)> cb = std::bind_front(
764                 processContainsSubordinateResponse, sat.first, asyncResp);
765 
766             // will ignore an expanded resource in the response if that resource
767             // is not already supported by the aggregating BMC
768             // TODO: Improve the processing so that we don't have to strip query
769             // params in this specific case
770             std::string targetURI(thisReq.url().path());
771             std::string data = thisReq.req.body();
772             client.sendDataWithCallback(
773                 std::move(data), std::string(sat.second.host()),
774                 sat.second.port_number(), targetURI, false /*useSSL*/,
775                 thisReq.fields(), thisReq.method(), cb);
776         }
777     }
778 
779   public:
780     explicit RedfishAggregator(boost::asio::io_context& ioc) :
781         client(ioc,
782                std::make_shared<crow::ConnectionPolicy>(getAggregationPolicy()))
783     {
784         getSatelliteConfigs(constructorCallback);
785     }
786     RedfishAggregator(const RedfishAggregator&) = delete;
787     RedfishAggregator& operator=(const RedfishAggregator&) = delete;
788     RedfishAggregator(RedfishAggregator&&) = delete;
789     RedfishAggregator& operator=(RedfishAggregator&&) = delete;
790     ~RedfishAggregator() = default;
791 
792     static RedfishAggregator& getInstance(boost::asio::io_context* io = nullptr)
793     {
794         static RedfishAggregator handler(*io);
795         return handler;
796     }
797 
798     // Polls D-Bus to get all available satellite config information
799     // Expects a handler which interacts with the returned configs
800     static void getSatelliteConfigs(
801         std::function<
802             void(const boost::system::error_code&,
803                  const std::unordered_map<std::string, boost::urls::url>&)>
804             handler)
805     {
806         BMCWEB_LOG_DEBUG << "Gathering satellite configs";
807         sdbusplus::message::object_path path("/xyz/openbmc_project/inventory");
808         dbus::utility::getManagedObjects(
809             "xyz.openbmc_project.EntityManager", path,
810             [handler{std::move(handler)}](
811                 const boost::system::error_code& ec,
812                 const dbus::utility::ManagedObjectType& objects) {
813             std::unordered_map<std::string, boost::urls::url> satelliteInfo;
814             if (ec)
815             {
816                 BMCWEB_LOG_ERROR << "DBUS response error " << ec.value() << ", "
817                                  << ec.message();
818                 handler(ec, satelliteInfo);
819                 return;
820             }
821 
822             // Maps a chosen alias representing a satellite BMC to a url
823             // containing the information required to create a http
824             // connection to the satellite
825             findSatelliteConfigs(objects, satelliteInfo);
826 
827             if (!satelliteInfo.empty())
828             {
829                 BMCWEB_LOG_DEBUG << "Redfish Aggregation enabled with "
830                                  << std::to_string(satelliteInfo.size())
831                                  << " satellite BMCs";
832             }
833             else
834             {
835                 BMCWEB_LOG_DEBUG
836                     << "No satellite BMCs detected.  Redfish Aggregation not enabled";
837             }
838             handler(ec, satelliteInfo);
839             });
840     }
841 
842     // Processes the response returned by a satellite BMC and loads its
843     // contents into asyncResp
844     static void
845         processResponse(std::string_view prefix,
846                         const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
847                         crow::Response& resp)
848     {
849         // 429 and 502 mean we didn't actually send the request so don't
850         // overwrite the response headers in that case
851         if ((resp.result() == boost::beast::http::status::too_many_requests) ||
852             (resp.result() == boost::beast::http::status::bad_gateway))
853         {
854             asyncResp->res.result(resp.result());
855             return;
856         }
857 
858         // We want to attempt prefix fixing regardless of response code
859         // The resp will not have a json component
860         // We need to create a json from resp's stringResponse
861         std::string_view contentType = resp.getHeaderValue("Content-Type");
862         if (boost::iequals(contentType, "application/json") ||
863             boost::iequals(contentType, "application/json; charset=utf-8"))
864         {
865             nlohmann::json jsonVal = nlohmann::json::parse(resp.body(), nullptr,
866                                                            false);
867             if (jsonVal.is_discarded())
868             {
869                 BMCWEB_LOG_ERROR << "Error parsing satellite response as JSON";
870                 messages::operationFailed(asyncResp->res);
871                 return;
872             }
873 
874             BMCWEB_LOG_DEBUG << "Successfully parsed satellite response";
875 
876             addPrefixes(jsonVal, prefix);
877 
878             BMCWEB_LOG_DEBUG << "Added prefix to parsed satellite response";
879 
880             asyncResp->res.result(resp.result());
881             asyncResp->res.jsonValue = std::move(jsonVal);
882 
883             BMCWEB_LOG_DEBUG << "Finished writing asyncResp";
884         }
885         else
886         {
887             // We allow any Content-Type that is not "application/json" now
888             asyncResp->res.result(resp.result());
889             asyncResp->res.write(resp.body());
890         }
891         addAggregatedHeaders(asyncResp->res, resp, prefix);
892     }
893 
894     // Processes the collection response returned by a satellite BMC and merges
895     // its "@odata.id" values
896     static void processCollectionResponse(
897         const std::string& prefix,
898         const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
899         crow::Response& resp)
900     {
901         // 429 and 502 mean we didn't actually send the request so don't
902         // overwrite the response headers in that case
903         if ((resp.result() == boost::beast::http::status::too_many_requests) ||
904             (resp.result() == boost::beast::http::status::bad_gateway))
905         {
906             return;
907         }
908 
909         if (resp.resultInt() != 200)
910         {
911             BMCWEB_LOG_DEBUG
912                 << "Collection resource does not exist in satellite BMC \""
913                 << prefix << "\"";
914             // Return the error if we haven't had any successes
915             if (asyncResp->res.resultInt() != 200)
916             {
917                 asyncResp->res.result(resp.result());
918                 asyncResp->res.write(resp.body());
919             }
920             return;
921         }
922 
923         // The resp will not have a json component
924         // We need to create a json from resp's stringResponse
925         std::string_view contentType = resp.getHeaderValue("Content-Type");
926         if (boost::iequals(contentType, "application/json") ||
927             boost::iequals(contentType, "application/json; charset=utf-8"))
928         {
929             nlohmann::json jsonVal = nlohmann::json::parse(resp.body(), nullptr,
930                                                            false);
931             if (jsonVal.is_discarded())
932             {
933                 BMCWEB_LOG_ERROR << "Error parsing satellite response as JSON";
934 
935                 // Notify the user if doing so won't overwrite a valid response
936                 if (asyncResp->res.resultInt() != 200)
937                 {
938                     messages::operationFailed(asyncResp->res);
939                 }
940                 return;
941             }
942 
943             BMCWEB_LOG_DEBUG << "Successfully parsed satellite response";
944 
945             // Now we need to add the prefix to the URIs contained in the
946             // response.
947             addPrefixes(jsonVal, prefix);
948 
949             BMCWEB_LOG_DEBUG << "Added prefix to parsed satellite response";
950 
951             // If this resource collection does not exist on the aggregating bmc
952             // and has not already been added from processing the response from
953             // a different satellite then we need to completely overwrite
954             // asyncResp
955             if (asyncResp->res.resultInt() != 200)
956             {
957                 // We only want to aggregate collections that contain a
958                 // "Members" array
959                 if ((!jsonVal.contains("Members")) &&
960                     (!jsonVal["Members"].is_array()))
961                 {
962                     BMCWEB_LOG_DEBUG
963                         << "Skipping aggregating unsupported resource";
964                     return;
965                 }
966 
967                 BMCWEB_LOG_DEBUG
968                     << "Collection does not exist, overwriting asyncResp";
969                 asyncResp->res.result(resp.result());
970                 asyncResp->res.jsonValue = std::move(jsonVal);
971                 asyncResp->res.addHeader("Content-Type", "application/json");
972 
973                 BMCWEB_LOG_DEBUG << "Finished overwriting asyncResp";
974             }
975             else
976             {
977                 // We only want to aggregate collections that contain a
978                 // "Members" array
979                 if ((!asyncResp->res.jsonValue.contains("Members")) &&
980                     (!asyncResp->res.jsonValue["Members"].is_array()))
981 
982                 {
983                     BMCWEB_LOG_DEBUG
984                         << "Skipping aggregating unsupported resource";
985                     return;
986                 }
987 
988                 BMCWEB_LOG_DEBUG << "Adding aggregated resources from \""
989                                  << prefix << "\" to collection";
990 
991                 // TODO: This is a potential race condition with multiple
992                 // satellites and the aggregating bmc attempting to write to
993                 // update this array.  May need to cascade calls to the next
994                 // satellite at the end of this function.
995                 // This is presumably not a concern when there is only a single
996                 // satellite since the aggregating bmc should have completed
997                 // before the response is received from the satellite.
998 
999                 auto& members = asyncResp->res.jsonValue["Members"];
1000                 auto& satMembers = jsonVal["Members"];
1001                 for (auto& satMem : satMembers)
1002                 {
1003                     members.emplace_back(std::move(satMem));
1004                 }
1005                 asyncResp->res.jsonValue["Members@odata.count"] =
1006                     members.size();
1007 
1008                 // TODO: Do we need to sort() after updating the array?
1009             }
1010         }
1011         else
1012         {
1013             BMCWEB_LOG_ERROR << "Received unparsable response from \"" << prefix
1014                              << "\"";
1015             // We received a response that was not a json.
1016             // Notify the user only if we did not receive any valid responses
1017             // and if the resource collection does not already exist on the
1018             // aggregating BMC
1019             if (asyncResp->res.resultInt() != 200)
1020             {
1021                 messages::operationFailed(asyncResp->res);
1022             }
1023         }
1024     } // End processCollectionResponse()
1025 
1026     // Processes the response returned by a satellite BMC and merges any
1027     // properties whose "@odata.id" value is the URI or either a top level
1028     // collection or is uptree from a top level collection
1029     static void processContainsSubordinateResponse(
1030         const std::string& prefix,
1031         const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
1032         crow::Response& resp)
1033     {
1034         // 429 and 502 mean we didn't actually send the request so don't
1035         // overwrite the response headers in that case
1036         if ((resp.result() == boost::beast::http::status::too_many_requests) ||
1037             (resp.result() == boost::beast::http::status::bad_gateway))
1038         {
1039             return;
1040         }
1041 
1042         if (resp.resultInt() != 200)
1043         {
1044             BMCWEB_LOG_DEBUG
1045                 << "Resource uptree from Collection does not exist in "
1046                 << "satellite BMC \"" << prefix << "\"";
1047             // Return the error if we haven't had any successes
1048             if (asyncResp->res.resultInt() != 200)
1049             {
1050                 asyncResp->res.result(resp.result());
1051                 asyncResp->res.write(resp.body());
1052             }
1053             return;
1054         }
1055 
1056         // The resp will not have a json component
1057         // We need to create a json from resp's stringResponse
1058         std::string_view contentType = resp.getHeaderValue("Content-Type");
1059         if (boost::iequals(contentType, "application/json") ||
1060             boost::iequals(contentType, "application/json; charset=utf-8"))
1061         {
1062             bool addedLinks = false;
1063             nlohmann::json jsonVal = nlohmann::json::parse(resp.body(), nullptr,
1064                                                            false);
1065             if (jsonVal.is_discarded())
1066             {
1067                 BMCWEB_LOG_ERROR << "Error parsing satellite response as JSON";
1068 
1069                 // Notify the user if doing so won't overwrite a valid response
1070                 if (asyncResp->res.resultInt() != 200)
1071                 {
1072                     messages::operationFailed(asyncResp->res);
1073                 }
1074                 return;
1075             }
1076 
1077             BMCWEB_LOG_DEBUG << "Successfully parsed satellite response";
1078 
1079             // Parse response and add properties missing from the AsyncResp
1080             // Valid properties will be of the form <property>.@odata.id and
1081             // @odata.id is a <URI>.  In other words, the json should contain
1082             // multiple properties such that
1083             // {"<property>":{"@odata.id": "<URI>"}}
1084             nlohmann::json::object_t* object =
1085                 jsonVal.get_ptr<nlohmann::json::object_t*>();
1086             if (object == nullptr)
1087             {
1088                 BMCWEB_LOG_ERROR << "Parsed JSON was not an object?";
1089                 return;
1090             }
1091 
1092             for (std::pair<const std::string, nlohmann::json>& prop : *object)
1093             {
1094                 if (!prop.second.contains("@odata.id"))
1095                 {
1096                     continue;
1097                 }
1098 
1099                 std::string* strValue =
1100                     prop.second["@odata.id"].get_ptr<std::string*>();
1101                 if (strValue == nullptr)
1102                 {
1103                     BMCWEB_LOG_CRITICAL << "Field wasn't a string????";
1104                     continue;
1105                 }
1106                 if (!searchCollectionsArray(*strValue, SearchType::CollOrCon))
1107                 {
1108                     continue;
1109                 }
1110 
1111                 addedLinks = true;
1112                 if (!asyncResp->res.jsonValue.contains(prop.first))
1113                 {
1114                     // Only add the property if it did not already exist
1115                     BMCWEB_LOG_DEBUG << "Adding link for " << *strValue
1116                                      << " from BMC " << prefix;
1117                     asyncResp->res.jsonValue[prop.first]["@odata.id"] =
1118                         *strValue;
1119                     continue;
1120                 }
1121             }
1122 
1123             // If we added links to a previously unsuccessful (non-200) response
1124             // then we need to make sure the response contains the bare minimum
1125             // amount of additional information that we'd expect to have been
1126             // populated.
1127             if (addedLinks && (asyncResp->res.resultInt() != 200))
1128             {
1129                 // This resource didn't locally exist or an error
1130                 // occurred while generating the response.  Remove any
1131                 // error messages and update the error code.
1132                 asyncResp->res.jsonValue.erase(
1133                     asyncResp->res.jsonValue.find("error"));
1134                 asyncResp->res.result(resp.result());
1135 
1136                 const auto& it1 = object->find("@odata.id");
1137                 if (it1 != object->end())
1138                 {
1139                     asyncResp->res.jsonValue["@odata.id"] = (it1->second);
1140                 }
1141                 const auto& it2 = object->find("@odata.type");
1142                 if (it2 != object->end())
1143                 {
1144                     asyncResp->res.jsonValue["@odata.type"] = (it2->second);
1145                 }
1146                 const auto& it3 = object->find("Id");
1147                 if (it3 != object->end())
1148                 {
1149                     asyncResp->res.jsonValue["Id"] = (it3->second);
1150                 }
1151                 const auto& it4 = object->find("Name");
1152                 if (it4 != object->end())
1153                 {
1154                     asyncResp->res.jsonValue["Name"] = (it4->second);
1155                 }
1156             }
1157         }
1158         else
1159         {
1160             BMCWEB_LOG_ERROR << "Received unparsable response from \"" << prefix
1161                              << "\"";
1162             // We received as response that was not a json
1163             // Notify the user only if we did not receive any valid responses,
1164             // and if the resource does not already exist on the aggregating BMC
1165             if (asyncResp->res.resultInt() != 200)
1166             {
1167                 messages::operationFailed(asyncResp->res);
1168             }
1169         }
1170     }
1171 
1172     // Entry point to Redfish Aggregation
1173     // Returns Result stating whether or not we still need to locally handle the
1174     // request
1175     static Result
1176         beginAggregation(const crow::Request& thisReq,
1177                          const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
1178     {
1179         using crow::utility::OrMorePaths;
1180         using crow::utility::readUrlSegments;
1181         const boost::urls::url_view url = thisReq.url();
1182 
1183         // We don't need to aggregate JsonSchemas due to potential issues such
1184         // as version mismatches between aggregator and satellite BMCs.  For
1185         // now assume that the aggregator has all the schemas and versions that
1186         // the aggregated server has.
1187         if (crow::utility::readUrlSegments(url, "redfish", "v1", "JsonSchemas",
1188                                            crow::utility::OrMorePaths()))
1189         {
1190             return Result::LocalHandle;
1191         }
1192 
1193         // The first two segments should be "/redfish/v1".  We need to check
1194         // that before we can search topCollections
1195         if (!crow::utility::readUrlSegments(url, "redfish", "v1",
1196                                             crow::utility::OrMorePaths()))
1197         {
1198             return Result::LocalHandle;
1199         }
1200 
1201         // Parse the URI to see if it begins with a known top level collection
1202         // such as:
1203         // /redfish/v1/Chassis
1204         // /redfish/v1/UpdateService/FirmwareInventory
1205         const boost::urls::segments_view urlSegments = url.segments();
1206         boost::urls::url currentUrl("/");
1207         boost::urls::segments_view::iterator it = urlSegments.begin();
1208         const boost::urls::segments_view::const_iterator end =
1209             urlSegments.end();
1210 
1211         // Skip past the leading "/redfish/v1"
1212         it++;
1213         it++;
1214         for (; it != end; it++)
1215         {
1216             const std::string& collectionItem = *it;
1217             if (std::binary_search(topCollections.begin(), topCollections.end(),
1218                                    currentUrl.buffer()))
1219             {
1220                 // We've matched a resource collection so this current segment
1221                 // might contain an aggregation prefix
1222                 // TODO: This needs to be rethought when we can support multiple
1223                 // satellites due to
1224                 // /redfish/v1/AggregationService/AggregationSources/5B247A
1225                 // being a local resource describing the satellite
1226                 if (collectionItem.starts_with("5B247A_"))
1227                 {
1228                     BMCWEB_LOG_DEBUG << "Need to forward a request";
1229 
1230                     // Extract the prefix from the request's URI, retrieve the
1231                     // associated satellite config information, and then forward
1232                     // the request to that satellite.
1233                     startAggregation(AggregationType::Resource, thisReq,
1234                                      asyncResp);
1235                     return Result::NoLocalHandle;
1236                 }
1237 
1238                 // Handle collection URI with a trailing backslash
1239                 // e.g. /redfish/v1/Chassis/
1240                 it++;
1241                 if ((it == end) && collectionItem.empty())
1242                 {
1243                     startAggregation(AggregationType::Collection, thisReq,
1244                                      asyncResp);
1245                 }
1246 
1247                 // We didn't recognize the prefix or it's a collection with a
1248                 // trailing "/".  In both cases we still want to locally handle
1249                 // the request
1250                 return Result::LocalHandle;
1251             }
1252 
1253             currentUrl.segments().push_back(collectionItem);
1254         }
1255 
1256         // If we made it here then currentUrl could contain a top level
1257         // collection URI without a trailing "/", e.g. /redfish/v1/Chassis
1258         if (std::binary_search(topCollections.begin(), topCollections.end(),
1259                                currentUrl.buffer()))
1260         {
1261             startAggregation(AggregationType::Collection, thisReq, asyncResp);
1262             return Result::LocalHandle;
1263         }
1264 
1265         // If nothing else then the request could be for a resource which has a
1266         // top level collection as a subordinate
1267         if (searchCollectionsArray(url.path(), SearchType::ContainsSubordinate))
1268         {
1269             startAggregation(AggregationType::ContainsSubordinate, thisReq,
1270                              asyncResp);
1271             return Result::LocalHandle;
1272         }
1273 
1274         BMCWEB_LOG_DEBUG << "Aggregation not required for " << url.buffer();
1275         return Result::LocalHandle;
1276     }
1277 };
1278 
1279 } // namespace redfish
1280