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