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