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 boost::system::error_code& ec, 243 const std::unordered_map<std::string, boost::urls::url>& satelliteInfo) 244 { 245 if (ec) 246 { 247 BMCWEB_LOG_ERROR << "Something went wrong while querying dbus!"; 248 return; 249 } 250 251 BMCWEB_LOG_DEBUG << "There were " 252 << std::to_string(satelliteInfo.size()) 253 << " satellite configs found at startup"; 254 } 255 256 // Search D-Bus objects for satellite config objects and add their 257 // information if valid 258 static void findSatelliteConfigs( 259 const dbus::utility::ManagedObjectType& objects, 260 std::unordered_map<std::string, boost::urls::url>& satelliteInfo) 261 { 262 for (const auto& objectPath : objects) 263 { 264 for (const auto& interface : objectPath.second) 265 { 266 if (interface.first == 267 "xyz.openbmc_project.Configuration.SatelliteController") 268 { 269 BMCWEB_LOG_DEBUG << "Found Satellite Controller at " 270 << objectPath.first.str; 271 272 if (!satelliteInfo.empty()) 273 { 274 BMCWEB_LOG_ERROR 275 << "Redfish Aggregation only supports one satellite!"; 276 BMCWEB_LOG_DEBUG << "Clearing all satellite data"; 277 satelliteInfo.clear(); 278 return; 279 } 280 281 // For now assume there will only be one satellite config. 282 // Assign it the name/prefix "5B247A" 283 addSatelliteConfig("5B247A", interface.second, 284 satelliteInfo); 285 } 286 } 287 } 288 } 289 290 // Parse the properties of a satellite config object and add the 291 // configuration if the properties are valid 292 static void addSatelliteConfig( 293 const std::string& name, 294 const dbus::utility::DBusPropertiesMap& properties, 295 std::unordered_map<std::string, boost::urls::url>& satelliteInfo) 296 { 297 boost::urls::url url; 298 299 for (const auto& prop : properties) 300 { 301 if (prop.first == "Hostname") 302 { 303 const std::string* propVal = 304 std::get_if<std::string>(&prop.second); 305 if (propVal == nullptr) 306 { 307 BMCWEB_LOG_ERROR << "Invalid Hostname value"; 308 return; 309 } 310 url.set_host(*propVal); 311 } 312 313 else if (prop.first == "Port") 314 { 315 const uint64_t* propVal = std::get_if<uint64_t>(&prop.second); 316 if (propVal == nullptr) 317 { 318 BMCWEB_LOG_ERROR << "Invalid Port value"; 319 return; 320 } 321 322 if (*propVal > std::numeric_limits<uint16_t>::max()) 323 { 324 BMCWEB_LOG_ERROR << "Port value out of range"; 325 return; 326 } 327 url.set_port(std::to_string(static_cast<uint16_t>(*propVal))); 328 } 329 330 else if (prop.first == "AuthType") 331 { 332 const std::string* propVal = 333 std::get_if<std::string>(&prop.second); 334 if (propVal == nullptr) 335 { 336 BMCWEB_LOG_ERROR << "Invalid AuthType value"; 337 return; 338 } 339 340 // For now assume authentication not required to communicate 341 // with the satellite BMC 342 if (*propVal != "None") 343 { 344 BMCWEB_LOG_ERROR 345 << "Unsupported AuthType value: " << *propVal 346 << ", only \"none\" is supported"; 347 return; 348 } 349 url.set_scheme("http"); 350 } 351 } // Finished reading properties 352 353 // Make sure all required config information was made available 354 if (url.host().empty()) 355 { 356 BMCWEB_LOG_ERROR << "Satellite config " << name << " missing Host"; 357 return; 358 } 359 360 if (!url.has_port()) 361 { 362 BMCWEB_LOG_ERROR << "Satellite config " << name << " missing Port"; 363 return; 364 } 365 366 if (!url.has_scheme()) 367 { 368 BMCWEB_LOG_ERROR << "Satellite config " << name 369 << " missing AuthType"; 370 return; 371 } 372 373 std::string resultString; 374 auto result = satelliteInfo.insert_or_assign(name, std::move(url)); 375 if (result.second) 376 { 377 resultString = "Added new satellite config "; 378 } 379 else 380 { 381 resultString = "Updated existing satellite config "; 382 } 383 384 BMCWEB_LOG_DEBUG << resultString << name << " at " 385 << result.first->second.scheme() << "://" 386 << result.first->second.encoded_host_and_port(); 387 } 388 389 enum AggregationType 390 { 391 Collection, 392 Resource, 393 }; 394 395 static void 396 startAggregation(AggregationType isCollection, 397 const crow::Request& thisReq, 398 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) 399 { 400 if ((isCollection == AggregationType::Collection) && 401 (thisReq.method() != boost::beast::http::verb::get)) 402 { 403 BMCWEB_LOG_DEBUG 404 << "Only aggregate GET requests to top level collections"; 405 return; 406 } 407 408 // Create a copy of thisReq so we we can still locally process the req 409 std::error_code ec; 410 auto localReq = std::make_shared<crow::Request>(thisReq.req, ec); 411 if (ec) 412 { 413 BMCWEB_LOG_ERROR << "Failed to create copy of request"; 414 if (isCollection != AggregationType::Collection) 415 { 416 messages::internalError(asyncResp->res); 417 } 418 return; 419 } 420 421 getSatelliteConfigs(std::bind_front(aggregateAndHandle, isCollection, 422 localReq, asyncResp)); 423 } 424 425 static void findSatellite( 426 const crow::Request& req, 427 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 428 const std::unordered_map<std::string, boost::urls::url>& satelliteInfo, 429 std::string_view memberName) 430 { 431 // Determine if the resource ID begins with a known prefix 432 for (const auto& satellite : satelliteInfo) 433 { 434 std::string targetPrefix = satellite.first; 435 targetPrefix += "_"; 436 if (memberName.starts_with(targetPrefix)) 437 { 438 BMCWEB_LOG_DEBUG << "\"" << satellite.first 439 << "\" is a known prefix"; 440 441 // Remove the known prefix from the request's URI and 442 // then forward to the associated satellite BMC 443 getInstance().forwardRequest(req, asyncResp, satellite.first, 444 satelliteInfo); 445 return; 446 } 447 } 448 449 // We didn't recognize the prefix and need to return a 404 450 std::string nameStr = req.url().segments().back(); 451 messages::resourceNotFound(asyncResp->res, "", nameStr); 452 } 453 454 // Intended to handle an incoming request based on if Redfish Aggregation 455 // is enabled. Forwards request to satellite BMC if it exists. 456 static void aggregateAndHandle( 457 AggregationType isCollection, 458 const std::shared_ptr<crow::Request>& sharedReq, 459 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 460 const boost::system::error_code& ec, 461 const std::unordered_map<std::string, boost::urls::url>& satelliteInfo) 462 { 463 if (sharedReq == nullptr) 464 { 465 return; 466 } 467 // Something went wrong while querying dbus 468 if (ec) 469 { 470 messages::internalError(asyncResp->res); 471 return; 472 } 473 474 // No satellite configs means we don't need to keep attempting to 475 // aggregate 476 if (satelliteInfo.empty()) 477 { 478 // For collections we'll also handle the request locally so we 479 // don't need to write an error code 480 if (isCollection == AggregationType::Resource) 481 { 482 std::string nameStr = sharedReq->url().segments().back(); 483 messages::resourceNotFound(asyncResp->res, "", nameStr); 484 } 485 return; 486 } 487 488 const crow::Request& thisReq = *sharedReq; 489 BMCWEB_LOG_DEBUG << "Aggregation is enabled, begin processing of " 490 << thisReq.target(); 491 492 // We previously determined the request is for a collection. No need to 493 // check again 494 if (isCollection == AggregationType::Collection) 495 { 496 BMCWEB_LOG_DEBUG << "Aggregating a collection"; 497 // We need to use a specific response handler and send the 498 // request to all known satellites 499 getInstance().forwardCollectionRequests(thisReq, asyncResp, 500 satelliteInfo); 501 return; 502 } 503 504 const boost::urls::segments_view urlSegments = thisReq.url().segments(); 505 boost::urls::url currentUrl("/"); 506 boost::urls::segments_view::iterator it = urlSegments.begin(); 507 const boost::urls::segments_view::const_iterator end = 508 urlSegments.end(); 509 510 // Skip past the leading "/redfish/v1" 511 it++; 512 it++; 513 for (; it != end; it++) 514 { 515 if (std::binary_search(topCollections.begin(), topCollections.end(), 516 currentUrl.buffer())) 517 { 518 // We've matched a resource collection so this current segment 519 // must contain an aggregation prefix 520 findSatellite(thisReq, asyncResp, satelliteInfo, *it); 521 return; 522 } 523 524 currentUrl.segments().push_back(*it); 525 } 526 527 // We shouldn't reach this point since we should've hit one of the 528 // previous exits 529 messages::internalError(asyncResp->res); 530 } 531 532 // Attempt to forward a request to the satellite BMC associated with the 533 // prefix. 534 void forwardRequest( 535 const crow::Request& thisReq, 536 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 537 const std::string& prefix, 538 const std::unordered_map<std::string, boost::urls::url>& satelliteInfo) 539 { 540 const auto& sat = satelliteInfo.find(prefix); 541 if (sat == satelliteInfo.end()) 542 { 543 // Realistically this shouldn't get called since we perform an 544 // earlier check to make sure the prefix exists 545 BMCWEB_LOG_ERROR << "Unrecognized satellite prefix \"" << prefix 546 << "\""; 547 return; 548 } 549 550 // We need to strip the prefix from the request's path 551 std::string targetURI(thisReq.target()); 552 size_t pos = targetURI.find(prefix + "_"); 553 if (pos == std::string::npos) 554 { 555 // If this fails then something went wrong 556 BMCWEB_LOG_ERROR << "Error removing prefix \"" << prefix 557 << "_\" from request URI"; 558 messages::internalError(asyncResp->res); 559 return; 560 } 561 targetURI.erase(pos, prefix.size() + 1); 562 563 std::function<void(crow::Response&)> cb = 564 std::bind_front(processResponse, prefix, asyncResp); 565 566 std::string data = thisReq.req.body(); 567 crow::HttpClient::getInstance().sendDataWithCallback( 568 data, id, std::string(sat->second.host()), 569 sat->second.port_number(), targetURI, false /*useSSL*/, 570 thisReq.fields(), thisReq.method(), retryPolicyName, cb); 571 } 572 573 // Forward a request for a collection URI to each known satellite BMC 574 void forwardCollectionRequests( 575 const crow::Request& thisReq, 576 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 577 const std::unordered_map<std::string, boost::urls::url>& satelliteInfo) 578 { 579 for (const auto& sat : satelliteInfo) 580 { 581 std::function<void(crow::Response&)> cb = std::bind_front( 582 processCollectionResponse, sat.first, asyncResp); 583 584 std::string targetURI(thisReq.target()); 585 std::string data = thisReq.req.body(); 586 crow::HttpClient::getInstance().sendDataWithCallback( 587 data, id, std::string(sat.second.host()), 588 sat.second.port_number(), targetURI, false /*useSSL*/, 589 thisReq.fields(), thisReq.method(), retryPolicyName, cb); 590 } 591 } 592 593 public: 594 RedfishAggregator(const RedfishAggregator&) = delete; 595 RedfishAggregator& operator=(const RedfishAggregator&) = delete; 596 RedfishAggregator(RedfishAggregator&&) = delete; 597 RedfishAggregator& operator=(RedfishAggregator&&) = delete; 598 ~RedfishAggregator() = default; 599 600 static RedfishAggregator& getInstance() 601 { 602 static RedfishAggregator handler; 603 return handler; 604 } 605 606 // Polls D-Bus to get all available satellite config information 607 // Expects a handler which interacts with the returned configs 608 static void getSatelliteConfigs( 609 std::function< 610 void(const boost::system::error_code&, 611 const std::unordered_map<std::string, boost::urls::url>&)> 612 handler) 613 { 614 BMCWEB_LOG_DEBUG << "Gathering satellite configs"; 615 crow::connections::systemBus->async_method_call( 616 [handler{std::move(handler)}]( 617 const boost::system::error_code& ec, 618 const dbus::utility::ManagedObjectType& objects) { 619 std::unordered_map<std::string, boost::urls::url> satelliteInfo; 620 if (ec) 621 { 622 BMCWEB_LOG_ERROR << "DBUS response error " << ec.value() << ", " 623 << ec.message(); 624 handler(ec, satelliteInfo); 625 return; 626 } 627 628 // Maps a chosen alias representing a satellite BMC to a url 629 // containing the information required to create a http 630 // connection to the satellite 631 findSatelliteConfigs(objects, satelliteInfo); 632 633 if (!satelliteInfo.empty()) 634 { 635 BMCWEB_LOG_DEBUG << "Redfish Aggregation enabled with " 636 << std::to_string(satelliteInfo.size()) 637 << " satellite BMCs"; 638 } 639 else 640 { 641 BMCWEB_LOG_DEBUG 642 << "No satellite BMCs detected. Redfish Aggregation not enabled"; 643 } 644 handler(ec, satelliteInfo); 645 }, 646 "xyz.openbmc_project.EntityManager", 647 "/xyz/openbmc_project/inventory", 648 "org.freedesktop.DBus.ObjectManager", "GetManagedObjects"); 649 } 650 651 // Processes the response returned by a satellite BMC and loads its 652 // contents into asyncResp 653 static void 654 processResponse(std::string_view 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 asyncResp->res.result(resp.result()); 663 return; 664 } 665 666 // We want to attempt prefix fixing regardless of response code 667 // The resp will not have a json component 668 // We need to create a json from resp's stringResponse 669 if (resp.getHeaderValue("Content-Type") == "application/json") 670 { 671 nlohmann::json jsonVal = 672 nlohmann::json::parse(resp.body(), nullptr, false); 673 if (jsonVal.is_discarded()) 674 { 675 BMCWEB_LOG_ERROR << "Error parsing satellite response as JSON"; 676 messages::operationFailed(asyncResp->res); 677 return; 678 } 679 680 BMCWEB_LOG_DEBUG << "Successfully parsed satellite response"; 681 682 addPrefixes(jsonVal, prefix); 683 684 BMCWEB_LOG_DEBUG << "Added prefix to parsed satellite response"; 685 686 asyncResp->res.result(resp.result()); 687 asyncResp->res.jsonValue = std::move(jsonVal); 688 689 BMCWEB_LOG_DEBUG << "Finished writing asyncResp"; 690 } 691 else 692 { 693 // We allow any Content-Type that is not "application/json" now 694 asyncResp->res.result(resp.result()); 695 asyncResp->res.write(resp.body()); 696 } 697 addAggregatedHeaders(asyncResp->res, resp, prefix); 698 } 699 700 // Processes the collection response returned by a satellite BMC and merges 701 // its "@odata.id" values 702 static void processCollectionResponse( 703 const std::string& prefix, 704 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 705 crow::Response& resp) 706 { 707 // 429 and 502 mean we didn't actually send the request so don't 708 // overwrite the response headers in that case 709 if ((resp.resultInt() == 429) || (resp.resultInt() == 502)) 710 { 711 return; 712 } 713 714 if (resp.resultInt() != 200) 715 { 716 BMCWEB_LOG_DEBUG 717 << "Collection resource does not exist in satellite BMC \"" 718 << prefix << "\""; 719 // Return the error if we haven't had any successes 720 if (asyncResp->res.resultInt() != 200) 721 { 722 asyncResp->res.stringResponse = std::move(resp.stringResponse); 723 } 724 return; 725 } 726 727 // The resp will not have a json component 728 // We need to create a json from resp's stringResponse 729 if (resp.getHeaderValue("Content-Type") == "application/json") 730 { 731 nlohmann::json jsonVal = 732 nlohmann::json::parse(resp.body(), nullptr, false); 733 if (jsonVal.is_discarded()) 734 { 735 BMCWEB_LOG_ERROR << "Error parsing satellite response as JSON"; 736 737 // Notify the user if doing so won't overwrite a valid response 738 if ((asyncResp->res.resultInt() != 200) && 739 (asyncResp->res.resultInt() != 429) && 740 (asyncResp->res.resultInt() != 502)) 741 { 742 messages::operationFailed(asyncResp->res); 743 } 744 return; 745 } 746 747 BMCWEB_LOG_DEBUG << "Successfully parsed satellite response"; 748 749 // Now we need to add the prefix to the URIs contained in the 750 // response. 751 addPrefixes(jsonVal, prefix); 752 753 BMCWEB_LOG_DEBUG << "Added prefix to parsed satellite response"; 754 755 // If this resource collection does not exist on the aggregating bmc 756 // and has not already been added from processing the response from 757 // a different satellite then we need to completely overwrite 758 // asyncResp 759 if (asyncResp->res.resultInt() != 200) 760 { 761 // We only want to aggregate collections that contain a 762 // "Members" array 763 if ((!jsonVal.contains("Members")) && 764 (!jsonVal["Members"].is_array())) 765 { 766 BMCWEB_LOG_DEBUG 767 << "Skipping aggregating unsupported resource"; 768 return; 769 } 770 771 BMCWEB_LOG_DEBUG 772 << "Collection does not exist, overwriting asyncResp"; 773 asyncResp->res.result(resp.result()); 774 asyncResp->res.jsonValue = std::move(jsonVal); 775 asyncResp->res.addHeader("Content-Type", "application/json"); 776 777 BMCWEB_LOG_DEBUG << "Finished overwriting asyncResp"; 778 } 779 else 780 { 781 // We only want to aggregate collections that contain a 782 // "Members" array 783 if ((!asyncResp->res.jsonValue.contains("Members")) && 784 (!asyncResp->res.jsonValue["Members"].is_array())) 785 786 { 787 BMCWEB_LOG_DEBUG 788 << "Skipping aggregating unsupported resource"; 789 return; 790 } 791 792 BMCWEB_LOG_DEBUG << "Adding aggregated resources from \"" 793 << prefix << "\" to collection"; 794 795 // TODO: This is a potential race condition with multiple 796 // satellites and the aggregating bmc attempting to write to 797 // update this array. May need to cascade calls to the next 798 // satellite at the end of this function. 799 // This is presumably not a concern when there is only a single 800 // satellite since the aggregating bmc should have completed 801 // before the response is received from the satellite. 802 803 auto& members = asyncResp->res.jsonValue["Members"]; 804 auto& satMembers = jsonVal["Members"]; 805 for (auto& satMem : satMembers) 806 { 807 members.push_back(std::move(satMem)); 808 } 809 asyncResp->res.jsonValue["Members@odata.count"] = 810 members.size(); 811 812 // TODO: Do we need to sort() after updating the array? 813 } 814 } 815 else 816 { 817 BMCWEB_LOG_ERROR << "Received unparsable response from \"" << prefix 818 << "\""; 819 // We received a response that was not a json. 820 // Notify the user only if we did not receive any valid responses, 821 // if the resource collection does not already exist on the 822 // aggregating BMC, and if we did not already set this warning due 823 // to a failure from a different satellite 824 if ((asyncResp->res.resultInt() != 200) && 825 (asyncResp->res.resultInt() != 429) && 826 (asyncResp->res.resultInt() != 502)) 827 { 828 messages::operationFailed(asyncResp->res); 829 } 830 } 831 } // End processCollectionResponse() 832 833 // Entry point to Redfish Aggregation 834 // Returns Result stating whether or not we still need to locally handle the 835 // request 836 static Result 837 beginAggregation(const crow::Request& thisReq, 838 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) 839 { 840 using crow::utility::OrMorePaths; 841 using crow::utility::readUrlSegments; 842 const boost::urls::url_view url = thisReq.url(); 843 844 // We don't need to aggregate JsonSchemas due to potential issues such 845 // as version mismatches between aggregator and satellite BMCs. For 846 // now assume that the aggregator has all the schemas and versions that 847 // the aggregated server has. 848 if (crow::utility::readUrlSegments(url, "redfish", "v1", "JsonSchemas", 849 crow::utility::OrMorePaths())) 850 { 851 return Result::LocalHandle; 852 } 853 854 // The first two segments should be "/redfish/v1". We need to check 855 // that before we can search topCollections 856 if (!crow::utility::readUrlSegments(url, "redfish", "v1", 857 crow::utility::OrMorePaths())) 858 { 859 return Result::LocalHandle; 860 } 861 862 // Parse the URI to see if it begins with a known top level collection 863 // such as: 864 // /redfish/v1/Chassis 865 // /redfish/v1/UpdateService/FirmwareInventory 866 const boost::urls::segments_view urlSegments = url.segments(); 867 boost::urls::url currentUrl("/"); 868 boost::urls::segments_view::iterator it = urlSegments.begin(); 869 const boost::urls::segments_view::const_iterator end = 870 urlSegments.end(); 871 872 // Skip past the leading "/redfish/v1" 873 it++; 874 it++; 875 for (; it != end; it++) 876 { 877 const std::string& collectionItem = *it; 878 if (std::binary_search(topCollections.begin(), topCollections.end(), 879 currentUrl.buffer())) 880 { 881 // We've matched a resource collection so this current segment 882 // might contain an aggregation prefix 883 // TODO: This needs to be rethought when we can support multiple 884 // satellites due to 885 // /redfish/v1/AggregationService/AggregationSources/5B247A 886 // being a local resource describing the satellite 887 if (collectionItem.starts_with("5B247A_")) 888 { 889 BMCWEB_LOG_DEBUG << "Need to forward a request"; 890 891 // Extract the prefix from the request's URI, retrieve the 892 // associated satellite config information, and then forward 893 // the request to that satellite. 894 startAggregation(AggregationType::Resource, thisReq, 895 asyncResp); 896 return Result::NoLocalHandle; 897 } 898 899 // Handle collection URI with a trailing backslash 900 // e.g. /redfish/v1/Chassis/ 901 it++; 902 if ((it == end) && collectionItem.empty()) 903 { 904 startAggregation(AggregationType::Collection, thisReq, 905 asyncResp); 906 } 907 908 // We didn't recognize the prefix or it's a collection with a 909 // trailing "/". In both cases we still want to locally handle 910 // the request 911 return Result::LocalHandle; 912 } 913 914 currentUrl.segments().push_back(collectionItem); 915 } 916 917 // If we made it here then currentUrl could contain a top level 918 // collection URI without a trailing "/", e.g. /redfish/v1/Chassis 919 if (std::binary_search(topCollections.begin(), topCollections.end(), 920 currentUrl.buffer())) 921 { 922 startAggregation(AggregationType::Collection, thisReq, asyncResp); 923 return Result::LocalHandle; 924 } 925 926 BMCWEB_LOG_DEBUG << "Aggregation not required"; 927 return Result::LocalHandle; 928 } 929 }; 930 931 } // namespace redfish 932