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