From c7fce288802ece4a6e1ff71ee060a44e0b8fe992 Mon Sep 17 00:00:00 2001 From: "Wludzik, Jozef" Date: Mon, 27 Apr 2020 17:24:15 +0200 Subject: [PATCH 1/4] Redfish TelemetryService schema implementation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added TelemetryService, MetricReports, MetricReportCollection, MetricReportDefinition and MetricReportDefinitionCollection schemas with GET method support. Added TelemetryService URI to root service. Implemented communication with backend - Telemetry. Added schemes attributes that are supported by Telemetry service design. User is able to fetch basic information about reports if Telemetry service is present in OpenBMC. Added util function that converts decimal value into duration format that is described by ISO 8601 and Redfish specification. Tested: - Succesfully passed RedfishServiceValidator.py - Verified DBus method calls to Telemetry service - Verified all possible pages that are displayed to user when: - Reports are fully defined in Telemetry - Reports are partially available in Telemetry - Telemetry is disabled - Verified time_utils::toDurationString() output Signed-off-by: Wludzik, Jozef Signed-off-by: Adrian Ambrożewicz Signed-off-by: Krzysztof Grobelny Change-Id: Ie6b0b49f4ef5eeaef07d1209b6c349270c04d570 --- redfish-core/include/redfish.hpp | 10 ++ redfish-core/include/utils/telemetry_utils.hpp | 71 ++++++++++ redfish-core/include/utils/time_utils.hpp | 78 +++++++++++ redfish-core/lib/metric_report.hpp | 162 +++++++++++++++++++++ redfish-core/lib/metric_report_definition.hpp | 186 +++++++++++++++++++++++++ redfish-core/lib/service_root.hpp | 2 + redfish-core/lib/telemetry_service.hpp | 93 +++++++++++++ 7 files changed, 602 insertions(+) create mode 100644 redfish-core/include/utils/telemetry_utils.hpp create mode 100644 redfish-core/include/utils/time_utils.hpp create mode 100644 redfish-core/lib/metric_report.hpp create mode 100644 redfish-core/lib/metric_report_definition.hpp create mode 100644 redfish-core/lib/telemetry_service.hpp diff --git a/redfish-core/include/redfish.hpp b/redfish-core/include/redfish.hpp index 54d5d0e..2587b37 100644 --- a/redfish-core/include/redfish.hpp +++ b/redfish-core/include/redfish.hpp @@ -25,6 +25,8 @@ #include "../lib/managers.hpp" #include "../lib/memory.hpp" #include "../lib/message_registries.hpp" +#include "../lib/metric_report.hpp" +#include "../lib/metric_report_definition.hpp" #include "../lib/network_protocol.hpp" #include "../lib/pcie.hpp" #include "../lib/power.hpp" @@ -36,6 +38,7 @@ #include "../lib/storage.hpp" #include "../lib/systems.hpp" #include "../lib/task.hpp" +#include "../lib/telemetry_service.hpp" #include "../lib/thermal.hpp" #include "../lib/update_service.hpp" #ifdef BMCWEB_ENABLE_VM_NBDPROXY @@ -207,6 +210,13 @@ class RedfishService nodes.emplace_back(std::make_unique(app)); nodes.emplace_back(std::make_unique(app)); + nodes.emplace_back(std::make_unique(app)); + nodes.emplace_back( + std::make_unique(app)); + nodes.emplace_back(std::make_unique(app)); + nodes.emplace_back(std::make_unique(app)); + nodes.emplace_back(std::make_unique(app)); + for (const auto& node : nodes) { node->initPrivileges(); diff --git a/redfish-core/include/utils/telemetry_utils.hpp b/redfish-core/include/utils/telemetry_utils.hpp new file mode 100644 index 0000000..8caee2d --- /dev/null +++ b/redfish-core/include/utils/telemetry_utils.hpp @@ -0,0 +1,71 @@ +#pragma once + +namespace redfish +{ + +namespace telemetry +{ + +constexpr const char* service = "xyz.openbmc_project.Telemetry"; +constexpr const char* reportInterface = "xyz.openbmc_project.Telemetry.Report"; +constexpr const char* metricReportDefinitionUri = + "/redfish/v1/TelemetryService/MetricReportDefinitions/"; +constexpr const char* metricReportUri = + "/redfish/v1/TelemetryService/MetricReports/"; + +inline void getReportCollection(const std::shared_ptr& asyncResp, + const std::string& uri) +{ + const std::array interfaces = {reportInterface}; + + crow::connections::systemBus->async_method_call( + [asyncResp, uri](const boost::system::error_code ec, + const std::vector& reportPaths) { + if (ec) + { + asyncResp->res.jsonValue["Members"] = nlohmann::json::array(); + asyncResp->res.jsonValue["Members@odata.count"] = 0; + return; + } + + nlohmann::json& members = asyncResp->res.jsonValue["Members"]; + members = nlohmann::json::array(); + + for (const std::string& path : reportPaths) + { + std::size_t pos = path.rfind('/'); + if (pos == std::string::npos) + { + BMCWEB_LOG_ERROR << "Failed to find '/' in " << path; + messages::internalError(asyncResp->res); + return; + } + if (path.size() <= (pos + 1)) + { + BMCWEB_LOG_ERROR << "Failed to parse path " << path; + messages::internalError(asyncResp->res); + return; + } + + members.push_back({{"@odata.id", uri + path.substr(pos + 1)}}); + } + + asyncResp->res.jsonValue["Members@odata.count"] = members.size(); + }, + "xyz.openbmc_project.ObjectMapper", + "/xyz/openbmc_project/object_mapper", + "xyz.openbmc_project.ObjectMapper", "GetSubTreePaths", + "/xyz/openbmc_project/Telemetry/Reports/TelemetryService", 1, + interfaces); +} + +inline std::string getDbusReportPath(const std::string& id) +{ + std::string path = + "/xyz/openbmc_project/Telemetry/Reports/TelemetryService/" + id; + dbus::utility::escapePathForDbus(path); + return path; +} + +} // namespace telemetry +} // namespace redfish diff --git a/redfish-core/include/utils/time_utils.hpp b/redfish-core/include/utils/time_utils.hpp new file mode 100644 index 0000000..dd4ea75 --- /dev/null +++ b/redfish-core/include/utils/time_utils.hpp @@ -0,0 +1,78 @@ +#pragma once + +#include +#include + +namespace redfish +{ + +namespace time_utils +{ + +namespace details +{ + +inline void leftZeroPadding(std::string& str, const std::size_t padding) +{ + if (str.size() < padding) + { + str.insert(0, padding - str.size(), '0'); + } +} +} // namespace details + +/** + * @brief Convert time value into duration format that is based on ISO 8601. + * Example output: "P12DT1M5.5S" + * Ref: Redfish Specification, Section 9.4.4. Duration values + */ +std::string toDurationString(std::chrono::milliseconds ms) +{ + if (ms < std::chrono::milliseconds::zero()) + { + return ""; + } + + std::string fmt; + fmt.reserve(sizeof("PxxxxxxxxxxxxDTxxHxxMxx.xxxxxxS")); + + using Days = std::chrono::duration>; + Days days = std::chrono::floor(ms); + ms -= days; + + std::chrono::hours hours = std::chrono::floor(ms); + ms -= hours; + + std::chrono::minutes minutes = std::chrono::floor(ms); + ms -= minutes; + + std::chrono::seconds seconds = std::chrono::floor(ms); + ms -= seconds; + + fmt = "P"; + if (days.count() > 0) + { + fmt += std::to_string(days.count()) + "D"; + } + fmt += "T"; + if (hours.count() > 0) + { + fmt += std::to_string(hours.count()) + "H"; + } + if (minutes.count() > 0) + { + fmt += std::to_string(minutes.count()) + "M"; + } + if (seconds.count() != 0 || ms.count() != 0) + { + fmt += std::to_string(seconds.count()) + "."; + std::string msStr = std::to_string(ms.count()); + details::leftZeroPadding(msStr, 3); + fmt += msStr + "S"; + } + + return fmt; +} + +} // namespace time_utils +} // namespace redfish diff --git a/redfish-core/lib/metric_report.hpp b/redfish-core/lib/metric_report.hpp new file mode 100644 index 0000000..050304c --- /dev/null +++ b/redfish-core/lib/metric_report.hpp @@ -0,0 +1,162 @@ +#pragma once + +#include "node.hpp" +#include "utils/telemetry_utils.hpp" + +namespace redfish +{ + +class MetricReportCollection : public Node +{ + public: + MetricReportCollection(App& app) : + Node(app, "/redfish/v1/TelemetryService/MetricReports/") + { + entityPrivileges = { + {boost::beast::http::verb::get, {{"Login"}}}, + {boost::beast::http::verb::head, {{"Login"}}}, + {boost::beast::http::verb::patch, {{"ConfigureManager"}}}, + {boost::beast::http::verb::put, {{"ConfigureManager"}}}, + {boost::beast::http::verb::delete_, {{"ConfigureManager"}}}, + {boost::beast::http::verb::post, {{"ConfigureManager"}}}}; + } + + private: + void doGet(crow::Response& res, const crow::Request&, + const std::vector&) override + { + res.jsonValue["@odata.type"] = + "#MetricReportCollection.MetricReportCollection"; + res.jsonValue["@odata.id"] = + "/redfish/v1/TelemetryService/MetricReports"; + res.jsonValue["Name"] = "Metric Report Collection"; + + auto asyncResp = std::make_shared(res); + telemetry::getReportCollection(asyncResp, telemetry::metricReportUri); + } +}; + +class MetricReport : public Node +{ + public: + MetricReport(App& app) : + Node(app, "/redfish/v1/TelemetryService/MetricReports//", + std::string()) + { + entityPrivileges = { + {boost::beast::http::verb::get, {{"Login"}}}, + {boost::beast::http::verb::head, {{"Login"}}}, + {boost::beast::http::verb::patch, {{"ConfigureManager"}}}, + {boost::beast::http::verb::put, {{"ConfigureManager"}}}, + {boost::beast::http::verb::delete_, {{"ConfigureManager"}}}, + {boost::beast::http::verb::post, {{"ConfigureManager"}}}}; + } + + private: + void doGet(crow::Response& res, const crow::Request&, + const std::vector& params) override + { + auto asyncResp = std::make_shared(res); + + if (params.size() != 1) + { + messages::internalError(asyncResp->res); + return; + } + + const std::string& id = params[0]; + const std::string reportPath = telemetry::getDbusReportPath(id); + + crow::connections::systemBus->async_method_call( + [asyncResp, id, reportPath](const boost::system::error_code& ec) { + if (ec.value() == EBADR) + { + messages::resourceNotFound(asyncResp->res, schemaType, id); + return; + } + if (ec) + { + BMCWEB_LOG_ERROR << "respHandler DBus error " << ec; + messages::internalError(asyncResp->res); + return; + } + + crow::connections::systemBus->async_method_call( + [asyncResp, + id](const boost::system::error_code ec, + const std::variant& ret) { + if (ec) + { + BMCWEB_LOG_ERROR << "respHandler DBus error " << ec; + messages::internalError(asyncResp->res); + return; + } + + fillReport(asyncResp, id, ret); + }, + telemetry::service, reportPath, + "org.freedesktop.DBus.Properties", "Get", + telemetry::reportInterface, "Readings"); + }, + telemetry::service, reportPath, telemetry::reportInterface, + "Update"); + } + + using Readings = + std::vector>; + using TimestampReadings = std::tuple; + + static nlohmann::json toMetricValues(const Readings& readings) + { + nlohmann::json metricValues = nlohmann::json::array_t(); + + for (auto& [id, metadata, sensorValue, timestamp] : readings) + { + nlohmann::json metadataJson = nlohmann::json::parse(metadata); + metricValues.push_back({ + {"MetricId", id}, + {"MetricDefinition", metadataJson.contains("MetricDefinition") + ? metadataJson["MetricDefinition"] + : nlohmann::json()}, + {"MetricProperty", metadataJson.contains("MetricProperty") + ? metadataJson["MetricProperty"] + : nlohmann::json()}, + {"MetricValue", std::to_string(sensorValue)}, + {"Timestamp", + crow::utility::getDateTime(static_cast(timestamp))}, + }); + } + + return metricValues; + } + + static void fillReport(const std::shared_ptr& asyncResp, + const std::string& id, + const std::variant& var) + { + asyncResp->res.jsonValue["@odata.type"] = schemaType; + asyncResp->res.jsonValue["@odata.id"] = telemetry::metricReportUri + id; + asyncResp->res.jsonValue["Id"] = id; + asyncResp->res.jsonValue["Name"] = id; + asyncResp->res.jsonValue["MetricReportDefinition"]["@odata.id"] = + telemetry::metricReportDefinitionUri + id; + + const TimestampReadings* timestampReadings = + std::get_if(&var); + if (!timestampReadings) + { + BMCWEB_LOG_ERROR << "Property type mismatch or property is missing"; + messages::internalError(asyncResp->res); + return; + } + + const auto& [timestamp, readings] = *timestampReadings; + asyncResp->res.jsonValue["Timestamp"] = + crow::utility::getDateTime(static_cast(timestamp)); + asyncResp->res.jsonValue["MetricValues"] = toMetricValues(readings); + } + + static constexpr const char* schemaType = + "#MetricReport.v1_3_0.MetricReport"; +}; +} // namespace redfish diff --git a/redfish-core/lib/metric_report_definition.hpp b/redfish-core/lib/metric_report_definition.hpp new file mode 100644 index 0000000..48c56e6 --- /dev/null +++ b/redfish-core/lib/metric_report_definition.hpp @@ -0,0 +1,186 @@ +#pragma once + +#include "node.hpp" +#include "utils/telemetry_utils.hpp" +#include "utils/time_utils.hpp" + +#include +#include + +namespace redfish +{ + +class MetricReportDefinitionCollection : public Node +{ + public: + MetricReportDefinitionCollection(App& app) : + Node(app, "/redfish/v1/TelemetryService/MetricReportDefinitions/") + { + entityPrivileges = { + {boost::beast::http::verb::get, {{"Login"}}}, + {boost::beast::http::verb::head, {{"Login"}}}, + {boost::beast::http::verb::patch, {{"ConfigureManager"}}}, + {boost::beast::http::verb::put, {{"ConfigureManager"}}}, + {boost::beast::http::verb::delete_, {{"ConfigureManager"}}}, + {boost::beast::http::verb::post, {{"ConfigureManager"}}}}; + } + + private: + void doGet(crow::Response& res, const crow::Request&, + const std::vector&) override + { + res.jsonValue["@odata.type"] = "#MetricReportDefinitionCollection." + "MetricReportDefinitionCollection"; + res.jsonValue["@odata.id"] = + "/redfish/v1/TelemetryService/MetricReportDefinitions"; + res.jsonValue["Name"] = "Metric Definition Collection"; + + auto asyncResp = std::make_shared(res); + telemetry::getReportCollection(asyncResp, + telemetry::metricReportDefinitionUri); + } +}; + +class MetricReportDefinition : public Node +{ + public: + MetricReportDefinition(App& app) : + Node(app, "/redfish/v1/TelemetryService/MetricReportDefinitions//", + std::string()) + { + entityPrivileges = { + {boost::beast::http::verb::get, {{"Login"}}}, + {boost::beast::http::verb::head, {{"Login"}}}, + {boost::beast::http::verb::patch, {{"ConfigureManager"}}}, + {boost::beast::http::verb::put, {{"ConfigureManager"}}}, + {boost::beast::http::verb::delete_, {{"ConfigureManager"}}}, + {boost::beast::http::verb::post, {{"ConfigureManager"}}}}; + } + + private: + void doGet(crow::Response& res, const crow::Request&, + const std::vector& params) override + { + auto asyncResp = std::make_shared(res); + + if (params.size() != 1) + { + messages::internalError(asyncResp->res); + return; + } + + const std::string& id = params[0]; + crow::connections::systemBus->async_method_call( + [asyncResp, + id](const boost::system::error_code ec, + const std::vector>>& ret) { + if (ec.value() == EBADR) + { + messages::resourceNotFound(asyncResp->res, schemaType, id); + return; + } + if (ec) + { + BMCWEB_LOG_ERROR << "respHandler DBus error " << ec; + messages::internalError(asyncResp->res); + return; + } + + fillReportDefinition(asyncResp, id, ret); + }, + telemetry::service, telemetry::getDbusReportPath(id), + "org.freedesktop.DBus.Properties", "GetAll", + telemetry::reportInterface); + } + + using ReadingParameters = + std::vector, + std::string, std::string, std::string>>; + + static void fillReportDefinition( + const std::shared_ptr& asyncResp, const std::string& id, + const std::vector< + std::pair>>& ret) + { + asyncResp->res.jsonValue["@odata.type"] = schemaType; + asyncResp->res.jsonValue["@odata.id"] = + telemetry::metricReportDefinitionUri + id; + asyncResp->res.jsonValue["Id"] = id; + asyncResp->res.jsonValue["Name"] = id; + asyncResp->res.jsonValue["MetricReport"]["@odata.id"] = + telemetry::metricReportUri + id; + asyncResp->res.jsonValue["Status"]["State"] = "Enabled"; + asyncResp->res.jsonValue["ReportUpdates"] = "Overwrite"; + + const bool* emitsReadingsUpdate = nullptr; + const bool* logToMetricReportsCollection = nullptr; + const ReadingParameters* readingParams = nullptr; + const std::string* reportingType = nullptr; + const uint64_t* interval = nullptr; + for (const auto& [key, var] : ret) + { + if (key == "EmitsReadingsUpdate") + { + emitsReadingsUpdate = std::get_if(&var); + } + else if (key == "LogToMetricReportsCollection") + { + logToMetricReportsCollection = std::get_if(&var); + } + else if (key == "ReadingParameters") + { + readingParams = std::get_if(&var); + } + else if (key == "ReportingType") + { + reportingType = std::get_if(&var); + } + else if (key == "Interval") + { + interval = std::get_if(&var); + } + } + if (!emitsReadingsUpdate || !logToMetricReportsCollection || + !readingParams || !reportingType || !interval) + { + BMCWEB_LOG_ERROR << "Property type mismatch or property is missing"; + messages::internalError(asyncResp->res); + return; + } + + std::vector redfishReportActions; + redfishReportActions.reserve(2); + if (*emitsReadingsUpdate) + { + redfishReportActions.emplace_back("RedfishEvent"); + } + if (*logToMetricReportsCollection) + { + redfishReportActions.emplace_back("LogToMetricReportsCollection"); + } + + nlohmann::json metrics = nlohmann::json::array(); + for (auto& [sensorPaths, operationType, id, metadata] : *readingParams) + { + nlohmann::json metadataJson = nlohmann::json::parse(metadata); + metrics.push_back({ + {"MetricId", id}, + {"MetricProperties", metadataJson.contains("MetricProperties") + ? metadataJson["MetricProperties"] + : nlohmann::json()}, + }); + } + asyncResp->res.jsonValue["Metrics"] = metrics; + asyncResp->res.jsonValue["MetricReportDefinitionType"] = *reportingType; + asyncResp->res.jsonValue["ReportActions"] = redfishReportActions; + asyncResp->res.jsonValue["Schedule"]["RecurrenceInterval"] = + time_utils::toDurationString(std::chrono::milliseconds(*interval)); + } + + static constexpr const char* schemaType = + "#MetricReportDefinition.v1_3_0.MetricReportDefinition"; +}; +} // namespace redfish diff --git a/redfish-core/lib/service_root.hpp b/redfish-core/lib/service_root.hpp index 629280c..3df5ec5 100644 --- a/redfish-core/lib/service_root.hpp +++ b/redfish-core/lib/service_root.hpp @@ -68,6 +68,8 @@ class ServiceRoot : public Node res.jsonValue["Tasks"] = {{"@odata.id", "/redfish/v1/TaskService"}}; res.jsonValue["EventService"] = { {"@odata.id", "/redfish/v1/EventService"}}; + res.jsonValue["TelemetryService"] = { + {"@odata.id", "/redfish/v1/TelemetryService"}}; res.end(); } diff --git a/redfish-core/lib/telemetry_service.hpp b/redfish-core/lib/telemetry_service.hpp new file mode 100644 index 0000000..a6acc34 --- /dev/null +++ b/redfish-core/lib/telemetry_service.hpp @@ -0,0 +1,93 @@ +#pragma once + +#include "node.hpp" +#include "utils/telemetry_utils.hpp" + +#include + +namespace redfish +{ + +class TelemetryService : public Node +{ + public: + TelemetryService(App& app) : Node(app, "/redfish/v1/TelemetryService/") + { + entityPrivileges = { + {boost::beast::http::verb::get, {{"Login"}}}, + {boost::beast::http::verb::head, {{"Login"}}}, + {boost::beast::http::verb::patch, {{"ConfigureManager"}}}, + {boost::beast::http::verb::put, {{"ConfigureManager"}}}, + {boost::beast::http::verb::delete_, {{"ConfigureManager"}}}, + {boost::beast::http::verb::post, {{"ConfigureManager"}}}}; + } + + private: + void doGet(crow::Response& res, const crow::Request&, + const std::vector&) override + { + res.jsonValue["@odata.type"] = + "#TelemetryService.v1_2_1.TelemetryService"; + res.jsonValue["@odata.id"] = "/redfish/v1/TelemetryService"; + res.jsonValue["Id"] = "TelemetryService"; + res.jsonValue["Name"] = "Telemetry Service"; + + res.jsonValue["LogService"]["@odata.id"] = + "/redfish/v1/Managers/bmc/LogServices/Journal"; + res.jsonValue["MetricReportDefinitions"]["@odata.id"] = + "/redfish/v1/TelemetryService/MetricReportDefinitions"; + res.jsonValue["MetricReports"]["@odata.id"] = + "/redfish/v1/TelemetryService/MetricReports"; + + auto asyncResp = std::make_shared(res); + crow::connections::systemBus->async_method_call( + [asyncResp]( + const boost::system::error_code ec, + const std::vector>>& ret) { + if (ec == boost::system::errc::host_unreachable) + { + asyncResp->res.jsonValue["Status"]["State"] = "Absent"; + return; + } + if (ec) + { + BMCWEB_LOG_ERROR << "respHandler DBus error " << ec; + messages::internalError(asyncResp->res); + return; + } + + asyncResp->res.jsonValue["Status"]["State"] = "Enabled"; + + const size_t* maxReports = nullptr; + const uint64_t* minInterval = nullptr; + for (const auto& [key, var] : ret) + { + if (key == "MaxReports") + { + maxReports = std::get_if(&var); + } + else if (key == "MinInterval") + { + minInterval = std::get_if(&var); + } + } + if (!maxReports || !minInterval) + { + BMCWEB_LOG_ERROR + << "Property type mismatch or property is missing"; + messages::internalError(asyncResp->res); + return; + } + + asyncResp->res.jsonValue["MaxReports"] = *maxReports; + asyncResp->res.jsonValue["MinCollectionInterval"] = + time_utils::toDurationString(std::chrono::milliseconds( + static_cast(*minInterval))); + }, + telemetry::service, "/xyz/openbmc_project/Telemetry/Reports", + "org.freedesktop.DBus.Properties", "GetAll", + "xyz.openbmc_project.Telemetry.ReportManager"); + } +}; +} // namespace redfish -- 2.16.6