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