Add PATCH for MetricReportDefinition

Support for PATCH method is added to Metric Report Definition,
now selected read/write Report properties can be modified by PATCH method

Tested:
- Added Report via POST, overwrite editable properties via PATCH and
  fetched Report via GET checking if received data is properly modified

Signed-off-by: Lukasz Kazmierczak <lukasz.kazmierczak@intel.com>
Signed-off-by: Krzysztof Grobelny <krzysztof.grobelny@intel.com>
Change-Id: If75110a92c55c9e4f2415f0ed4471baa802643ff
diff --git a/redfish-core/include/utils/telemetry_utils.hpp b/redfish-core/include/utils/telemetry_utils.hpp
index 33e31f0..c5c88fa 100644
--- a/redfish-core/include/utils/telemetry_utils.hpp
+++ b/redfish-core/include/utils/telemetry_utils.hpp
@@ -20,14 +20,14 @@
 constexpr const char* service = "xyz.openbmc_project.Telemetry";
 constexpr const char* reportInterface = "xyz.openbmc_project.Telemetry.Report";
 
-inline std::string getDbusReportPath(const std::string& id)
+inline std::string getDbusReportPath(std::string_view id)
 {
     sdbusplus::message::object_path reportsPath(
         "/xyz/openbmc_project/Telemetry/Reports/TelemetryService");
     return {reportsPath / id};
 }
 
