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 RedfishAggregator() : 378 client(std::make_shared<crow::ConnectionPolicy>(getAggregationPolicy())) 379 { 380 getSatelliteConfigs(constructorCallback); 381 } 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 " 396 << std::to_string(satelliteInfo.size()) 397 << " satellite configs found at startup"; 398 } 399 400 // Search D-Bus objects for satellite config objects and add their 401 // information if valid 402 static void findSatelliteConfigs( 403 const dbus::utility::ManagedObjectType& objects, 404 std::unordered_map<std::string, boost::urls::url>& satelliteInfo) 405 { 406 for (const auto& objectPath : objects) 407 { 408 for (const auto& interface : objectPath.second) 409 { 410 if (interface.first == 411 "xyz.openbmc_project.Configuration.SatelliteController") 412 { 413 BMCWEB_LOG_DEBUG << "Found Satellite Controller at " 414 << objectPath.first.str; 415 416 if (!satelliteInfo.empty()) 417 { 418 BMCWEB_LOG_ERROR 419 << "Redfish Aggregation only supports one satellite!"; 420 BMCWEB_LOG_DEBUG << "Clearing all satellite data"; 421 satelliteInfo.clear(); 422 return; 423 } 424 425 // For now assume there will only be one satellite config. 426 // Assign it the name/prefix "5B247A" 427 addSatelliteConfig("5B247A", interface.second, 428 satelliteInfo); 429 } 430 } 431 } 432 } 433 434 // Parse the properties of a satellite config object and add the 435 // configuration if the properties are valid 436 static void addSatelliteConfig( 437 const std::string& name, 438 const dbus::utility::DBusPropertiesMap& properties, 439 std::unordered_map<std::string, boost::urls::url>& satelliteInfo) 440 { 441 boost::urls::url url; 442 443 for (const auto& prop : properties) 444 { 445 if (prop.first == "Hostname") 446 { 447 const std::string* propVal = 448 std::get_if<std::string>(&prop.second); 449 if (propVal == nullptr) 450 { 451 BMCWEB_LOG_ERROR << "Invalid Hostname value"; 452 return; 453 } 454 url.set_host(*propVal); 455 } 456 457 else if (prop.first == "Port") 458 { 459 const uint64_t* propVal = std::get_if<uint64_t>(&prop.second); 460 if (propVal == nullptr) 461 { 462 BMCWEB_LOG_ERROR << "Invalid Port value"; 463 return; 464 } 465 466 if (*propVal > std::numeric_limits<uint16_t>::max()) 467 { 468 BMCWEB_LOG_ERROR << "Port value out of range"; 469 return; 470 } 471 url.set_port(std::to_string(static_cast<uint16_t>(*propVal))); 472 } 473 474 else if (prop.first == "AuthType") 475 { 476 const std::string* propVal = 477 std::get_if<std::string>(&prop.second); 478 if (propVal == nullptr) 479 { 480 BMCWEB_LOG_ERROR << "Invalid AuthType value"; 481 return; 482 } 483 484 // For now assume authentication not required to communicate 485 // with the satellite BMC 486 if (*propVal != "None") 487 { 488 BMCWEB_LOG_ERROR 489 << "Unsupported AuthType value: " << *propVal 490 << ", only \"none\" is supported"; 491 return; 492 } 493 url.set_scheme("http"); 494 } 495 } // Finished reading properties 496 497 // Make sure all required config information was made available 498 if (url.host().empty()) 499 { 500 BMCWEB_LOG_ERROR << "Satellite config " << name << " missing Host"; 501 return; 502 } 503 504 if (!url.has_port()) 505 { 506 BMCWEB_LOG_ERROR << "Satellite config " << name << " missing Port"; 507 return; 508 } 509 510 if (!url.has_scheme()) 511 { 512 BMCWEB_LOG_ERROR << "Satellite config " << name 513 << " missing AuthType"; 514 return; 515 } 516 517 std::string resultString; 518 auto result = satelliteInfo.insert_or_assign(name, std::move(url)); 519 if (result.second) 520 { 521 resultString = "Added new satellite config "; 522 } 523 else 524 { 525 resultString = "Updated existing satellite config "; 526 } 527 528 BMCWEB_LOG_DEBUG << resultString << name << " at " 529 << result.first->second.scheme() << "://" 530 << result.first->second.encoded_host_and_port(); 531 } 532 533 enum AggregationType 534 { 535 Collection, 536 Resource, 537 }; 538 539 static void 540 startAggregation(AggregationType isCollection, 541 const crow::Request& thisReq, 542 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) 543 { 544 if ((isCollection == AggregationType::Collection) && 545 (thisReq.method() != boost::beast::http::verb::get)) 546 { 547 BMCWEB_LOG_DEBUG 548 << "Only aggregate GET requests to top level collections"; 549 return; 550 } 551 552 // Create a copy of thisReq so we we can still locally process the req 553 std::error_code ec; 554 auto localReq = std::make_shared<crow::Request>(thisReq.req, ec); 555 if (ec) 556 { 557 BMCWEB_LOG_ERROR << "Failed to create copy of request"; 558 if (isCollection != AggregationType::Collection) 559 { 560 messages::internalError(asyncResp->res); 561 } 562 return; 563 } 564 565 getSatelliteConfigs(std::bind_front(aggregateAndHandle, isCollection, 566 localReq, asyncResp)); 567 } 568 569 static void findSatellite( 570 const crow::Request& req, 571 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 572 const std::unordered_map<std::string, boost::urls::url>& satelliteInfo, 573 std::string_view memberName) 574 { 575 // Determine if the resource ID begins with a known prefix 576 for (const auto& satellite : satelliteInfo) 577 { 578 std::string targetPrefix = satellite.first; 579 targetPrefix += "_"; 580 if (memberName.starts_with(targetPrefix)) 581 { 582 BMCWEB_LOG_DEBUG << "\"" << satellite.first 583 << "\" is a known prefix"; 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 isCollection, 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 we'll also handle the request locally so we 623 // don't need to write an error code 624 if (isCollection == AggregationType::Resource) 625 { 626 std::string nameStr = sharedReq->url().segments().back(); 627 messages::resourceNotFound(asyncResp->res, "", nameStr); 628 } 629 return; 630 } 631 632 const crow::Request& thisReq = *sharedReq; 633 BMCWEB_LOG_DEBUG << "Aggregation is enabled, begin processing of " 634 << thisReq.target(); 635 636 // We previously determined the request is for a collection. No need to 637 // check again 638 if (isCollection == AggregationType::Collection) 639 { 640 BMCWEB_LOG_DEBUG << "Aggregating a collection"; 641 // We need to use a specific response handler and send the 642 // request to all known satellites 643 getInstance().forwardCollectionRequests(thisReq, asyncResp, 644 satelliteInfo); 645 return; 646 } 647 648 const boost::urls::segments_view urlSegments = thisReq.url().segments(); 649 boost::urls::url currentUrl("/"); 650 boost::urls::segments_view::iterator it = urlSegments.begin(); 651 const boost::urls::segments_view::const_iterator end = 652 urlSegments.end(); 653 654 // Skip past the leading "/redfish/v1" 655 it++; 656 it++; 657 for (; it != end; it++) 658 { 659 if (std::binary_search(topCollections.begin(), topCollections.end(), 660 currentUrl.buffer())) 661 { 662 // We've matched a resource collection so this current segment 663 // must contain an aggregation prefix 664 findSatellite(thisReq, asyncResp, satelliteInfo, *it); 665 return; 666 } 667 668 currentUrl.segments().push_back(*it); 669 } 670 671 // We shouldn't reach this point since we should've hit one of the 672 // previous exits 673 messages::internalError(asyncResp->res); 674 } 675 676 // Attempt to forward a request to the satellite BMC associated with the 677 // prefix. 678 void forwardRequest( 679 const crow::Request& thisReq, 680 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 681 const std::string& prefix, 682 const std::unordered_map<std::string, boost::urls::url>& satelliteInfo) 683 { 684 const auto& sat = satelliteInfo.find(prefix); 685 if (sat == satelliteInfo.end()) 686 { 687 // Realistically this shouldn't get called since we perform an 688 // earlier check to make sure the prefix exists 689 BMCWEB_LOG_ERROR << "Unrecognized satellite prefix \"" << prefix 690 << "\""; 691 return; 692 } 693 694 // We need to strip the prefix from the request's path 695 std::string targetURI(thisReq.target()); 696 size_t pos = targetURI.find(prefix + "_"); 697 if (pos == std::string::npos) 698 { 699 // If this fails then something went wrong 700 BMCWEB_LOG_ERROR << "Error removing prefix \"" << prefix 701 << "_\" from request URI"; 702 messages::internalError(asyncResp->res); 703 return; 704 } 705 targetURI.erase(pos, prefix.size() + 1); 706 707 std::function<void(crow::Response&)> cb = 708 std::bind_front(processResponse, prefix, asyncResp); 709 710 std::string data = thisReq.req.body(); 711 client.sendDataWithCallback(data, std::string(sat->second.host()), 712 sat->second.port_number(), targetURI, 713 false /*useSSL*/, thisReq.fields(), 714 thisReq.method(), cb); 715 } 716 717 // Forward a request for a collection URI to each known satellite BMC 718 void forwardCollectionRequests( 719 const crow::Request& thisReq, 720 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 721 const std::unordered_map<std::string, boost::urls::url>& satelliteInfo) 722 { 723 for (const auto& sat : satelliteInfo) 724 { 725 std::function<void(crow::Response&)> cb = std::bind_front( 726 processCollectionResponse, sat.first, asyncResp); 727 728 std::string targetURI(thisReq.target()); 729 std::string data = thisReq.req.body(); 730 client.sendDataWithCallback(data, std::string(sat.second.host()), 731 sat.second.port_number(), targetURI, 732 false /*useSSL*/, thisReq.fields(), 733 thisReq.method(), cb); 734 } 735 } 736 737 public: 738 RedfishAggregator(const RedfishAggregator&) = delete; 739 RedfishAggregator& operator=(const RedfishAggregator&) = delete; 740 RedfishAggregator(RedfishAggregator&&) = delete; 741 RedfishAggregator& operator=(RedfishAggregator&&) = delete; 742 ~RedfishAggregator() = default; 743 744 static RedfishAggregator& getInstance() 745 { 746 static RedfishAggregator handler; 747 return handler; 748 } 749 750 // Polls D-Bus to get all available satellite config information 751 // Expects a handler which interacts with the returned configs 752 static void getSatelliteConfigs( 753 std::function< 754 void(const boost::system::error_code&, 755 const std::unordered_map<std::string, boost::urls::url>&)> 756 handler) 757 { 758 BMCWEB_LOG_DEBUG << "Gathering satellite configs"; 759 crow::connections::systemBus->async_method_call( 760 [handler{std::move(handler)}]( 761 const boost::system::error_code& ec, 762 const dbus::utility::ManagedObjectType& objects) { 763 std::unordered_map<std::string, boost::urls::url> satelliteInfo; 764 if (ec) 765 { 766 BMCWEB_LOG_ERROR << "DBUS response error " << ec.value() << ", " 767 << ec.message(); 768 handler(ec, satelliteInfo); 769 return; 770 } 771 772 // Maps a chosen alias representing a satellite BMC to a url 773 // containing the information required to create a http 774 // connection to the satellite 775 findSatelliteConfigs(objects, satelliteInfo); 776 777 if (!satelliteInfo.empty()) 778 { 779 BMCWEB_LOG_DEBUG << "Redfish Aggregation enabled with " 780 << std::to_string(satelliteInfo.size()) 781 << " satellite BMCs"; 782 } 783 else 784 { 785 BMCWEB_LOG_DEBUG 786 << "No satellite BMCs detected. Redfish Aggregation not enabled"; 787 } 788 handler(ec, satelliteInfo); 789 }, 790 "xyz.openbmc_project.EntityManager", 791 "/xyz/openbmc_project/inventory", 792 "org.freedesktop.DBus.ObjectManager", "GetManagedObjects"); 793 } 794 795 // Processes the response returned by a satellite BMC and loads its 796 // contents into asyncResp 797 static void 798 processResponse(std::string_view prefix, 799 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 800 crow::Response& resp) 801 { 802 // 429 and 502 mean we didn't actually send the request so don't 803 // overwrite the response headers in that case 804 if ((resp.resultInt() == 429) || (resp.resultInt() == 502)) 805 { 806 asyncResp->res.result(resp.result()); 807 return; 808 } 809 810 // We want to attempt prefix fixing regardless of response code 811 // The resp will not have a json component 812 // We need to create a json from resp's stringResponse 813 if (resp.getHeaderValue("Content-Type") == "application/json") 814 { 815 nlohmann::json jsonVal = 816 nlohmann::json::parse(resp.body(), nullptr, false); 817 if (jsonVal.is_discarded()) 818 { 819 BMCWEB_LOG_ERROR << "Error parsing satellite response as JSON"; 820 messages::operationFailed(asyncResp->res); 821 return; 822 } 823 824 BMCWEB_LOG_DEBUG << "Successfully parsed satellite response"; 825 826 addPrefixes(jsonVal, prefix); 827 828 BMCWEB_LOG_DEBUG << "Added prefix to parsed satellite response"; 829 830 asyncResp->res.result(resp.result()); 831 asyncResp->res.jsonValue = std::move(jsonVal); 832 833 BMCWEB_LOG_DEBUG << "Finished writing asyncResp"; 834 } 835 else 836 { 837 // We allow any Content-Type that is not "application/json" now 838 asyncResp->res.result(resp.result()); 839 asyncResp->res.write(resp.body()); 840 } 841 addAggregatedHeaders(asyncResp->res, resp, prefix); 842 } 843 844 // Processes the collection response returned by a satellite BMC and merges 845 // its "@odata.id" values 846 static void processCollectionResponse( 847 const std::string& 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.resultInt() == 429) || (resp.resultInt() == 502)) 854 { 855 return; 856 } 857 858 if (resp.resultInt() != 200) 859 { 860 BMCWEB_LOG_DEBUG 861 << "Collection resource does not exist in satellite BMC \"" 862 << prefix << "\""; 863 // Return the error if we haven't had any successes 864 if (asyncResp->res.resultInt() != 200) 865 { 866 asyncResp->res.stringResponse = std::move(resp.stringResponse); 867 } 868 return; 869 } 870 871 // The resp will not have a json component 872 // We need to create a json from resp's stringResponse 873 if (resp.getHeaderValue("Content-Type") == "application/json") 874 { 875 nlohmann::json jsonVal = 876 nlohmann::json::parse(resp.body(), nullptr, false); 877 if (jsonVal.is_discarded()) 878 { 879 BMCWEB_LOG_ERROR << "Error parsing satellite response as JSON"; 880 881 // Notify the user if doing so won't overwrite a valid response 882 if ((asyncResp->res.resultInt() != 200) && 883 (asyncResp->res.resultInt() != 429) && 884 (asyncResp->res.resultInt() != 502)) 885 { 886 messages::operationFailed(asyncResp->res); 887 } 888 return; 889 } 890 891 BMCWEB_LOG_DEBUG << "Successfully parsed satellite response"; 892 893 // Now we need to add the prefix to the URIs contained in the 894 // response. 895 addPrefixes(jsonVal, prefix); 896 897 BMCWEB_LOG_DEBUG << "Added prefix to parsed satellite response"; 898 899 // If this resource collection does not exist on the aggregating bmc 900 // and has not already been added from processing the response from 901 // a different satellite then we need to completely overwrite 902 // asyncResp 903 if (asyncResp->res.resultInt() != 200) 904 { 905 // We only want to aggregate collections that contain a 906 // "Members" array 907 if ((!jsonVal.contains("Members")) && 908 (!jsonVal["Members"].is_array())) 909 { 910 BMCWEB_LOG_DEBUG 911 << "Skipping aggregating unsupported resource"; 912 return; 913 } 914 915 BMCWEB_LOG_DEBUG 916 << "Collection does not exist, overwriting asyncResp"; 917 asyncResp->res.result(resp.result()); 918 asyncResp->res.jsonValue = std::move(jsonVal); 919 asyncResp->res.addHeader("Content-Type", "application/json"); 920 921 BMCWEB_LOG_DEBUG << "Finished overwriting asyncResp"; 922 } 923 else 924 { 925 // We only want to aggregate collections that contain a 926 // "Members" array 927 if ((!asyncResp->res.jsonValue.contains("Members")) && 928 (!asyncResp->res.jsonValue["Members"].is_array())) 929 930 { 931 BMCWEB_LOG_DEBUG 932 << "Skipping aggregating unsupported resource"; 933 return; 934 } 935 936 BMCWEB_LOG_DEBUG << "Adding aggregated resources from \"" 937 << prefix << "\" to collection"; 938 939 // TODO: This is a potential race condition with multiple 940 // satellites and the aggregating bmc attempting to write to 941 // update this array. May need to cascade calls to the next 942 // satellite at the end of this function. 943 // This is presumably not a concern when there is only a single 944 // satellite since the aggregating bmc should have completed 945 // before the response is received from the satellite. 946 947 auto& members = asyncResp->res.jsonValue["Members"]; 948 auto& satMembers = jsonVal["Members"]; 949 for (auto& satMem : satMembers) 950 { 951 members.push_back(std::move(satMem)); 952 } 953 asyncResp->res.jsonValue["Members@odata.count"] = 954 members.size(); 955 956 // TODO: Do we need to sort() after updating the array? 957 } 958 } 959 else 960 { 961 BMCWEB_LOG_ERROR << "Received unparsable response from \"" << prefix 962 << "\""; 963 // We received a response that was not a json. 964 // Notify the user only if we did not receive any valid responses, 965 // if the resource collection does not already exist on the 966 // aggregating BMC, and if we did not already set this warning due 967 // to a failure from a different satellite 968 if ((asyncResp->res.resultInt() != 200) && 969 (asyncResp->res.resultInt() != 429) && 970 (asyncResp->res.resultInt() != 502)) 971 { 972 messages::operationFailed(asyncResp->res); 973 } 974 } 975 } // End processCollectionResponse() 976 977 // Entry point to Redfish Aggregation 978 // Returns Result stating whether or not we still need to locally handle the 979 // request 980 static Result 981 beginAggregation(const crow::Request& thisReq, 982 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) 983 { 984 using crow::utility::OrMorePaths; 985 using crow::utility::readUrlSegments; 986 const boost::urls::url_view url = thisReq.url(); 987 988 // We don't need to aggregate JsonSchemas due to potential issues such 989 // as version mismatches between aggregator and satellite BMCs. For 990 // now assume that the aggregator has all the schemas and versions that 991 // the aggregated server has. 992 if (crow::utility::readUrlSegments(url, "redfish", "v1", "JsonSchemas", 993 crow::utility::OrMorePaths())) 994 { 995 return Result::LocalHandle; 996 } 997 998 // The first two segments should be "/redfish/v1". We need to check 999 // that before we can search topCollections 1000 if (!crow::utility::readUrlSegments(url, "redfish", "v1", 1001 crow::utility::OrMorePaths())) 1002 { 1003 return Result::LocalHandle; 1004 } 1005 1006 // Parse the URI to see if it begins with a known top level collection 1007 // such as: 1008 // /redfish/v1/Chassis 1009 // /redfish/v1/UpdateService/FirmwareInventory 1010 const boost::urls::segments_view urlSegments = url.segments(); 1011 boost::urls::url currentUrl("/"); 1012 boost::urls::segments_view::iterator it = urlSegments.begin(); 1013 const boost::urls::segments_view::const_iterator end = 1014 urlSegments.end(); 1015 1016 // Skip past the leading "/redfish/v1" 1017 it++; 1018 it++; 1019 for (; it != end; it++) 1020 { 1021 const std::string& collectionItem = *it; 1022 if (std::binary_search(topCollections.begin(), topCollections.end(), 1023 currentUrl.buffer())) 1024 { 1025 // We've matched a resource collection so this current segment 1026 // might contain an aggregation prefix 1027 // TODO: This needs to be rethought when we can support multiple 1028 // satellites due to 1029 // /redfish/v1/AggregationService/AggregationSources/5B247A 1030 // being a local resource describing the satellite 1031 if (collectionItem.starts_with("5B247A_")) 1032 { 1033 BMCWEB_LOG_DEBUG << "Need to forward a request"; 1034 1035 // Extract the prefix from the request's URI, retrieve the 1036 // associated satellite config information, and then forward 1037 // the request to that satellite. 1038 startAggregation(AggregationType::Resource, thisReq, 1039 asyncResp); 1040 return Result::NoLocalHandle; 1041 } 1042 1043 // Handle collection URI with a trailing backslash 1044 // e.g. /redfish/v1/Chassis/ 1045 it++; 1046 if ((it == end) && collectionItem.empty()) 1047 { 1048 startAggregation(AggregationType::Collection, thisReq, 1049 asyncResp); 1050 } 1051 1052 // We didn't recognize the prefix or it's a collection with a 1053 // trailing "/". In both cases we still want to locally handle 1054 // the request 1055 return Result::LocalHandle; 1056 } 1057 1058 currentUrl.segments().push_back(collectionItem); 1059 } 1060 1061 // If we made it here then currentUrl could contain a top level 1062 // collection URI without a trailing "/", e.g. /redfish/v1/Chassis 1063 if (std::binary_search(topCollections.begin(), topCollections.end(), 1064 currentUrl.buffer())) 1065 { 1066 startAggregation(AggregationType::Collection, thisReq, asyncResp); 1067 return Result::LocalHandle; 1068 } 1069 1070 BMCWEB_LOG_DEBUG << "Aggregation not required"; 1071 return Result::LocalHandle; 1072 } 1073 }; 1074 1075 } // namespace redfish 1076