diff options
Diffstat (limited to 'meta-openbmc-mods/meta-common/recipes-phosphor/interfaces/bmcweb/telemetry/0002-Add-POST-and-DELETE-in-MetricReportDefinitions.patch')
-rw-r--r-- | meta-openbmc-mods/meta-common/recipes-phosphor/interfaces/bmcweb/telemetry/0002-Add-POST-and-DELETE-in-MetricReportDefinitions.patch | 683 |
1 files changed, 683 insertions, 0 deletions
diff --git a/meta-openbmc-mods/meta-common/recipes-phosphor/interfaces/bmcweb/telemetry/0002-Add-POST-and-DELETE-in-MetricReportDefinitions.patch b/meta-openbmc-mods/meta-common/recipes-phosphor/interfaces/bmcweb/telemetry/0002-Add-POST-and-DELETE-in-MetricReportDefinitions.patch new file mode 100644 index 000000000..b04a72c9f --- /dev/null +++ b/meta-openbmc-mods/meta-common/recipes-phosphor/interfaces/bmcweb/telemetry/0002-Add-POST-and-DELETE-in-MetricReportDefinitions.patch @@ -0,0 +1,683 @@ +From 0784af276b72e5df9c545d83bc989833ac2935c4 Mon Sep 17 00:00:00 2001 +From: "Wludzik, Jozef" <jozef.wludzik@intel.com> +Date: Mon, 18 May 2020 11:56:57 +0200 +Subject: [PATCH 2/4] Add POST and DELETE in MetricReportDefinitions + +Added POST action in MetricReportDefinitions node to allow user +to add new MetricReportDefinition. Using minimal set of +MetricReportDefinition parameters from user bmcweb converts it to +DBus call "AddReport" to Telemetry that serves as a backend +for Redfish TelemetryService. +Added DELETE request in MetricReportDefinitions node to allow user +to remove report from Telemetry. +Added conversion from string that represents duration format into +its numeric equivalent. + +Tested: + - Succesfully passed RedfishServiceValidator.py + - Validated good cases with different parameters for POST action + - Validated bad cases with different parameters for POST action + - Verified time_utils::fromDurationString() + - Verified that reports are removed on DELETE request + +Signed-off-by: Wludzik, Jozef <jozef.wludzik@intel.com> +Signed-off-by: Krzysztof Grobelny <krzysztof.grobelny@intel.com> +Change-Id: I2fed96848594451e22fde686f8c066d7770cc65a +--- + redfish-core/include/utils/telemetry_utils.hpp | 5 +- + redfish-core/include/utils/time_utils.hpp | 145 +++++++++- + redfish-core/lib/metric_report_definition.hpp | 382 ++++++++++++++++++++++++- + 3 files changed, 516 insertions(+), 16 deletions(-) + +diff --git a/redfish-core/include/utils/telemetry_utils.hpp b/redfish-core/include/utils/telemetry_utils.hpp +index 8caee2d..acb739d 100644 +--- a/redfish-core/include/utils/telemetry_utils.hpp ++++ b/redfish-core/include/utils/telemetry_utils.hpp +@@ -12,6 +12,8 @@ constexpr const char* metricReportDefinitionUri = + "/redfish/v1/TelemetryService/MetricReportDefinitions/"; + constexpr const char* metricReportUri = + "/redfish/v1/TelemetryService/MetricReports/"; ++constexpr const char* reportDir = ++ "/xyz/openbmc_project/Telemetry/Reports/TelemetryService/"; + + inline void getReportCollection(const std::shared_ptr<AsyncResp>& asyncResp, + const std::string& uri) +@@ -61,8 +63,7 @@ inline void getReportCollection(const std::shared_ptr<AsyncResp>& asyncResp, + + inline std::string getDbusReportPath(const std::string& id) + { +- std::string path = +- "/xyz/openbmc_project/Telemetry/Reports/TelemetryService/" + id; ++ std::string path = reportDir + id; + dbus::utility::escapePathForDbus(path); + return path; + } +diff --git a/redfish-core/include/utils/time_utils.hpp b/redfish-core/include/utils/time_utils.hpp +index dd4ea75..d8985ab 100644 +--- a/redfish-core/include/utils/time_utils.hpp ++++ b/redfish-core/include/utils/time_utils.hpp +@@ -1,7 +1,12 @@ + #pragma once + ++#include "logging.hpp" ++ ++#include <charconv> + #include <chrono> ++#include <optional> + #include <string> ++#include <system_error> + + namespace redfish + { +@@ -12,6 +17,8 @@ namespace time_utils + namespace details + { + ++using Days = std::chrono::duration<long long, std::ratio<24 * 60 * 60>>; ++ + inline void leftZeroPadding(std::string& str, const std::size_t padding) + { + if (str.size() < padding) +@@ -19,8 +26,143 @@ inline void leftZeroPadding(std::string& str, const std::size_t padding) + str.insert(0, padding - str.size(), '0'); + } + } ++ ++inline bool fromChars(const char* start, const char* end, ++ std::chrono::milliseconds::rep& val) ++{ ++ auto [ptr, ec] = std::from_chars(start, end, val); ++ if (ptr != end) ++ { ++ BMCWEB_LOG_ERROR ++ << "Failed to convert string to decimal because of unexpected sign"; ++ return false; ++ } ++ if (ec != std::errc()) ++ { ++ BMCWEB_LOG_ERROR << "Failed to convert string to decimal with err: " ++ << static_cast<int>(ec) << "(" ++ << std::make_error_code(ec).message() << ")"; ++ return false; ++ } ++ return true; ++} ++ ++template <typename T> ++bool fromDurationItem(std::string_view& fmt, const char postfix, ++ std::chrono::milliseconds& out) ++{ ++ const size_t pos = fmt.find(postfix); ++ if (pos == std::string::npos) ++ { ++ return true; ++ } ++ if ((pos + 1U) > fmt.size()) ++ { ++ return false; ++ } ++ ++ std::chrono::milliseconds::rep v = 0; ++ if constexpr (std::is_same_v<T, std::chrono::milliseconds>) ++ { ++ std::string str(fmt.data(), std::min<size_t>(pos, 3U)); ++ while (str.size() < 3U) ++ { ++ str += '0'; ++ } ++ if (!fromChars(str.data(), str.data() + str.size(), v)) ++ { ++ return false; ++ } ++ } ++ else ++ { ++ if (!fromChars(fmt.data(), fmt.data() + pos, v)) ++ { ++ return false; ++ } ++ } ++ ++ out += T(v); ++ if (out < T(v) || ++ std::chrono::duration_cast<T>(std::chrono::milliseconds::max()) ++ .count() < v) ++ { ++ return false; ++ } ++ ++ fmt.remove_prefix(pos + 1U); ++ return true; ++} + } // namespace details + ++/** ++ * @brief Convert string that represents value in Duration Format to its numeric ++ * equivalent. ++ */ ++std::optional<std::chrono::milliseconds> ++ fromDurationString(const std::string& str) ++{ ++ std::chrono::milliseconds out = std::chrono::milliseconds::zero(); ++ std::string_view v = str; ++ ++ if (v.empty()) ++ { ++ return out; ++ } ++ if (v.front() != 'P') ++ { ++ BMCWEB_LOG_ERROR << "Invalid duration format: " << str; ++ return std::nullopt; ++ } ++ ++ v.remove_prefix(1); ++ if (!details::fromDurationItem<details::Days>(v, 'D', out)) ++ { ++ BMCWEB_LOG_ERROR << "Invalid duration format: " << str; ++ return std::nullopt; ++ } ++ ++ if (v.empty()) ++ { ++ return out; ++ } ++ if (v.front() != 'T') ++ { ++ BMCWEB_LOG_ERROR << "Invalid duration format: " << str; ++ return std::nullopt; ++ } ++ ++ v.remove_prefix(1); ++ if (!details::fromDurationItem<std::chrono::hours>(v, 'H', out) || ++ !details::fromDurationItem<std::chrono::minutes>(v, 'M', out)) ++ { ++ BMCWEB_LOG_ERROR << "Invalid duration format: " << str; ++ return std::nullopt; ++ } ++ ++ if (v.find('.') != std::string::npos && v.find('S') != std::string::npos) ++ { ++ if (!details::fromDurationItem<std::chrono::seconds>(v, '.', out) || ++ !details::fromDurationItem<std::chrono::milliseconds>(v, 'S', out)) ++ { ++ BMCWEB_LOG_ERROR << "Invalid duration format: " << str; ++ return std::nullopt; ++ } ++ } ++ else if (!details::fromDurationItem<std::chrono::seconds>(v, 'S', out)) ++ { ++ BMCWEB_LOG_ERROR << "Invalid duration format: " << str; ++ return std::nullopt; ++ } ++ ++ if (!v.empty()) ++ { ++ BMCWEB_LOG_ERROR << "Invalid duration format: " << str; ++ return std::nullopt; ++ } ++ return out; ++} ++ + /** + * @brief Convert time value into duration format that is based on ISO 8601. + * Example output: "P12DT1M5.5S" +@@ -36,8 +178,7 @@ std::string toDurationString(std::chrono::milliseconds ms) + std::string fmt; + fmt.reserve(sizeof("PxxxxxxxxxxxxDTxxHxxMxx.xxxxxxS")); + +- using Days = std::chrono::duration<long, std::ratio<24 * 60 * 60>>; +- Days days = std::chrono::floor<Days>(ms); ++ details::Days days = std::chrono::floor<details::Days>(ms); + ms -= days; + + std::chrono::hours hours = std::chrono::floor<std::chrono::hours>(ms); +diff --git a/redfish-core/lib/metric_report_definition.hpp b/redfish-core/lib/metric_report_definition.hpp +index 48c56e6..d5a540d 100644 +--- a/redfish-core/lib/metric_report_definition.hpp ++++ b/redfish-core/lib/metric_report_definition.hpp +@@ -1,15 +1,26 @@ + #pragma once + + #include "node.hpp" ++#include "sensors.hpp" + #include "utils/telemetry_utils.hpp" + #include "utils/time_utils.hpp" + ++#include <boost/container/flat_map.hpp> ++ + #include <tuple> + #include <variant> + + namespace redfish + { + ++namespace telemetry ++{ ++ ++using ReadingParameters = ++ std::vector<std::tuple<std::vector<sdbusplus::message::object_path>, ++ std::string, std::string, std::string>>; ++} // namespace telemetry ++ + class MetricReportDefinitionCollection : public Node + { + public: +@@ -39,6 +50,318 @@ class MetricReportDefinitionCollection : public Node + telemetry::getReportCollection(asyncResp, + telemetry::metricReportDefinitionUri); + } ++ ++ struct AddReportArgs ++ { ++ std::string name; ++ std::string reportingType; ++ bool emitsReadingsUpdate = false; ++ bool logToMetricReportsCollection = false; ++ uint64_t interval = 0; ++ std::vector<std::pair<std::string, std::vector<std::string>>> metrics; ++ }; ++ ++ void doPost(crow::Response& res, const crow::Request& req, ++ const std::vector<std::string>&) override ++ { ++ auto asyncResp = std::make_shared<AsyncResp>(res); ++ AddReportArgs args; ++ if (!getUserParameters(res, req, args)) ++ { ++ return; ++ } ++ ++ boost::container::flat_set<std::pair<std::string, std::string>> ++ chassisSensors; ++ if (!getChassisSensorNode(asyncResp, args.metrics, chassisSensors)) ++ { ++ return; ++ } ++ ++ auto addReportReq = ++ std::make_shared<AddReport>(std::move(args), asyncResp); ++ for (const auto& [chassis, sensorType] : chassisSensors) ++ { ++ retrieveUriToDbusMap( ++ chassis, sensorType, ++ [asyncResp, addReportReq]( ++ const boost::beast::http::status status, ++ const boost::container::flat_map<std::string, std::string>& ++ uriToDbus) { ++ if (status != boost::beast::http::status::ok) ++ { ++ BMCWEB_LOG_ERROR << "Failed to retrieve URI to dbus " ++ "sensors map with err " ++ << static_cast<unsigned>(status); ++ messages::internalError(asyncResp->res); ++ return; ++ } ++ addReportReq->insert(uriToDbus); ++ }); ++ } ++ } ++ ++ static bool toDbusReportActions(crow::Response& res, ++ std::vector<std::string>& actions, ++ AddReportArgs& args) ++ { ++ size_t index = 0; ++ for (auto& action : actions) ++ { ++ if (action == "RedfishEvent") ++ { ++ args.emitsReadingsUpdate = true; ++ } ++ else if (action == "LogToMetricReportsCollection") ++ { ++ args.logToMetricReportsCollection = true; ++ } ++ else ++ { ++ messages::propertyValueNotInList( ++ res, action, "ReportActions/" + std::to_string(index)); ++ return false; ++ } ++ index++; ++ } ++ return true; ++ } ++ ++ static bool getUserParameters(crow::Response& res, const crow::Request& req, ++ AddReportArgs& args) ++ { ++ std::vector<nlohmann::json> metrics; ++ std::vector<std::string> reportActions; ++ std::optional<nlohmann::json> schedule; ++ if (!json_util::readJson(req, res, "Id", args.name, "Metrics", metrics, ++ "MetricReportDefinitionType", ++ args.reportingType, "ReportActions", ++ reportActions, "Schedule", schedule)) ++ { ++ return false; ++ } ++ ++ constexpr const char* allowedCharactersInName = ++ "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"; ++ if (args.name.empty() || ++ args.name.find_first_not_of(allowedCharactersInName) != ++ std::string::npos) ++ { ++ BMCWEB_LOG_ERROR << "Failed to match " << args.name ++ << " with allowed character " ++ << allowedCharactersInName; ++ messages::propertyValueIncorrect(res, "Id", args.name); ++ return false; ++ } ++ ++ if (args.reportingType != "Periodic" && ++ args.reportingType != "OnRequest") ++ { ++ messages::propertyValueNotInList(res, args.reportingType, ++ "MetricReportDefinitionType"); ++ return false; ++ } ++ ++ if (!toDbusReportActions(res, reportActions, args)) ++ { ++ return false; ++ } ++ ++ if (args.reportingType == "Periodic") ++ { ++ if (!schedule) ++ { ++ messages::createFailedMissingReqProperties(res, "Schedule"); ++ return false; ++ } ++ ++ std::string durationStr; ++ if (!json_util::readJson(*schedule, res, "RecurrenceInterval", ++ durationStr)) ++ { ++ return false; ++ } ++ ++ std::optional<std::chrono::milliseconds> durationNum = ++ time_utils::fromDurationString(durationStr); ++ if (!durationNum) ++ { ++ messages::propertyValueIncorrect(res, "RecurrenceInterval", ++ durationStr); ++ return false; ++ } ++ args.interval = static_cast<uint64_t>(durationNum->count()); ++ } ++ ++ args.metrics.reserve(metrics.size()); ++ for (auto& m : metrics) ++ { ++ std::string id; ++ std::vector<std::string> uris; ++ if (!json_util::readJson(m, res, "MetricId", id, "MetricProperties", ++ uris)) ++ { ++ return false; ++ } ++ ++ args.metrics.emplace_back(std::move(id), std::move(uris)); ++ } ++ ++ return true; ++ } ++ ++ static bool getChassisSensorNode( ++ const std::shared_ptr<AsyncResp>& asyncResp, ++ const std::vector<std::pair<std::string, std::vector<std::string>>>& ++ metrics, ++ boost::container::flat_set<std::pair<std::string, std::string>>& ++ matched) ++ { ++ for (const auto& [id, uris] : metrics) ++ { ++ for (size_t i = 0; i < uris.size(); i++) ++ { ++ const std::string& uri = uris[i]; ++ std::string chassis; ++ std::string node; ++ ++ if (!boost::starts_with(uri, "/redfish/v1/Chassis/") || ++ !dbus::utility::getNthStringFromPath(uri, 3, chassis) || ++ !dbus::utility::getNthStringFromPath(uri, 4, node)) ++ { ++ BMCWEB_LOG_ERROR << "Failed to get chassis and sensor Node " ++ "from " ++ << uri; ++ messages::propertyValueIncorrect(asyncResp->res, uri, ++ "MetricProperties/" + ++ std::to_string(i)); ++ return false; ++ } ++ ++ if (boost::ends_with(node, "#")) ++ { ++ node.pop_back(); ++ } ++ ++ matched.emplace(std::move(chassis), std::move(node)); ++ } ++ } ++ return true; ++ } ++ ++ class AddReport ++ { ++ public: ++ AddReport(AddReportArgs argsIn, std::shared_ptr<AsyncResp> asyncResp) : ++ asyncResp{std::move(asyncResp)}, args{std::move(argsIn)} ++ {} ++ ~AddReport() ++ { ++ if (asyncResp->res.result() != boost::beast::http::status::ok) ++ { ++ return; ++ } ++ ++ telemetry::ReadingParameters readingParams; ++ readingParams.reserve(args.metrics.size()); ++ ++ for (const auto& [id, uris] : args.metrics) ++ { ++ std::vector<sdbusplus::message::object_path> dbusPaths; ++ dbusPaths.reserve(uris.size()); ++ ++ for (size_t i = 0; i < uris.size(); i++) ++ { ++ const std::string& uri = uris[i]; ++ auto el = uriToDbus.find(uri); ++ if (el == uriToDbus.end()) ++ { ++ BMCWEB_LOG_ERROR << "Failed to find DBus sensor " ++ "corresponding to URI " ++ << uri; ++ messages::propertyValueNotInList(asyncResp->res, uri, ++ "MetricProperties/" + ++ std::to_string(i)); ++ return; ++ } ++ ++ dbusPaths.emplace_back(el->second); ++ } ++ ++ nlohmann::json metadata; ++ metadata["MetricProperties"] = uris; ++ if (uris.size() == 1) ++ { ++ metadata["MetricProperty"] = uris[0]; ++ } ++ readingParams.emplace_back(std::move(dbusPaths), "SINGLE", id, ++ metadata.dump()); ++ } ++ ++ crow::connections::systemBus->async_method_call( ++ [asyncResp = asyncResp, name = args.name]( ++ const boost::system::error_code ec, const std::string&) { ++ if (ec == boost::system::errc::file_exists) ++ { ++ messages::resourceAlreadyExists( ++ asyncResp->res, "MetricReportDefinition", "Id", ++ name); ++ return; ++ } ++ if (ec == boost::system::errc::too_many_files_open) ++ { ++ messages::createLimitReachedForResource(asyncResp->res); ++ return; ++ } ++ if (ec == boost::system::errc::argument_list_too_long) ++ { ++ messages::propertyValueNotInList( ++ asyncResp->res, "/Exceeds supported size/", ++ "Metrics"); ++ return; ++ } ++ if (ec == boost::system::errc::not_supported) ++ { ++ messages::propertyValueNotInList( ++ asyncResp->res, ++ "/Only single property per metric is supported/", ++ "MetricProperties"); ++ return; ++ } ++ if (ec == boost::system::errc::invalid_argument) ++ { ++ messages::propertyValueNotInList( ++ asyncResp->res, "/Less then MinInterval/", ++ "RecurrenceInterval"); ++ return; ++ } ++ if (ec) ++ { ++ messages::internalError(asyncResp->res); ++ BMCWEB_LOG_ERROR << "respHandler DBus error " << ec; ++ return; ++ } ++ ++ messages::created(asyncResp->res); ++ }, ++ telemetry::service, "/xyz/openbmc_project/Telemetry/Reports", ++ "xyz.openbmc_project.Telemetry.ReportManager", "AddReport", ++ "TelemetryService/" + args.name, args.reportingType, ++ args.emitsReadingsUpdate, args.logToMetricReportsCollection, ++ args.interval, readingParams); ++ } ++ ++ void insert( ++ const boost::container::flat_map<std::string, std::string>& el) ++ { ++ uriToDbus.insert(el.begin(), el.end()); ++ } ++ ++ private: ++ std::shared_ptr<AsyncResp> asyncResp; ++ AddReportArgs args; ++ boost::container::flat_map<std::string, std::string> uriToDbus{}; ++ }; + }; + + class MetricReportDefinition : public Node +@@ -73,9 +396,10 @@ class MetricReportDefinition : public Node + crow::connections::systemBus->async_method_call( + [asyncResp, + id](const boost::system::error_code ec, +- const std::vector<std::pair< +- std::string, std::variant<bool, ReadingParameters, +- std::string, uint64_t>>>& ret) { ++ const std::vector< ++ std::pair<std::string, ++ std::variant<bool, telemetry::ReadingParameters, ++ std::string, uint64_t>>>& ret) { + if (ec.value() == EBADR) + { + messages::resourceNotFound(asyncResp->res, schemaType, id); +@@ -95,15 +419,11 @@ class MetricReportDefinition : public Node + telemetry::reportInterface); + } + +- using ReadingParameters = +- std::vector<std::tuple<std::vector<sdbusplus::message::object_path>, +- std::string, std::string, std::string>>; +- + static void fillReportDefinition( + const std::shared_ptr<AsyncResp>& asyncResp, const std::string& id, +- const std::vector< +- std::pair<std::string, std::variant<bool, ReadingParameters, +- std::string, uint64_t>>>& ret) ++ const std::vector<std::pair< ++ std::string, std::variant<bool, telemetry::ReadingParameters, ++ std::string, uint64_t>>>& ret) + { + asyncResp->res.jsonValue["@odata.type"] = schemaType; + asyncResp->res.jsonValue["@odata.id"] = +@@ -117,7 +437,7 @@ class MetricReportDefinition : public Node + + const bool* emitsReadingsUpdate = nullptr; + const bool* logToMetricReportsCollection = nullptr; +- const ReadingParameters* readingParams = nullptr; ++ const telemetry::ReadingParameters* readingParams = nullptr; + const std::string* reportingType = nullptr; + const uint64_t* interval = nullptr; + for (const auto& [key, var] : ret) +@@ -132,7 +452,7 @@ class MetricReportDefinition : public Node + } + else if (key == "ReadingParameters") + { +- readingParams = std::get_if<ReadingParameters>(&var); ++ readingParams = std::get_if<telemetry::ReadingParameters>(&var); + } + else if (key == "ReportingType") + { +@@ -180,6 +500,44 @@ class MetricReportDefinition : public Node + time_utils::toDurationString(std::chrono::milliseconds(*interval)); + } + ++ void doDelete(crow::Response& res, const crow::Request&, ++ const std::vector<std::string>& params) override ++ { ++ auto asyncResp = std::make_shared<AsyncResp>(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](const boost::system::error_code ec) { ++ /* ++ * boost::system::errc and std::errc are missing value for ++ * EBADR error that is defined in Linux. ++ */ ++ 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; ++ } ++ ++ asyncResp->res.result(boost::beast::http::status::no_content); ++ }, ++ telemetry::service, reportPath, "xyz.openbmc_project.Object.Delete", ++ "Delete"); ++ } ++ + static constexpr const char* schemaType = + "#MetricReportDefinition.v1_3_0.MetricReportDefinition"; + }; +-- +2.16.6 + |