| #ifndef THIRD_PARTY_GBMCWEB_REDFISH_CORE_INCLUDE_UTILS_NVME_METRIC_UTILS_H_ |
| #define THIRD_PARTY_GBMCWEB_REDFISH_CORE_INCLUDE_UTILS_NVME_METRIC_UTILS_H_ |
| |
| #include <systemd/sd-bus.h> |
| |
| #include <algorithm> |
| #include <array> |
| #include <cstdint> |
| #include <cstdlib> |
| #include <ctime> |
| #include <format> // NOLINT |
| #include <functional> |
| #include <iterator> |
| #include <memory> |
| #include <optional> |
| #include <string> |
| #include <string_view> |
| #include <unordered_set> |
| #include <utility> |
| #include <vector> |
| |
| #include "absl/container/flat_hash_set.h" |
| #include "absl/time/clock.h" |
| #include "absl/time/time.h" |
| #include "boost/asio/posix/stream_descriptor.hpp" // NOLINT |
| #include "boost/endian.hpp" // NOLINT |
| #include "boost/system/error_code.hpp" // NOLINT |
| #include "boost/url/url.hpp" // NOLINT |
| #include "logging.hpp" |
| #include "utility.hpp" |
| #include "async_resp.hpp" |
| #include "dbus_utility.hpp" |
| #include "error_messages.hpp" |
| #include "storage_utils.hpp" |
| #include "time_utils.hpp" |
| #include "managed_store.hpp" |
| #include "managed_store_types.hpp" |
| #include "sdbusplus/message.hpp" |
| #include "sdbusplus/message/native_types.hpp" |
| |
| namespace redfish { |
| |
| using MetricStoreCb = |
| std::function<void(const std::shared_ptr<bmcweb::AsyncResp>& resp, |
| const std::optional<std::string>& data)>; |
| // Define the header struct |
| |
| struct NVMeMetricHeader { |
| boost::endian::little_uint32_t lens = 0; // Header length |
| boost::endian::little_uint16_t version = 0; // Header version |
| uint8_t dataFormat = 0; // the data format. 0x00 = Raw NVMe format |
| uint8_t reserved = 0; |
| boost::endian::little_uint64_t startTime = |
| 0; // time stamp when the Metric started the retrival from the |
| // device |
| boost::endian::little_uint64_t finishTime = |
| 0; // time stamp when the Metric finished the retrival from |
| // the device |
| }; |
| |
| inline std::optional<NVMeMetricHeader> extractHeader( |
| const std::string& buffer) { |
| if (buffer.size() < sizeof(NVMeMetricHeader)) { |
| return std::nullopt; |
| } |
| |
| NVMeMetricHeader header; |
| std::string::const_iterator it = buffer.begin(); |
| |
| std::copy(it, it + sizeof(header.lens), |
| reinterpret_cast<char*>(&header.lens)); // NOLINT |
| std::advance(it, sizeof(header.lens)); |
| |
| std::copy(it, it + sizeof(header.version), |
| reinterpret_cast<char*>(&header.version)); // NOLINT |
| std::advance(it, sizeof(header.version)); |
| |
| std::copy(it, it + sizeof(header.dataFormat), |
| reinterpret_cast<char*>(&header.dataFormat)); // NOLINT |
| std::advance(it, sizeof(header.dataFormat)); |
| |
| std::copy(it, it + sizeof(header.reserved), |
| reinterpret_cast<char*>(&header.reserved)); // NOLINT |
| std::advance(it, sizeof(header.reserved)); |
| |
| std::copy(it, it + sizeof(header.startTime), |
| reinterpret_cast<char*>(&header.startTime)); // NOLINT |
| std::advance(it, sizeof(header.startTime)); |
| |
| std::copy(it, it + sizeof(header.finishTime), |
| reinterpret_cast<char*>(&header.finishTime)); // NOLINT |
| std::advance(it, sizeof(header.finishTime)); |
| |
| return header; |
| } |
| |
| /** |
| * @brief Verifies that a D-Bus object implements a required set of interfaces. |
| * |
| * This function is used as a prerequisite check before making D-Bus calls |
| * or fetching properties to ensure the target resource actually supports the |
| * expected functionality. For example, before fetching NVMe metrics, it can |
| * verify the controller implements both the NVMeAdmin and MetricStore |
| * interfaces. |
| * |
| * It constructs a checklist of strictly required interfaces from |
| * `requiredIfaces` and the specific `interface` argument. It then |
| * cross-references this checklist against `ifaceNames`, which are the actual |
| * interfaces exposed by the D-Bus object. |
| * |
| * If any required interface is missing, it automatically populates the |
| * `asyncResp` with a standard Redfish ResourceNotFound error message indicating |
| * the missing interface and returns false. |
| * |
| * @param asyncResp Async response object to populate with an error if check |
| * fails. |
| * @param resourcePath The D-Bus object path being inspected. |
| * @param ifaceNames The actual D-Bus interfaces implemented by the object. |
| * @param requiredIfaces A set of additional required D-Bus interfaces. |
| * @param interface The primary interface that is required. |
| * @return true if all required interfaces are present, false otherwise. |
| */ |
| inline bool checkRequiredInterfaces( |
| const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, |
| const sdbusplus::message::object_path& resourcePath, |
| const std::vector<std::string>& ifaceNames, |
| const absl::flat_hash_set<std::string>& requiredIfaces, |
| std::string_view interface) { |
| absl::flat_hash_set<std::string_view> checklist = {{interface}}; |
| checklist.insert(requiredIfaces.begin(), requiredIfaces.end()); |
| |
| for (const std::string& iface : ifaceNames) { |
| absl::flat_hash_set<std::string_view>::iterator find = |
| checklist.find(iface); |
| if (find != checklist.end()) { |
| checklist.erase(*find); |
| } |
| if (checklist.empty()) { |
| break; |
| } |
| } |
| if (!checklist.empty()) { |
| redfish::messages::resourceNotFound( |
| asyncResp->res, std::string(*(checklist.begin())), resourcePath.str); |
| return false; |
| } |
| return true; |
| } |
| |
| inline void fetchFileUtil( |
| const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, |
| const std::shared_ptr<boost::asio::posix::stream_descriptor>& fileConn, |
| MetricStoreCb&& cb, |
| const std::shared_ptr<std::array<char, 1024>>& readBuffer = |
| std::make_shared<std::array<char, 1024>>(), |
| const std::shared_ptr<std::string>& output = |
| std::make_shared<std::string>()) { |
| if (!fileConn) { |
| BMCWEB_LOG_ERROR << "Fetch File Context is not setup properly"; |
| redfish::messages::internalError(asyncResp->res); |
| return; |
| } |
| |
| fileConn->async_read_some( |
| boost::asio::buffer(*readBuffer), |
| [asyncResp, fileConn, readBuffer, output, cb{std::move(cb)}]( |
| const boost::system::error_code& ec, |
| const std::size_t& bytesTransferred) mutable { |
| if (ec == boost::asio::error::eof) { |
| cb(asyncResp, *output); |
| fileConn->close(); |
| return; |
| } |
| if (ec) { |
| BMCWEB_LOG_ERROR << "Failed to async_read_some" << ec.message(); |
| redfish::messages::internalError(asyncResp->res); |
| fileConn->close(); |
| return; |
| } |
| *output += std::string(readBuffer->begin(), |
| readBuffer->begin() + bytesTransferred); |
| fetchFileUtil(asyncResp, fileConn, std::move(cb), readBuffer, output); |
| }); |
| } |
| |
| constexpr const char nvmeMetricInfc[] = "xyz.openbmc_project.NVMe.MetricStore"; |
| |
| template <const char* INFC = nvmeMetricInfc> |
| inline void tryPopulateMetricCollection( |
| const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, |
| const std::string& path, const dbus::utility::MapperServiceMap& ifaces, |
| const boost::urls::url& parentUrl) { |
| managedStore::ManagedObjectStoreContext context(asyncResp); |
| std::optional<std::string> service = |
| storage_utils::matchServiceName(ifaces, INFC); |
| if (!service) { |
| return; |
| } |
| redfish::storage_utils::getProperty<std::vector<std::string>>( |
| *service, path, INFC, "MetricCollection", context, |
| [asyncResp, parentUrl](const boost::system::error_code& ec, |
| const std::vector<std::string>& metricCollection) { |
| if (ec) { |
| return; |
| } |
| |
| for (const std::string& metric : metricCollection) { |
| boost::urls::url dataUri(parentUrl); |
| crow::utility::appendUrlPieces(dataUri, "Oem", "Google", "Metrics", |
| metric); |
| asyncResp->res.jsonValue["Oem"]["Google"][metric]["DataUri"] = |
| dataUri; |
| } |
| }); |
| } |
| |
| /** Fetch the Metric for given {systemName, storageId, controllerId, metricId} |
| * File the resp fields in terms of the data and time stamps |
| */ |
| |
| inline void defaultMetricCallback( |
| const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, |
| const std::optional<std::string>& output) { |
| // if the output is nullopt then the cache is invalid |
| if (!output.has_value()) { |
| BMCWEB_LOG_ERROR << "Failed to read metric: Cache invalid"; |
| redfish::messages::internalError(asyncResp->res); |
| return; |
| } |
| const std::string& data = output.value(); |
| std::optional<NVMeMetricHeader> header = extractHeader(data); |
| if (!header.has_value()) { |
| BMCWEB_LOG_ERROR << "Failed to read metric header"; |
| redfish::messages::internalError(asyncResp->res); |
| return; |
| } |
| asyncResp->res.jsonValue["StartTime"] = |
| static_cast<uint64_t>(header.value().startTime); |
| asyncResp->res.jsonValue["FinishTime"] = |
| static_cast<uint64_t>(header.value().finishTime); |
| |
| // Convert boot time (ms) to UTC |
| struct timespec ts_boot; |
| if (clock_gettime(CLOCK_BOOTTIME, &ts_boot) == 0) { |
| uint64_t nowBootMs = static_cast<uint64_t>(ts_boot.tv_sec) * 1000 + |
| static_cast<uint64_t>(ts_boot.tv_nsec) / 1000000; |
| uint64_t nowSystemMs = |
| static_cast<uint64_t>(absl::ToUnixMillis(absl::Now())); |
| int64_t offsetMs = |
| static_cast<int64_t>(nowSystemMs) - static_cast<int64_t>(nowBootMs); |
| |
| asyncResp->res.jsonValue["StartTimeUTC"] = |
| time_utils::getDateTimeUintMs(static_cast<uint64_t>( |
| static_cast<int64_t>(header.value().startTime) + offsetMs)); |
| asyncResp->res.jsonValue["FinishTimeUTC"] = |
| time_utils::getDateTimeUintMs(static_cast<uint64_t>( |
| static_cast<int64_t>(header.value().finishTime) + offsetMs)); |
| } |
| |
| asyncResp->res.jsonValue["Metric"] = |
| ::crow::utility::base64encode(data.substr(sizeof(NVMeMetricHeader))); |
| } |
| |
| /** |
| * @details fetch the metric in the given resource dbus path from metric store |
| * and trigger the cb with the data if succeed. |
| * |
| * Exception Handling: |
| * If the expected service or the required interfaces or the metric itself is |
| * not available, it will set resource not found in the provided asyncResp and |
| * will not trigger cb. If while fetching metric, the service throws dbus |
| * exception Unavailable, then that means the service could not cache the metric |
| * yet. In that case the callback cb will be called with data as nullopt. The |
| * consumer can either set internal error or treat that as success case. For all |
| * other exceptions, it will set internal error in asyncResp and will not |
| * trigger cb. |
| */ |
| template <const char* INFC = nvmeMetricInfc> |
| inline void nvmeMetricFetcher( |
| const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, |
| const sdbusplus::message::object_path& resourcePath, |
| const dbus::utility::MapperServiceMap& resourceIfaces, |
| const std::string& metricId, std::unordered_set<std::string> requiredIfaces, |
| MetricStoreCb&& cb = defaultMetricCallback) { |
| // Find the controller and then create a dbus call to the metric name |
| // Before you do check for the presence of the interface using association |
| // get sub tree |
| auto foundService = std::find_if( |
| resourceIfaces.begin(), resourceIfaces.end(), |
| [](const std::pair<std::string, |
| dbus::utility::MapperGetSubTreePathsResponse>& pair) { |
| return pair.first == "xyz.openbmc_project.NVMe"; |
| }); |
| |
| if (foundService == resourceIfaces.end()) { |
| redfish::messages::resourceNotFound( |
| asyncResp->res, "xyz.openbmc_project.NVMe", resourcePath.str); |
| return; |
| } |
| const auto& service = foundService->first; |
| auto ifaceNames = foundService->second; |
| |
| // examine if the controller resource has all required interfaces for |
| // metric |
| |
| std::unordered_set<std::string> checklist = {{INFC}}; |
| checklist.insert(requiredIfaces.begin(), requiredIfaces.end()); |
| |
| for (const auto& iface : ifaceNames) { |
| auto find = checklist.find(iface); |
| if (find != checklist.end()) { |
| checklist.erase(*find); |
| } |
| if (checklist.empty()) { |
| break; |
| } |
| } |
| if (!checklist.empty()) { |
| redfish::messages::resourceNotFound(asyncResp->res, *(checklist.begin()), |
| resourcePath.str); |
| return; |
| } |
| |
| // query the metric from MetricStore |
| managedStore::ManagedObjectStoreContext requestContext(asyncResp); |
| |
| redfish::dbus_utils::getProperty<std::vector<std::string>>( |
| service, resourcePath, INFC, "MetricCollection", requestContext, |
| [asyncResp, service, resourcePath, metricId, cb{std::move(cb)}]( |
| const boost::system::error_code ec, |
| const std::vector<std::string>& metricCollection) mutable { |
| if (ec) { |
| BMCWEB_LOG_ERROR << std::format("[{}, {}]Failed to enumerate Metric", |
| resourcePath.str, metricId); |
| redfish::messages::internalError(asyncResp->res); |
| return; |
| } |
| |
| auto res = std::find(metricCollection.begin(), metricCollection.end(), |
| metricId); |
| if (res == metricCollection.end()) { |
| redfish::messages::resourceNotFound(asyncResp->res, INFC, metricId); |
| return; |
| } |
| |
| // read metric |
| #ifdef UNIT_TEST_BUILD |
| dynamic_cast<managedStore::MockSerializedManagedObjectStore*>( |
| managedStore::GetManagedObjectStore()) |
| ->PostDbusCallToIoContextThreadSafe( |
| #else |
| managedStore::GetManagedObjectStore() |
| ->PostDbusCallToIoContextThreadSafe( |
| #endif |
| asyncResp->strand_, |
| [asyncResp, service, resourcePath, metricId, cb{std::move(cb)}]( |
| const boost::system::error_code ec, |
| const sdbusplus::message_t& msg, |
| const sdbusplus::message::unix_fd& fd) mutable { |
| if (ec) { |
| const sd_bus_error* dbusError = msg.get_error(); |
| if (std::string_view( |
| "xyz.openbmc_project.Common.Error.Unavailable") == |
| dbusError->name) { |
| // if cache is invalid then the service throws unavailble |
| cb(asyncResp, std::nullopt); |
| return; |
| } |
| BMCWEB_LOG_ERROR |
| << std::format("[{}, {}]Failed to read Metric", |
| resourcePath.str, metricId); |
| redfish::messages::internalError(asyncResp->res); |
| return; |
| } |
| |
| int dupFd = dup(fd.fd); |
| fetchFileUtil( |
| asyncResp, |
| std::make_shared<boost::asio::posix::stream_descriptor>( |
| managedStore::GetManagedObjectStore()->GetIoContext(), |
| dupFd), |
| std::move(cb)); |
| }, |
| service, resourcePath, INFC, "GetMetric", metricId); |
| }); |
| } |
| |
| /** |
| * @details refresh the metric in the given resource dbus path from metric store |
| * and trigger the cb with the data if succeed. |
| * |
| * Exception Handling: |
| * If the expected service or the required interfaces or the metric itself is |
| * not available, it will set resource not found in the provided asyncResp and |
| * will not trigger cb. If while fetching metric, the service throws dbus |
| * exception Unavailable, then that means the service could not cache the metric |
| * yet. In that case the callback cb will be called with data as nullopt. The |
| * consumer can either set internal error or treat that as success case. For all |
| * other exceptions, it will set internal error in asyncResp and will not |
| * trigger cb. |
| */ |
| template <const char* INFC = nvmeMetricInfc> |
| inline void nvmeMetricRefresher( |
| const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, |
| const sdbusplus::message::object_path& resourcePath, |
| const dbus::utility::MapperServiceMap& resourceIfaces, |
| const std::string& metricId, |
| const absl::flat_hash_set<std::string>& requiredIfaces, |
| MetricStoreCb&& cb = defaultMetricCallback) { |
| // Find the controller and then create a dbus call to the metric name |
| // Before you do check for the presence of the interface using association |
| // get sub tree |
| dbus::utility::MapperServiceMap::const_iterator foundService = std::find_if( |
| resourceIfaces.begin(), resourceIfaces.end(), |
| [](const std::pair<std::string, |
| dbus::utility::MapperGetSubTreePathsResponse>& pair) { |
| return pair.first == "xyz.openbmc_project.NVMe"; |
| }); |
| |
| if (foundService == resourceIfaces.end()) { |
| redfish::messages::resourceNotFound( |
| asyncResp->res, "xyz.openbmc_project.NVMe", resourcePath.str); |
| return; |
| } |
| const std::string& service = foundService->first; |
| std::vector<std::string> ifaceNames = foundService->second; |
| |
| // examine if the controller resource has all required interfaces for |
| // metric |
| if (!checkRequiredInterfaces(asyncResp, resourcePath, ifaceNames, |
| requiredIfaces, INFC)) { |
| return; |
| } |
| |
| // query the metric from MetricStore |
| managedStore::ManagedObjectStoreContext requestContext(asyncResp); |
| |
| redfish::dbus_utils::getProperty<std::vector<std::string>>( |
| service, resourcePath, INFC, "MetricCollection", requestContext, |
| [asyncResp, service, resourcePath, metricId, cb{std::move(cb)}]( |
| const boost::system::error_code ec, |
| const std::vector<std::string>& metricCollection) mutable { |
| if (ec) { |
| BMCWEB_LOG_ERROR << std::format("[{}, {}]Failed to enumerate Metric", |
| resourcePath.str, metricId); |
| redfish::messages::internalError(asyncResp->res); |
| return; |
| } |
| |
| std::vector<std::string>::const_iterator res = std::find( |
| metricCollection.begin(), metricCollection.end(), metricId); |
| if (res == metricCollection.end()) { |
| redfish::messages::resourceNotFound(asyncResp->res, INFC, metricId); |
| return; |
| } |
| |
| // refresh metric |
| managedStore::GetManagedObjectStore() |
| ->PostDbusCallWithStringRetMsgAndFd( |
| asyncResp->strand_, |
| [asyncResp, service, resourcePath, metricId, cb{std::move(cb)}]( |
| const boost::system::error_code ec, |
| const sdbusplus::message_t& msg, |
| const sdbusplus::message::unix_fd& fd) mutable { |
| if (ec) { |
| const sd_bus_error* dbusError = msg.get_error(); |
| if (dbusError != nullptr && |
| std::string_view( |
| "xyz.openbmc_project.Common.Error.Unavailable") == |
| dbusError->name) { |
| // if cache is invalid then the service throws unavailble |
| cb(asyncResp, std::nullopt); |
| return; |
| } |
| BMCWEB_LOG_ERROR |
| << std::format("[{}, {}]Failed to refresh Metric", |
| resourcePath.str, metricId); |
| redfish::messages::internalError(asyncResp->res); |
| return; |
| } |
| |
| int dupFd = dup(fd.fd); |
| fetchFileUtil( |
| asyncResp, |
| std::make_shared<boost::asio::posix::stream_descriptor>( |
| managedStore::GetManagedObjectStore()->GetIoContext(), |
| dupFd), |
| std::move(cb)); |
| }, |
| service, resourcePath, INFC, "RefreshMetric", metricId); |
| }); |
| } |
| |
| template <const char* INFC = nvmeMetricInfc> |
| inline void nvmeControllerMetricFetcher( |
| const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, |
| const std::string& storageId, const std::string& controllerId, |
| const std::string& metricId, MetricStoreCb&& cb = defaultMetricCallback) { |
| redfish::storage_utils::findStorageAndController( |
| asyncResp, storageId, controllerId, |
| [asyncResp, metricId, cb{std::move(cb)}]( |
| const sdbusplus::message::object_path& path, |
| const dbus::utility::MapperServiceMap& ifaces) mutable { |
| nvmeMetricFetcher<INFC>(asyncResp, path, ifaces, metricId, |
| {"xyz.openbmc_project.NVMe.NVMeAdmin"}, |
| std::move(cb)); |
| }); |
| } |
| |
| template <const char* INFC = nvmeMetricInfc> |
| inline void nvmeControllerMetricRefresher( |
| const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, |
| const std::string& storageId, const std::string& controllerId, |
| const std::string& metricId, MetricStoreCb&& cb = defaultMetricCallback) { |
| redfish::storage_utils::findStorageAndController( |
| asyncResp, storageId, controllerId, |
| [asyncResp, metricId, cb{std::move(cb)}]( |
| const sdbusplus::message::object_path& path, |
| const dbus::utility::MapperServiceMap& ifaces) mutable { |
| nvmeMetricRefresher<INFC>(asyncResp, path, ifaces, metricId, |
| {"xyz.openbmc_project.NVMe.NVMeAdmin"}, |
| std::move(cb)); |
| }); |
| } |
| } // namespace redfish |
| |
| #endif // THIRD_PARTY_GBMCWEB_REDFISH_CORE_INCLUDE_UTILS_NVME_METRIC_UTILS_H_ |