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 enum class Result 17 { 18 LocalHandle, 19 NoLocalHandle 20 }; 21 22 // clang-format off 23 // These are all of the properties as of version 2022.2 of the Redfish Resource 24 // and Schema Guide whose Type is "string (URI)" and the name does not end in a 25 // case-insensitive form of "uri". That version of the schema is associated 26 // with version 1.16.0 of the Redfish Specification. Going forward, new URI 27 // properties should end in URI so this list should not need to be maintained as 28 // the spec is updated. NOTE: These have been pre-sorted in order to be 29 // compatible with binary search 30 constexpr std::array nonUriProperties{ 31 "@Redfish.ActionInfo", 32 // "@odata.context", // We can't fix /redfish/v1/$metadata URIs 33 "@odata.id", 34 // "Destination", // Only used by EventService and won't be a Redfish URI 35 // "HostName", // Isn't actually a Redfish URI 36 "Image", 37 "MetricProperty", 38 // "OriginOfCondition", // Is URI when in request, but is object in response 39 "TaskMonitor", 40 "target", // normal string, but target URI for POST to invoke an action 41 }; 42 // clang-format on 43 44 // Determines if the passed property contains a URI. Those property names 45 // either end with a case-insensitive version of "uri" or are specifically 46 // defined in the above array. 47 inline bool isPropertyUri(std::string_view propertyName) 48 { 49 return boost::iends_with(propertyName, "uri") || 50 std::binary_search(nonUriProperties.begin(), nonUriProperties.end(), 51 propertyName); 52 } 53 54 static inline void addPrefixToStringItem(std::string& strValue, 55 std::string_view prefix) 56 { 57 // Make sure the value is a properly formatted URI 58 auto parsed = boost::urls::parse_relative_ref(strValue); 59 if (!parsed) 60 { 61 BMCWEB_LOG_CRITICAL << "Couldn't parse URI from resource " << strValue; 62 return; 63 } 64 65 boost::urls::url_view thisUrl = *parsed; 66 67 // We don't need to aggregate JsonSchemas due to potential issues such as 68 // version mismatches between aggregator and satellite BMCs. For now 69 // assume that the aggregator has all the schemas and versions that the 70 // aggregated server has. 71 if (crow::utility::readUrlSegments(thisUrl, "redfish", "v1", "JsonSchemas", 72 crow::utility::OrMorePaths())) 73 { 74 BMCWEB_LOG_DEBUG << "Skipping JsonSchemas URI prefix fixing"; 75 return; 76 } 77 78 // The first two segments should be "/redfish/v1". We need to check that 79 // before we can search topCollections 80 if (!crow::utility::readUrlSegments(thisUrl, "redfish", "v1", 81 crow::utility::OrMorePaths())) 82 { 83 return; 84 } 85 86 // Check array adding a segment each time until collection is identified 87 // Add prefix to segment after the collection 88 const boost::urls::segments_view urlSegments = thisUrl.segments(); 89 bool addedPrefix = false; 90 boost::urls::url url("/"); 91 boost::urls::segments_view::iterator it = urlSegments.begin(); 92 const boost::urls::segments_view::const_iterator end = urlSegments.end(); 93 94 // Skip past the leading "/redfish/v1" 95 it++; 96 it++; 97 for (; it != end; it++) 98 { 99 // Trailing "/" will result in an empty segment. In that case we need 100 // to return so we don't apply a prefix to top level collections such 101 // as "/redfish/v1/Chassis/" 102 if ((*it).empty()) 103 { 104 return; 105 } 106 107 if (std::binary_search(topCollections.begin(), topCollections.end(), 108 url.buffer())) 109 { 110 std::string collectionItem(prefix); 111 collectionItem += "_" + (*it); 112 url.segments().push_back(collectionItem); 113 it++; 114 addedPrefix = true; 115 break; 116 } 117 118 url.segments().push_back(*it); 119 } 120 121 // Finish constructing the URL here (if needed) to avoid additional checks 122 for (; it != end; it++) 123 { 124 url.segments().push_back(*it); 125 } 126 127 if (addedPrefix) 128 { 129 url.segments().insert(url.segments().begin(), {"redfish", "v1"}); 130 strValue = url.buffer(); 131 } 132 } 133 134 static inline void addPrefixToItem(nlohmann::json& item, 135 std::string_view prefix) 136 { 137 std::string* strValue = item.get_ptr<std::string*>(); 138 if (strValue == nullptr) 139 { 140 BMCWEB_LOG_CRITICAL << "Field wasn't a string????"; 141 return; 142 } 143 addPrefixToStringItem(*strValue, prefix); 144 item = *strValue; 145 } 146 147 static inline void addAggregatedHeaders(crow::Response& asyncResp, 148 const crow::Response& resp, 149 std::string_view prefix) 150 { 151 if (!resp.getHeaderValue("Content-Type").empty()) 152 { 153 asyncResp.addHeader(boost::beast::http::field::content_type, 154 resp.getHeaderValue("Content-Type")); 155 } 156 if (!resp.getHeaderValue("Allow").empty()) 157 { 158 asyncResp.addHeader(boost::beast::http::field::allow, 159 resp.getHeaderValue("Allow")); 160 } 161 std::string_view header = resp.getHeaderValue("Location"); 162 if (!header.empty()) 163 { 164 std::string location(header); 165 addPrefixToStringItem(location, prefix); 166 asyncResp.addHeader(boost::beast::http::field::location, location); 167 } 168 if (!resp.getHeaderValue("Retry-After").empty()) 169 { 170 asyncResp.addHeader(boost::beast::http::field::retry_after, 171 resp.getHeaderValue("Retry-After")); 172 } 173 // TODO: we need special handling for Link Header Value 174 } 175 176 // Search the json for all URIs and add the supplied prefix if the URI is for 177 // an aggregated resource. 178 static inline void addPrefixes(nlohmann::json& json, std::string_view prefix) 179 { 180 nlohmann::json::object_t* object = 181 json.get_ptr<nlohmann::json::object_t*>(); 182 if (object != nullptr) 183 { 184 for (std::pair<const std::string, nlohmann::json>& item : *object) 185 { 186 if (isPropertyUri(item.first)) 187 { 188 addPrefixToItem(item.second, prefix); 189 continue; 190 } 191 192 // Recusively parse the rest of the json 193 addPrefixes(item.second, prefix); 194 } 195 return; 196 } 197 nlohmann::json::array_t* array = json.get_ptr<nlohmann::json::array_t*>(); 198 if (array != nullptr) 199 { 200 for (nlohmann::json& item : *array) 201 { 202 addPrefixes(item, prefix); 203 } 204 } 205 } 206 207 class RedfishAggregator 208 { 209 private: 210 const std::string retryPolicyName = "RedfishAggregation"; 211 const std::string retryPolicyAction = "TerminateAfterRetries"; 212 const uint32_t retryAttempts = 1; 213 const uint32_t retryTimeoutInterval = 0; 214 const std::string id = "Aggregator"; 215 216 RedfishAggregator() 217 { 218 getSatelliteConfigs(constructorCallback); 219 220 // Setup the retry policy to be used by Redfish Aggregation 221 crow::HttpClient::getInstance().setRetryConfig( 222 retryAttempts, retryTimeoutInterval, aggregationRetryHandler, 223 retryPolicyName); 224 crow::HttpClient::getInstance().setRetryPolicy(retryPolicyAction, 225 retryPolicyName); 226 } 227 228 static inline boost::system::error_code 229 aggregationRetryHandler(unsigned int respCode) 230 { 231 // Allow all response codes because we want to surface any satellite 232 // issue to the client 233 BMCWEB_LOG_DEBUG << "Received " << respCode 234 << " response from satellite"; 235 return boost::system::errc::make_error_code( 236 boost::system::errc::success); 237 } 238 239 // Dummy callback used by the Constructor so that it can report the number 240 // of satellite configs when the class is first created 241 static void constructorCallback( 242 const std::unordered_map<std::string, boost::urls::url>& satelliteInfo) 243 { 244 BMCWEB_LOG_DEBUG << "There were " 245 << std::to_string(satelliteInfo.size()) 246 << " satellite configs found at startup"; 247 } 248 249 // Polls D-Bus to get all available satellite config information 250 // Expects a handler which interacts with the returned configs 251 static void getSatelliteConfigs( 252 const std::function<void( 253 const std::unordered_map<std::string, boost::urls::url>&)>& handler) 254 { 255 BMCWEB_LOG_DEBUG << "Gathering satellite configs"; 256 crow::connections::systemBus->async_method_call( 257 [handler](const boost::system::error_code ec, 258 const dbus::utility::ManagedObjectType& objects) { 259 if (ec) 260 { 261 BMCWEB_LOG_ERROR << "DBUS response error " << ec.value() << ", " 262 << ec.message(); 263 return; 264 } 265 266 // Maps a chosen alias representing a satellite BMC to a url 267 // containing the information required to create a http 268 // connection to the satellite 269 std::unordered_map<std::string, boost::urls::url> satelliteInfo; 270 271 findSatelliteConfigs(objects, satelliteInfo); 272 273 if (!satelliteInfo.empty()) 274 { 275 BMCWEB_LOG_DEBUG << "Redfish Aggregation enabled with " 276 << std::to_string(satelliteInfo.size()) 277 << " satellite BMCs"; 278 } 279 else 280 { 281 BMCWEB_LOG_DEBUG 282 << "No satellite BMCs detected. Redfish Aggregation not enabled"; 283 } 284 handler(satelliteInfo); 285 }, 286 "xyz.openbmc_project.EntityManager", 287 "/xyz/openbmc_project/inventory", 288 "org.freedesktop.DBus.ObjectManager", "GetManagedObjects"); 289 } 290 291 // Search D-Bus objects for satellite config objects and add their 292 // information if valid 293 static void findSatelliteConfigs( 294 const dbus::utility::ManagedObjectType& objects, 295 std::unordered_map<std::string, boost::urls::url>& satelliteInfo) 296 { 297 for (const auto& objectPath : objects) 298 { 299 for (const auto& interface : objectPath.second) 300 { 301 if (interface.first == 302 "xyz.openbmc_project.Configuration.SatelliteController") 303 { 304 BMCWEB_LOG_DEBUG << "Found Satellite Controller at " 305 << objectPath.first.str; 306 307 if (!satelliteInfo.empty()) 308 { 309 BMCWEB_LOG_ERROR 310 << "Redfish Aggregation only supports one satellite!"; 311 BMCWEB_LOG_DEBUG << "Clearing all satellite data"; 312 satelliteInfo.clear(); 313 return; 314 } 315 316 // For now assume there will only be one satellite config. 317 // Assign it the name/prefix "5B247A" 318 addSatelliteConfig("5B247A", interface.second, 319 satelliteInfo); 320 } 321 } 322 } 323 } 324 325 // Parse the properties of a satellite config object and add the 326 // configuration if the properties are valid 327 static void addSatelliteConfig( 328 const std::string& name, 329 const dbus::utility::DBusPropertiesMap& properties, 330 std::unordered_map<std::string, boost::urls::url>& satelliteInfo) 331 { 332 boost::urls::url url; 333 334 for (const auto& prop : properties) 335 { 336 if (prop.first == "Hostname") 337 { 338 const std::string* propVal = 339 std::get_if<std::string>(&prop.second); 340 if (propVal == nullptr) 341 { 342 BMCWEB_LOG_ERROR << "Invalid Hostname value"; 343 return; 344 } 345 url.set_host(*propVal); 346 } 347 348 else if (prop.first == "Port") 349 { 350 const uint64_t* propVal = std::get_if<uint64_t>(&prop.second); 351 if (propVal == nullptr) 352 { 353 BMCWEB_LOG_ERROR << "Invalid Port value"; 354 return; 355 } 356 357 if (*propVal > std::numeric_limits<uint16_t>::max()) 358 { 359 BMCWEB_LOG_ERROR << "Port value out of range"; 360 return; 361 } 362 url.set_port(std::to_string(static_cast<uint16_t>(*propVal))); 363 } 364 365 else if (prop.first == "AuthType") 366 { 367 const std::string* propVal = 368 std::get_if<std::string>(&prop.second); 369 if (propVal == nullptr) 370 { 371 BMCWEB_LOG_ERROR << "Invalid AuthType value"; 372 return; 373 } 374 375 // For now assume authentication not required to communicate 376 // with the satellite BMC 377 if (*propVal != "None") 378 { 379 BMCWEB_LOG_ERROR 380 << "Unsupported AuthType value: " << *propVal 381 << ", only \"none\" is supported"; 382 return; 383 } 384 url.set_scheme("http"); 385 } 386 } // Finished reading properties 387 388 // Make sure all required config information was made available 389 if (url.host().empty()) 390 { 391 BMCWEB_LOG_ERROR << "Satellite config " << name << " missing Host"; 392 return; 393 } 394 395 if (!url.has_port()) 396 { 397 BMCWEB_LOG_ERROR << "Satellite config " << name << " missing Port"; 398 return; 399 } 400 401 if (!url.has_scheme()) 402 { 403 BMCWEB_LOG_ERROR << "Satellite config " << name 404 << " missing AuthType"; 405 return; 406 } 407 408 std::string resultString; 409 auto result = satelliteInfo.insert_or_assign(name, std::move(url)); 410 if (result.second) 411 { 412 resultString = "Added new satellite config "; 413 } 414 else 415 { 416 resultString = "Updated existing satellite config "; 417 } 418 419 BMCWEB_LOG_DEBUG << resultString << name << " at " 420 << result.first->second.scheme() << "://" 421 << result.first->second.encoded_host_and_port(); 422 } 423 424 enum AggregationType 425 { 426 Collection, 427 Resource, 428 }; 429 430 static void 431 startAggregation(AggregationType isCollection, 432 const crow::Request& thisReq, 433 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) 434 { 435 if ((isCollection == AggregationType::Collection) && 436 (thisReq.method() != boost::beast::http::verb::get)) 437 { 438 BMCWEB_LOG_DEBUG 439 << "Only aggregate GET requests to top level collections"; 440 return; 441 } 442 443 // Create a copy of thisReq so we we can still locally process the req 444 std::error_code ec; 445 auto localReq = std::make_shared<crow::Request>(thisReq.req, ec); 446 if (ec) 447 { 448 BMCWEB_LOG_ERROR << "Failed to create copy of request"; 449 if (isCollection != AggregationType::Collection) 450 { 451 messages::internalError(asyncResp->res); 452 } 453 return; 454 } 455 456 getSatelliteConfigs(std::bind_front(aggregateAndHandle, isCollection, 457 localReq, asyncResp)); 458 } 459 460 static void findSatellite( 461 const crow::Request& req, 462 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 463 const std::unordered_map<std::string, boost::urls::url>& satelliteInfo, 464 std::string_view memberName) 465 { 466 // Determine if the resource ID begins with a known prefix 467 for (const auto& satellite : satelliteInfo) 468 { 469 std::string targetPrefix = satellite.first; 470 targetPrefix += "_"; 471 if (memberName.starts_with(targetPrefix)) 472 { 473 BMCWEB_LOG_DEBUG << "\"" << satellite.first 474 << "\" is a known prefix"; 475 476 // Remove the known prefix from the request's URI and 477 // then forward to the associated satellite BMC 478 getInstance().forwardRequest(req, asyncResp, satellite.first, 479 satelliteInfo); 480 return; 481 } 482 } 483 484 // We didn't recognize the prefix and need to return a 404 485 std::string nameStr = req.urlView.segments().back(); 486 messages::resourceNotFound(asyncResp->res, "", nameStr); 487 } 488 489 // Intended to handle an incoming request based on if Redfish Aggregation 490 // is enabled. Forwards request to satellite BMC if it exists. 491 static void aggregateAndHandle( 492 AggregationType isCollection, 493 const std::shared_ptr<crow::Request>& sharedReq, 494 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 495 const std::unordered_map<std::string, boost::urls::url>& satelliteInfo) 496 { 497 if (sharedReq == nullptr) 498 { 499 return; 500 } 501 502 // No satellite configs means we don't need to keep attempting to 503 // aggregate 504 if (satelliteInfo.empty()) 505 { 506 // For collections we'll also handle the request locally so we 507 // don't need to write an error code 508 if (isCollection == AggregationType::Resource) 509 { 510 std::string nameStr = sharedReq->urlView.segments().back(); 511 messages::resourceNotFound(asyncResp->res, "", nameStr); 512 } 513 return; 514 } 515 516 const crow::Request& thisReq = *sharedReq; 517 BMCWEB_LOG_DEBUG << "Aggregation is enabled, begin processing of " 518 << thisReq.target(); 519 520 // We previously determined the request is for a collection. No need to 521 // check again 522 if (isCollection == AggregationType::Collection) 523 { 524 BMCWEB_LOG_DEBUG << "Aggregating a collection"; 525 // We need to use a specific response handler and send the 526 // request to all known satellites 527 getInstance().forwardCollectionRequests(thisReq, asyncResp, 528 satelliteInfo); 529 return; 530 } 531 532 const boost::urls::segments_view urlSegments = 533 thisReq.urlView.segments(); 534 boost::urls::url currentUrl("/"); 535 boost::urls::segments_view::iterator it = urlSegments.begin(); 536 const boost::urls::segments_view::const_iterator end = 537 urlSegments.end(); 538 539 // Skip past the leading "/redfish/v1" 540 it++; 541 it++; 542 for (; it != end; it++) 543 { 544 if (std::binary_search(topCollections.begin(), topCollections.end(), 545 currentUrl.buffer())) 546 { 547 // We've matched a resource collection so this current segment 548 // must contain an aggregation prefix 549 findSatellite(thisReq, asyncResp, satelliteInfo, *it); 550 return; 551 } 552 553 currentUrl.segments().push_back(*it); 554 } 555 556 // We shouldn't reach this point since we should've hit one of the 557 // previous exits 558 messages::internalError(asyncResp->res); 559 } 560 561 // Attempt to forward a request to the satellite BMC associated with the 562 // prefix. 563 void forwardRequest( 564 const crow::Request& thisReq, 565 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 566 const std::string& prefix, 567 const std::unordered_map<std::string, boost::urls::url>& satelliteInfo) 568 { 569 const auto& sat = satelliteInfo.find(prefix); 570 if (sat == satelliteInfo.end()) 571 { 572 // Realistically this shouldn't get called since we perform an 573 // earlier check to make sure the prefix exists 574 BMCWEB_LOG_ERROR << "Unrecognized satellite prefix \"" << prefix 575 << "\""; 576 return; 577 } 578 579 // We need to strip the prefix from the request's path 580 std::string targetURI(thisReq.target()); 581 size_t pos = targetURI.find(prefix + "_"); 582 if (pos == std::string::npos) 583 { 584 // If this fails then something went wrong 585 BMCWEB_LOG_ERROR << "Error removing prefix \"" << prefix 586 << "_\" from request URI"; 587 messages::internalError(asyncResp->res); 588 return; 589 } 590 targetURI.erase(pos, prefix.size() + 1); 591 592 std::function<void(crow::Response&)> cb = 593 std::bind_front(processResponse, prefix, asyncResp); 594 595 std::string data = thisReq.req.body(); 596 crow::HttpClient::getInstance().sendDataWithCallback( 597 data, id, std::string(sat->second.host()), 598 sat->second.port_number(), targetURI, false /*useSSL*/, 599 thisReq.fields, thisReq.method(), retryPolicyName, cb); 600 } 601 602 // Forward a request for a collection URI to each known satellite BMC 603 void forwardCollectionRequests( 604 const crow::Request& thisReq, 605 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 606 const std::unordered_map<std::string, boost::urls::url>& satelliteInfo) 607 { 608 for (const auto& sat : satelliteInfo) 609 { 610 std::function<void(crow::Response&)> cb = std::bind_front( 611 processCollectionResponse, sat.first, asyncResp); 612 613 std::string targetURI(thisReq.target()); 614 std::string data = thisReq.req.body(); 615 crow::HttpClient::getInstance().sendDataWithCallback( 616 data, id, std::string(sat.second.host()), 617 sat.second.port_number(), targetURI, false /*useSSL*/, 618 thisReq.fields, thisReq.method(), retryPolicyName, cb); 619 } 620 } 621 622 public: 623 RedfishAggregator(const RedfishAggregator&) = delete; 624 RedfishAggregator& operator=(const RedfishAggregator&) = delete; 625 RedfishAggregator(RedfishAggregator&&) = delete; 626 RedfishAggregator& operator=(RedfishAggregator&&) = delete; 627 ~RedfishAggregator() = default; 628 629 static RedfishAggregator& getInstance() 630 { 631 static RedfishAggregator handler; 632 return handler; 633 } 634 635 // Processes the response returned by a satellite BMC and loads its 636 // contents into asyncResp 637 static void 638 processResponse(std::string_view prefix, 639 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 640 crow::Response& resp) 641 { 642 // 429 and 502 mean we didn't actually send the request so don't 643 // overwrite the response headers in that case 644 if ((resp.resultInt() == 429) || (resp.resultInt() == 502)) 645 { 646 asyncResp->res.result(resp.result()); 647 return; 648 } 649 650 // We want to attempt prefix fixing regardless of response code 651 // The resp will not have a json component 652 // We need to create a json from resp's stringResponse 653 if (resp.getHeaderValue("Content-Type") == "application/json") 654 { 655 nlohmann::json jsonVal = 656 nlohmann::json::parse(resp.body(), nullptr, false); 657 if (jsonVal.is_discarded()) 658 { 659 BMCWEB_LOG_ERROR << "Error parsing satellite response as JSON"; 660 messages::operationFailed(asyncResp->res); 661 return; 662 } 663 664 BMCWEB_LOG_DEBUG << "Successfully parsed satellite response"; 665 666 addPrefixes(jsonVal, prefix); 667 668 BMCWEB_LOG_DEBUG << "Added prefix to parsed satellite response"; 669 670 asyncResp->res.result(resp.result()); 671 asyncResp->res.jsonValue = std::move(jsonVal); 672 673 BMCWEB_LOG_DEBUG << "Finished writing asyncResp"; 674 } 675 else 676 { 677 // We allow any Content-Type that is not "application/json" now 678 asyncResp->res.result(resp.result()); 679 asyncResp->res.write(resp.body()); 680 } 681 addAggregatedHeaders(asyncResp->res, resp, prefix); 682 } 683 684 // Processes the collection response returned by a satellite BMC and merges 685 // its "@odata.id" values 686 static void processCollectionResponse( 687 const std::string& prefix, 688 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 689 crow::Response& resp) 690 { 691 // 429 and 502 mean we didn't actually send the request so don't 692 // overwrite the response headers in that case 693 if ((resp.resultInt() == 429) || (resp.resultInt() == 502)) 694 { 695 return; 696 } 697 698 if (resp.resultInt() != 200) 699 { 700 BMCWEB_LOG_DEBUG 701 << "Collection resource does not exist in satellite BMC \"" 702 << prefix << "\""; 703 // Return the error if we haven't had any successes 704 if (asyncResp->res.resultInt() != 200) 705 { 706 asyncResp->res.stringResponse = std::move(resp.stringResponse); 707 } 708 return; 709 } 710 711 // The resp will not have a json component 712 // We need to create a json from resp's stringResponse 713 if (resp.getHeaderValue("Content-Type") == "application/json") 714 { 715 nlohmann::json jsonVal = 716 nlohmann::json::parse(resp.body(), nullptr, false); 717 if (jsonVal.is_discarded()) 718 { 719 BMCWEB_LOG_ERROR << "Error parsing satellite response as JSON"; 720 721 // Notify the user if doing so won't overwrite a valid response 722 if ((asyncResp->res.resultInt() != 200) && 723 (asyncResp->res.resultInt() != 429) && 724 (asyncResp->res.resultInt() != 502)) 725 { 726 messages::operationFailed(asyncResp->res); 727 } 728 return; 729 } 730 731 BMCWEB_LOG_DEBUG << "Successfully parsed satellite response"; 732 733 // Now we need to add the prefix to the URIs contained in the 734 // response. 735 addPrefixes(jsonVal, prefix); 736 737 BMCWEB_LOG_DEBUG << "Added prefix to parsed satellite response"; 738 739 // If this resource collection does not exist on the aggregating bmc 740 // and has not already been added from processing the response from 741 // a different satellite then we need to completely overwrite 742 // asyncResp 743 if (asyncResp->res.resultInt() != 200) 744 { 745 // We only want to aggregate collections that contain a 746 // "Members" array 747 if ((!jsonVal.contains("Members")) && 748 (!jsonVal["Members"].is_array())) 749 { 750 BMCWEB_LOG_DEBUG 751 << "Skipping aggregating unsupported resource"; 752 return; 753 } 754 755 BMCWEB_LOG_DEBUG 756 << "Collection does not exist, overwriting asyncResp"; 757 asyncResp->res.result(resp.result()); 758 asyncResp->res.jsonValue = std::move(jsonVal); 759 asyncResp->res.addHeader("Content-Type", "application/json"); 760 761 BMCWEB_LOG_DEBUG << "Finished overwriting asyncResp"; 762 } 763 else 764 { 765 // We only want to aggregate collections that contain a 766 // "Members" array 767 if ((!asyncResp->res.jsonValue.contains("Members")) && 768 (!asyncResp->res.jsonValue["Members"].is_array())) 769 770 { 771 BMCWEB_LOG_DEBUG 772 << "Skipping aggregating unsupported resource"; 773 return; 774 } 775 776 BMCWEB_LOG_DEBUG << "Adding aggregated resources from \"" 777 << prefix << "\" to collection"; 778 779 // TODO: This is a potential race condition with multiple 780 // satellites and the aggregating bmc attempting to write to 781 // update this array. May need to cascade calls to the next 782 // satellite at the end of this function. 783 // This is presumably not a concern when there is only a single 784 // satellite since the aggregating bmc should have completed 785 // before the response is received from the satellite. 786 787 auto& members = asyncResp->res.jsonValue["Members"]; 788 auto& satMembers = jsonVal["Members"]; 789 for (auto& satMem : satMembers) 790 { 791 members.push_back(std::move(satMem)); 792 } 793 asyncResp->res.jsonValue["Members@odata.count"] = 794 members.size(); 795 796 // TODO: Do we need to sort() after updating the array? 797 } 798 } 799 else 800 { 801 BMCWEB_LOG_ERROR << "Received unparsable response from \"" << prefix 802 << "\""; 803 // We received a response that was not a json. 804 // Notify the user only if we did not receive any valid responses, 805 // if the resource collection does not already exist on the 806 // aggregating BMC, and if we did not already set this warning due 807 // to a failure from a different satellite 808 if ((asyncResp->res.resultInt() != 200) && 809 (asyncResp->res.resultInt() != 429) && 810 (asyncResp->res.resultInt() != 502)) 811 { 812 messages::operationFailed(asyncResp->res); 813 } 814 } 815 } // End processCollectionResponse() 816 817 // Entry point to Redfish Aggregation 818 // Returns Result stating whether or not we still need to locally handle the 819 // request 820 static Result 821 beginAggregation(const crow::Request& thisReq, 822 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) 823 { 824 using crow::utility::OrMorePaths; 825 using crow::utility::readUrlSegments; 826 const boost::urls::url_view url = thisReq.urlView; 827 828 // We don't need to aggregate JsonSchemas due to potential issues such 829 // as version mismatches between aggregator and satellite BMCs. For 830 // now assume that the aggregator has all the schemas and versions that 831 // the aggregated server has. 832 if (crow::utility::readUrlSegments(url, "redfish", "v1", "JsonSchemas", 833 crow::utility::OrMorePaths())) 834 { 835 return Result::LocalHandle; 836 } 837 838 // The first two segments should be "/redfish/v1". We need to check 839 // that before we can search topCollections 840 if (!crow::utility::readUrlSegments(url, "redfish", "v1", 841 crow::utility::OrMorePaths())) 842 { 843 return Result::LocalHandle; 844 } 845 846 // Parse the URI to see if it begins with a known top level collection 847 // such as: 848 // /redfish/v1/Chassis 849 // /redfish/v1/UpdateService/FirmwareInventory 850 const boost::urls::segments_view urlSegments = url.segments(); 851 boost::urls::url currentUrl("/"); 852 boost::urls::segments_view::iterator it = urlSegments.begin(); 853 const boost::urls::segments_view::const_iterator end = 854 urlSegments.end(); 855 856 // Skip past the leading "/redfish/v1" 857 it++; 858 it++; 859 for (; it != end; it++) 860 { 861 const std::string& collectionItem = *it; 862 if (std::binary_search(topCollections.begin(), topCollections.end(), 863 currentUrl.buffer())) 864 { 865 // We've matched a resource collection so this current segment 866 // might contain an aggregation prefix 867 if (collectionItem.starts_with("5B247A")) 868 { 869 BMCWEB_LOG_DEBUG << "Need to forward a request"; 870 871 // Extract the prefix from the request's URI, retrieve the 872 // associated satellite config information, and then forward 873 // the request to that satellite. 874 startAggregation(AggregationType::Resource, thisReq, 875 asyncResp); 876 return Result::NoLocalHandle; 877 } 878 879 // Handle collection URI with a trailing backslash 880 // e.g. /redfish/v1/Chassis/ 881 it++; 882 if ((it == end) && collectionItem.empty()) 883 { 884 startAggregation(AggregationType::Collection, thisReq, 885 asyncResp); 886 } 887 888 // We didn't recognize the prefix or it's a collection with a 889 // trailing "/". In both cases we still want to locally handle 890 // the request 891 return Result::LocalHandle; 892 } 893 894 currentUrl.segments().push_back(collectionItem); 895 } 896 897 // If we made it here then currentUrl could contain a top level 898 // collection URI without a trailing "/", e.g. /redfish/v1/Chassis 899 if (std::binary_search(topCollections.begin(), topCollections.end(), 900 currentUrl.buffer())) 901 { 902 startAggregation(AggregationType::Collection, thisReq, asyncResp); 903 return Result::LocalHandle; 904 } 905 906 BMCWEB_LOG_DEBUG << "Aggregation not required"; 907 return Result::LocalHandle; 908 } 909 }; 910 911 } // namespace redfish 912