-inline std::string getDbusTriggerPath(const std::string& id)
+inline std::string getDbusTriggerPath(std::string_view id)
 {
     sdbusplus::message::object_path triggersPath(
         "/xyz/openbmc_project/Telemetry/Triggers/TelemetryService");
diff --git a/redfish-core/lib/metric_report_definition.hpp b/redfish-core/lib/metric_report_definition.hpp
index 5daaeba..e428161 100644
--- a/redfish-core/lib/metric_report_definition.hpp
+++ b/redfish-core/lib/metric_report_definition.hpp
@@ -37,6 +37,38 @@
     std::vector<std::tuple<sdbusplus::message::object_path, std::string>>,
     std::string, std::string, uint64_t>>;
 
+inline bool verifyCommonErrors(crow::Response& res, const std::string& id,
+                               const boost::system::error_code& ec)
+{
+    if (ec.value() == EBADR || ec == boost::system::errc::host_unreachable)
+    {
+        messages::resourceNotFound(res, "MetricReportDefinition", id);
+        return false;
+    }
+
+    if (ec == boost::system::errc::file_exists)
+    {
+        messages::resourceAlreadyExists(res, "MetricReportDefinition", "Id",
+                                        id);
+        return false;
+    }
+
+    if (ec == boost::system::errc::too_many_files_open)
+    {
+        messages::createLimitReachedForResource(res);
+        return false;
+    }
+
+    if (ec)
+    {
+        BMCWEB_LOG_ERROR("DBUS response error {}", ec);
+        messages::internalError(res);
+        return false;
+    }
+
+    return true;
+}
+
 inline metric_report_definition::ReportActionsEnum
     toRedfishReportAction(std::string_view dbusValue)
 {
@@ -377,7 +409,7 @@
 
 inline bool toDbusReportActions(crow::Response& res,
                                 const std::vector<std::string>& actions,
-                                AddReportArgs& args)
+                                std::vector<std::string>& outReportActions)
 {
     size_t index = 0;
     for (const std::string& action : actions)
@@ -385,13 +417,12 @@
         std::string dbusReportAction = toDbusReportAction(action);
         if (dbusReportAction.empty())
         {
-            messages::propertyValueNotInList(res, nlohmann::json(action).dump(),
-                                             "ReportActions/" +
-                                                 std::to_string(index));
+            messages::propertyValueNotInList(
+                res, action, "ReportActions/" + std::to_string(index));
             return false;
         }
 
-        args.reportActions.emplace_back(std::move(dbusReportAction));
+        outReportActions.emplace_back(std::move(dbusReportAction));
         index++;
     }
     return true;
@@ -561,7 +592,7 @@
 
     if (reportActionsStr)
     {
-        if (!toDbusReportActions(res, *reportActionsStr, args))
+        if (!toDbusReportActions(res, *reportActionsStr, args.reportActions))
         {
             return false;
         }
@@ -630,7 +661,7 @@
     AddReport(AddReportArgs argsIn,
               const std::shared_ptr<bmcweb::AsyncResp>& asyncRespIn) :
         asyncResp(asyncRespIn),
-        args{std::move(argsIn)}
+        args(std::move(argsIn))
     {}
 
     ~AddReport()
@@ -740,7 +771,290 @@
     AddReportArgs args;
     boost::container::flat_map<std::string, std::string> uriToDbus{};
 };
-} // namespace telemetry
+
+class UpdateMetrics
+{
+  public:
+    UpdateMetrics(std::string_view idIn,
+                  const std::shared_ptr<bmcweb::AsyncResp>& asyncRespIn) :
+        id(idIn),
+        asyncResp(asyncRespIn)
+    {}
+
+    ~UpdateMetrics()
+    {
+        try
+        {
+            setReadingParams();
+        }
+        catch (const std::exception& e)
+        {
+            BMCWEB_LOG_ERROR("{}", e.what());
+        }
+        catch (...)
+        {
+            BMCWEB_LOG_ERROR("Unknown error");
+        }
+    }
+
+    UpdateMetrics(const UpdateMetrics&) = delete;
+    UpdateMetrics(UpdateMetrics&&) = delete;
+    UpdateMetrics& operator=(const UpdateMetrics&) = delete;
+    UpdateMetrics& operator=(UpdateMetrics&&) = delete;
+
+    std::string id;
+    std::map<std::string, std::string> metricPropertyToDbusPaths;
+
+    void insert(const std::map<std::string, std::string>&
+                    additionalMetricPropertyToDbusPaths)
+    {
+        metricPropertyToDbusPaths.insert(
+            additionalMetricPropertyToDbusPaths.begin(),
+            additionalMetricPropertyToDbusPaths.end());
+    }
+
+    void emplace(std::span<const std::tuple<sdbusplus::message::object_path,
+                                            std::string>>
+                     pathAndUri,
+                 const AddReportArgs::MetricArgs& metricArgs)
+    {
+        readingParamsUris.emplace_back(metricArgs.uris);
+        readingParams.emplace_back(
+            std::vector(pathAndUri.begin(), pathAndUri.end()),
+            metricArgs.collectionFunction, metricArgs.collectionTimeScope,
+            metricArgs.collectionDuration);
+    }
+
+    void setReadingParams()
+    {
+        if (asyncResp->res.result() != boost::beast::http::status::ok)
+        {
+            return;
+        }
+
+        for (size_t index = 0; index < readingParamsUris.size(); ++index)
+        {
+            std::span<const std::string> newUris = readingParamsUris[index];
+
+            const std::optional<std::vector<
+                std::tuple<sdbusplus::message::object_path, std::string>>>
+                readingParam = sensorPathToUri(newUris);
+
+            if (!readingParam)
+            {
+                return;
+            }
+
+            std::get<0>(readingParams[index]) = *readingParam;
+        }
+
+        crow::connections::systemBus->async_method_call(
+            [asyncResp(this->asyncResp),
+             reportId = id](const boost::system::error_code& ec) {
+            if (!verifyCommonErrors(asyncResp->res, reportId, ec))
+            {
+                return;
+            }
+            },
+            "xyz.openbmc_project.Telemetry", getDbusReportPath(id),
+            "org.freedesktop.DBus.Properties", "Set",
+            "xyz.openbmc_project.Telemetry.Report", "ReadingParameters",
+            dbus::utility::DbusVariantType{readingParams});
+    }
+
+  private:
+    std::optional<
+        std::vector<std::tuple<sdbusplus::message::object_path, std::string>>>
+        sensorPathToUri(std::span<const std::string> uris) const
+    {
+        std::vector<std::tuple<sdbusplus::message::object_path, std::string>>
+            result;
+
+        for (const std::string& uri : uris)
+        {
+            auto it = metricPropertyToDbusPaths.find(uri);
+            if (it == metricPropertyToDbusPaths.end())
+            {
+                messages::propertyValueNotInList(asyncResp->res, uri,
+                                                 "MetricProperties");
+                return {};
+            }
+            result.emplace_back(it->second, uri);
+        }
+
+        return result;
+    }
+
+    const std::shared_ptr<bmcweb::AsyncResp> asyncResp;
+    std::vector<std::vector<std::string>> readingParamsUris;
+    ReadingParameters readingParams{};
+};
+
+inline void
+    setReportEnabled(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+                     std::string_view id, bool enabled)
+{
+    crow::connections::systemBus->async_method_call(
+        [asyncResp, id = std::string(id)](const boost::system::error_code& ec) {
+        if (!verifyCommonErrors(asyncResp->res, id, ec))
+        {
+            return;
+        }
+        },
+        "xyz.openbmc_project.Telemetry", getDbusReportPath(id),
+        "org.freedesktop.DBus.Properties", "Set",
+        "xyz.openbmc_project.Telemetry.Report", "Enabled",
+        dbus::utility::DbusVariantType{enabled});
+}
+
+inline void setReportTypeAndInterval(
+    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, std::string_view id,
+    const std::string& reportingType, uint64_t recurrenceInterval)
+{
+    crow::connections::systemBus->async_method_call(
+        [asyncResp, id = std::string(id)](const boost::system::error_code& ec) {
+        if (!verifyCommonErrors(asyncResp->res, id, ec))
+        {
+            return;
+        }
+        },
+        "xyz.openbmc_project.Telemetry", getDbusReportPath(id),
+        "xyz.openbmc_project.Telemetry.Report", "SetReportingProperties",
+        reportingType, recurrenceInterval);
+}
+
+inline void
+    setReportUpdates(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+                     std::string_view id, const std::string& reportUpdates)
+{
+    crow::connections::systemBus->async_method_call(
+        [asyncResp, id = std::string(id)](const boost::system::error_code& ec) {
+        if (!verifyCommonErrors(asyncResp->res, id, ec))
+        {
+            return;
+        }
+        },
+        "xyz.openbmc_project.Telemetry", getDbusReportPath(id),
+        "org.freedesktop.DBus.Properties", "Set",
+        "xyz.openbmc_project.Telemetry.Report", "ReportUpdates",
+        dbus::utility::DbusVariantType{reportUpdates});
+}
+
+inline void
+    setReportActions(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+                     std::string_view id,
+                     const std::vector<std::string>& dbusReportActions)
+{
+    crow::connections::systemBus->async_method_call(
+        [asyncResp, id = std::string(id)](const boost::system::error_code& ec) {
+        if (!verifyCommonErrors(asyncResp->res, id, ec))
+        {
+            return;
+        }
+        },
+        "xyz.openbmc_project.Telemetry", getDbusReportPath(id),
+        "org.freedesktop.DBus.Properties", "Set",
+        "xyz.openbmc_project.Telemetry.Report", "ReportActions",
+        dbus::utility::DbusVariantType{dbusReportActions});
+}
+
+inline void
+    setReportMetrics(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+                     std::string_view id, std::span<nlohmann::json> metrics)
+{
+    sdbusplus::asio::getAllProperties(
+        *crow::connections::systemBus, telemetry::service,
+        telemetry::getDbusReportPath(id), telemetry::reportInterface,
+        [asyncResp, id = std::string(id),
+         redfishMetrics = std::vector<nlohmann::json>(metrics.begin(),
+                                                      metrics.end())](
+            boost::system::error_code ec,
+            const dbus::utility::DBusPropertiesMap& properties) mutable {
+        if (!redfish::telemetry::verifyCommonErrors(asyncResp->res, id, ec))
+        {
+            return;
+        }
+
+        ReadingParameters readingParams;
+
+        const bool success = sdbusplus::unpackPropertiesNoThrow(
+            dbus_utils::UnpackErrorPrinter(), properties, "ReadingParameters",
+            readingParams);
+
+        if (!success)
+        {
+            messages::internalError(asyncResp->res);
+            return;
+        }
+
+        auto updateMetricsReq = std::make_shared<UpdateMetrics>(id, asyncResp);
+
+        boost::container::flat_set<std::pair<std::string, std::string>>
+            chassisSensors;
+
+        size_t index = 0;
+        for (nlohmann::json& metric : redfishMetrics)
+        {
+            if (metric.is_null())
+            {
+                continue;
+            }
+
+            AddReportArgs::MetricArgs metricArgs;
+            std::vector<
+                std::tuple<sdbusplus::message::object_path, std::string>>
+                pathAndUri;
+
+            if (index < readingParams.size())
+            {
+                const ReadingParameters::value_type& existing =
+                    readingParams[index];
+
+                pathAndUri = std::get<0>(existing);
+                metricArgs.collectionFunction = std::get<1>(existing);
+                metricArgs.collectionTimeScope = std::get<2>(existing);
+                metricArgs.collectionDuration = std::get<3>(existing);
+            }
+
+            if (!getUserMetric(asyncResp->res, metric, metricArgs))
+            {
+                return;
+            }
+
+            std::optional<IncorrectMetricUri> error =
+                getChassisSensorNode(metricArgs.uris, chassisSensors);
+
+            if (error)
+            {
+                messages::propertyValueIncorrect(
+                    asyncResp->res, error->uri,
+                    "MetricProperties/" + std::to_string(error->index));
+                return;
+            }
+
+            updateMetricsReq->emplace(pathAndUri, metricArgs);
+            index++;
+        }
+
+        for (const auto& [chassis, sensorType] : chassisSensors)
+        {
+            retrieveUriToDbusMap(
+                chassis, sensorType,
+                [asyncResp, updateMetricsReq](
+                    const boost::beast::http::status status,
+                    const std::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;
+                }
+                updateMetricsReq->insert(uriToDbus);
+                });
+        }
+        });
+}
 
 inline void handleMetricReportDefinitionCollectionHead(
     App& app, const crow::Request& req,
@@ -763,6 +1077,9 @@
     {
         return;
     }
+    asyncResp->res.addHeader(
+        boost::beast::http::field::link,
+        "</redfish/v1/JsonSchemas/MetricReportDefinition/MetricReportDefinition.json>; rel=describedby");
 
     asyncResp->res.jsonValue["@odata.type"] =
         "#MetricReportDefinitionCollection."
@@ -780,6 +1097,177 @@
 }
 
 inline void
