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