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