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 | 671 |
1 files changed, 671 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..aaf3f17c7 --- /dev/null +++ b/meta-openbmc-mods/meta-common/recipes-phosphor/interfaces/bmcweb/telemetry/0002-Add-POST-and-DELETE-in-MetricReportDefinitions.patch @@ -0,0 +1,671 @@ +From 03c4ece83b58b954323111a1a7b2bf2b61402d7e 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. +Added unit tests for conversion from and to Duration format. + +Tested: + - Tested using witherspoon image on QEMU + - Verified POST action in different cases: + - all parameters are provided, new report is added to collection + - some parameters are missing or invalid, user gets response with + description of the issue + - Verified that reports are removed on DELETE request + - Verified that on invalid DELETE request user receives response + with error + - Verified time_utils::fromDurationString() + - Succesfully passed RedfishServiceValidator.py + +Signed-off-by: Wludzik, Jozef <jozef.wludzik@intel.com> +Signed-off-by: Krzysztof Grobelny <krzysztof.grobelny@intel.com> +Change-Id: I2fed96848594451e22fde686f8c066d7770cc65a +--- + meson.build | 1 + + .../include/utils/telemetry_utils.hpp | 2 + + redfish-core/include/utils/time_utils.hpp | 138 +++++++- + redfish-core/lib/metric_report_definition.hpp | 328 ++++++++++++++++++ + redfish-core/ut/time_utils_test.cpp | 63 ++++ + 5 files changed, 530 insertions(+), 2 deletions(-) + create mode 100644 redfish-core/ut/time_utils_test.cpp + +diff --git a/meson.build b/meson.build +index 66a066b..22a8c4a 100644 +--- a/meson.build ++++ b/meson.build +@@ -345,6 +345,7 @@ srcfiles_unittest = ['include/ut/dbus_utility_test.cpp', + 'redfish-core/ut/privileges_test.cpp', + 'redfish-core/ut/lock_test.cpp', + 'redfish-core/ut/configfile_test.cpp', ++ 'redfish-core/ut/time_utils_test.cpp', + 'http/ut/utility_test.cpp'] + + # Gather the Configuration data +diff --git a/redfish-core/include/utils/telemetry_utils.hpp b/redfish-core/include/utils/telemetry_utils.hpp +index a3a8156..0a3af5f 100644 +--- a/redfish-core/include/utils/telemetry_utils.hpp ++++ b/redfish-core/include/utils/telemetry_utils.hpp +@@ -1,5 +1,7 @@ + #pragma once + ++#include "dbus_utility.hpp" ++ + namespace redfish + { + +diff --git a/redfish-core/include/utils/time_utils.hpp b/redfish-core/include/utils/time_utils.hpp +index 4a87ba0..9965d4d 100644 +--- a/redfish-core/include/utils/time_utils.hpp ++++ b/redfish-core/include/utils/time_utils.hpp +@@ -1,7 +1,13 @@ + #pragma once + ++#include "logging.hpp" ++ ++#include <charconv> + #include <chrono> ++#include <cmath> ++#include <optional> + #include <string> ++#include <system_error> + + namespace redfish + { +@@ -12,6 +18,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 +27,135 @@ inline void leftZeroPadding(std::string& str, const std::size_t padding) + str.insert(0, padding - str.size(), '0'); + } + } ++ ++template <typename FromTime> ++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; ++ } ++ ++ const char* end; ++ std::chrono::milliseconds::rep ticks = 0; ++ if constexpr (std::is_same_v<FromTime, std::chrono::milliseconds>) ++ { ++ end = fmt.data() + std::min<size_t>(pos, 3U); ++ } ++ else ++ { ++ end = fmt.data() + pos; ++ } ++ ++ auto [ptr, ec] = std::from_chars(fmt.data(), end, ticks); ++ if (ptr != end || ec != std::errc()) ++ { ++ BMCWEB_LOG_ERROR << "Failed to convert string to decimal with err: " ++ << static_cast<int>(ec) << "(" ++ << std::make_error_code(ec).message() << "), ptr{" ++ << static_cast<const void*>(ptr) << "} != end{" ++ << static_cast<const void*>(end) << "})"; ++ return false; ++ } ++ ++ if constexpr (std::is_same_v<FromTime, std::chrono::milliseconds>) ++ { ++ ticks *= static_cast<std::chrono::milliseconds::rep>( ++ std::pow(10, 3 - std::min<size_t>(pos, 3U))); ++ } ++ if (ticks < 0) ++ { ++ return false; ++ } ++ ++ out += FromTime(ticks); ++ const auto maxConversionRange = ++ std::chrono::duration_cast<FromTime>(std::chrono::milliseconds::max()) ++ .count(); ++ if (out < FromTime(ticks) || maxConversionRange < ticks) ++ { ++ 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 +171,7 @@ inline 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 59025d9..fcbc99c 100644 +--- a/redfish-core/lib/metric_report_definition.hpp ++++ b/redfish-core/lib/metric_report_definition.hpp +@@ -1,9 +1,12 @@ + #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> + +@@ -95,6 +98,252 @@ inline void fillReportDefinition( + asyncResp->res.jsonValue["Schedule"]["RecurrenceInterval"] = + time_utils::toDurationString(std::chrono::milliseconds(*interval)); + } ++ ++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; ++}; ++ ++inline 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; ++} ++ ++inline 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; ++} ++ ++inline 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) ++ { ++ 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; ++ } ++ ++ const std::string& dbusPath = el->second; ++ readingParams.emplace_back(dbusPath, "SINGLE", id, uri); ++ } ++ } ++ ++ crow::connections::systemBus->async_method_call( ++ [asyncResp = std::move(asyncResp), name = args.name, ++ uriToDbus = std::move(uriToDbus)]( ++ 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) ++ { ++ nlohmann::json metricProperties = nlohmann::json::array(); ++ for (const auto& [uri, _] : uriToDbus) ++ { ++ metricProperties.emplace_back(uri); ++ } ++ messages::propertyValueIncorrect( ++ asyncResp->res, metricProperties, "MetricProperties"); ++ 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{}; ++}; + } // namespace telemetry + + class MetricReportDefinitionCollection : public Node +@@ -126,6 +375,46 @@ class MetricReportDefinitionCollection : public Node + telemetry::getReportCollection(asyncResp, + telemetry::metricReportDefinitionUri); + } ++ ++ void doPost(crow::Response& res, const crow::Request& req, ++ const std::vector<std::string>&) override ++ { ++ auto asyncResp = std::make_shared<AsyncResp>(res); ++ telemetry::AddReportArgs args; ++ if (!telemetry::getUserParameters(res, req, args)) ++ { ++ return; ++ } ++ ++ boost::container::flat_set<std::pair<std::string, std::string>> ++ chassisSensors; ++ if (!telemetry::getChassisSensorNode(asyncResp, args.metrics, ++ chassisSensors)) ++ { ++ return; ++ } ++ ++ auto addReportReq = ++ std::make_shared<telemetry::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); ++ return; ++ } ++ addReportReq->insert(uriToDbus); ++ }); ++ } ++ } + }; + + class MetricReportDefinition : public Node +@@ -184,5 +473,44 @@ class MetricReportDefinition : public Node + "org.freedesktop.DBus.Properties", "GetAll", + telemetry::reportInterface); + } ++ ++ 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, ++ "MetricReportDefinition", 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"); ++ } + }; + } // namespace redfish +diff --git a/redfish-core/ut/time_utils_test.cpp b/redfish-core/ut/time_utils_test.cpp +new file mode 100644 +index 0000000..70999ce +--- /dev/null ++++ b/redfish-core/ut/time_utils_test.cpp +@@ -0,0 +1,63 @@ ++#include "utils/time_utils.hpp" ++ ++#include <gmock/gmock.h> ++ ++using namespace testing; ++ ++class FromDurationTest : ++ public Test, ++ public WithParamInterface< ++ std::pair<std::string, std::optional<std::chrono::milliseconds>>> ++{}; ++ ++INSTANTIATE_TEST_SUITE_P( ++ _, FromDurationTest, ++ Values(std::make_pair("PT12S", std::chrono::milliseconds(12000)), ++ std::make_pair("PT0.204S", std::chrono::milliseconds(204)), ++ std::make_pair("PT0.2S", std::chrono::milliseconds(200)), ++ std::make_pair("PT50M", std::chrono::milliseconds(3000000)), ++ std::make_pair("PT23H", std::chrono::milliseconds(82800000)), ++ std::make_pair("P51D", std::chrono::milliseconds(4406400000)), ++ std::make_pair("PT2H40M10.1S", std::chrono::milliseconds(9610100)), ++ std::make_pair("P20DT2H40M10.1S", ++ std::chrono::milliseconds(1737610100)), ++ std::make_pair("", std::chrono::milliseconds(0)), ++ std::make_pair("PTS", std::nullopt), ++ std::make_pair("P1T", std::nullopt), ++ std::make_pair("PT100M1000S100", std::nullopt), ++ std::make_pair("PDTHMS", std::nullopt), ++ std::make_pair("P99999999999999999DT", std::nullopt), ++ std::make_pair("PD222T222H222M222.222S", std::nullopt), ++ std::make_pair("PT99999H9999999999999999999999M99999999999S", ++ std::nullopt), ++ std::make_pair("PT-9H", std::nullopt))); ++ ++TEST_P(FromDurationTest, convertToMilliseconds) ++{ ++ const auto& [str, expected] = GetParam(); ++ EXPECT_THAT(redfish::time_utils::fromDurationString(str), Eq(expected)); ++} ++ ++class ToDurationTest : ++ public Test, ++ public WithParamInterface<std::pair<std::chrono::milliseconds, std::string>> ++{}; ++ ++INSTANTIATE_TEST_SUITE_P( ++ _, ToDurationTest, ++ Values(std::make_pair(std::chrono::milliseconds(12000), "PT12.000S"), ++ std::make_pair(std::chrono::milliseconds(204), "PT0.204S"), ++ std::make_pair(std::chrono::milliseconds(200), "PT0.200S"), ++ std::make_pair(std::chrono::milliseconds(3000000), "PT50M"), ++ std::make_pair(std::chrono::milliseconds(82800000), "PT23H"), ++ std::make_pair(std::chrono::milliseconds(4406400000), "P51DT"), ++ std::make_pair(std::chrono::milliseconds(9610100), "PT2H40M10.100S"), ++ std::make_pair(std::chrono::milliseconds(1737610100), ++ "P20DT2H40M10.100S"), ++ std::make_pair(std::chrono::milliseconds(-250), ""))); ++ ++TEST_P(ToDurationTest, convertToDuration) ++{ ++ const auto& [ms, expected] = GetParam(); ++ EXPECT_THAT(redfish::time_utils::toDurationString(ms), Eq(expected)); ++} +-- +2.17.1 + |