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 static void startAggregation( 591 AggregationType aggType, const crow::Request& thisReq, 592 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) 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 // Polls D-Bus to get all available satellite config information 893 // Expects a handler which interacts with the returned configs 894 static void getSatelliteConfigs( 895 std::function< 896 void(const boost::system::error_code&, 897 const std::unordered_map<std::string, boost::urls::url>&)> 898 handler) 899 { 900 BMCWEB_LOG_DEBUG("Gathering satellite configs"); 901 sdbusplus::message::object_path path("/xyz/openbmc_project/inventory"); 902 dbus::utility::getManagedObjects( 903 "xyz.openbmc_project.EntityManager", path, 904 [handler{std::move(handler)}]( 905 const boost::system::error_code& ec, 906 const dbus::utility::ManagedObjectType& objects) { 907 std::unordered_map<std::string, boost::urls::url> satelliteInfo; 908 if (ec) 909 { 910 BMCWEB_LOG_ERROR("DBUS response error {}, {}", ec.value(), 911 ec.message()); 912 handler(ec, satelliteInfo); 913 return; 914 } 915 916 // Maps a chosen alias representing a satellite BMC to a url 917 // containing the information required to create a http 918 // connection to the satellite 919 findSatelliteConfigs(objects, satelliteInfo); 920 921 if (!satelliteInfo.empty()) 922 { 923 BMCWEB_LOG_DEBUG( 924 "Redfish Aggregation enabled with {} satellite BMCs", 925 std::to_string(satelliteInfo.size())); 926 } 927 else 928 { 929 BMCWEB_LOG_DEBUG( 930 "No satellite BMCs detected. Redfish Aggregation not enabled"); 931 } 932 handler(ec, satelliteInfo); 933 }); 934 } 935 936 // Processes the response returned by a satellite BMC and loads its 937 // contents into asyncResp 938 static void processResponse( 939 std::string_view prefix, 940 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 941 crow::Response& resp) 942 { 943 // 429 and 502 mean we didn't actually send the request so don't 944 // overwrite the response headers in that case 945 if ((resp.result() == boost::beast::http::status::too_many_requests) || 946 (resp.result() == boost::beast::http::status::bad_gateway)) 947 { 948 asyncResp->res.result(resp.result()); 949 return; 950 } 951 952 // We want to attempt prefix fixing regardless of response code 953 // The resp will not have a json component 954 // We need to create a json from resp's stringResponse 955 if (isJsonContentType(resp.getHeaderValue("Content-Type"))) 956 { 957 nlohmann::json jsonVal = 958 nlohmann::json::parse(*resp.body(), nullptr, false); 959 if (jsonVal.is_discarded()) 960 { 961 BMCWEB_LOG_ERROR("Error parsing satellite response as JSON"); 962 messages::operationFailed(asyncResp->res); 963 return; 964 } 965 966 BMCWEB_LOG_DEBUG("Successfully parsed satellite response"); 967 968 addPrefixes(jsonVal, prefix); 969 970 BMCWEB_LOG_DEBUG("Added prefix to parsed satellite response"); 971 972 asyncResp->res.result(resp.result()); 973 asyncResp->res.jsonValue = std::move(jsonVal); 974 975 BMCWEB_LOG_DEBUG("Finished writing asyncResp"); 976 } 977 else 978 { 979 // We allow any Content-Type that is not "application/json" now 980 asyncResp->res.result(resp.result()); 981 asyncResp->res.copyBody(resp); 982 } 983 addAggregatedHeaders(asyncResp->res, resp, prefix); 984 } 985 986 // Processes the collection response returned by a satellite BMC and merges 987 // its "@odata.id" values 988 static void processCollectionResponse( 989 const std::string& prefix, 990 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 991 crow::Response& resp) 992 { 993 // 429 and 502 mean we didn't actually send the request so don't 994 // overwrite the response headers in that case 995 if ((resp.result() == boost::beast::http::status::too_many_requests) || 996 (resp.result() == boost::beast::http::status::bad_gateway)) 997 { 998 return; 999 } 1000 1001 if (resp.resultInt() != 200) 1002 { 1003 BMCWEB_LOG_DEBUG( 1004 "Collection resource does not exist in satellite BMC \"{}\"", 1005 prefix); 1006 // Return the error if we haven't had any successes 1007 if (asyncResp->res.resultInt() != 200) 1008 { 1009 asyncResp->res.result(resp.result()); 1010 asyncResp->res.copyBody(resp); 1011 } 1012 return; 1013 } 1014 1015 // The resp will not have a json component 1016 // We need to create a json from resp's stringResponse 1017 if (isJsonContentType(resp.getHeaderValue("Content-Type"))) 1018 { 1019 nlohmann::json jsonVal = 1020 nlohmann::json::parse(*resp.body(), nullptr, false); 1021 if (jsonVal.is_discarded()) 1022 { 1023 BMCWEB_LOG_ERROR("Error parsing satellite response as JSON"); 1024 1025 // Notify the user if doing so won't overwrite a valid response 1026 if (asyncResp->res.resultInt() != 200) 1027 { 1028 messages::operationFailed(asyncResp->res); 1029 } 1030 return; 1031 } 1032 1033 BMCWEB_LOG_DEBUG("Successfully parsed satellite response"); 1034 1035 // Now we need to add the prefix to the URIs contained in the 1036 // response. 1037 addPrefixes(jsonVal, prefix); 1038 1039 BMCWEB_LOG_DEBUG("Added prefix to parsed satellite response"); 1040 1041 // If this resource collection does not exist on the aggregating bmc 1042 // and has not already been added from processing the response from 1043 // a different satellite then we need to completely overwrite 1044 // asyncResp 1045 if (asyncResp->res.resultInt() != 200) 1046 { 1047 // We only want to aggregate collections that contain a 1048 // "Members" array 1049 if ((!jsonVal.contains("Members")) && 1050 (!jsonVal["Members"].is_array())) 1051 { 1052 BMCWEB_LOG_DEBUG( 1053 "Skipping aggregating unsupported resource"); 1054 return; 1055 } 1056 1057 BMCWEB_LOG_DEBUG( 1058 "Collection does not exist, overwriting asyncResp"); 1059 asyncResp->res.result(resp.result()); 1060 asyncResp->res.jsonValue = std::move(jsonVal); 1061 asyncResp->res.addHeader("Content-Type", "application/json"); 1062 1063 BMCWEB_LOG_DEBUG("Finished overwriting asyncResp"); 1064 } 1065 else 1066 { 1067 // We only want to aggregate collections that contain a 1068 // "Members" array 1069 if ((!asyncResp->res.jsonValue.contains("Members")) && 1070 (!asyncResp->res.jsonValue["Members"].is_array())) 1071 1072 { 1073 BMCWEB_LOG_DEBUG( 1074 "Skipping aggregating unsupported resource"); 1075 return; 1076 } 1077 1078 BMCWEB_LOG_DEBUG( 1079 "Adding aggregated resources from \"{}\" to collection", 1080 prefix); 1081 1082 // TODO: This is a potential race condition with multiple 1083 // satellites and the aggregating bmc attempting to write to 1084 // update this array. May need to cascade calls to the next 1085 // satellite at the end of this function. 1086 // This is presumably not a concern when there is only a single 1087 // satellite since the aggregating bmc should have completed 1088 // before the response is received from the satellite. 1089 1090 auto& members = asyncResp->res.jsonValue["Members"]; 1091 auto& satMembers = jsonVal["Members"]; 1092 for (auto& satMem : satMembers) 1093 { 1094 members.emplace_back(std::move(satMem)); 1095 } 1096 asyncResp->res.jsonValue["Members@odata.count"] = 1097 members.size(); 1098 1099 // TODO: Do we need to sort() after updating the array? 1100 } 1101 } 1102 else 1103 { 1104 BMCWEB_LOG_ERROR("Received unparsable response from \"{}\"", 1105 prefix); 1106 // We received a response that was not a json. 1107 // Notify the user only if we did not receive any valid responses 1108 // and if the resource collection does not already exist on the 1109 // aggregating BMC 1110 if (asyncResp->res.resultInt() != 200) 1111 { 1112 messages::operationFailed(asyncResp->res); 1113 } 1114 } 1115 } // End processCollectionResponse() 1116 1117 // Processes the response returned by a satellite BMC and merges any 1118 // properties whose "@odata.id" value is the URI or either a top level 1119 // collection or is uptree from a top level collection 1120 static void processContainsSubordinateResponse( 1121 const std::string& prefix, 1122 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 1123 crow::Response& resp) 1124 { 1125 // 429 and 502 mean we didn't actually send the request so don't 1126 // overwrite the response headers in that case 1127 if ((resp.result() == boost::beast::http::status::too_many_requests) || 1128 (resp.result() == boost::beast::http::status::bad_gateway)) 1129 { 1130 return; 1131 } 1132 1133 if (resp.resultInt() != 200) 1134 { 1135 BMCWEB_LOG_DEBUG( 1136 "Resource uptree from Collection does not exist in satellite BMC \"{}\"", 1137 prefix); 1138 // Return the error if we haven't had any successes 1139 if (asyncResp->res.resultInt() != 200) 1140 { 1141 asyncResp->res.result(resp.result()); 1142 asyncResp->res.copyBody(resp); 1143 } 1144 return; 1145 } 1146 1147 // The resp will not have a json component 1148 // We need to create a json from resp's stringResponse 1149 if (isJsonContentType(resp.getHeaderValue("Content-Type"))) 1150 { 1151 bool addedLinks = false; 1152 nlohmann::json jsonVal = 1153 nlohmann::json::parse(*resp.body(), nullptr, false); 1154 if (jsonVal.is_discarded()) 1155 { 1156 BMCWEB_LOG_ERROR("Error parsing satellite response as JSON"); 1157 1158 // Notify the user if doing so won't overwrite a valid response 1159 if (asyncResp->res.resultInt() != 200) 1160 { 1161 messages::operationFailed(asyncResp->res); 1162 } 1163 return; 1164 } 1165 1166 BMCWEB_LOG_DEBUG("Successfully parsed satellite response"); 1167 1168 // Parse response and add properties missing from the AsyncResp 1169 // Valid properties will be of the form <property>.@odata.id and 1170 // @odata.id is a <URI>. In other words, the json should contain 1171 // multiple properties such that 1172 // {"<property>":{"@odata.id": "<URI>"}} 1173 nlohmann::json::object_t* object = 1174 jsonVal.get_ptr<nlohmann::json::object_t*>(); 1175 if (object == nullptr) 1176 { 1177 BMCWEB_LOG_ERROR("Parsed JSON was not an object?"); 1178 return; 1179 } 1180 1181 for (std::pair<const std::string, nlohmann::json>& prop : *object) 1182 { 1183 if (!prop.second.contains("@odata.id")) 1184 { 1185 continue; 1186 } 1187 1188 std::string* strValue = 1189 prop.second["@odata.id"].get_ptr<std::string*>(); 1190 if (strValue == nullptr) 1191 { 1192 BMCWEB_LOG_CRITICAL("Field wasn't a string????"); 1193 continue; 1194 } 1195 if (!searchCollectionsArray(*strValue, SearchType::CollOrCon)) 1196 { 1197 continue; 1198 } 1199 1200 addedLinks = true; 1201 if (!asyncResp->res.jsonValue.contains(prop.first)) 1202 { 1203 // Only add the property if it did not already exist 1204 BMCWEB_LOG_DEBUG("Adding link for {} from BMC {}", 1205 *strValue, prefix); 1206 asyncResp->res.jsonValue[prop.first]["@odata.id"] = 1207 *strValue; 1208 continue; 1209 } 1210 } 1211 1212 // If we added links to a previously unsuccessful (non-200) response 1213 // then we need to make sure the response contains the bare minimum 1214 // amount of additional information that we'd expect to have been 1215 // populated. 1216 if (addedLinks && (asyncResp->res.resultInt() != 200)) 1217 { 1218 // This resource didn't locally exist or an error 1219 // occurred while generating the response. Remove any 1220 // error messages and update the error code. 1221 asyncResp->res.jsonValue.erase( 1222 asyncResp->res.jsonValue.find("error")); 1223 asyncResp->res.result(resp.result()); 1224 1225 const auto& it1 = object->find("@odata.id"); 1226 if (it1 != object->end()) 1227 { 1228 asyncResp->res.jsonValue["@odata.id"] = (it1->second); 1229 } 1230 const auto& it2 = object->find("@odata.type"); 1231 if (it2 != object->end()) 1232 { 1233 asyncResp->res.jsonValue["@odata.type"] = (it2->second); 1234 } 1235 const auto& it3 = object->find("Id"); 1236 if (it3 != object->end()) 1237 { 1238 asyncResp->res.jsonValue["Id"] = (it3->second); 1239 } 1240 const auto& it4 = object->find("Name"); 1241 if (it4 != object->end()) 1242 { 1243 asyncResp->res.jsonValue["Name"] = (it4->second); 1244 } 1245 } 1246 } 1247 else 1248 { 1249 BMCWEB_LOG_ERROR("Received unparsable response from \"{}\"", 1250 prefix); 1251 // We received as response that was not a json 1252 // Notify the user only if we did not receive any valid responses, 1253 // and if the resource does not already exist on the aggregating BMC 1254 if (asyncResp->res.resultInt() != 200) 1255 { 1256 messages::operationFailed(asyncResp->res); 1257 } 1258 } 1259 } 1260 1261 // Entry point to Redfish Aggregation 1262 // Returns Result stating whether or not we still need to locally handle the 1263 // request 1264 static Result beginAggregation( 1265 const crow::Request& thisReq, 1266 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) 1267 { 1268 using crow::utility::OrMorePaths; 1269 using crow::utility::readUrlSegments; 1270 boost::urls::url_view url = thisReq.url(); 1271 1272 // We don't need to aggregate JsonSchemas due to potential issues such 1273 // as version mismatches between aggregator and satellite BMCs. For 1274 // now assume that the aggregator has all the schemas and versions that 1275 // the aggregated server has. 1276 if (crow::utility::readUrlSegments(url, "redfish", "v1", "JsonSchemas", 1277 crow::utility::OrMorePaths())) 1278 { 1279 return Result::LocalHandle; 1280 } 1281 1282 // The first two segments should be "/redfish/v1". We need to check 1283 // that before we can search topCollections 1284 if (!crow::utility::readUrlSegments(url, "redfish", "v1", 1285 crow::utility::OrMorePaths())) 1286 { 1287 return Result::LocalHandle; 1288 } 1289 1290 // Parse the URI to see if it begins with a known top level collection 1291 // such as: 1292 // /redfish/v1/Chassis 1293 // /redfish/v1/UpdateService/FirmwareInventory 1294 const boost::urls::segments_view urlSegments = url.segments(); 1295 boost::urls::url currentUrl("/"); 1296 boost::urls::segments_view::const_iterator it = urlSegments.begin(); 1297 boost::urls::segments_view::const_iterator end = urlSegments.end(); 1298 1299 // Skip past the leading "/redfish/v1" 1300 it++; 1301 it++; 1302 for (; it != end; it++) 1303 { 1304 const std::string& collectionItem = *it; 1305 if (std::binary_search(topCollections.begin(), topCollections.end(), 1306 currentUrl.buffer())) 1307 { 1308 // We've matched a resource collection so this current segment 1309 // might contain an aggregation prefix 1310 // TODO: This needs to be rethought when we can support multiple 1311 // satellites due to 1312 // /redfish/v1/AggregationService/AggregationSources/5B247A 1313 // being a local resource describing the satellite 1314 if (collectionItem.starts_with("5B247A_")) 1315 { 1316 BMCWEB_LOG_DEBUG("Need to forward a request"); 1317 1318 // Extract the prefix from the request's URI, retrieve the 1319 // associated satellite config information, and then forward 1320 // the request to that satellite. 1321 startAggregation(AggregationType::Resource, thisReq, 1322 asyncResp); 1323 return Result::NoLocalHandle; 1324 } 1325 1326 // Handle collection URI with a trailing backslash 1327 // e.g. /redfish/v1/Chassis/ 1328 it++; 1329 if ((it == end) && collectionItem.empty()) 1330 { 1331 startAggregation(AggregationType::Collection, thisReq, 1332 asyncResp); 1333 } 1334 1335 // We didn't recognize the prefix or it's a collection with a 1336 // trailing "/". In both cases we still want to locally handle 1337 // the request 1338 return Result::LocalHandle; 1339 } 1340 1341 currentUrl.segments().push_back(collectionItem); 1342 } 1343 1344 // If we made it here then currentUrl could contain a top level 1345 // collection URI without a trailing "/", e.g. /redfish/v1/Chassis 1346 if (std::binary_search(topCollections.begin(), topCollections.end(), 1347 currentUrl.buffer())) 1348 { 1349 startAggregation(AggregationType::Collection, thisReq, asyncResp); 1350 return Result::LocalHandle; 1351 } 1352 1353 // If nothing else then the request could be for a resource which has a 1354 // top level collection as a subordinate 1355 if (searchCollectionsArray(url.path(), SearchType::ContainsSubordinate)) 1356 { 1357 startAggregation(AggregationType::ContainsSubordinate, thisReq, 1358 asyncResp); 1359 return Result::LocalHandle; 1360 } 1361 1362 BMCWEB_LOG_DEBUG("Aggregation not required for {}", url.buffer()); 1363 return Result::LocalHandle; 1364 } 1365 }; 1366 1367 } // namespace redfish 1368