summaryrefslogtreecommitdiff
path: root/meta-openbmc-mods/meta-common/recipes-phosphor/interfaces/bmcweb/telemetry/0002-Add-POST-and-DELETE-in-MetricReportDefinitions.patch
diff options
context:
space:
mode:
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.patch671
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
+