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