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
diff --git a/meson.build b/meson.build
index 66a066b..22a8c4a 100644
--- a/meson.build
+++ b/meson.build
@@ -345,6 +345,7 @@
                      '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 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,9 +27,136 @@
         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"
  *        Ref: Redfish Specification, Section 9.4.4. Duration values
@@ -36,8 +171,7 @@
     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 @@
     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 @@
         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 @@
             "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));
+}