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