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