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