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