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