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