summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCarson Labrado <clabrado@google.com>2023-02-14 02:54:59 +0300
committerEd Tanous <ed@tanous.net>2023-05-02 20:30:16 +0300
commit46b302837c3c44376da3ba6b72fb70b7b2ce1c11 (patch)
treea399a734e93f0b93cccc9a3777042a24f9965b7e
parentdf7f12f0d4111a4646d5cad261473894676df132 (diff)
downloadbmcweb-46b302837c3c44376da3ba6b72fb70b7b2ce1c11.tar.xz
Aggregation: Process subordinate top collections
Adds a function to process responses from URIs that are uptree from a top level collection. A follow-up patch will hook this into the aggregation code to allow adding links to top level collections which are only supported by satellite BMCs. Adds test cases to validate this function is working correctly. Tested: New test cases pass Signed-off-by: Carson Labrado <clabrado@google.com> Change-Id: I7f0fd6c3955398e2fde136c1d3b37a6bf4bf06b9
-rw-r--r--redfish-core/include/redfish_aggregator.hpp178
-rw-r--r--test/redfish-core/include/redfish_aggregator_test.cpp157
2 files changed, 320 insertions, 15 deletions
diff --git a/redfish-core/include/redfish_aggregator.hpp b/redfish-core/include/redfish_aggregator.hpp
index 245c698233..9168cc5ec5 100644
--- a/redfish-core/include/redfish_aggregator.hpp
+++ b/redfish-core/include/redfish_aggregator.hpp
@@ -801,7 +801,8 @@ class RedfishAggregator
{
// 429 and 502 mean we didn't actually send the request so don't
// overwrite the response headers in that case
- if ((resp.resultInt() == 429) || (resp.resultInt() == 502))
+ if ((resp.result() == boost::beast::http::status::too_many_requests) ||
+ (resp.result() == boost::beast::http::status::bad_gateway))
{
asyncResp->res.result(resp.result());
return;
@@ -810,7 +811,9 @@ class RedfishAggregator
// 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
- if (resp.getHeaderValue("Content-Type") == "application/json")
+ 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);
@@ -850,7 +853,8 @@ class RedfishAggregator
{
// 429 and 502 mean we didn't actually send the request so don't
// overwrite the response headers in that case
- if ((resp.resultInt() == 429) || (resp.resultInt() == 502))
+ if ((resp.result() == boost::beast::http::status::too_many_requests) ||
+ (resp.result() == boost::beast::http::status::bad_gateway))
{
return;
}
@@ -863,14 +867,17 @@ class RedfishAggregator
// Return the error if we haven't had any successes
if (asyncResp->res.resultInt() != 200)
{
- asyncResp->res.stringResponse = std::move(resp.stringResponse);
+ 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
- if (resp.getHeaderValue("Content-Type") == "application/json")
+ 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);
@@ -879,9 +886,7 @@ class RedfishAggregator
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) &&
- (asyncResp->res.resultInt() != 429) &&
- (asyncResp->res.resultInt() != 502))
+ if (asyncResp->res.resultInt() != 200)
{
messages::operationFailed(asyncResp->res);
}
@@ -961,19 +966,162 @@ class RedfishAggregator
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,
- // if the resource collection does not already exist on the
- // aggregating BMC, and if we did not already set this warning due
- // to a failure from a different satellite
- if ((asyncResp->res.resultInt() != 200) &&
- (asyncResp->res.resultInt() != 429) &&
- (asyncResp->res.resultInt() != 502))
+ // 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;
+ }
+
+ BMCWEB_LOG_DEBUG << "Adding link for " << *strValue
+ << " from BMC " << prefix;
+ addedLinks = true;
+ if (!asyncResp->res.jsonValue.contains(prop.first))
+ {
+ // Only add the property if it did not already exist
+ 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
diff --git a/test/redfish-core/include/redfish_aggregator_test.cpp b/test/redfish-core/include/redfish_aggregator_test.cpp
index da3209ab9b..2d19cee705 100644
--- a/test/redfish-core/include/redfish_aggregator_test.cpp
+++ b/test/redfish-core/include/redfish_aggregator_test.cpp
@@ -647,5 +647,162 @@ TEST(searchCollectionsArray, collectionOrContainsURIs)
EXPECT_FALSE(isCollOrCon("/redfish/v1/UpdateService/SoftwareInventory2"));
}
+TEST(processContainsSubordinateResponse, addLinks)
+{
+ crow::Response resp;
+ resp.result(200);
+ nlohmann::json jsonValue;
+ resp.addHeader("Content-Type", "application/json");
+ jsonValue["@odata.id"] = "/redfish/v1";
+ jsonValue["Fabrics"]["@odata.id"] = "/redfish/v1/Fabrics";
+ jsonValue["Test"]["@odata.id"] = "/redfish/v1/Test";
+ jsonValue["TelemetryService"]["@odata.id"] = "/redfish/v1/TelemetryService";
+ jsonValue["UpdateService"]["@odata.id"] = "/redfish/v1/UpdateService";
+ resp.body() =
+ jsonValue.dump(2, ' ', true, nlohmann::json::error_handler_t::replace);
+
+ auto asyncResp = std::make_shared<bmcweb::AsyncResp>();
+ asyncResp->res.result(200);
+ asyncResp->res.jsonValue["@odata.id"] = "/redfish/v1";
+ asyncResp->res.jsonValue["Chassis"]["@odata.id"] = "/redfish/v1/Chassis";
+
+ RedfishAggregator::processContainsSubordinateResponse("prefix", asyncResp,
+ resp);
+ EXPECT_EQ(asyncResp->res.jsonValue["Chassis"]["@odata.id"],
+ "/redfish/v1/Chassis");
+ EXPECT_EQ(asyncResp->res.jsonValue["Fabrics"]["@odata.id"],
+ "/redfish/v1/Fabrics");
+ EXPECT_EQ(asyncResp->res.jsonValue["TelemetryService"]["@odata.id"],
+ "/redfish/v1/TelemetryService");
+ EXPECT_EQ(asyncResp->res.jsonValue["UpdateService"]["@odata.id"],
+ "/redfish/v1/UpdateService");
+ EXPECT_FALSE(asyncResp->res.jsonValue.contains("Test"));
+}
+
+TEST(processContainsSubordinateResponse, localNotOK)
+{
+ auto asyncResp = std::make_shared<bmcweb::AsyncResp>();
+ asyncResp->res.addHeader("Content-Type", "application/json");
+ messages::resourceNotFound(asyncResp->res, "", "");
+
+ // This field was added by resourceNotFound()
+ // Sanity test to make sure it gets removed later
+ EXPECT_TRUE(asyncResp->res.jsonValue.contains("error"));
+
+ crow::Response resp;
+ resp.result(200);
+ nlohmann::json jsonValue;
+ resp.addHeader("Content-Type", "application/json");
+ jsonValue["@odata.id"] = "/redfish/v1";
+ jsonValue["@odata.type"] = "#ServiceRoot.v1_11_0.ServiceRoot";
+ jsonValue["Id"] = "RootService";
+ jsonValue["Name"] = "Root Service";
+ jsonValue["Fabrics"]["@odata.id"] = "/redfish/v1/Fabrics";
+ jsonValue["Test"]["@odata.id"] = "/redfish/v1/Test";
+ jsonValue["TelemetryService"]["@odata.id"] = "/redfish/v1/TelemetryService";
+ jsonValue["UpdateService"]["@odata.id"] = "/redfish/v1/UpdateService";
+ resp.body() =
+ jsonValue.dump(2, ' ', true, nlohmann::json::error_handler_t::replace);
+
+ RedfishAggregator::processContainsSubordinateResponse("prefix", asyncResp,
+ resp);
+
+ // Most of the response should get copied over since asyncResp is a 404
+ EXPECT_EQ(asyncResp->res.resultInt(), 200);
+ EXPECT_EQ(asyncResp->res.jsonValue["@odata.id"], "/redfish/v1");
+ EXPECT_EQ(asyncResp->res.jsonValue["@odata.type"],
+ "#ServiceRoot.v1_11_0.ServiceRoot");
+ EXPECT_EQ(asyncResp->res.jsonValue["Id"], "RootService");
+ EXPECT_EQ(asyncResp->res.jsonValue["Name"], "Root Service");
+
+ EXPECT_EQ(asyncResp->res.jsonValue["Fabrics"]["@odata.id"],
+ "/redfish/v1/Fabrics");
+ EXPECT_EQ(asyncResp->res.jsonValue["TelemetryService"]["@odata.id"],
+ "/redfish/v1/TelemetryService");
+ EXPECT_EQ(asyncResp->res.jsonValue["UpdateService"]["@odata.id"],
+ "/redfish/v1/UpdateService");
+ EXPECT_FALSE(asyncResp->res.jsonValue.contains("Test"));
+ EXPECT_FALSE(asyncResp->res.jsonValue.contains("error"));
+
+ // Test for local response being partially populated before throwing error
+ asyncResp = std::make_shared<bmcweb::AsyncResp>();
+ asyncResp->res.addHeader("Content-Type", "application/json");
+ asyncResp->res.jsonValue["Chassis"]["@odata.id"] = "/redfish/v1/Chassis";
+ asyncResp->res.jsonValue["Fake"]["@odata.id"] = "/redfish/v1/Fake";
+ messages::internalError(asyncResp->res);
+
+ RedfishAggregator::processContainsSubordinateResponse("prefix", asyncResp,
+ resp);
+
+ // These should also be copied over since asyncResp is a 500
+ EXPECT_EQ(asyncResp->res.resultInt(), 200);
+ EXPECT_EQ(asyncResp->res.jsonValue["@odata.id"], "/redfish/v1");
+ EXPECT_EQ(asyncResp->res.jsonValue["@odata.type"],
+ "#ServiceRoot.v1_11_0.ServiceRoot");
+ EXPECT_EQ(asyncResp->res.jsonValue["Id"], "RootService");
+ EXPECT_EQ(asyncResp->res.jsonValue["Name"], "Root Service");
+
+ EXPECT_EQ(asyncResp->res.jsonValue["Fabrics"]["@odata.id"],
+ "/redfish/v1/Fabrics");
+ EXPECT_EQ(asyncResp->res.jsonValue["TelemetryService"]["@odata.id"],
+ "/redfish/v1/TelemetryService");
+ EXPECT_EQ(asyncResp->res.jsonValue["UpdateService"]["@odata.id"],
+ "/redfish/v1/UpdateService");
+ EXPECT_FALSE(asyncResp->res.jsonValue.contains("Test"));
+ EXPECT_FALSE(asyncResp->res.jsonValue.contains("error"));
+
+ // These fields should still be present
+ EXPECT_EQ(asyncResp->res.jsonValue["Chassis"]["@odata.id"],
+ "/redfish/v1/Chassis");
+ EXPECT_EQ(asyncResp->res.jsonValue["Fake"]["@odata.id"],
+ "/redfish/v1/Fake");
+}
+
+TEST(processContainsSubordinateResponse, noValidLinks)
+{
+ auto asyncResp = std::make_shared<bmcweb::AsyncResp>();
+ asyncResp->res.result(500);
+ asyncResp->res.jsonValue["Chassis"]["@odata.id"] = "/redfish/v1/Chassis";
+
+ crow::Response resp;
+ resp.result(200);
+ nlohmann::json jsonValue;
+ resp.addHeader("Content-Type", "application/json");
+ jsonValue["@odata.id"] = "/redfish/v1";
+ resp.body() =
+ jsonValue.dump(2, ' ', true, nlohmann::json::error_handler_t::replace);
+
+ RedfishAggregator::processContainsSubordinateResponse("prefix", asyncResp,
+ resp);
+
+ // We won't add any links from response so asyncResp shouldn't change
+ EXPECT_EQ(asyncResp->res.resultInt(), 500);
+ EXPECT_EQ(asyncResp->res.jsonValue["Chassis"]["@odata.id"],
+ "/redfish/v1/Chassis");
+ EXPECT_FALSE(asyncResp->res.jsonValue.contains("@odata.id"));
+
+ // Sat response is non-500 so it shouldn't get copied over
+ asyncResp->res.result(200);
+ resp.result(500);
+ jsonValue["Fabrics"]["@odata.id"] = "/redfish/v1/Fabrics";
+ jsonValue["Test"]["@odata.id"] = "/redfish/v1/Test";
+ jsonValue["TelemetryService"]["@odata.id"] = "/redfish/v1/TelemetryService";
+ jsonValue["UpdateService"]["@odata.id"] = "/redfish/v1/UpdateService";
+ resp.body() =
+ jsonValue.dump(2, ' ', true, nlohmann::json::error_handler_t::replace);
+
+ RedfishAggregator::processContainsSubordinateResponse("prefix", asyncResp,
+ resp);
+
+ EXPECT_EQ(asyncResp->res.resultInt(), 200);
+ EXPECT_EQ(asyncResp->res.jsonValue["Chassis"]["@odata.id"],
+ "/redfish/v1/Chassis");
+ EXPECT_FALSE(asyncResp->res.jsonValue.contains("@odata.id"));
+ EXPECT_FALSE(asyncResp->res.jsonValue.contains("Fabrics"));
+ EXPECT_FALSE(asyncResp->res.jsonValue.contains("Test"));
+ EXPECT_FALSE(asyncResp->res.jsonValue.contains("TelemetryService"));
+ EXPECT_FALSE(asyncResp->res.jsonValue.contains("UpdateService"));
+}
+
} // namespace
} // namespace redfish