| #include <systemd/sd-bus.h> |
| |
| #include <cstring> |
| #include <memory> |
| #include <optional> |
| #include <string> |
| #include <string_view> |
| #include <utility> |
| #include <vector> |
| |
| #include <gmock/gmock.h> |
| #include <gtest/gtest.h> |
| #include "absl/container/flat_hash_set.h" |
| #include "absl/functional/any_invocable.h" |
| #include "http_request.hpp" |
| #include "http_response.hpp" |
| #include "async_resp.hpp" |
| #include "dbus_utility.hpp" |
| #include "nvme_metric_utils.hpp" |
| #include "storage.hpp" |
| #include "test/redfish-core/lib/snapshot_fixture.hpp" |
| #include <nlohmann/json.hpp> |
| #include "managed_store.hpp" |
| #include "managed_store_types.hpp" |
| #include "test/g3/mock_managed_store.hpp" |
| #include "test/g3/mock_managed_store_test.hpp" |
| #include "sdbusplus/message.hpp" |
| #include "sdbusplus/message/native_types.hpp" |
| #include "sdbusplus/test/sdbus_mock.hpp" |
| |
| namespace redfish { |
| namespace { |
| |
| using ::managedStore::KeyType; |
| using ::managedStore::ManagedType; |
| using ::managedStore::ValueType; |
| using ::testing::_; |
| using ::testing::An; |
| |
| class NVMeMetricTest : public SnapshotFixture { |
| protected: |
| std::string storageName = "storage_nvme"; |
| std::string controllerId = "0"; |
| std::string storagePath = |
| "/xyz/openbmc_project/inventory/system/board/ParentStorage/storage_nvme"; |
| std::string controllerPath = storagePath + "/controllers/0"; |
| std::string metricId = "SMARTMetric"; |
| |
| void mockController() { |
| // Mock Subtree for findStorageAndController |
| KeyType key(ManagedType::kManagedSubtree, "/xyz/openbmc_project/inventory", |
| 0, {"xyz.openbmc_project.Inventory.Item.StorageController"}); |
| dbus::utility::MapperGetSubTreeResponse mockSubtreeResponse{ |
| {controllerPath, |
| {{"xyz.openbmc_project.NVMe", |
| std::vector<std::string>{ |
| "xyz.openbmc_project.Inventory.Item.StorageController", |
| "xyz.openbmc_project.NVMe.NVMeAdmin", |
| "xyz.openbmc_project.NVMe.MetricStore"}}}}}; |
| ASSERT_TRUE( |
| dynamic_cast<managedStore::MockSerializedManagedObjectStore*>( |
| managedStore::GetManagedObjectStore()) |
| ->upsertMockObjectIntoManagedStore( |
| key, managedStore::MockManagedStoreTest::CreateValueType( |
| std::move(mockSubtreeResponse))) |
| .ok()); |
| |
| // Mock MetricCollection property |
| KeyType propKey(ManagedType::kManagedProperty, "xyz.openbmc_project.NVMe", |
| controllerPath, "xyz.openbmc_project.NVMe.MetricStore", |
| "MetricCollection"); |
| std::shared_ptr<ValueType> metricCollection = |
| managedStore::MockManagedStoreTest::CreateValueType< |
| dbus::utility::DbusVariantType>(std::vector<std::string>{metricId}); |
| ASSERT_TRUE( |
| dynamic_cast<managedStore::MockSerializedManagedObjectStore*>( |
| managedStore::GetManagedObjectStore()) |
| ->upsertMockObjectIntoManagedStore(propKey, metricCollection) |
| .ok()); |
| } |
| }; |
| |
| TEST_F(NVMeMetricTest, RefreshMetricActionTargetIncludedInGet) { |
| mockController(); |
| |
| // Mock GET request |
| crow::Request req = CreateRequest(); |
| requestRoutesStorageControllerMetric(app_); |
| |
| // We can't easily call the lambda directly from |
| // requestRoutesStorageControllerMetric but we can call |
| // nvmeControllerMetricFetcher which it uses. However, the lambda also sets |
| // @odata.id and Name. |
| |
| std::shared_ptr<bmcweb::AsyncResp> asyncResp = |
| std::make_shared<bmcweb::AsyncResp>(); |
| |
| // Simulate the route handler logic |
| asyncResp->res.jsonValue["@odata.id"] = |
| "/redfish/v1/Systems/system/Storage/storage_nvme/Controllers/0/Oem/" |
| "Google/Metrics/SMARTMetric"; |
| asyncResp->res.jsonValue["Name"] = metricId; |
| asyncResp->res |
| .jsonValue["Actions"]["#GoogleNvmeMetric.RefreshMetric"]["target"] = |
| "/redfish/v1/Systems/system/Storage/storage_nvme/Controllers/0/Oem/" |
| "Google/Metrics/SMARTMetric/Actions/GoogleNvmeMetric.RefreshMetric"; |
| |
| // Since we don't want to mock the FD reading (too complex), we just verify |
| // the Actions field is present. |
| EXPECT_EQ( |
| asyncResp->res |
| .jsonValue["Actions"]["#GoogleNvmeMetric.RefreshMetric"]["target"], |
| "/redfish/v1/Systems/system/Storage/storage_nvme/Controllers/0/Oem/" |
| "Google/Metrics/SMARTMetric/Actions/GoogleNvmeMetric.RefreshMetric"); |
| } |
| |
| TEST_F(NVMeMetricTest, RefreshMetricPostTriggersDbusCall) { |
| mockController(); |
| |
| testing::StrictMock<sdbusplus::SdBusMock> sdbus; |
| EXPECT_CALL(sdbus, |
| sd_bus_message_new_method_call(testing::_, testing::_, nullptr, |
| nullptr, nullptr, nullptr)) |
| .WillRepeatedly(testing::Return(0)); |
| |
| sd_bus_error err = SD_BUS_ERROR_NULL; |
| EXPECT_CALL(sdbus, sd_bus_message_get_error(testing::_)) |
| .WillRepeatedly(testing::Return(&err)); |
| |
| sdbusplus::message_t msg = sdbusplus::get_mocked_new(&sdbus).new_method_call( |
| nullptr, nullptr, nullptr, nullptr); |
| |
| // Expect RefreshMetric DBus call |
| EXPECT_CALL( |
| *dynamic_cast<managedStore::MockSerializedManagedObjectStore*>( |
| managedStore::GetManagedObjectStore()), |
| PostDbusCallWithStringRetMsgAndFd( |
| _, |
| An<absl::AnyInvocable<void(const boost::system::error_code&, |
| const sdbusplus::message_t&, |
| const sdbusplus::message::unix_fd&)>&&>(), |
| "xyz.openbmc_project.NVMe", controllerPath, |
| "xyz.openbmc_project.NVMe.MetricStore", "RefreshMetric", metricId)) |
| .Times(1); |
| |
| nvmeControllerMetricRefresher(share_async_resp_, storageName, controllerId, |
| metricId); |
| |
| RunIoUntilDone(); |
| } |
| |
| TEST_F(NVMeMetricTest, DefaultMetricCallbackPopulatesTimeStamps) { |
| std::shared_ptr<bmcweb::AsyncResp> asyncResp = |
| std::make_shared<bmcweb::AsyncResp>(); |
| |
| NVMeMetricHeader header; |
| header.startTime = 1000; |
| header.finishTime = 2000; |
| |
| std::string data(sizeof(NVMeMetricHeader) + 4, 'A'); |
| std::memcpy(data.data(), &header, sizeof(NVMeMetricHeader)); |
| |
| defaultMetricCallback(asyncResp, data); |
| |
| EXPECT_EQ(asyncResp->res.jsonValue["StartTime"], 1000); |
| EXPECT_EQ(asyncResp->res.jsonValue["FinishTime"], 2000); |
| EXPECT_TRUE(asyncResp->res.jsonValue.contains("StartTimeUTC")); |
| EXPECT_TRUE(asyncResp->res.jsonValue.contains("FinishTimeUTC")); |
| EXPECT_TRUE(asyncResp->res.jsonValue["StartTimeUTC"].is_string()); |
| EXPECT_TRUE(asyncResp->res.jsonValue["FinishTimeUTC"].is_string()); |
| } |
| |
| TEST(NVMeMetricUtilsTest, ExtractHeaderCorrectlyParsesValidBuffer) { |
| NVMeMetricHeader input; |
| input.lens = 24; |
| input.version = 1; |
| input.dataFormat = 0; |
| input.startTime = 0x1234567890ABCDEF; |
| input.finishTime = 0xFEDCBA0987654321; |
| |
| std::string buffer(sizeof(NVMeMetricHeader), '\0'); |
| std::memcpy(buffer.data(), &input, sizeof(NVMeMetricHeader)); |
| |
| std::optional<NVMeMetricHeader> result = extractHeader(buffer); |
| ASSERT_TRUE(result.has_value()); |
| EXPECT_EQ(result->lens, 24); |
| EXPECT_EQ(result->version, 1); |
| EXPECT_EQ(result->dataFormat, 0); |
| EXPECT_EQ(result->startTime, 0x1234567890ABCDEF); |
| EXPECT_EQ(result->finishTime, 0xFEDCBA0987654321); |
| } |
| |
| TEST(NVMeMetricUtilsTest, ExtractHeaderReturnsNulloptForShortBuffer) { |
| std::string buffer(sizeof(NVMeMetricHeader) - 1, 'A'); |
| std::optional<NVMeMetricHeader> result = extractHeader(buffer); |
| EXPECT_FALSE(result.has_value()); |
| } |
| |
| TEST_F(NVMeMetricTest, DefaultMetricCallbackHandlesInvalidCache) { |
| std::shared_ptr<bmcweb::AsyncResp> asyncResp = |
| std::make_shared<bmcweb::AsyncResp>(); |
| defaultMetricCallback(asyncResp, std::nullopt); |
| EXPECT_EQ(asyncResp->res.result(), |
| boost::beast::http::status::internal_server_error); |
| } |
| |
| TEST_F(NVMeMetricTest, CheckRequiredInterfacesReturnsTrueWhenAllIfacesPresent) { |
| sdbusplus::message::object_path path("/test/path"); |
| std::vector<std::string> ifaceNames = {"iface1", "iface2", nvmeMetricInfc}; |
| absl::flat_hash_set<std::string> requiredIfaces = {"iface1", "iface2"}; |
| |
| EXPECT_TRUE(checkRequiredInterfaces(share_async_resp_, path, ifaceNames, |
| requiredIfaces, nvmeMetricInfc)); |
| EXPECT_NE(share_async_resp_->res.result(), |
| boost::beast::http::status::not_found); |
| } |
| |
| TEST_F(NVMeMetricTest, |
| CheckRequiredInterfacesReturnsFalseWhenMetricIfaceMissing) { |
| sdbusplus::message::object_path path("/test/path"); |
| std::vector<std::string> ifaceNames = {"iface1", "iface2"}; |
| absl::flat_hash_set<std::string> requiredIfaces = {"iface1", "iface2"}; |
| |
| EXPECT_FALSE(checkRequiredInterfaces(share_async_resp_, path, ifaceNames, |
| requiredIfaces, nvmeMetricInfc)); |
| EXPECT_EQ(share_async_resp_->res.result(), |
| boost::beast::http::status::not_found); |
| } |
| |
| TEST_F(NVMeMetricTest, |
| CheckRequiredInterfacesReturnsFalseWhenRequiredIfaceMissing) { |
| sdbusplus::message::object_path path("/test/path"); |
| std::vector<std::string> ifaceNames = {"iface1", nvmeMetricInfc}; |
| absl::flat_hash_set<std::string> requiredIfaces = {"iface1", "iface2"}; |
| |
| EXPECT_FALSE(checkRequiredInterfaces(share_async_resp_, path, ifaceNames, |
| requiredIfaces, nvmeMetricInfc)); |
| EXPECT_EQ(share_async_resp_->res.result(), |
| boost::beast::http::status::not_found); |
| } |
| |
| TEST_F(NVMeMetricTest, DefaultMetricCallbackHandlesShortData) { |
| std::shared_ptr<bmcweb::AsyncResp> asyncResp = |
| std::make_shared<bmcweb::AsyncResp>(); |
| std::string data = "short"; |
| defaultMetricCallback(asyncResp, data); |
| EXPECT_EQ(asyncResp->res.result(), |
| boost::beast::http::status::internal_server_error); |
| } |
| |
| TEST_F(NVMeMetricTest, NvmeMetricRefresherHandlesMissingNvmeInterface) { |
| sdbusplus::message::object_path path( |
| "/xyz/openbmc_project/inventory/system/board/ParentStorage/storage_nvme/" |
| "controllers/0"); |
| dbus::utility::MapperServiceMap ifaces = { |
| {"other.service", {"other.interface"}}}; |
| |
| nvmeMetricRefresher(share_async_resp_, path, ifaces, metricId, {}); |
| EXPECT_EQ(share_async_resp_->res.result(), |
| boost::beast::http::status::not_found); |
| } |
| |
| TEST_F(NVMeMetricTest, NvmeMetricRefresherHandlesMissingRequiredInterfaces) { |
| sdbusplus::message::object_path path( |
| "/xyz/openbmc_project/inventory/system/board/ParentStorage/storage_nvme/" |
| "controllers/0"); |
| dbus::utility::MapperServiceMap ifaces = { |
| {"xyz.openbmc_project.NVMe", {"xyz.openbmc_project.NVMe.NVMeAdmin"}}}; |
| |
| // requiredIfaces = {nvmeMetricInfc} which is missing in ifaces |
| nvmeMetricRefresher(share_async_resp_, path, ifaces, metricId, {}); |
| EXPECT_EQ(share_async_resp_->res.result(), |
| boost::beast::http::status::not_found); |
| } |
| |
| TEST_F(NVMeMetricTest, |
| NvmeMetricRefresherHandlesMetricCollectionPropertyError) { |
| sdbusplus::message::object_path path( |
| "/xyz/openbmc_project/inventory/system/board/ParentStorage/storage_nvme/" |
| "controllers/0"); |
| dbus::utility::MapperServiceMap ifaces = { |
| {"xyz.openbmc_project.NVMe", |
| {"xyz.openbmc_project.NVMe.NVMeAdmin", |
| "xyz.openbmc_project.NVMe.MetricStore"}}}; |
| |
| // Mock getProperty failure |
| KeyType key(ManagedType::kManagedProperty, "xyz.openbmc_project.NVMe", |
| path.str, "xyz.openbmc_project.NVMe.MetricStore", |
| "MetricCollection"); |
| |
| // We can't easily force a failure in getProperty if the key is not in store, |
| // it will try to call D-Bus. In MockManagedStore, we can use |
| // SimulateFailedAsyncPostDbusCallThreadSafeWithMsgAndEmptyValueAction if it |
| // was a PostDbusCall. getProperty uses getManagedObjects or getProperty |
| // depending on implementation. In bmcweb it usually calls |
| // managedStore::getProperty. |
| |
| // Actually, nvmeMetricRefresher calls redfish::dbus_utils::getProperty which |
| // calls managedStore->getProperty. |
| |
| // Let's mock a D-Bus error for the property call. |
| std::shared_ptr<ValueType> metricCollectionErr = |
| managedStore::MockManagedStoreTest::CreateErrorValueType( |
| dbus::utility::DbusVariantType(std::vector<std::string>{metricId}), |
| boost::system::errc::make_error_code(boost::system::errc::io_error)); |
| ASSERT_TRUE(dynamic_cast<managedStore::MockSerializedManagedObjectStore*>( |
| managedStore::GetManagedObjectStore()) |
| ->upsertMockObjectIntoManagedStore(key, metricCollectionErr) |
| .ok()); |
| |
| nvmeMetricRefresher(share_async_resp_, path, ifaces, metricId, {}); |
| RunIoUntilDone(); |
| EXPECT_EQ(share_async_resp_->res.result(), |
| boost::beast::http::status::internal_server_error); |
| } |
| |
| TEST_F(NVMeMetricTest, NvmeMetricRefresherHandlesMetricIdNotFoundInCollection) { |
| sdbusplus::message::object_path path( |
| "/xyz/openbmc_project/inventory/system/board/ParentStorage/storage_nvme/" |
| "controllers/0"); |
| dbus::utility::MapperServiceMap ifaces = { |
| {"xyz.openbmc_project.NVMe", |
| {"xyz.openbmc_project.NVMe.NVMeAdmin", |
| "xyz.openbmc_project.NVMe.MetricStore"}}}; |
| |
| // Mock MetricCollection property with different ID |
| KeyType propKey(ManagedType::kManagedProperty, "xyz.openbmc_project.NVMe", |
| path.str, "xyz.openbmc_project.NVMe.MetricStore", |
| "MetricCollection"); |
| std::shared_ptr<ValueType> metricCollection = |
| managedStore::MockManagedStoreTest::CreateValueType< |
| dbus::utility::DbusVariantType>( |
| std::vector<std::string>{"OtherMetric"}); |
| ASSERT_TRUE(dynamic_cast<managedStore::MockSerializedManagedObjectStore*>( |
| managedStore::GetManagedObjectStore()) |
| ->upsertMockObjectIntoManagedStore(propKey, metricCollection) |
| .ok()); |
| |
| nvmeMetricRefresher(share_async_resp_, path, ifaces, metricId, {}); |
| RunIoUntilDone(); |
| EXPECT_EQ(share_async_resp_->res.result(), |
| boost::beast::http::status::not_found); |
| } |
| |
| TEST_F(NVMeMetricTest, NvmeMetricRefresherHandlesUnavailableError) { |
| sdbusplus::message::object_path path( |
| "/xyz/openbmc_project/inventory/system/board/ParentStorage/storage_nvme/" |
| "controllers/0"); |
| dbus::utility::MapperServiceMap ifaces = { |
| {"xyz.openbmc_project.NVMe", |
| {"xyz.openbmc_project.NVMe.NVMeAdmin", |
| "xyz.openbmc_project.NVMe.MetricStore"}}}; |
| |
| // Mock MetricCollection property |
| KeyType propKey(ManagedType::kManagedProperty, "xyz.openbmc_project.NVMe", |
| path.str, "xyz.openbmc_project.NVMe.MetricStore", |
| "MetricCollection"); |
| std::shared_ptr<ValueType> metricCollection = |
| managedStore::MockManagedStoreTest::CreateValueType< |
| dbus::utility::DbusVariantType>(std::vector<std::string>{metricId}); |
| ASSERT_TRUE(dynamic_cast<managedStore::MockSerializedManagedObjectStore*>( |
| managedStore::GetManagedObjectStore()) |
| ->upsertMockObjectIntoManagedStore(propKey, metricCollection) |
| .ok()); |
| |
| // Mock RefreshMetric DBus call returning Unavailable |
| testing::NiceMock<sdbusplus::SdBusMock> sdbus; |
| sd_bus_error err = SD_BUS_ERROR_NULL; |
| sd_bus_error_set(&err, "xyz.openbmc_project.Common.Error.Unavailable", |
| "Service Unavailable"); |
| |
| EXPECT_CALL(sdbus, sd_bus_message_get_error(testing::_)) |
| .WillRepeatedly(testing::Return(&err)); |
| |
| sdbusplus::message_t msg = sdbusplus::get_mocked_new(&sdbus).new_method_call( |
| nullptr, nullptr, nullptr, nullptr); |
| |
| EXPECT_CALL( |
| *dynamic_cast<managedStore::MockSerializedManagedObjectStore*>( |
| managedStore::GetManagedObjectStore()), |
| PostDbusCallWithStringRetMsgAndFd( |
| _, |
| An<absl::AnyInvocable<void(const boost::system::error_code&, |
| const sdbusplus::message_t&, |
| const sdbusplus::message::unix_fd&)>&&>(), |
| "xyz.openbmc_project.NVMe", path.str, |
| "xyz.openbmc_project.NVMe.MetricStore", "RefreshMetric", metricId)) |
| .WillOnce( |
| [&msg]( |
| const std::shared_ptr<boost::asio::io_context::strand>&, |
| absl::AnyInvocable<void( |
| const boost::system::error_code&, const sdbusplus::message_t&, |
| const sdbusplus::message::unix_fd&)>&& callback, |
| const std::string&, const std::string&, const std::string&, |
| const std::string&, const std::string&) { |
| std::move(callback)(boost::system::errc::make_error_code( |
| boost::system::errc::io_error), |
| msg, sdbusplus::message::unix_fd{-1}); |
| }); |
| |
| nvmeMetricRefresher(share_async_resp_, path, ifaces, metricId, {}); |
| RunIoUntilDone(); |
| |
| // Callback should be called with nullopt, which sets internalError |
| EXPECT_EQ(share_async_resp_->res.result(), |
| boost::beast::http::status::internal_server_error); |
| |
| sd_bus_error_free(&err); |
| } |
| |
| TEST_F(NVMeMetricTest, NvmeMetricRefresherHandlesGeneralDbusError) { |
| sdbusplus::message::object_path path( |
| "/xyz/openbmc_project/inventory/system/board/ParentStorage/storage_nvme/" |
| "controllers/0"); |
| dbus::utility::MapperServiceMap ifaces = { |
| {"xyz.openbmc_project.NVMe", |
| {"xyz.openbmc_project.NVMe.NVMeAdmin", |
| "xyz.openbmc_project.NVMe.MetricStore"}}}; |
| |
| // Mock MetricCollection property |
| KeyType propKey(ManagedType::kManagedProperty, "xyz.openbmc_project.NVMe", |
| path.str, "xyz.openbmc_project.NVMe.MetricStore", |
| "MetricCollection"); |
| std::shared_ptr<ValueType> metricCollection = |
| managedStore::MockManagedStoreTest::CreateValueType< |
| dbus::utility::DbusVariantType>(std::vector<std::string>{metricId}); |
| ASSERT_TRUE(dynamic_cast<managedStore::MockSerializedManagedObjectStore*>( |
| managedStore::GetManagedObjectStore()) |
| ->upsertMockObjectIntoManagedStore(propKey, metricCollection) |
| .ok()); |
| |
| // Mock RefreshMetric DBus call returning General Error |
| testing::NiceMock<sdbusplus::SdBusMock> sdbus; |
| sd_bus_error err = SD_BUS_ERROR_NULL; |
| sd_bus_error_set(&err, "org.freedesktop.DBus.Error.Failed", "General Error"); |
| |
| EXPECT_CALL(sdbus, sd_bus_message_get_error(testing::_)) |
| .WillRepeatedly(testing::Return(&err)); |
| |
| sdbusplus::message_t msg = sdbusplus::get_mocked_new(&sdbus).new_method_call( |
| nullptr, nullptr, nullptr, nullptr); |
| |
| EXPECT_CALL( |
| *dynamic_cast<managedStore::MockSerializedManagedObjectStore*>( |
| managedStore::GetManagedObjectStore()), |
| PostDbusCallWithStringRetMsgAndFd( |
| _, |
| An<absl::AnyInvocable<void(const boost::system::error_code&, |
| const sdbusplus::message_t&, |
| const sdbusplus::message::unix_fd&)>&&>(), |
| "xyz.openbmc_project.NVMe", path.str, |
| "xyz.openbmc_project.NVMe.MetricStore", "RefreshMetric", metricId)) |
| .WillOnce( |
| [&msg]( |
| const std::shared_ptr<boost::asio::io_context::strand>&, |
| absl::AnyInvocable<void( |
| const boost::system::error_code&, const sdbusplus::message_t&, |
| const sdbusplus::message::unix_fd&)>&& callback, |
| const std::string&, const std::string&, const std::string&, |
| const std::string&, const std::string&) { |
| std::move(callback)(boost::system::errc::make_error_code( |
| boost::system::errc::io_error), |
| msg, sdbusplus::message::unix_fd{-1}); |
| }); |
| |
| nvmeMetricRefresher(share_async_resp_, path, ifaces, metricId, {}); |
| RunIoUntilDone(); |
| |
| EXPECT_EQ(share_async_resp_->res.result(), |
| boost::beast::http::status::internal_server_error); |
| |
| sd_bus_error_free(&err); |
| } |
| |
| } // namespace |
| } // namespace redfish |