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