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