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