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