+    handleReportPatch(App& app, const crow::Request& req,
+                      const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+                      std::string_view id)
+{
+    if (!redfish::setUpRedfishRoute(app, req, asyncResp))
+    {
+        return;
+    }
+
+    std::optional<std::string> reportingTypeStr;
+    std::optional<std::string> reportUpdatesStr;
+    std::optional<bool> metricReportDefinitionEnabled;
+    std::optional<std::vector<nlohmann::json>> metrics;
+    std::optional<std::vector<std::string>> reportActionsStr;
+    std::optional<nlohmann::json> schedule;
+
+    if (!json_util::readJsonPatch(
+            req, asyncResp->res, "Metrics", metrics,
+            "MetricReportDefinitionType", reportingTypeStr, "ReportUpdates",
+            reportUpdatesStr, "ReportActions", reportActionsStr, "Schedule",
+            schedule, "MetricReportDefinitionEnabled",
+            metricReportDefinitionEnabled))
+    {
+        return;
+    }
+
+    if (metricReportDefinitionEnabled)
+    {
+        setReportEnabled(asyncResp, id, *metricReportDefinitionEnabled);
+    }
+
+    if (reportUpdatesStr)
+    {
+        std::string dbusReportUpdates = toDbusReportUpdates(*reportUpdatesStr);
+        if (dbusReportUpdates.empty())
+        {
+            messages::propertyValueNotInList(asyncResp->res, *reportUpdatesStr,
+                                             "ReportUpdates");
+            return;
+        }
+        setReportUpdates(asyncResp, id, dbusReportUpdates);
+    }
+
+    if (reportActionsStr)
+    {
+        std::vector<std::string> dbusReportActions;
+        if (!toDbusReportActions(asyncResp->res, *reportActionsStr,
+                                 dbusReportActions))
+        {
+            return;
+        }
+        setReportActions(asyncResp, id, dbusReportActions);
+    }
+
+    if (reportingTypeStr || schedule)
+    {
+        std::string dbusReportingType;
+        if (reportingTypeStr)
+        {
+            dbusReportingType = toDbusReportingType(*reportingTypeStr);
+            if (dbusReportingType.empty())
+            {
+                messages::propertyValueNotInList(asyncResp->res,
+                                                 *reportingTypeStr,
+                                                 "MetricReportDefinitionType");
+                return;
+            }
+        }
+
+        uint64_t recurrenceInterval = std::numeric_limits<uint64_t>::max();
+        if (schedule)
+        {
+            std::string durationStr;
+            if (!json_util::readJson(*schedule, asyncResp->res,
+                                     "RecurrenceInterval", durationStr))
+            {
+                return;
+            }
+
+            std::optional<std::chrono::milliseconds> durationNum =
+                time_utils::fromDurationString(durationStr);
+            if (!durationNum || durationNum->count() < 0)
+            {
+                messages::propertyValueIncorrect(
+                    asyncResp->res, "RecurrenceInterval", durationStr);
+                return;
+            }
+
+            recurrenceInterval = static_cast<uint64_t>(durationNum->count());
+        }
+
+        setReportTypeAndInterval(asyncResp, id, dbusReportingType,
+                                 recurrenceInterval);
+    }
+
+    if (metrics)
+    {
+        setReportMetrics(asyncResp, id, *metrics);
+    }
+}
+
+inline void
+    handleReportDelete(App& app, const crow::Request& req,
+                       const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+                       std::string_view id)
+{
+    if (!redfish::setUpRedfishRoute(app, req, asyncResp))
+    {
+        return;
+    }
+
+    const std::string reportPath = getDbusReportPath(id);
+
+    crow::connections::systemBus->async_method_call(
+        [asyncResp,
+         reportId = std::string(id)](const boost::system::error_code& ec) {
+        if (!verifyCommonErrors(asyncResp->res, reportId, ec))
+        {
+            return;
+        }
+        asyncResp->res.result(boost::beast::http::status::no_content);
+        },
+        service, reportPath, "xyz.openbmc_project.Object.Delete", "Delete");
+}
+} // namespace telemetry
+
+inline void handleMetricReportDefinitionsPost(
+    App& app, const crow::Request& req,
+    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
+{
+    if (!redfish::setUpRedfishRoute(app, req, asyncResp))
+    {
+        return;
+    }
+
+    telemetry::AddReportArgs args;
+    if (!telemetry::getUserParameters(asyncResp->res, req, args))
+    {
+        return;
+    }
+
+    boost::container::flat_set<std::pair<std::string, std::string>>
+        chassisSensors;
+    if (!telemetry::getChassisSensorNodeFromMetrics(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 std::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);
+            });
+    }
+}
+
+inline void
     handleMetricReportHead(App& app, const crow::Request& req,
                            const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
                            const std::string& /*id*/)
