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