| #pragma once |
| |
| #include "bmcweb_config.h" |
| |
| #include "aggregation_utils.hpp" |
| #include "dbus_utility.hpp" |
| #include "error_messages.hpp" |
| #include "http_client.hpp" |
| #include "http_connection.hpp" |
| #include "managed_store.hpp" |
| #include "request_stats.hpp" |
| |
| #include <boost/algorithm/string/predicate.hpp> |
| |
| #include <array> |
| #include <cstdint> |
| #include <random> |
| |
| #ifdef UNIT_TEST_BUILD |
| #include "test/g3/mock_managed_store.hpp" // NOLINT |
| #endif |
| |
| namespace redfish |
| { |
| |
| constexpr unsigned int aggregatorReadBodyLimit = 50 * 1024 * 1024; // 50MB |
| |
| enum class Result : std::uint8_t |
| { |
| LocalHandle, |
| NoLocalHandle |
| }; |
| |
| enum class SearchType : std::uint8_t |
| { |
| Collection, |
| CollOrCon, |
| ContainsSubordinate, |
| Resource |
| }; |
| |
| struct RdeSatelliteConfig |
| { |
| std::string name; |
| std::string vid; |
| std::string udevid; |
| std::string usbport; |
| std::string objectpath; |
| }; |
| |
| // clang-format off |
| // These are all of the properties as of version 2022.2 of the Redfish Resource |
| // and Schema Guide whose Type is "string (URI)" and the name does not end in a |
| // case-insensitive form of "uri". That version of the schema is associated |
| // with version 1.16.0 of the Redfish Specification. Going forward, new URI |
| // properties should end in URI so this list should not need to be maintained as |
| // the spec is updated. NOTE: These have been pre-sorted in order to be |
| // compatible with binary search |
| constexpr std::array nonUriProperties{ |
| "@Redfish.ActionInfo", |
| // "@odata.context", // We can't fix /redfish/v1/$metadata URIs |
| "@odata.id", |
| // "Destination", // Only used by EventService and won't be a Redfish URI |
| // "HostName", // Isn't actually a Redfish URI |
| "Image", |
| "MetricProperty", |
| // "OriginOfCondition", // Is URI when in request, but is object in response |
| "TaskMonitor", |
| "target", // normal string, but target URI for POST to invoke an action |
| }; |
| // clang-format on |
| |
| // Search the top collection array to determine if the passed URI is of a |
| // desired type |
| inline bool searchCollectionsArray(std::string_view uri, |
| const SearchType searchType) |
| { |
| boost::system::result<boost::urls::url> parsedUrl = |
| boost::urls::parse_relative_ref(uri); |
| |
| if (!parsedUrl) |
| { |
| BMCWEB_LOG_ERROR << "Failed to get target URI from " << uri; |
| return false; |
| } |
| |
| parsedUrl->normalize(); |
| boost::urls::segments_ref segments = parsedUrl->segments(); |
| if (!segments.is_absolute()) |
| { |
| return false; |
| } |
| |
| // The passed URI must begin with "/redfish/v1", but we have to strip it |
| // from the URI since topCollections does not include it in its URIs. |
| if (segments.size() < 2) |
| { |
| return false; |
| } |
| if (segments.front() != "redfish") |
| { |
| return false; |
| } |
| segments.erase(segments.begin()); |
| if (segments.front() != "v1") |
| { |
| return false; |
| } |
| segments.erase(segments.begin()); |
| |
| // Exclude the trailing "/" if it exists such as in "/redfish/v1/". |
| if (!segments.empty() && segments.back().empty()) |
| { |
| segments.pop_back(); |
| } |
| |
| // If no segments then the passed URI was either "/redfish/v1" or |
| // "/redfish/v1/". |
| if (segments.empty()) |
| { |
| return (searchType == SearchType::ContainsSubordinate) || |
| (searchType == SearchType::CollOrCon); |
| } |
| std::string_view url = segments.buffer(); |
| const auto* it = std::ranges::lower_bound(topCollections, url); |
| if (it == topCollections.end()) |
| { |
| // parsedUrl is alphabetically after the last entry in the array so it |
| // can't be a top collection or up tree from a top collection |
| return false; |
| } |
| |
| boost::urls::url collectionUrl(*it); |
| boost::urls::segments_view collectionSegments = collectionUrl.segments(); |
| boost::urls::segments_view::iterator itCollection = |
| collectionSegments.begin(); |
| const boost::urls::segments_view::const_iterator endCollection = |
| collectionSegments.end(); |
| |
| // Each segment in the passed URI should match the found collection |
| for (const auto& segment : segments) |
| { |
| if (itCollection == endCollection) |
| { |
| // Leftover segments means the target is for an aggregation |
| // supported resource |
| return searchType == SearchType::Resource; |
| } |
| |
| if (segment != (*itCollection)) |
| { |
| return false; |
| } |
| itCollection++; |
| } |
| |
| // No remaining segments means the passed URI was a top level collection |
| if (searchType == SearchType::Collection) |
| { |
| return itCollection == endCollection; |
| } |
| if (searchType == SearchType::ContainsSubordinate) |
| { |
| return itCollection != endCollection; |
| } |
| |
| // Return this check instead of "true" in case other SearchTypes get added |
| return searchType == SearchType::CollOrCon; |
| } |
| |
| // Strip query params which are incompatible with aggregation. |
| // Note, this still doesn't work for collections that might return less than the |
| // complete collection by default, but hopefully those are rare/nonexistent in |
| // top collections. bmcweb doesn't implement any of these. |
| inline void parameterRemove(crow::Request& localReq) |
| { |
| boost::urls::url& urlNew = localReq.mutableUrl(); |
| auto paramsIt = urlNew.params().begin(); |
| bool foundSkip = false; |
| while (paramsIt != urlNew.params().end()) |
| { |
| const boost::urls::param& param = *paramsIt; |
| |
| // Plugins will not apply correctly to resources pulled in by offloading |
| // $expand to a satellite BMC. Don't forward $expand in that case. |
| if constexpr (enablePlatform22) |
| { |
| if (param.key == "$expand") |
| { |
| BMCWEB_LOG_DEBUG << "Erasing $expand from forwarded request"; |
| paramsIt = urlNew.params().erase(paramsIt); |
| continue; |
| } |
| } |
| |
| // By the spec, no other params should be provided with only. Don't |
| // forward any params and let the aggregating BMC handle them. |
| if (param.key == "only") |
| { |
| BMCWEB_LOG_DEBUG << |
| "Erasing all params from request to top level collection"; |
| urlNew.params().clear(); |
| foundSkip = false; |
| break; |
| } |
| |
| if (param.key == "$skip") |
| { |
| foundSkip = true; |
| } |
| paramsIt++; |
| } |
| |
| if (foundSkip) |
| { |
| // Remove $skip and any other param that is supposed to processed after |
| // it. Applying $skip twice would produce different results. |
| // Forwarding just the non-$skip params would result in partially |
| // processing the params in the wrong order. |
| paramsIt = urlNew.params().begin(); |
| while (paramsIt != urlNew.params().end()) |
| { |
| const boost::urls::param& param = *paramsIt; |
| // According to the Redfish spec, $filter is the only param that is |
| // processed before $skip and is thus safe to be forwarded. All |
| // others need to be removed |
| if (param.key != "$filter") |
| { |
| BMCWEB_LOG_DEBUG << "Erasing \"" << param.key |
| << "\" param from request to top level collection"; |
| paramsIt = urlNew.params().erase(paramsIt); |
| continue; |
| } |
| // Pass $filter parameter |
| paramsIt++; |
| } |
| } |
| localReq.target(urlNew.buffer()); |
| } |
| |
| // Determines if the passed property contains a URI. Those property names |
| // either end with a case-insensitive version of "uri" or are specifically |
| // defined in the above array. |
| inline bool isPropertyUri(std::string_view propertyName) |
| { |
| return boost::iends_with(propertyName, "uri") || |
| std::binary_search(nonUriProperties.begin(), nonUriProperties.end(), |
| propertyName); |
| } |
| |
| static inline void addPrefixToStringItem(std::string& strValue, |
| std::string_view prefix) |
| { |
| // Make sure the value is a properly formatted URI |
| auto parsed = boost::urls::parse_relative_ref(strValue); |
| if (!parsed) |
| { |
| BMCWEB_LOG_CRITICAL << "Couldn't parse URI from resource " << strValue; |
| return; |
| } |
| |
| boost::urls::url_view thisUrl = *parsed; |
| |
| // We don't need to aggregate JsonSchemas due to potential issues such as |
| // version mismatches between aggregator and satellite BMCs. For now |
| // assume that the aggregator has all the schemas and versions that the |
| // aggregated server has. |
| if (crow::utility::readUrlSegments(thisUrl, "redfish", "v1", "JsonSchemas", |
| crow::utility::OrMorePaths())) |
| { |
| BMCWEB_LOG_DEBUG << "Skipping JsonSchemas URI prefix fixing"; |
| return; |
| } |
| |
| // The first two segments should be "/redfish/v1". We need to check that |
| // before we can search topCollections |
| if (!crow::utility::readUrlSegments(thisUrl, "redfish", "v1", |
| crow::utility::OrMorePaths())) |
| { |
| return; |
| } |
| |
| // Check array adding a segment each time until collection is identified |
| // Add prefix to segment after the collection |
| const boost::urls::segments_view urlSegments = thisUrl.segments(); |
| bool addedPrefix = false; |
| boost::urls::url url("/"); |
| boost::urls::segments_view::iterator it = urlSegments.begin(); |
| const boost::urls::segments_view::const_iterator end = urlSegments.end(); |
| |
| // Skip past the leading "/redfish/v1" |
| it++; |
| it++; |
| for (; it != end; it++) |
| { |
| // Trailing "/" will result in an empty segment. In that case we need |
| // to return so we don't apply a prefix to top level collections such |
| // as "/redfish/v1/Chassis/" |
| if ((*it).empty()) |
| { |
| return; |
| } |
| |
| if (std::binary_search(topCollections.begin(), topCollections.end(), |
| url.buffer())) |
| { |
| std::string collectionItem(prefix); |
| collectionItem += "_" + (*it); |
| url.segments().push_back(collectionItem); |
| it++; |
| addedPrefix = true; |
| break; |
| } |
| |
| url.segments().push_back(*it); |
| } |
| |
| // Finish constructing the URL here (if needed) to avoid additional checks |
| for (; it != end; it++) |
| { |
| url.segments().push_back(*it); |
| } |
| |
| if (addedPrefix) |
| { |
| url.segments().insert(url.segments().begin(), {"redfish", "v1"}); |
| // Preserve the fragment from the original URL |
| if (thisUrl.has_fragment()) |
| { |
| url.set_fragment(thisUrl.fragment()); |
| } |
| strValue = url.buffer(); |
| } |
| } |
| |
| static inline void addPrefixToItem(nlohmann::json& item, |
| std::string_view prefix) |
| { |
| std::string* strValue = item.get_ptr<std::string*>(); |
| if (strValue == nullptr) |
| { |
| BMCWEB_LOG_CRITICAL << "Field wasn't a string????"; |
| return; |
| } |
| addPrefixToStringItem(*strValue, prefix); |
| item = *strValue; |
| } |
| |
| static inline int generateRandomInt() |
| { |
| std::random_device randomDevice; |
| std::mt19937 generator(randomDevice()); |
| std::uniform_int_distribution<> distribution(1, 9999); |
| int randomNumber = distribution(generator); |
| return randomNumber; |
| } |
| |
| static inline void addAggregatedHeaders(crow::Response& asyncResp, |
| const crow::Response& resp, |
| std::string_view prefix) |
| { |
| if (!resp.getHeaderValue("Content-Type").empty()) |
| { |
| asyncResp.addHeader(boost::beast::http::field::content_type, |
| resp.getHeaderValue("Content-Type")); |
| } |
| if (!resp.getHeaderValue("Allow").empty()) |
| { |
| asyncResp.addHeader(boost::beast::http::field::allow, |
| resp.getHeaderValue("Allow")); |
| } |
| std::string_view header = resp.getHeaderValue("Location"); |
| if (!header.empty()) |
| { |
| std::string location(header); |
| addPrefixToStringItem(location, prefix); |
| asyncResp.addHeader(boost::beast::http::field::location, location); |
| } |
| if (!resp.getHeaderValue("Retry-After").empty()) |
| { |
| asyncResp.addHeader(boost::beast::http::field::retry_after, |
| resp.getHeaderValue("Retry-After")); |
| } |
| // TODO: we need special handling for Link Header Value |
| } |
| |
| // Fix HTTP headers which appear in responses from Task resources among others |
| static inline void addPrefixToHeadersInResp(nlohmann::json& json, |
| std::string_view prefix) |
| { |
| // The passed in "HttpHeaders" should be an array of headers |
| nlohmann::json::array_t* array = json.get_ptr<nlohmann::json::array_t*>(); |
| if (array == nullptr) |
| { |
| BMCWEB_LOG_ERROR << "Field wasn't an array_t????"; |
| return; |
| } |
| |
| for (nlohmann::json& item : *array) |
| { |
| // Each header is a single string with the form "<Field>: <Value>" |
| std::string* strHeader = item.get_ptr<std::string*>(); |
| if (strHeader == nullptr) |
| { |
| BMCWEB_LOG_CRITICAL << "Field wasn't a string????"; |
| continue; |
| } |
| |
| constexpr std::string_view location = "Location: "; |
| if (strHeader->starts_with(location)) |
| { |
| std::string header = strHeader->substr(location.size()); |
| addPrefixToStringItem(header, prefix); |
| *strHeader = std::string(location) + header; |
| } |
| } |
| } |
| |
| // Search the json for all URIs and add the supplied prefix if the URI is for |
| // an aggregated resource. |
| static inline void addPrefixes(nlohmann::json& json, std::string_view prefix) |
| { |
| nlohmann::json::object_t* object = |
| json.get_ptr<nlohmann::json::object_t*>(); |
| if (object != nullptr) |
| { |
| for (std::pair<const std::string, nlohmann::json>& item : *object) |
| { |
| if (isPropertyUri(item.first)) |
| { |
| addPrefixToItem(item.second, prefix); |
| continue; |
| } |
| |
| // "HttpHeaders" contains HTTP headers. Among those we need to |
| // attempt to fix the "Location" header |
| if (item.first == "HttpHeaders") |
| { |
| addPrefixToHeadersInResp(item.second, prefix); |
| continue; |
| } |
| |
| // Recusively parse the rest of the json |
| addPrefixes(item.second, prefix); |
| } |
| return; |
| } |
| nlohmann::json::array_t* array = json.get_ptr<nlohmann::json::array_t*>(); |
| if (array != nullptr) |
| { |
| for (nlohmann::json& item : *array) |
| { |
| addPrefixes(item, prefix); |
| } |
| } |
| } |
| |
| inline std::unordered_map<std::string, std::string> |
| rdePrefixMap({{"/xyz/openbmc_project/rde_devices/1_1_1_1", "BCX5VT"}, // NOLINT |
| {"/xyz/openbmc_project/rde_devices/1_1_2_1", "A61MJ0"}, |
| {"/xyz/openbmc_project/rde_devices/1_1_3_1", "K298LC"}, |
| {"/xyz/openbmc_project/rde_devices/1_1_4_1", "2HMMCS"}}); |
| |
| inline std::unordered_map<std::string, std::string> |
| rdeServiceLabelMap({{"BCX5VT", "DOWNLINK"}, // NOLINT |
| {"A61MJ0", "DOWNLINK"}, |
| {"K298LC", "DOWNLINK"}, |
| {"2HMMCS", "PCIE0"}}); |
| |
| inline static bool generatePrefix(const std::string& objPath, std::string& prefix) |
| { |
| const auto& it = rdePrefixMap.find(objPath); |
| if (it == rdePrefixMap.end()) |
| { |
| BMCWEB_LOG_DEBUG << "objPath does not exist in prefix map " << objPath |
| << "\n"; |
| return false; |
| } |
| prefix = it->second; |
| return true; |
| } |
| |
| static inline bool isMemberStartsWithKnownPrefix(const std::string& memberName) |
| { |
| BMCWEB_LOG_DEBUG << "isMemberStartsWithKnownPrefix"; |
| for (auto const& [path, prefix] : rdePrefixMap) |
| { |
| BMCWEB_LOG_DEBUG << "path " << path << " " << prefix; |
| if (memberName.starts_with(prefix)) |
| { |
| BMCWEB_LOG_DEBUG << " Known prefix " << memberName; |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| inline boost::system::error_code aggregationRetryHandler(unsigned int respCode) |
| { |
| // Allow all response codes because we want to surface any satellite |
| // issue to the client |
| BMCWEB_LOG_DEBUG << "Received " << respCode << " response from satellite"; |
| return boost::system::errc::make_error_code(boost::system::errc::success); |
| } |
| |
| inline crow::ConnectionPolicy getAggregationPolicy() |
| { |
| return {.maxRetryAttempts = 1, |
| .requestByteLimit = aggregatorReadBodyLimit, |
| .maxConnections = 20, |
| .retryPolicyAction = "TerminateAfterRetries", |
| .retryIntervalSecs = std::chrono::seconds(0), |
| .invalidResp = aggregationRetryHandler}; |
| } |
| |
| class RedfishAggregator |
| { |
| private: |
| crow::HttpClient client; |
| |
| RedfishAggregator() : |
| client(std::make_shared<crow::ConnectionPolicy>(getAggregationPolicy())) |
| { |
| getSatelliteConfigs(constructorCallback); |
| } |
| |
| // Dummy callback used by the Constructor so that it can report the number |
| // of satellite configs when the class is first created |
| static void constructorCallback( |
| const boost::system::error_code& ec, |
| const std::unordered_map<std::string, boost::urls::url>& satelliteInfo, |
| const std::unordered_map<std::string, RdeSatelliteConfig>& |
| rdeSatelliteInfo) |
| { |
| if (ec) |
| { |
| BMCWEB_LOG_ERROR << "Something went wrong while querying dbus!"; |
| return; |
| } |
| |
| BMCWEB_LOG_DEBUG << "There were " |
| << std::to_string(satelliteInfo.size()) |
| << " satellite configs found at startup"; |
| BMCWEB_LOG_DEBUG << "There were " |
| << std::to_string(rdeSatelliteInfo.size()) |
| << " RDE Device configs found at startup"; |
| } |
| |
| // Search D-Bus objects for satellite config objects and add their |
| // information if valid |
| static void findSatelliteConfigs( |
| const dbus::utility::ManagedObjectType& objects, |
| std::unordered_map<std::string, boost::urls::url>& satelliteInfo, |
| std::unordered_map<std::string, RdeSatelliteConfig>& rdeSatelliteInfo) |
| { |
| for (const auto& objectPath : objects) |
| { |
| for (const auto& interface : objectPath.second) |
| { |
| if (interface.first == |
| "xyz.openbmc_project.Configuration.SatelliteController") |
| { |
| BMCWEB_LOG_DEBUG << "Found Satellite Controller at " |
| << objectPath.first.str; |
| |
| if (!satelliteInfo.empty()) |
| { |
| BMCWEB_LOG_ERROR |
| << "Redfish Aggregation only supports one satellite!"; |
| BMCWEB_LOG_DEBUG << "Clearing all satellite data"; |
| satelliteInfo.clear(); |
| return; |
| } |
| |
| // For now assume there will only be one satellite config. |
| // Assign it the name/prefix "5B247A" |
| addSatelliteConfig("5B247A", interface.second, |
| satelliteInfo); |
| } |
| else if ( |
| interface.first == |
| "xyz.openbmc_project.Configuration.RdeSatelliteController") |
| { |
| BMCWEB_LOG_DEBUG << "Found RDE Satellite Controller at " |
| << objectPath.first.str; |
| addRdeSatelliteConfig(interface.second, rdeSatelliteInfo); |
| } |
| } |
| } |
| } |
| |
| // Parse the properties of a satellite config object and add the |
| // configuration if the properties are valid |
| static void addSatelliteConfig( |
| const std::string& name, |
| const dbus::utility::DBusPropertiesMap& properties, |
| std::unordered_map<std::string, boost::urls::url>& satelliteInfo) |
| { |
| boost::urls::url url; |
| |
| for (const auto& prop : properties) |
| { |
| if (prop.first == "Hostname") |
| { |
| const std::string* propVal = |
| std::get_if<std::string>(&prop.second); |
| if (propVal == nullptr) |
| { |
| BMCWEB_LOG_ERROR << "Invalid Hostname value"; |
| return; |
| } |
| url.set_host(*propVal); |
| } |
| |
| else if (prop.first == "Port") |
| { |
| const uint64_t* propVal = std::get_if<uint64_t>(&prop.second); |
| if (propVal == nullptr) |
| { |
| BMCWEB_LOG_ERROR << "Invalid Port value"; |
| return; |
| } |
| |
| if (*propVal > std::numeric_limits<uint16_t>::max()) |
| { |
| BMCWEB_LOG_ERROR << "Port value out of range"; |
| return; |
| } |
| url.set_port(std::to_string(static_cast<uint16_t>(*propVal))); |
| } |
| |
| else if (prop.first == "AuthType") |
| { |
| const std::string* propVal = |
| std::get_if<std::string>(&prop.second); |
| if (propVal == nullptr) |
| { |
| BMCWEB_LOG_ERROR << "Invalid AuthType value"; |
| return; |
| } |
| |
| // For now assume authentication not required to communicate |
| // with the satellite BMC |
| if (*propVal != "None") |
| { |
| BMCWEB_LOG_ERROR |
| << "Unsupported AuthType value: " << *propVal |
| << ", only \"none\" is supported"; |
| return; |
| } |
| url.set_scheme("http"); |
| } |
| } // Finished reading properties |
| |
| // Make sure all required config information was made available |
| if (url.host().empty()) |
| { |
| BMCWEB_LOG_ERROR << "Satellite config " << name << " missing Host"; |
| return; |
| } |
| |
| if (!url.has_port()) |
| { |
| BMCWEB_LOG_ERROR << "Satellite config " << name << " missing Port"; |
| return; |
| } |
| |
| if (!url.has_scheme()) |
| { |
| BMCWEB_LOG_ERROR << "Satellite config " << name |
| << " missing AuthType"; |
| return; |
| } |
| |
| std::string resultString; |
| auto result = satelliteInfo.insert_or_assign(name, std::move(url)); |
| if (result.second) |
| { |
| resultString = "Added new satellite config "; |
| } |
| else |
| { |
| resultString = "Updated existing satellite config "; |
| } |
| |
| BMCWEB_LOG_DEBUG << resultString << name << " at " |
| << result.first->second.scheme() << "://" |
| << result.first->second.encoded_host_and_port(); |
| } |
| |
| // Parse the properties of a RDE Device config object and add the |
| // configuration if the properties are valid |
| static void addRdeSatelliteConfig( |
| const dbus::utility::DBusPropertiesMap& properties, |
| std::unordered_map<std::string, RdeSatelliteConfig>& rdeSatelliteInfo) |
| { |
| RdeSatelliteConfig rdeConfig; |
| std::string name; |
| for (const auto& prop : properties) |
| { |
| if (prop.first == "Name") |
| { |
| const std::string* propVal = |
| std::get_if<std::string>(&prop.second); |
| if (propVal == nullptr) |
| { |
| BMCWEB_LOG_ERROR << "Invalid Name value"; |
| return; |
| } |
| rdeConfig.name = *propVal; |
| } |
| else if (prop.first == "VID") |
| { |
| const std::string* propVal = |
| std::get_if<std::string>(&prop.second); |
| if (propVal == nullptr) |
| { |
| BMCWEB_LOG_ERROR << "Invalid VID value"; |
| return; |
| } |
| rdeConfig.vid = *propVal; |
| } |
| else if (prop.first == "UDEVID") |
| { |
| const std::string* propVal = |
| std::get_if<std::string>(&prop.second); |
| if (propVal == nullptr) |
| { |
| BMCWEB_LOG_ERROR << "Invalid UDEVID value"; |
| return; |
| } |
| rdeConfig.udevid = *propVal; |
| } |
| else if (prop.first == "USBPORT") |
| { |
| const std::string* propVal = |
| std::get_if<std::string>(&prop.second); |
| if (propVal == nullptr) |
| { |
| BMCWEB_LOG_ERROR << "Invalid USBPORT value"; |
| return; |
| } |
| rdeConfig.usbport = *propVal; |
| } |
| } // Finished reading properties |
| |
| if (rdeConfig.udevid.empty()) |
| { |
| BMCWEB_LOG_ERROR << "Empty udevid"; |
| return; |
| } |
| rdeConfig.objectpath = |
| "/xyz/openbmc_project/rde_devices/" + rdeConfig.udevid; |
| |
| // Set the prefix to a random string 'E0SB8D' |
| // (TODO) Generate a unique random prefix for each RDE Device |
| if constexpr (bmcwebEnableRdeDevice) |
| { |
| if (!generatePrefix(rdeConfig.objectpath, name)) |
| { |
| BMCWEB_LOG_ERROR << " Failed to get a prefix for " |
| << rdeConfig.objectpath; |
| return; |
| } |
| BMCWEB_LOG_DEBUG << " Got prefix " << name; |
| } |
| std::string resultString; |
| auto result = rdeSatelliteInfo.emplace(name, std::move(rdeConfig)); |
| if (result.second) |
| { |
| resultString = "Added new RDE Device config "; |
| } |
| else |
| { |
| resultString = "Updated existing RDE Device config "; |
| } |
| } |
| |
| enum AggregationType : std::uint8_t |
| { |
| Collection, |
| ContainsSubordinate, |
| Resource, |
| }; |
| |
| static void |
| startAggregation(AggregationType aggType, const crow::Request& thisReq, |
| const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) |
| { |
| if (thisReq.method() != boost::beast::http::verb::get) |
| { |
| if (aggType == AggregationType::Collection) |
| { |
| BMCWEB_LOG_DEBUG |
| << "Only aggregate GET requests to top level collections"; |
| return; |
| } |
| |
| if (aggType == AggregationType::ContainsSubordinate) |
| { |
| BMCWEB_LOG_DEBUG << "Only aggregate GET requests when uptree of" |
| << " a top level collection"; |
| return; |
| } |
| } |
| |
| // Create a copy of thisReq so we we can still locally process the req |
| std::error_code ec; |
| auto localReq = std::make_shared<crow::Request>(thisReq.req, ec); |
| if (ec) |
| { |
| BMCWEB_LOG_ERROR << "Failed to create copy of request"; |
| if (aggType == AggregationType::Resource) |
| { |
| messages::internalError(asyncResp->res); |
| } |
| return; |
| } |
| |
| // Strip query params that don't work correctly if forwarded |
| parameterRemove(*localReq); |
| |
| getSatelliteConfigs( |
| std::bind_front(aggregateAndHandle, aggType, localReq, asyncResp)); |
| } |
| |
| static void findSatellite( |
| const crow::Request& req, |
| const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, |
| const std::unordered_map<std::string, boost::urls::url>& satelliteInfo, |
| const std::unordered_map<std::string, RdeSatelliteConfig>& |
| rdeSatelliteInfo, |
| std::string_view memberName) |
| { |
| bool validPrefix = false; |
| // Determine if the resource ID begins with a known prefix |
| for (const auto& satellite : satelliteInfo) |
| { |
| std::string targetPrefix = satellite.first; |
| targetPrefix += "_"; |
| if (memberName.starts_with(targetPrefix)) |
| { |
| BMCWEB_LOG_DEBUG << "\"" << satellite.first |
| << "\" is a known prefix"; |
| |
| // Remove the known prefix from the request's URI and |
| // then forward to the associated satellite BMC |
| getInstance().forwardRequest(req, asyncResp, satellite.first, |
| satelliteInfo); |
| validPrefix = true; |
| } |
| } |
| // Determine if the resource ID begins with a known prefix |
| for (const auto& rdeSatellite : rdeSatelliteInfo) |
| { |
| std::string targetPrefix = rdeSatellite.first; |
| targetPrefix += "_"; |
| if (memberName.starts_with(targetPrefix)) |
| { |
| BMCWEB_LOG_DEBUG << "\"" << rdeSatellite.first |
| << "\" is a known prefix"; |
| // Remove the known prefix from the request's URI and |
| // then forward to RDE Daemon |
| forwardRdeRequest(req, asyncResp, rdeSatellite.first, |
| rdeSatelliteInfo); |
| validPrefix = true; |
| } |
| } |
| if (validPrefix) |
| { |
| return; |
| } |
| // We didn't recognize the prefix and need to return a 404 |
| std::string nameStr = req.url().segments().back(); |
| messages::resourceNotFound(asyncResp->res, "", nameStr); |
| } |
| |
| // Intended to handle an incoming request based on if Redfish Aggregation |
| // is enabled. Forwards request to satellite BMC if it exists. |
| static void aggregateAndHandle( |
| AggregationType aggType, |
| const std::shared_ptr<crow::Request>& sharedReq, |
| const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, |
| const boost::system::error_code& ec, |
| const std::unordered_map<std::string, boost::urls::url>& satelliteInfo, |
| const std::unordered_map<std::string, RdeSatelliteConfig>& |
| rdeSatelliteInfo) |
| { |
| if (sharedReq == nullptr) |
| { |
| return; |
| } |
| // Something went wrong while querying dbus |
| if (ec) |
| { |
| messages::internalError(asyncResp->res); |
| return; |
| } |
| |
| // No satellite or RDE configs means we don't need to keep attempting to |
| // aggregate |
| if (satelliteInfo.empty() && rdeSatelliteInfo.empty()) |
| { |
| // For collections or resources that can contain a subordinate |
| // top level collection we'll also handle the request locally so we |
| // don't need to write an error code |
| if (aggType == AggregationType::Resource) |
| { |
| std::string nameStr = sharedReq->url().segments().back(); |
| messages::resourceNotFound(asyncResp->res, "", nameStr); |
| } |
| return; |
| } |
| |
| const crow::Request& thisReq = *sharedReq; |
| if constexpr (enablePlatform9) |
| { |
| BMCWEB_LOG_DEBUG << "Aggregation is enabled, begin processing of " |
| << thisReq.url(); |
| } |
| else |
| { |
| BMCWEB_LOG_DEBUG << "Aggregation is enabled, begin processing of " |
| << thisReq.target(); |
| } |
| |
| // We previously determined the request is for a collection. No need to |
| // check again |
| if (aggType == AggregationType::Collection) |
| { |
| BMCWEB_LOG_DEBUG << "Aggregating a collection"; |
| // We need to use a specific response handler and send the |
| // request to all known satellites |
| getInstance().forwardCollectionRequests( |
| thisReq, asyncResp, satelliteInfo, rdeSatelliteInfo); |
| return; |
| } |
| |
| // We previously determined the request may contain a subordinate |
| // collection. No need to check again |
| if (aggType == AggregationType::ContainsSubordinate) |
| { |
| BMCWEB_LOG_DEBUG |
| << "Aggregating what may have a subordinate collection"; |
| // We need to use a specific response handler and send the |
| // request to all known satellites |
| getInstance().forwardContainsSubordinateRequests(thisReq, asyncResp, |
| satelliteInfo); |
| return; |
| } |
| |
| const boost::urls::segments_view urlSegments = thisReq.url().segments(); |
| boost::urls::url currentUrl("/"); |
| boost::urls::segments_view::iterator it = urlSegments.begin(); |
| const boost::urls::segments_view::const_iterator end = |
| urlSegments.end(); |
| |
| // Skip past the leading "/redfish/v1" |
| it++; |
| it++; |
| for (; it != end; it++) |
| { |
| if (std::binary_search(topCollections.begin(), topCollections.end(), |
| currentUrl.buffer())) |
| { |
| // We've matched a resource collection so this current segment |
| // must contain an aggregation prefix |
| findSatellite(thisReq, asyncResp, satelliteInfo, |
| rdeSatelliteInfo, *it); |
| return; |
| } |
| |
| currentUrl.segments().push_back(*it); |
| } |
| |
| // We shouldn't reach this point since we should've hit one of the |
| // previous exits |
| messages::internalError(asyncResp->res); |
| } |
| |
| // Attempt to forward a request to the satellite BMC associated with the |
| // prefix. |
| void forwardRequest( |
| const crow::Request& thisReq, |
| const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, |
| const std::string& prefix, |
| const std::unordered_map<std::string, boost::urls::url>& satelliteInfo) |
| { |
| const auto& sat = satelliteInfo.find(prefix); |
| if (sat == satelliteInfo.end()) |
| { |
| // Realistically this shouldn't get called since we perform an |
| // earlier check to make sure the prefix exists |
| BMCWEB_LOG_ERROR << "Unrecognized satellite prefix \"" << prefix |
| << "\""; |
| return; |
| } |
| |
| // We need to strip the prefix from the request's path |
| std::string targetURI; |
| if constexpr (enablePlatform9) |
| { |
| targetURI = std::string(thisReq.url().path()); |
| } |
| else |
| { |
| targetURI = std::string(thisReq.target()); |
| } |
| |
| size_t pos = targetURI.find(prefix + "_"); |
| if (pos == std::string::npos) |
| { |
| // If this fails then something went wrong |
| BMCWEB_LOG_ERROR << "Error removing prefix \"" << prefix |
| << "_\" from request URI"; |
| messages::internalError(asyncResp->res); |
| return; |
| } |
| targetURI.erase(pos, prefix.size() + 1); |
| |
| std::function<void(crow::Response&)> cb = |
| std::bind_front(processResponse, prefix, asyncResp); |
| |
| std::string data = thisReq.req.body(); |
| client.sendDataWithCallback(data, std::string(sat->second.host()), |
| sat->second.port_number(), targetURI, |
| false /*useSSL*/, thisReq.fields(), |
| thisReq.method(), cb); |
| } |
| |
| // Forward a request for a collection URI to each known satellite BMC |
| void forwardCollectionRequests( |
| const crow::Request& thisReq, |
| const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, |
| const std::unordered_map<std::string, boost::urls::url>& satelliteInfo, |
| const std::unordered_map<std::string, RdeSatelliteConfig>& |
| rdeSatelliteInfo) |
| { |
| for (const auto& sat : satelliteInfo) |
| { |
| std::function<void(crow::Response&)> cb = std::bind_front( |
| processCollectionResponse, sat.first, asyncResp); |
| |
| std::string targetURI; |
| if constexpr (enablePlatform9) |
| { |
| targetURI = std::string(thisReq.url().path()); |
| } |
| else |
| { |
| targetURI = std::string(thisReq.target()); |
| } |
| std::string data = thisReq.req.body(); |
| client.sendDataWithCallback(data, std::string(sat.second.host()), |
| sat.second.port_number(), targetURI, |
| false /*useSSL*/, thisReq.fields(), |
| thisReq.method(), cb); |
| } |
| for (const auto& rsat : rdeSatelliteInfo) |
| { |
| |
| uint8_t operationId = 1; |
| std::string requestPayload; |
| BMCWEB_LOG_DEBUG << " Collection Request: dbus call to RDE Daemon " |
| << " operationId " << operationId; |
| std::string targetURI(thisReq.target()); |
| if (managedStore::RequestStatsStore::isRdeRateLimitEnabled() && |
| managedStore::RequestStatsStore::instance() |
| .getCurrentRdeRequestSkips() > 0) |
| { |
| BMCWEB_LOG_DEBUG << " Skipping the request " << targetURI; |
| asyncResp->requestStatsContext->updateRdeSkips(); |
| managedStore::RequestStatsStore::instance() |
| .updateCurrentRdeRequestSkips(); |
| return; |
| } |
| BMCWEB_LOG_DEBUG << "Collections objpath " << rsat.second.objectpath |
| << " targetURI " << targetURI << " udevid " |
| << rsat.second.udevid; |
| asyncResp->res.startTimetrace(rsat.first); |
| if (managedStore::RequestStatsStore::isRdeRateLimitEnabled()) |
| { |
| asyncResp->requestStatsContext->updateRdeReqs(); |
| } |
| managedStore::GetManagedObjectStore()->PostDbusCallToIoContextThreadSafe( |
| asyncResp->strand_, |
| [&operationId, rsat, |
| asyncResp](const boost::system::error_code ec, |
| const std::string& jsonString) { |
| if (ec) |
| { |
| BMCWEB_LOG_ERROR << "DBUS response error operationId " |
| << operationId << " , " << ec.value() |
| << ", " << ec.message(); |
| if (managedStore::RequestStatsStore:: |
| isRdeRateLimitEnabled()) |
| { |
| asyncResp->requestStatsContext->updateRdeDbusErrors(); |
| managedStore::RequestStatsStore::instance() |
| .updateCurrentRdeDbusErrors(); |
| if (managedStore::RequestStatsStore::instance() |
| .getCurrentRdeDbusErrors() > |
| managedStore::RequestStatsStore:: |
| rdeErrorThreshold() && |
| managedStore::RequestStatsStore::instance() |
| .getCurrentRdeRequestSkips() <= 0) |
| { |
| |
| BMCWEB_LOG_ERROR << "RDE Rate Limit enabled"; |
| asyncResp->requestStatsContext |
| ->updateRdeRateLimitToggles(); |
| managedStore::RequestStatsStore::instance() |
| .setCurrentRdeRequestSkips( |
| managedStore::RequestStatsStore:: |
| rdeMaxSkips()); |
| } |
| } |
| return; |
| } |
| if (managedStore::RequestStatsStore::isRdeRateLimitEnabled()) |
| { |
| managedStore::RequestStatsStore::instance() |
| .setCurrentRdeDbusErrors(0); |
| managedStore::RequestStatsStore::instance() |
| .setCurrentRdeRequestSkips(0); |
| } |
| processRdeCollectionResponse(rsat.first, asyncResp, jsonString); |
| }, |
| "xyz.openbmc_project.rdeoperation", rsat.second.objectpath, |
| "xyz.openbmc_project.RdeDevice", "execute_rde", |
| generateRandomInt(), operationId, targetURI, rsat.second.udevid, |
| requestPayload); |
| } |
| } |
| |
| // Forward request for a URI that is uptree of a top level collection to |
| // each known satellite BMC |
| void forwardContainsSubordinateRequests( |
| const crow::Request& thisReq, |
| const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, |
| const std::unordered_map<std::string, boost::urls::url>& satelliteInfo) |
| { |
| for (const auto& sat : satelliteInfo) |
| { |
| std::function<void(crow::Response&)> cb = std::bind_front( |
| processContainsSubordinateResponse, sat.first, asyncResp); |
| |
| std::string targetURI(thisReq.target()); |
| std::string data = thisReq.req.body(); |
| client.sendDataWithCallback(data, std::string(sat.second.host()), |
| sat.second.port_number(), targetURI, |
| false /*useSSL*/, thisReq.fields(), |
| thisReq.method(), cb); |
| } |
| } |
| |
| // Attempt to forward a request to the RDE Daemon associated with the |
| // prefix. |
| static void forwardRdeRequest( |
| const crow::Request& thisReq, |
| const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, |
| const std::string& prefix, |
| const std::unordered_map<std::string, RdeSatelliteConfig>& |
| rdeSatelliteInfo) |
| { |
| const auto& sat = rdeSatelliteInfo.find(prefix); |
| if (sat == rdeSatelliteInfo.end()) |
| { |
| // Realistically this shouldn't get called since we perform an |
| // earlier check to make sure the prefix exists |
| BMCWEB_LOG_ERROR << "Unrecognized RDE Device prefix \"" << prefix |
| << "\""; |
| return; |
| } |
| |
| // We need to strip the prefix from the request's path |
| std::string targetURI(thisReq.target()); |
| if (managedStore::RequestStatsStore::isRdeRateLimitEnabled() && |
| managedStore::RequestStatsStore::instance() |
| .getCurrentRdeRequestSkips() > 0) |
| { |
| BMCWEB_LOG_DEBUG << " Skipping the request " << targetURI; |
| asyncResp->requestStatsContext->updateRdeSkips(); |
| managedStore::RequestStatsStore::instance() |
| .updateCurrentRdeRequestSkips(); |
| return; |
| } |
| size_t pos = targetURI.find(prefix + "_"); |
| if (pos == std::string::npos) |
| { |
| // If this fails then something went wrong |
| BMCWEB_LOG_ERROR << "Error removing prefix \"" << prefix |
| << "_\" from request URI"; |
| messages::internalError(asyncResp->res); |
| return; |
| } |
| targetURI.erase(pos, prefix.size() + 1); |
| |
| uint8_t operationId = 1; |
| std::string requestPayload; |
| boost::beast::http::verb method = thisReq.method(); |
| if (thisReq.method() == boost::beast::http::verb::post) |
| { |
| operationId = 8; |
| requestPayload = thisReq.body(); |
| } |
| else if (thisReq.method() == boost::beast::http::verb::patch) |
| { |
| operationId = 4; |
| requestPayload = thisReq.body(); |
| } |
| BMCWEB_LOG_DEBUG |
| << " Resource Request: dbus call to RDE Daemon operationId " |
| << operationId; |
| BMCWEB_LOG_DEBUG << "objpath " << sat->second.objectpath |
| << " targetURI " << targetURI << " udevid " |
| << sat->second.udevid << " method " << method; |
| asyncResp->res.startTimetrace(prefix); |
| if (managedStore::RequestStatsStore::isRdeRateLimitEnabled()) |
| { |
| asyncResp->requestStatsContext->updateRdeReqs(); |
| } |
| managedStore::GetManagedObjectStore()->PostDbusCallToIoContextThreadSafe( |
| asyncResp->strand_, |
| [&operationId, method, prefix, |
| asyncResp](const boost::system::error_code ec, |
| const std::string& jsonString) { |
| if (ec) |
| { |
| BMCWEB_LOG_ERROR << "DBUS response error operationId " |
| << operationId << " , " << ec.value() << ", " |
| << ec.message(); |
| if (managedStore::RequestStatsStore::isRdeRateLimitEnabled()) |
| |
| { |
| asyncResp->requestStatsContext->updateRdeDbusErrors(); |
| managedStore::RequestStatsStore::instance() |
| .updateCurrentRdeDbusErrors(); |
| |
| if (managedStore::RequestStatsStore::instance() |
| .getCurrentRdeDbusErrors() > |
| managedStore::RequestStatsStore:: |
| rdeErrorThreshold() && |
| managedStore::RequestStatsStore::instance() |
| .getCurrentRdeRequestSkips() <= 0) |
| { |
| BMCWEB_LOG_ERROR << "RDE Rate limiting enabled"; |
| asyncResp->requestStatsContext |
| ->updateRdeRateLimitToggles(); |
| managedStore::RequestStatsStore::instance() |
| .setCurrentRdeRequestSkips( |
| managedStore::RequestStatsStore::rdeMaxSkips()); |
| } |
| } |
| return; |
| } |
| if (managedStore::RequestStatsStore::isRdeRateLimitEnabled()) |
| { |
| managedStore::RequestStatsStore::instance() |
| .setCurrentRdeDbusErrors(0); |
| managedStore::RequestStatsStore::instance() |
| .setCurrentRdeRequestSkips(0); |
| } |
| processRdeResponse(prefix, method, asyncResp, jsonString); |
| }, |
| "xyz.openbmc_project.rdeoperation", sat->second.objectpath, |
| "xyz.openbmc_project.RdeDevice", "execute_rde", generateRandomInt(), |
| operationId, targetURI, sat->second.udevid, requestPayload); |
| } |
| // Processes the response returned by a RDE Device and loads its |
| // contents into asyncResp |
| static void |
| processRdeResponse(std::string_view prefix, |
| boost::beast::http::verb method, |
| const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, |
| const std::string& respString) |
| { |
| |
| nlohmann::json jsonVal = |
| nlohmann::json::parse(respString, nullptr, false); |
| if (jsonVal.is_discarded()) |
| { |
| BMCWEB_LOG_ERROR << "Error parsing RDE Device response as JSON " |
| << respString; |
| messages::operationFailed(asyncResp->res); |
| return; |
| } |
| |
| BMCWEB_LOG_DEBUG << "Successfully parsed RDE Device response"; |
| |
| if (method == boost::beast::http::verb::post) |
| { |
| if (jsonVal["Status"] == "Completed") |
| { |
| asyncResp->res.result(200); |
| } |
| else |
| { |
| messages::operationFailed(asyncResp->res); |
| } |
| return; |
| } |
| // TODO: For collections we want to add the satellite responses to |
| // our response rather than just straight overwriting them if our |
| // local handling was successful (i.e. would return a 200). |
| addPrefixes(jsonVal, prefix); |
| |
| if constexpr (bmcwebEnableRdeDevice) |
| { |
| if (jsonVal.contains( |
| "/Location/PartLocation/ServiceLabel"_json_pointer)) |
| { |
| BMCWEB_LOG_DEBUG << " @odata.type " << jsonVal["@odata.type"]; |
| if (jsonVal["Model"] == platform6Chassis1 || |
| jsonVal["Model"] == platform6Chassis2) |
| { |
| BMCWEB_LOG_DEBUG << "Setting up service label " |
| << rdeServiceLabelMap[std::string{prefix}] |
| << " for prefix " << prefix; |
| jsonVal["Location"]["PartLocation"]["ServiceLabel"] = |
| rdeServiceLabelMap[std::string{prefix}]; |
| } |
| } |
| auto it = jsonVal.find("Id"); |
| if (it != jsonVal.end() && it->is_string()) |
| { |
| std::string* id_ptr = it->get_ptr<std::string*>(); |
| if (id_ptr != nullptr) |
| { |
| id_ptr->insert(0, std::string(prefix) + "_"); |
| } |
| } |
| } |
| |
| asyncResp->res.result(200); |
| asyncResp->res.jsonValue = std::move(jsonVal); |
| asyncResp->res.endTimetrace(std::string(prefix)); |
| |
| BMCWEB_LOG_DEBUG << "Finished writing asyncResp"; |
| } |
| |
| // Processes the collection response returned by a RDE Device and merges |
| // its "@odata.id" values |
| static void processRdeCollectionResponse( |
| const std::string& prefix, |
| const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, |
| const std::string& respString) |
| { |
| nlohmann::json jsonVal = |
| nlohmann::json::parse(respString, nullptr, false); |
| if (jsonVal.is_discarded()) |
| { |
| BMCWEB_LOG_ERROR << "Error parsing RDEd response as JSON " |
| << respString; |
| |
| // Notify the user if doing so won't overwrite a valid response |
| if ((asyncResp->res.resultInt() != 200) && |
| (asyncResp->res.resultInt() != 502)) |
| { |
| messages::operationFailed(asyncResp->res); |
| } |
| return; |
| } |
| |
| BMCWEB_LOG_DEBUG << "Successfully parsed RDEd response"; |
| |
| // Now we need to add the prefix to the URIs contained in the |
| // response. |
| addPrefixes(jsonVal, prefix); |
| |
| BMCWEB_LOG_DEBUG << "Added prefix to parsed RDE Device response"; |
| |
| // If this resource collection does not exist on the aggregating bmc |
| // and has not already been added from processing the response from |
| // a different satellite then we need to completely overwrite |
| // asyncResp |
| if (asyncResp->res.resultInt() != 200) |
| { |
| // We only want to aggregate collections that contain a |
| // "Members" array |
| if ((!jsonVal.contains("Members")) && |
| (!jsonVal["Members"].is_array())) |
| { |
| BMCWEB_LOG_DEBUG << "Skipping aggregating unsupported resource"; |
| return; |
| } |
| |
| BMCWEB_LOG_DEBUG |
| << "Collection does not exist, overwriting asyncResp"; |
| asyncResp->res.jsonValue = std::move(jsonVal); |
| |
| BMCWEB_LOG_DEBUG << "Finished overwriting asyncResp"; |
| } |
| else |
| { |
| // We only want to aggregate collections that contain a |
| // "Members" array |
| if ((!asyncResp->res.jsonValue.contains("Members")) && |
| (!asyncResp->res.jsonValue["Members"].is_array())) |
| |
| { |
| BMCWEB_LOG_DEBUG << "Skipping aggregating unsupported resource"; |
| return; |
| } |
| |
| BMCWEB_LOG_DEBUG << "Adding aggregated resources from \"" << prefix |
| << "\" to collection"; |
| |
| // TODO: This is a potential race condition with multiple |
| // satellites and the aggregating bmc attempting to write to |
| // update this array. May need to cascade calls to the next |
| // satellite at the end of this function. |
| auto& members = asyncResp->res.jsonValue["Members"]; |
| auto& satMembers = jsonVal["Members"]; |
| for (auto& satMem : satMembers) |
| { |
| members.push_back(std::move(satMem)); |
| } |
| asyncResp->res.jsonValue["Members@odata.count"] = members.size(); |
| |
| // TODO: Do we need to sort() after updating the array? |
| } |
| asyncResp->res.endTimetrace(prefix); |
| } // End processRdeCollectionResponse() |
| |
| public: |
| RedfishAggregator(const RedfishAggregator&) = delete; |
| RedfishAggregator& operator=(const RedfishAggregator&) = delete; |
| RedfishAggregator(RedfishAggregator&&) = delete; |
| RedfishAggregator& operator=(RedfishAggregator&&) = delete; |
| ~RedfishAggregator() = default; |
| |
| static RedfishAggregator& getInstance() |
| { |
| static RedfishAggregator handler; |
| return handler; |
| } |
| |
| // Polls D-Bus to get all available satellite config information |
| // Expects a handler which interacts with the returned configs |
| static void getSatelliteConfigs( |
| std::function< |
| void(const boost::system::error_code&, |
| const std::unordered_map<std::string, boost::urls::url>&, |
| const std::unordered_map<std::string, RdeSatelliteConfig>&)> |
| handler) |
| { |
| BMCWEB_LOG_DEBUG << "Gathering satellite configs"; |
| |
| managedStore::ManagedObjectStoreContext context(nullptr); |
| managedStore::GetManagedObjectStore()->getManagedObjectsWithContext( |
| "xyz.openbmc_project.EntityManager", |
| {"/xyz/openbmc_project/inventory"}, |
| context, |
| [handler{std::move(handler)}]( |
| const boost::system::error_code& ec, |
| const dbus::utility::ManagedObjectType& objects) { |
| std::unordered_map<std::string, boost::urls::url> satelliteInfo; |
| std::unordered_map<std::string, RdeSatelliteConfig> |
| rdeSatelliteInfo; |
| if (ec) |
| { |
| BMCWEB_LOG_ERROR << "DBUS response error " << ec.value() << ", " |
| << ec.message(); |
| handler(ec, satelliteInfo, rdeSatelliteInfo); |
| return; |
| } |
| |
| // Maps a chosen alias representing a satellite BMC to a url |
| // containing the information required to create a http |
| // connection to the satellite |
| findSatelliteConfigs(objects, satelliteInfo, rdeSatelliteInfo); |
| |
| if (!satelliteInfo.empty()) |
| { |
| BMCWEB_LOG_DEBUG << "Redfish Aggregation enabled with " |
| << std::to_string(satelliteInfo.size()) |
| << " satellite BMCs"; |
| } |
| else if (!rdeSatelliteInfo.empty()) |
| { |
| BMCWEB_LOG_DEBUG << "Redfish Aggregation enabled with " |
| << std::to_string(rdeSatelliteInfo.size()) |
| << " RDE Device"; |
| } |
| else |
| { |
| BMCWEB_LOG_DEBUG |
| << "No satellite BMCs detected. Redfish Aggregation not enabled"; |
| } |
| handler(ec, satelliteInfo, rdeSatelliteInfo); |
| }); |
| } |
| |
| // Processes the response returned by a satellite BMC and loads its |
| // contents into asyncResp |
| static void |
| processResponse(std::string_view prefix, |
| const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, |
| crow::Response& resp) |
| { |
| // 429 and 502 mean we didn't actually send the request so don't |
| // overwrite the response headers in that case |
| if ((resp.result() == boost::beast::http::status::too_many_requests) || |
| (resp.result() == boost::beast::http::status::bad_gateway)) |
| { |
| asyncResp->res.result(resp.result()); |
| return; |
| } |
| |
| // We want to attempt prefix fixing regardless of response code |
| // The resp will not have a json component |
| // We need to create a json from resp's stringResponse |
| std::string_view contentType = resp.getHeaderValue("Content-Type"); |
| if (boost::iequals(contentType, "application/json") || |
| boost::iequals(contentType, "application/json; charset=utf-8")) |
| { |
| nlohmann::json jsonVal = |
| nlohmann::json::parse(resp.body(), nullptr, false); |
| if (jsonVal.is_discarded()) |
| { |
| BMCWEB_LOG_ERROR << "Error parsing satellite response as JSON"; |
| messages::operationFailed(asyncResp->res); |
| return; |
| } |
| |
| BMCWEB_LOG_DEBUG << "Successfully parsed satellite response"; |
| |
| addPrefixes(jsonVal, prefix); |
| |
| if constexpr (enablePlatform9) |
| { |
| // Hardcode relation to aggregating BMC |
| if ((jsonVal.contains("@odata.id")) && |
| (jsonVal["@odata.id"].is_string())) |
| { |
| std::string targetURI(jsonVal["@odata.id"]); |
| std::string aggRootURI(prefix); |
| aggRootURI = "/redfish/v1/Chassis/" + aggRootURI + "_" + |
| std::string(platform9Chassis5); |
| if (targetURI == aggRootURI) |
| { |
| jsonVal["Links"]["ContainedBy"] = { |
| {"@odata.id", "/redfish/v1/Chassis/" + |
| std::string(platform9Chassis4)}}; |
| } |
| } |
| // End hardcoded workaround |
| } |
| |
| BMCWEB_LOG_DEBUG << "Added prefix to parsed satellite response"; |
| |
| asyncResp->res.result(resp.result()); |
| asyncResp->res.jsonValue = std::move(jsonVal); |
| |
| BMCWEB_LOG_DEBUG << "Finished writing asyncResp"; |
| } |
| else |
| { |
| // We allow any Content-Type that is not "application/json" now |
| asyncResp->res.result(resp.result()); |
| asyncResp->res.write(resp.body()); |
| } |
| addAggregatedHeaders(asyncResp->res, resp, prefix); |
| } |
| |
| // Processes the collection response returned by a satellite BMC and merges |
| // its "@odata.id" values |
| static void processCollectionResponse( |
| const std::string& prefix, |
| const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, |
| crow::Response& resp) |
| { |
| // 429 and 502 mean we didn't actually send the request so don't |
| // overwrite the response headers in that case |
| if ((resp.result() == boost::beast::http::status::too_many_requests) || |
| (resp.result() == boost::beast::http::status::bad_gateway)) |
| { |
| return; |
| } |
| |
| if (resp.resultInt() != 200) |
| { |
| BMCWEB_LOG_DEBUG |
| << "Collection resource does not exist in satellite BMC \"" |
| << prefix << "\""; |
| // Return the error if we haven't had any successes |
| if (asyncResp->res.resultInt() != 200) |
| { |
| asyncResp->res.result(resp.result()); |
| asyncResp->res.write(resp.body()); |
| } |
| return; |
| } |
| |
| // The resp will not have a json component |
| // We need to create a json from resp's stringResponse |
| std::string_view contentType = resp.getHeaderValue("Content-Type"); |
| if (boost::iequals(contentType, "application/json") || |
| boost::iequals(contentType, "application/json; charset=utf-8")) |
| { |
| nlohmann::json jsonVal = |
| nlohmann::json::parse(resp.body(), nullptr, false); |
| if (jsonVal.is_discarded()) |
| { |
| BMCWEB_LOG_ERROR << "Error parsing satellite response as JSON"; |
| |
| // Notify the user if doing so won't overwrite a valid response |
| if (asyncResp->res.resultInt() != 200) |
| { |
| messages::operationFailed(asyncResp->res); |
| } |
| return; |
| } |
| |
| BMCWEB_LOG_DEBUG << "Successfully parsed satellite response"; |
| |
| // Now we need to add the prefix to the URIs contained in the |
| // response. |
| addPrefixes(jsonVal, prefix); |
| |
| BMCWEB_LOG_DEBUG << "Added prefix to parsed satellite response"; |
| |
| // If this resource collection does not exist on the aggregating bmc |
| // and has not already been added from processing the response from |
| // a different satellite then we need to completely overwrite |
| // asyncResp |
| if (asyncResp->res.resultInt() != 200) |
| { |
| BMCWEB_LOG_DEBUG |
| << "Collection does not exist, overwriting asyncResp"; |
| asyncResp->res.result(resp.result()); |
| asyncResp->res.jsonValue = std::move(jsonVal); |
| asyncResp->res.addHeader("Content-Type", "application/json"); |
| |
| BMCWEB_LOG_DEBUG << "Finished overwriting asyncResp"; |
| } |
| else |
| { |
| BMCWEB_LOG_DEBUG << "Adding aggregated resources from \"" |
| << prefix << "\" to collection"; |
| |
| // TODO: This is a potential race condition with multiple |
| // satellites and the aggregating bmc attempting to write to |
| // update this array. May need to cascade calls to the next |
| // satellite at the end of this function. |
| // This is presumably not a concern when there is only a single |
| // satellite since the aggregating bmc should have completed |
| // before the response is received from the satellite. |
| |
| auto& members = asyncResp->res.jsonValue["Members"]; |
| if (!members.is_array()) |
| { |
| // Satellite response is being processed before top level |
| // collection request has been locally handled so we need |
| // to create Members array |
| BMCWEB_LOG_DEBUG << "Created new Members array"; |
| members = nlohmann::json::array(); |
| } |
| auto& satMembers = jsonVal["Members"]; |
| for (auto& satMem : satMembers) |
| { |
| members.push_back(std::move(satMem)); |
| } |
| asyncResp->res.jsonValue["Members@odata.count"] = |
| members.size(); |
| |
| // TODO: Do we need to sort() after updating the array? |
| } |
| } |
| else |
| { |
| BMCWEB_LOG_ERROR << "Received unparsable response from \"" << prefix |
| << "\""; |
| // We received a response that was not a json. |
| // Notify the user only if we did not receive any valid responses |
| // and if the resource collection does not already exist on the |
| // aggregating BMC |
| if (asyncResp->res.resultInt() != 200) |
| { |
| messages::operationFailed(asyncResp->res); |
| } |
| } |
| } // End processCollectionResponse() |
| |
| // Processes the response returned by a satellite BMC and merges any |
| // properties whose "@odata.id" value is the URI or either a top level |
| // collection or is uptree from a top level collection |
| static void processContainsSubordinateResponse( |
| const std::string& prefix, |
| const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, |
| crow::Response& resp) |
| { |
| // 429 and 502 mean we didn't actually send the request so don't |
| // overwrite the response headers in that case |
| if ((resp.result() == boost::beast::http::status::too_many_requests) || |
| (resp.result() == boost::beast::http::status::bad_gateway)) |
| { |
| return; |
| } |
| |
| if (resp.resultInt() != 200) |
| { |
| BMCWEB_LOG_DEBUG |
| << "Resource uptree from Collection does not exist in " |
| << "satellite BMC \"" << prefix << "\""; |
| // Return the error if we haven't had any successes |
| if (asyncResp->res.resultInt() != 200) |
| { |
| asyncResp->res.result(resp.result()); |
| asyncResp->res.write(resp.body()); |
| } |
| return; |
| } |
| |
| // The resp will not have a json component |
| // We need to create a json from resp's stringResponse |
| std::string_view contentType = resp.getHeaderValue("Content-Type"); |
| if (boost::iequals(contentType, "application/json") || |
| boost::iequals(contentType, "application/json; charset=utf-8")) |
| { |
| bool addedLinks = false; |
| nlohmann::json jsonVal = |
| nlohmann::json::parse(resp.body(), nullptr, false); |
| if (jsonVal.is_discarded()) |
| { |
| BMCWEB_LOG_ERROR << "Error parsing satellite response as JSON"; |
| |
| // Notify the user if doing so won't overwrite a valid response |
| if (asyncResp->res.resultInt() != 200) |
| { |
| messages::operationFailed(asyncResp->res); |
| } |
| return; |
| } |
| |
| BMCWEB_LOG_DEBUG << "Successfully parsed satellite response"; |
| |
| // Parse response and add properties missing from the AsyncResp |
| // Valid properties will be of the form <property>.@odata.id and |
| // @odata.id is a <URI>. In other words, the json should contain |
| // multiple properties such that |
| // {"<property>":{"@odata.id": "<URI>"}} |
| nlohmann::json::object_t* object = |
| jsonVal.get_ptr<nlohmann::json::object_t*>(); |
| if (object == nullptr) |
| { |
| BMCWEB_LOG_ERROR << "Parsed JSON was not an object?"; |
| return; |
| } |
| |
| for (std::pair<const std::string, nlohmann::json>& prop : *object) |
| { |
| if (!prop.second.contains("@odata.id")) |
| { |
| continue; |
| } |
| |
| std::string* strValue = |
| prop.second["@odata.id"].get_ptr<std::string*>(); |
| if (strValue == nullptr) |
| { |
| BMCWEB_LOG_CRITICAL << "Field wasn't a string????"; |
| continue; |
| } |
| if (!searchCollectionsArray(*strValue, SearchType::CollOrCon)) |
| { |
| continue; |
| } |
| |
| addedLinks = true; |
| if (!asyncResp->res.jsonValue.contains(prop.first)) |
| { |
| // Only add the property if it did not already exist |
| BMCWEB_LOG_DEBUG << "Adding link for " << *strValue |
| << " from BMC " << prefix; |
| asyncResp->res.jsonValue[prop.first]["@odata.id"] = |
| *strValue; |
| continue; |
| } |
| } |
| |
| // If we added links to a previously unsuccessful (non-200) response |
| // then we need to make sure the response contains the bare minimum |
| // amount of additional information that we'd expect to have been |
| // populated. |
| if (addedLinks && (asyncResp->res.resultInt() != 200)) |
| { |
| // This resource didn't locally exist or an error |
| // occurred while generating the response. Remove any |
| // error messages and update the error code. |
| asyncResp->res.jsonValue.erase( |
| asyncResp->res.jsonValue.find("error")); |
| asyncResp->res.result(resp.result()); |
| |
| const auto& it1 = object->find("@odata.id"); |
| if (it1 != object->end()) |
| { |
| asyncResp->res.jsonValue["@odata.id"] = (it1->second); |
| } |
| const auto& it2 = object->find("@odata.type"); |
| if (it2 != object->end()) |
| { |
| asyncResp->res.jsonValue["@odata.type"] = (it2->second); |
| } |
| const auto& it3 = object->find("Id"); |
| if (it3 != object->end()) |
| { |
| asyncResp->res.jsonValue["Id"] = (it3->second); |
| } |
| const auto& it4 = object->find("Name"); |
| if (it4 != object->end()) |
| { |
| asyncResp->res.jsonValue["Name"] = (it4->second); |
| } |
| } |
| } |
| else |
| { |
| BMCWEB_LOG_ERROR << "Received unparsable response from \"" << prefix |
| << "\""; |
| // We received as response that was not a json |
| // Notify the user only if we did not receive any valid responses, |
| // and if the resource does not already exist on the aggregating BMC |
| if (asyncResp->res.resultInt() != 200) |
| { |
| messages::operationFailed(asyncResp->res); |
| } |
| } |
| } |
| |
| // Entry point to Redfish Aggregation |
| // Returns Result stating whether or not we still need to locally handle the |
| // request |
| static Result |
| beginAggregation(const crow::Request& thisReq, |
| const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) |
| { |
| using crow::utility::OrMorePaths; |
| using crow::utility::readUrlSegments; |
| const boost::urls::url_view url = thisReq.url(); |
| |
| // We don't need to aggregate JsonSchemas due to potential issues such |
| // as version mismatches between aggregator and satellite BMCs. For |
| // now assume that the aggregator has all the schemas and versions that |
| // the aggregated server has. |
| if (crow::utility::readUrlSegments(url, "redfish", "v1", "JsonSchemas", |
| crow::utility::OrMorePaths())) |
| { |
| return Result::LocalHandle; |
| } |
| |
| // The first two segments should be "/redfish/v1". We need to check |
| // that before we can search topCollections |
| if (!crow::utility::readUrlSegments(url, "redfish", "v1", |
| crow::utility::OrMorePaths())) |
| { |
| return Result::LocalHandle; |
| } |
| |
| // Parse the URI to see if it begins with a known top level collection |
| // such as: |
| // /redfish/v1/Chassis |
| // /redfish/v1/UpdateService/FirmwareInventory |
| const boost::urls::segments_view urlSegments = url.segments(); |
| boost::urls::url currentUrl("/"); |
| boost::urls::segments_view::iterator it = urlSegments.begin(); |
| const boost::urls::segments_view::const_iterator end = |
| urlSegments.end(); |
| |
| // Skip past the leading "/redfish/v1" |
| it++; |
| it++; |
| for (; it != end; it++) |
| { |
| const std::string& collectionItem = *it; |
| if (std::binary_search(topCollections.begin(), topCollections.end(), |
| currentUrl.buffer())) |
| { |
| // We've matched a resource collection so this current segment |
| // might contain an aggregation prefix |
| // TODO: This needs to be rethought when we can support multiple |
| // satellites due to |
| // /redfish/v1/AggregationService/AggregationSources/5B247A |
| // being a local resource describing the satellite |
| if (collectionItem.starts_with("5B247A_") || |
| isMemberStartsWithKnownPrefix(collectionItem)) |
| { |
| BMCWEB_LOG_DEBUG << "Need to forward a request"; |
| |
| // Extract the prefix from the request's URI, retrieve the |
| // associated satellite config information, and then forward |
| // the request to that satellite. |
| startAggregation(AggregationType::Resource, thisReq, |
| asyncResp); |
| return Result::NoLocalHandle; |
| } |
| |
| // Handle collection URI with a trailing backslash |
| // e.g. /redfish/v1/Chassis/ |
| it++; |
| if ((it == end) && collectionItem.empty()) |
| { |
| startAggregation(AggregationType::Collection, thisReq, |
| asyncResp); |
| } |
| |
| // We didn't recognize the prefix or it's a collection with a |
| // trailing "/". In both cases we still want to locally handle |
| // the request |
| return Result::LocalHandle; |
| } |
| |
| currentUrl.segments().push_back(collectionItem); |
| } |
| |
| // If we made it here then currentUrl could contain a top level |
| // collection URI without a trailing "/", e.g. /redfish/v1/Chassis |
| if (std::binary_search(topCollections.begin(), topCollections.end(), |
| currentUrl.buffer())) |
| { |
| startAggregation(AggregationType::Collection, thisReq, asyncResp); |
| return Result::LocalHandle; |
| } |
| |
| // If nothing else then the request could be for a resource which has a |
| // top level collection as a subordinate |
| if (searchCollectionsArray(url.buffer(), |
| SearchType::ContainsSubordinate)) |
| { |
| startAggregation(AggregationType::ContainsSubordinate, thisReq, |
| asyncResp); |
| return Result::LocalHandle; |
| } |
| |
| BMCWEB_LOG_DEBUG << "Aggregation not required"; |
| return Result::LocalHandle; |
| } |
| }; |
| |
| } // namespace redfish |