@@ -811,16 +1299,8 @@
         telemetry::getDbusReportPath(id), telemetry::reportInterface,
         [asyncResp, id](const boost::system::error_code& ec,
                         const dbus::utility::DBusPropertiesMap& properties) {
-        if (ec.value() == EBADR || ec == boost::system::errc::host_unreachable)
+        if (!redfish::telemetry::verifyCommonErrors(asyncResp->res, id, ec))
         {
-            messages::resourceNotFound(asyncResp->res, "MetricReportDefinition",
-                                       id);
-            return;
-        }
-        if (ec)
-        {
-            BMCWEB_LOG_ERROR("respHandler DBus error {}", ec);
-            messages::internalError(asyncResp->res);
             return;
         }
 
@@ -871,57 +1351,19 @@
     BMCWEB_ROUTE(app, "/redfish/v1/TelemetryService/MetricReportDefinitions/")
         .privileges(redfish::privileges::headMetricReportDefinitionCollection)
         .methods(boost::beast::http::verb::head)(std::bind_front(
-            handleMetricReportDefinitionCollectionHead, std::ref(app)));
+            telemetry::handleMetricReportDefinitionCollectionHead,
+            std::ref(app)));
 
     BMCWEB_ROUTE(app, "/redfish/v1/TelemetryService/MetricReportDefinitions/")
         .privileges(redfish::privileges::getMetricReportDefinitionCollection)
         .methods(boost::beast::http::verb::get)(std::bind_front(
-            handleMetricReportDefinitionCollectionGet, std::ref(app)));
+            telemetry::handleMetricReportDefinitionCollectionGet,
+            std::ref(app)));
 
     BMCWEB_ROUTE(app, "/redfish/v1/TelemetryService/MetricReportDefinitions/")
         .privileges(redfish::privileges::postMetricReportDefinitionCollection)
         .methods(boost::beast::http::verb::post)(
-            [&app](const crow::Request& req,
-                   const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) {
-        if (!redfish::setUpRedfishRoute(app, req, asyncResp))
-        {
-            return;
-        }
-
-        telemetry::AddReportArgs args;
-        if (!telemetry::getUserParameters(asyncResp->res, req, args))
-        {
-            return;
-        }
-
-        boost::container::flat_set<std::pair<std::string, std::string>>
-            chassisSensors;
-        if (!telemetry::getChassisSensorNodeFromMetrics(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 std::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);
-                });
-        }
-        });
+            std::bind_front(handleMetricReportDefinitionsPost, std::ref(app)));
 }
 
 inline void requestRoutesMetricReportDefinition(App& app)
@@ -940,8 +1382,14 @@
 
     BMCWEB_ROUTE(app,
                  "/redfish/v1/TelemetryService/MetricReportDefinitions/<str>/")
-        .privileges(redfish::privileges::deleteMetricReportDefinitionCollection)
+        .privileges(redfish::privileges::deleteMetricReportDefinition)
         .methods(boost::beast::http::verb::delete_)(
             std::bind_front(handleMetricReportDelete, std::ref(app)));
+
+    BMCWEB_ROUTE(app,
+                 "/redfish/v1/TelemetryService/MetricReportDefinitions/<str>/")
+        .privileges(redfish::privileges::patchMetricReportDefinition)
+        .methods(boost::beast::http::verb::patch)(
+            std::bind_front(telemetry::handleReportPatch, std::ref(app)));
 }
 } // namespace redfish