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 constexpr std::string_view serviceRootUri = "/redfish/v1"; 61 62 // The passed URI must begin with "/redfish/v1", but we have to strip it 63 // from the URI since topCollections does not include it in its URIs 64 if (!uri.starts_with(serviceRootUri)) 65 { 66 return false; 67 } 68 69 // Catch empty final segments such as "/redfish/v1/Chassis//" 70 if (uri.ends_with("//")) 71 { 72 return false; 73 } 74 75 std::size_t parseCount = uri.size() - serviceRootUri.size(); 76 // Don't include the trailing "/" if it exists such as in "/redfish/v1/" 77 if (uri.ends_with("/")) 78 { 79 parseCount--; 80 } 81 82 boost::system::result<boost::urls::url_view> parsedUrl = 83 boost::urls::parse_relative_ref( 84 uri.substr(serviceRootUri.size(), parseCount)); 85 if (!parsedUrl) 86 { 87 BMCWEB_LOG_ERROR("Failed to get target URI from {}", 88 uri.substr(serviceRootUri.size())); 89 return false; 90 } 91 92 if (!parsedUrl->segments().is_absolute() && !parsedUrl->segments().empty()) 93 { 94 return false; 95 } 96 97 // If no segments() then the passed URI was either "/redfish/v1" or 98 // "/redfish/v1/". 99 if (parsedUrl->segments().empty()) 100 { 101 return (searchType == SearchType::ContainsSubordinate) || 102 (searchType == SearchType::CollOrCon); 103 } 104 std::string_view url = parsedUrl->buffer(); 105 const auto* it = std::ranges::lower_bound(topCollections, url); 106 if (it == topCollections.end()) 107 { 108 // parsedUrl is alphabetically after the last entry in the array so it 109 // can't be a top collection or up tree from a top collection 110 return false; 111 } 112 113 boost::urls::url collectionUrl(*it); 114 boost::urls::segments_view collectionSegments = collectionUrl.segments(); 115 boost::urls::segments_view::iterator itCollection = 116 collectionSegments.begin(); 117 const boost::urls::segments_view::const_iterator endCollection = 118 collectionSegments.end(); 119 120 // Each segment in the passed URI should match the found collection 121 for (const auto& segment : parsedUrl->segments()) 122 { 123 if (itCollection == endCollection) 124 { 125 // Leftover segments means the target is for an aggregation 126 // supported resource 127 return searchType == SearchType::Resource; 128 } 129 130 if (segment != (*itCollection)) 131 { 132 return false; 133 } 134 itCollection++; 135 } 136 137 // No remaining segments means the passed URI was a top level collection 138 if (searchType == SearchType::Collection) 139 { 140 return itCollection == endCollection; 141 } 142 if (searchType == SearchType::ContainsSubordinate) 143 { 144 return itCollection != endCollection; 145 } 146 147 // Return this check instead of "true" in case other SearchTypes get added 148 return searchType == SearchType::CollOrCon; 149 } 150 151 // Determines if the passed property contains a URI. Those property names 152 // either end with a case-insensitive version of "uri" or are specifically 153 // defined in the above array. 154 inline bool isPropertyUri(std::string_view propertyName) 155 { 156 if (propertyName.ends_with("uri") || propertyName.ends_with("Uri") || 157 propertyName.ends_with("URI")) 158 { 159 return true; 160 } 161 return std::binary_search(nonUriProperties.begin(), nonUriProperties.end(), 162 propertyName); 163 } 164 165 static inline void addPrefixToStringItem(std::string& strValue, 166 std::string_view prefix) 167 { 168 // Make sure the value is a properly formatted URI 169 auto parsed = boost::urls::parse_relative_ref(strValue); 170 if (!parsed) 171 { 172 // Note that DMTF URIs such as 173 // https://redfish.dmtf.org/registries/Base.1.15.0.json will fail this 174 // check and that's okay 175 BMCWEB_LOG_DEBUG("Couldn't parse URI from resource {}", strValue); 176 return; 177 } 178 179 boost::urls::url_view thisUrl = *parsed; 180 181 // We don't need to aggregate JsonSchemas due to potential issues such as 182 // version mismatches between aggregator and satellite BMCs. For now 183 // assume that the aggregator has all the schemas and versions that the 184 // aggregated server has. 185 if (crow::utility::readUrlSegments(thisUrl, "redfish", "v1", "JsonSchemas", 186 crow::utility::OrMorePaths())) 187 { 188 BMCWEB_LOG_DEBUG("Skipping JsonSchemas URI prefix fixing"); 189 return; 190 } 191 192 // The first two segments should be "/redfish/v1". We need to check that 193 // before we can search topCollections 194 if (!crow::utility::readUrlSegments(thisUrl, "redfish", "v1", 195 crow::utility::OrMorePaths())) 196 { 197 return; 198 } 199 200 // Check array adding a segment each time until collection is identified 201 // Add prefix to segment after the collection 202 const boost::urls::segments_view urlSegments = thisUrl.segments(); 203 bool addedPrefix = false; 204 boost::urls::url url("/"); 205 boost::urls::segments_view::const_iterator it = urlSegments.begin(); 206 const boost::urls::segments_view::const_iterator end = urlSegments.end(); 207 208 // Skip past the leading "/redfish/v1" 209 it++; 210 it++; 211 for (; it != end; it++) 212 { 213 // Trailing "/" will result in an empty segment. In that case we need 214 // to return so we don't apply a prefix to top level collections such 215 // as "/redfish/v1/Chassis/" 216 if ((*it).empty()) 217 { 218 return; 219 } 220 221 if (std::binary_search(topCollections.begin(), topCollections.end(), 222 url.buffer())) 223 { 224 std::string collectionItem(prefix); 225 collectionItem += "_" + (*it); 226 url.segments().push_back(collectionItem); 227 it++; 228 addedPrefix = true; 229 break; 230 } 231 232 url.segments().push_back(*it); 233 } 234 235 // Finish constructing the URL here (if needed) to avoid additional checks 236 for (; it != end; it++) 237 { 238 url.segments().push_back(*it); 239 } 240 241 if (addedPrefix) 242 { 243 url.segments().insert(url.segments().begin(), {"redfish", "v1"}); 244 strValue = url.buffer(); 245 } 246 } 247 248 static inline void addPrefixToItem(nlohmann::json& item, 249 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 static 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 static 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 static 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 = 1, 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, thisReq.fields(), 741 thisReq.method(), cb); 742 } 743 744 // Forward a request for a collection URI to each known satellite BMC 745 void forwardCollectionRequests( 746 const crow::Request& thisReq, 747 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 748 const std::unordered_map<std::string, boost::urls::url>& satelliteInfo) 749 { 750 for (const auto& sat : satelliteInfo) 751 { 752 std::function<void(crow::Response&)> cb = std::bind_front( 753 processCollectionResponse, sat.first, asyncResp); 754 755 boost::urls::url url(sat.second); 756 url.set_path(thisReq.url().path()); 757 if (thisReq.url().has_query()) 758 { 759 url.set_query(thisReq.url().query()); 760 } 761 std::string data = thisReq.body(); 762 client.sendDataWithCallback(std::move(data), url, thisReq.fields(), 763 thisReq.method(), cb); 764 } 765 } 766 767 // Forward request for a URI that is uptree of a top level collection to 768 // each known satellite BMC 769 void forwardContainsSubordinateRequests( 770 const crow::Request& thisReq, 771 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 772 const std::unordered_map<std::string, boost::urls::url>& satelliteInfo) 773 { 774 for (const auto& sat : satelliteInfo) 775 { 776 std::function<void(crow::Response&)> cb = std::bind_front( 777 processContainsSubordinateResponse, sat.first, asyncResp); 778 779 // will ignore an expanded resource in the response if that resource 780 // is not already supported by the aggregating BMC 781 // TODO: Improve the processing so that we don't have to strip query 782 // params in this specific case 783 boost::urls::url url(sat.second); 784 url.set_path(thisReq.url().path()); 785 786 std::string data = thisReq.body(); 787 788 client.sendDataWithCallback(std::move(data), url, thisReq.fields(), 789 thisReq.method(), cb); 790 } 791 } 792 793 public: 794 explicit RedfishAggregator(boost::asio::io_context& ioc) : 795 client(ioc, 796 std::make_shared<crow::ConnectionPolicy>(getAggregationPolicy())) 797 { 798 getSatelliteConfigs(constructorCallback); 799 } 800 RedfishAggregator(const RedfishAggregator&) = delete; 801 RedfishAggregator& operator=(const RedfishAggregator&) = delete; 802 RedfishAggregator(RedfishAggregator&&) = delete; 803 RedfishAggregator& operator=(RedfishAggregator&&) = delete; 804 ~RedfishAggregator() = default; 805 806 static RedfishAggregator& getInstance(boost::asio::io_context* io = nullptr) 807 { 808 static RedfishAggregator handler(*io); 809 return handler; 810 } 811 812 // Polls D-Bus to get all available satellite config information 813 // Expects a handler which interacts with the returned configs 814 static void getSatelliteConfigs( 815 std::function< 816 void(const boost::system::error_code&, 817 const std::unordered_map<std::string, boost::urls::url>&)> 818 handler) 819 { 820 BMCWEB_LOG_DEBUG("Gathering satellite configs"); 821 sdbusplus::message::object_path path("/xyz/openbmc_project/inventory"); 822 dbus::utility::getManagedObjects( 823 "xyz.openbmc_project.EntityManager", path, 824 [handler{std::move(handler)}]( 825 const boost::system::error_code& ec, 826 const dbus::utility::ManagedObjectType& objects) { 827 std::unordered_map<std::string, boost::urls::url> satelliteInfo; 828 if (ec) 829 { 830 BMCWEB_LOG_ERROR("DBUS response error {}, {}", ec.value(), 831 ec.message()); 832 handler(ec, satelliteInfo); 833 return; 834 } 835 836 // Maps a chosen alias representing a satellite BMC to a url 837 // containing the information required to create a http 838 // connection to the satellite 839 findSatelliteConfigs(objects, satelliteInfo); 840 841 if (!satelliteInfo.empty()) 842 { 843 BMCWEB_LOG_DEBUG( 844 "Redfish Aggregation enabled with {} satellite BMCs", 845 std::to_string(satelliteInfo.size())); 846 } 847 else 848 { 849 BMCWEB_LOG_DEBUG( 850 "No satellite BMCs detected. Redfish Aggregation not enabled"); 851 } 852 handler(ec, satelliteInfo); 853 }); 854 } 855 856 // Processes the response returned by a satellite BMC and loads its 857 // contents into asyncResp 858 static void 859 processResponse(std::string_view prefix, 860 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 861 crow::Response& resp) 862 { 863 // 429 and 502 mean we didn't actually send the request so don't 864 // overwrite the response headers in that case 865 if ((resp.result() == boost::beast::http::status::too_many_requests) || 866 (resp.result() == boost::beast::http::status::bad_gateway)) 867 { 868 asyncResp->res.result(resp.result()); 869 return; 870 } 871 872 // We want to attempt prefix fixing regardless of response code 873 // The resp will not have a json component 874 // We need to create a json from resp's stringResponse 875 if (isJsonContentType(resp.getHeaderValue("Content-Type"))) 876 { 877 nlohmann::json jsonVal = nlohmann::json::parse(*resp.body(), 878 nullptr, 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.copyBody(resp); 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.copyBody(resp); 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 if (isJsonContentType(resp.getHeaderValue("Content-Type"))) 938 { 939 nlohmann::json jsonVal = nlohmann::json::parse(*resp.body(), 940 nullptr, false); 941 if (jsonVal.is_discarded()) 942 { 943 BMCWEB_LOG_ERROR("Error parsing satellite response as JSON"); 944 945 // Notify the user if doing so won't overwrite a valid response 946 if (asyncResp->res.resultInt() != 200) 947 { 948 messages::operationFailed(asyncResp->res); 949 } 950 return; 951 } 952 953 BMCWEB_LOG_DEBUG("Successfully parsed satellite response"); 954 955 // Now we need to add the prefix to the URIs contained in the 956 // response. 957 addPrefixes(jsonVal, prefix); 958 959 BMCWEB_LOG_DEBUG("Added prefix to parsed satellite response"); 960 961 // If this resource collection does not exist on the aggregating bmc 962 // and has not already been added from processing the response from 963 // a different satellite then we need to completely overwrite 964 // asyncResp 965 if (asyncResp->res.resultInt() != 200) 966 { 967 // We only want to aggregate collections that contain a 968 // "Members" array 969 if ((!jsonVal.contains("Members")) && 970 (!jsonVal["Members"].is_array())) 971 { 972 BMCWEB_LOG_DEBUG( 973 "Skipping aggregating unsupported resource"); 974 return; 975 } 976 977 BMCWEB_LOG_DEBUG( 978 "Collection does not exist, overwriting asyncResp"); 979 asyncResp->res.result(resp.result()); 980 asyncResp->res.jsonValue = std::move(jsonVal); 981 asyncResp->res.addHeader("Content-Type", "application/json"); 982 983 BMCWEB_LOG_DEBUG("Finished overwriting asyncResp"); 984 } 985 else 986 { 987 // We only want to aggregate collections that contain a 988 // "Members" array 989 if ((!asyncResp->res.jsonValue.contains("Members")) && 990 (!asyncResp->res.jsonValue["Members"].is_array())) 991 992 { 993 BMCWEB_LOG_DEBUG( 994 "Skipping aggregating unsupported resource"); 995 return; 996 } 997 998 BMCWEB_LOG_DEBUG( 999 "Adding aggregated resources from \"{}\" to collection", 1000 prefix); 1001 1002 // TODO: This is a potential race condition with multiple 1003 // satellites and the aggregating bmc attempting to write to 1004 // update this array. May need to cascade calls to the next 1005 // satellite at the end of this function. 1006 // This is presumably not a concern when there is only a single 1007 // satellite since the aggregating bmc should have completed 1008 // before the response is received from the satellite. 1009 1010 auto& members = asyncResp->res.jsonValue["Members"]; 1011 auto& satMembers = jsonVal["Members"]; 1012 for (auto& satMem : satMembers) 1013 { 1014 members.emplace_back(std::move(satMem)); 1015 } 1016 asyncResp->res.jsonValue["Members@odata.count"] = 1017 members.size(); 1018 1019 // TODO: Do we need to sort() after updating the array? 1020 } 1021 } 1022 else 1023 { 1024 BMCWEB_LOG_ERROR("Received unparsable response from \"{}\"", 1025 prefix); 1026 // We received a response that was not a json. 1027 // Notify the user only if we did not receive any valid responses 1028 // and if the resource collection does not already exist on the 1029 // aggregating BMC 1030 if (asyncResp->res.resultInt() != 200) 1031 { 1032 messages::operationFailed(asyncResp->res); 1033 } 1034 } 1035 } // End processCollectionResponse() 1036 1037 // Processes the response returned by a satellite BMC and merges any 1038 // properties whose "@odata.id" value is the URI or either a top level 1039 // collection or is uptree from a top level collection 1040 static void processContainsSubordinateResponse( 1041 const std::string& prefix, 1042 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 1043 crow::Response& resp) 1044 { 1045 // 429 and 502 mean we didn't actually send the request so don't 1046 // overwrite the response headers in that case 1047 if ((resp.result() == boost::beast::http::status::too_many_requests) || 1048 (resp.result() == boost::beast::http::status::bad_gateway)) 1049 { 1050 return; 1051 } 1052 1053 if (resp.resultInt() != 200) 1054 { 1055 BMCWEB_LOG_DEBUG( 1056 "Resource uptree from Collection does not exist in satellite BMC \"{}\"", 1057 prefix); 1058 // Return the error if we haven't had any successes 1059 if (asyncResp->res.resultInt() != 200) 1060 { 1061 asyncResp->res.result(resp.result()); 1062 asyncResp->res.copyBody(resp); 1063 } 1064 return; 1065 } 1066 1067 // The resp will not have a json component 1068 // We need to create a json from resp's stringResponse 1069 if (isJsonContentType(resp.getHeaderValue("Content-Type"))) 1070 { 1071 bool addedLinks = false; 1072 nlohmann::json jsonVal = nlohmann::json::parse(*resp.body(), 1073 nullptr, false); 1074 if (jsonVal.is_discarded()) 1075 { 1076 BMCWEB_LOG_ERROR("Error parsing satellite response as JSON"); 1077 1078 // Notify the user if doing so won't overwrite a valid response 1079 if (asyncResp->res.resultInt() != 200) 1080 { 1081 messages::operationFailed(asyncResp->res); 1082 } 1083 return; 1084 } 1085 1086 BMCWEB_LOG_DEBUG("Successfully parsed satellite response"); 1087 1088 // Parse response and add properties missing from the AsyncResp 1089 // Valid properties will be of the form <property>.@odata.id and 1090 // @odata.id is a <URI>. In other words, the json should contain 1091 // multiple properties such that 1092 // {"<property>":{"@odata.id": "<URI>"}} 1093 nlohmann::json::object_t* object = 1094 jsonVal.get_ptr<nlohmann::json::object_t*>(); 1095 if (object == nullptr) 1096 { 1097 BMCWEB_LOG_ERROR("Parsed JSON was not an object?"); 1098 return; 1099 } 1100 1101 for (std::pair<const std::string, nlohmann::json>& prop : *object) 1102 { 1103 if (!prop.second.contains("@odata.id")) 1104 { 1105 continue; 1106 } 1107 1108 std::string* strValue = 1109 prop.second["@odata.id"].get_ptr<std::string*>(); 1110 if (strValue == nullptr) 1111 { 1112 BMCWEB_LOG_CRITICAL("Field wasn't a string????"); 1113 continue; 1114 } 1115 if (!searchCollectionsArray(*strValue, SearchType::CollOrCon)) 1116 { 1117 continue; 1118 } 1119 1120 addedLinks = true; 1121 if (!asyncResp->res.jsonValue.contains(prop.first)) 1122 { 1123 // Only add the property if it did not already exist 1124 BMCWEB_LOG_DEBUG("Adding link for {} from BMC {}", 1125 *strValue, prefix); 1126 asyncResp->res.jsonValue[prop.first]["@odata.id"] = 1127 *strValue; 1128 continue; 1129 } 1130 } 1131 1132 // If we added links to a previously unsuccessful (non-200) response 1133 // then we need to make sure the response contains the bare minimum 1134 // amount of additional information that we'd expect to have been 1135 // populated. 1136 if (addedLinks && (asyncResp->res.resultInt() != 200)) 1137 { 1138 // This resource didn't locally exist or an error 1139 // occurred while generating the response. Remove any 1140 // error messages and update the error code. 1141 asyncResp->res.jsonValue.erase( 1142 asyncResp->res.jsonValue.find("error")); 1143 asyncResp->res.result(resp.result()); 1144 1145 const auto& it1 = object->find("@odata.id"); 1146 if (it1 != object->end()) 1147 { 1148 asyncResp->res.jsonValue["@odata.id"] = (it1->second); 1149 } 1150 const auto& it2 = object->find("@odata.type"); 1151 if (it2 != object->end()) 1152 { 1153 asyncResp->res.jsonValue["@odata.type"] = (it2->second); 1154 } 1155 const auto& it3 = object->find("Id"); 1156 if (it3 != object->end()) 1157 { 1158 asyncResp->res.jsonValue["Id"] = (it3->second); 1159 } 1160 const auto& it4 = object->find("Name"); 1161 if (it4 != object->end()) 1162 { 1163 asyncResp->res.jsonValue["Name"] = (it4->second); 1164 } 1165 } 1166 } 1167 else 1168 { 1169 BMCWEB_LOG_ERROR("Received unparsable response from \"{}\"", 1170 prefix); 1171 // We received as response that was not a json 1172 // Notify the user only if we did not receive any valid responses, 1173 // and if the resource does not already exist on the aggregating BMC 1174 if (asyncResp->res.resultInt() != 200) 1175 { 1176 messages::operationFailed(asyncResp->res); 1177 } 1178 } 1179 } 1180 1181 // Entry point to Redfish Aggregation 1182 // Returns Result stating whether or not we still need to locally handle the 1183 // request 1184 static Result 1185 beginAggregation(const crow::Request& thisReq, 1186 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) 1187 { 1188 using crow::utility::OrMorePaths; 1189 using crow::utility::readUrlSegments; 1190 boost::urls::url_view url = thisReq.url(); 1191 1192 // We don't need to aggregate JsonSchemas due to potential issues such 1193 // as version mismatches between aggregator and satellite BMCs. For 1194 // now assume that the aggregator has all the schemas and versions that 1195 // the aggregated server has. 1196 if (crow::utility::readUrlSegments(url, "redfish", "v1", "JsonSchemas", 1197 crow::utility::OrMorePaths())) 1198 { 1199 return Result::LocalHandle; 1200 } 1201 1202 // The first two segments should be "/redfish/v1". We need to check 1203 // that before we can search topCollections 1204 if (!crow::utility::readUrlSegments(url, "redfish", "v1", 1205 crow::utility::OrMorePaths())) 1206 { 1207 return Result::LocalHandle; 1208 } 1209 1210 // Parse the URI to see if it begins with a known top level collection 1211 // such as: 1212 // /redfish/v1/Chassis 1213 // /redfish/v1/UpdateService/FirmwareInventory 1214 const boost::urls::segments_view urlSegments = url.segments(); 1215 boost::urls::url currentUrl("/"); 1216 boost::urls::segments_view::const_iterator it = urlSegments.begin(); 1217 boost::urls::segments_view::const_iterator end = urlSegments.end(); 1218 1219 // Skip past the leading "/redfish/v1" 1220 it++; 1221 it++; 1222 for (; it != end; it++) 1223 { 1224 const std::string& collectionItem = *it; 1225 if (std::binary_search(topCollections.begin(), topCollections.end(), 1226 currentUrl.buffer())) 1227 { 1228 // We've matched a resource collection so this current segment 1229 // might contain an aggregation prefix 1230 // TODO: This needs to be rethought when we can support multiple 1231 // satellites due to 1232 // /redfish/v1/AggregationService/AggregationSources/5B247A 1233 // being a local resource describing the satellite 1234 if (collectionItem.starts_with("5B247A_")) 1235 { 1236 BMCWEB_LOG_DEBUG("Need to forward a request"); 1237 1238 // Extract the prefix from the request's URI, retrieve the 1239 // associated satellite config information, and then forward 1240 // the request to that satellite. 1241 startAggregation(AggregationType::Resource, thisReq, 1242 asyncResp); 1243 return Result::NoLocalHandle; 1244 } 1245 1246 // Handle collection URI with a trailing backslash 1247 // e.g. /redfish/v1/Chassis/ 1248 it++; 1249 if ((it == end) && collectionItem.empty()) 1250 { 1251 startAggregation(AggregationType::Collection, thisReq, 1252 asyncResp); 1253 } 1254 1255 // We didn't recognize the prefix or it's a collection with a 1256 // trailing "/". In both cases we still want to locally handle 1257 // the request 1258 return Result::LocalHandle; 1259 } 1260 1261 currentUrl.segments().push_back(collectionItem); 1262 } 1263 1264 // If we made it here then currentUrl could contain a top level 1265 // collection URI without a trailing "/", e.g. /redfish/v1/Chassis 1266 if (std::binary_search(topCollections.begin(), topCollections.end(), 1267 currentUrl.buffer())) 1268 { 1269 startAggregation(AggregationType::Collection, thisReq, asyncResp); 1270 return Result::LocalHandle; 1271 } 1272 1273 // If nothing else then the request could be for a resource which has a 1274 // top level collection as a subordinate 1275 if (searchCollectionsArray(url.path(), SearchType::ContainsSubordinate)) 1276 { 1277 startAggregation(AggregationType::ContainsSubordinate, thisReq, 1278 asyncResp); 1279 return Result::LocalHandle; 1280 } 1281 1282 BMCWEB_LOG_DEBUG("Aggregation not required for {}", url.buffer()); 1283 return Result::LocalHandle; 1284 } 1285 }; 1286 1287 } // namespace redfish 1288