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