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