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