From 008cc1b35ccb1508d3c71ff5c6cfc6c772f1744c Mon Sep 17 00:00:00 2001 From: Szymon Dompke Date: Wed, 17 Nov 2021 18:18:16 +0100 Subject: [PATCH] Add support for POST on TriggersCollection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added POST method on /redfish/v1/TelemetryService/Triggers uri, which creates new trigger in telemetry service, by using dbus call AddTrigger. By DMTF, most of the properties are not required, and as such are treated as optional. Some values can be deduced from others (like 'MetricType', depending on 'DiscreteTriggers' or 'NumericThresholds'). All properties provided in POST body by user will be verified against each other, and errors will be raised. Few examples of such situations: - 'MetricType' is set to 'Discrete' but 'NumericThresholds' was passed. - 'MetricType' is set to 'Numeric' but "DiscreteTriggers' or 'DiscreteTriggerCondition' were passed - 'DiscreteTriggerCondition' is set to 'Specified' but 'DiscreteTriggers' is an empty array or was not passed. - 'DiscreteTriggerCondition' is set to 'Changed' but 'DiscreteTriggers' is passed and is not an empty array. Example 1 – Trigger with discrete values: { "Id": "TestTrigger", "MetricType": "Discrete", "TriggerActions": [ "RedfishEvent" ], "DiscreteTriggerCondition": "Specified", "DiscreteTriggers": [ { "Value": "55.88", "DwellTime": "PT0.001S", "Severity": "Warning" }, { "Name": "My discrete trigger", "Value": "55.88", "DwellTime": "PT0.001S", "Severity": "OK" }, { "Value": "55.88", "DwellTime": "PT0.001S", "Severity": "Critical" } ], "MetricProperties": [ "/redfish/v1/Chassis/AC_Baseboard/Thermal#/Fans/0/Reading" ], "Links": { "MetricReportDefinitions": [] } } Example 2 – trigger with numeric threshold: { "Id": "TestTrigger2", "Name": "My Numeric Trigger", "MetricType": "Numeric", "TriggerActions": [ "RedfishEvent", "RedfishMetricReport" ], "NumericThresholds": { "UpperCritical": { "Reading": 50, "Activation": "Increasing", "DwellTime": "PT0.001S" }, "UpperWarning": { "Reading": 48.1, "Activation": "Increasing", "DwellTime": "PT0.004S" } }, "MetricProperties": [ "/redfish/v1/Chassis/AC_Baseboard/Thermal#/Fans/0/Reading", "/redfish/v1/Chassis/AC_Baseboard/Thermal#/Fans/17/Reading" ], "Links": { "MetricReportDefinitions": [ "/redfish/v1/TelemetryService/MetricReportDefinitions/PowerMetrics", "/redfish/v1/TelemetryService/MetricReportDefinitions/PowerMetricStats", "/redfish/v1/TelemetryService/MetricReportDefinitions/PlatformPowerUsage" ] } } Tested: - Triggers were successfully created with above example message bodies. This can be checked by calling: 'busctl tree xyz.openbmc_project.Telemetry'. - Expected errors were returned for messages with incorrect or mutually exclusive properties and incorrect values. - Redfish service validator is passing. Signed-off-by: Szymon Dompke Change-Id: Ief8c76de8aa660ae0d2dbe4610c26a28186a290a --- redfish-core/include/utils/finalizer.hpp | 35 ++ .../include/utils/telemetry_utils.hpp | 82 +++ redfish-core/lib/metric_report_definition.hpp | 31 +- redfish-core/lib/trigger.hpp | 526 +++++++++++++++++- 4 files changed, 642 insertions(+), 32 deletions(-) create mode 100644 redfish-core/include/utils/finalizer.hpp diff --git a/redfish-core/include/utils/finalizer.hpp b/redfish-core/include/utils/finalizer.hpp new file mode 100644 index 0000000..cb98507 --- /dev/null +++ b/redfish-core/include/utils/finalizer.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include + +namespace redfish +{ + +namespace utils +{ + +class Finalizer +{ + public: + Finalizer() = delete; + Finalizer(std::function finalizer) : finalizer(std::move(finalizer)) + {} + + Finalizer(const Finalizer&) = delete; + Finalizer(Finalizer&&) = delete; + + ~Finalizer() + { + if (finalizer) + { + finalizer(); + } + } + + private: + std::function finalizer; +}; + +} // namespace utils + +} // namespace redfish \ No newline at end of file diff --git a/redfish-core/include/utils/telemetry_utils.hpp b/redfish-core/include/utils/telemetry_utils.hpp index 8aeff0d..c68e40c 100644 --- a/redfish-core/include/utils/telemetry_utils.hpp +++ b/redfish-core/include/utils/telemetry_utils.hpp @@ -13,6 +13,9 @@ constexpr const char* metricReportDefinitionUri = "/redfish/v1/TelemetryService/MetricReportDefinitions"; constexpr const char* metricReportUri = "/redfish/v1/TelemetryService/MetricReports"; +constexpr const char* triggerInterface = + "xyz.openbmc_project.Telemetry.Trigger"; +constexpr const char* triggerUri = "/redfish/v1/TelemetryService/Triggers"; inline std::string getDbusReportPath(const std::string& id) { @@ -28,5 +31,84 @@ inline std::string getDbusTriggerPath(const std::string& id) return {triggersPath / id}; } +inline std::optional + getReportNameFromReportDefinitionUri(const std::string& uri) +{ + constexpr const char* uriPattern = + "/redfish/v1/TelemetryService/MetricReportDefinitions/"; + constexpr size_t idx = std::string_view(uriPattern).length(); + if (boost::starts_with(uri, uriPattern)) + { + return uri.substr(idx); + } + return std::nullopt; +} + +inline std::optional + getTriggerIdFromDbusPath(const std::string& dbusPath) +{ + constexpr const char* triggerTree = + "/xyz/openbmc_project/Telemetry/Triggers/TelemetryService/"; + constexpr size_t idx = std::string_view(triggerTree).length(); + if (boost::starts_with(dbusPath, triggerTree)) + { + return dbusPath.substr(idx); + } + return std::nullopt; +} + +inline bool getChassisSensorNode( + const std::shared_ptr& asyncResp, + const std::vector& uris, + boost::container::flat_set>& matched) +{ + size_t uriIdx = 0; + for (const std::string& uri : uris) + { + 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(uriIdx)); + return false; + } + + if (boost::ends_with(node, "#")) + { + node.pop_back(); + } + + matched.emplace(std::move(chassis), std::move(node)); + uriIdx++; + } + return true; +} + +inline std::optional + redfishActionToDbusAction(const std::string& redfishAction) +{ + if (redfishAction == "RedfishMetricReport") + { + return "UpdateReport"; + } + if (redfishAction == "RedfishEvent") + { + return "RedfishEvent"; + } + if (redfishAction == "LogToLogService") + { + return "LogToLogService"; + } + return std::nullopt; +} + } // namespace telemetry } // namespace redfish diff --git a/redfish-core/lib/metric_report_definition.hpp b/redfish-core/lib/metric_report_definition.hpp index cb38633..4007544 100644 --- a/redfish-core/lib/metric_report_definition.hpp +++ b/redfish-core/lib/metric_report_definition.hpp @@ -217,7 +217,7 @@ inline bool getUserParameters(crow::Response& res, const crow::Request& req, return true; } -inline bool getChassisSensorNode( +inline bool getChassisSensorNodeFromMetrics( const std::shared_ptr& asyncResp, const std::vector>>& metrics, @@ -225,30 +225,9 @@ inline bool getChassisSensorNode( { for (const auto& [id, uris] : metrics) { - for (size_t i = 0; i < uris.size(); i++) + if (!getChassisSensorNode(asyncResp, uris, matched)) { - 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 false; } } return true; @@ -382,8 +361,8 @@ inline void requestRoutesMetricReportDefinitionCollection(App& app) boost::container::flat_set> chassisSensors; - if (!telemetry::getChassisSensorNode(asyncResp, args.metrics, - chassisSensors)) + if (!telemetry::getChassisSensorNodeFromMetrics( + asyncResp, args.metrics, chassisSensors)) { return; } diff --git a/redfish-core/lib/trigger.hpp b/redfish-core/lib/trigger.hpp index 210468c..01c150e 100644 --- a/redfish-core/lib/trigger.hpp +++ b/redfish-core/lib/trigger.hpp @@ -1,7 +1,9 @@ #pragma once -#include "utils/collection.hpp" +#include "sensors.hpp" +#include "utils/finalizer.hpp" #include "utils/telemetry_utils.hpp" +#include "utils/time_utils.hpp" #include #include @@ -14,9 +16,10 @@ namespace redfish { namespace telemetry { -constexpr const char* triggerInterface = - "xyz.openbmc_project.Telemetry.Trigger"; -constexpr const char* triggerUri = "/redfish/v1/TelemetryService/Triggers"; + +static constexpr std::array + supportedNumericThresholdNames = {"UpperCritical", "LowerCritical", + "UpperWarning", "LowerWarning"}; using NumericThresholdParams = std::tuple; @@ -24,6 +27,10 @@ using NumericThresholdParams = using DiscreteThresholdParams = std::tuple; +using TriggerThresholdParams = + std::variant, + std::vector>; + using TriggerThresholdParamsExt = std::variant, std::vector>; @@ -35,6 +42,455 @@ using TriggerGetParamsVariant = std::variant>; +namespace add_trigger +{ + +enum class MetricType +{ + Discrete, + Numeric +}; + +enum class DiscreteCondition +{ + Specified, + Changed +}; + +struct Context +{ + struct + { + std::string id; + std::string name; + std::vector actions; + std::vector> + sensors; + std::vector reportNames; + TriggerThresholdParams thresholds; + } dbusArgs; + + struct + { + std::optional discreteCondition; + std::optional metricType; + std::optional> metricProperties; + } parsedInfo; + + boost::container::flat_map uriToDbusMerged{}; +}; + +inline std::optional getMetricType(const std::string& metricType) +{ + if (metricType == "Discrete") + { + return MetricType::Discrete; + } + if (metricType == "Numeric") + { + return MetricType::Numeric; + } + return std::nullopt; +} + +inline std::optional + getDiscreteCondition(const std::string& discreteTriggerCondition) +{ + if (discreteTriggerCondition == "Specified") + { + return DiscreteCondition::Specified; + } + if (discreteTriggerCondition == "Changed") + { + return DiscreteCondition::Changed; + } + return std::nullopt; +} + +inline bool parseNumericThresholds(crow::Response& res, + nlohmann::json& numericThresholds, + Context& ctx) +{ + if (!numericThresholds.is_object()) + { + messages::propertyValueTypeError(res, numericThresholds.dump(), + "NumericThresholds"); + return false; + } + + std::vector parsedParams; + parsedParams.reserve(numericThresholds.size()); + + for (auto& [thresholdName, thresholdData] : numericThresholds.items()) + { + if (std::find(supportedNumericThresholdNames.begin(), + supportedNumericThresholdNames.end(), + thresholdName) == supportedNumericThresholdNames.end()) + { + messages::propertyUnknown(res, thresholdName); + return false; + } + + double reading = .0; + std::string activation; + std::string dwellTimeStr; + + if (!json_util::readJson(thresholdData, res, "Reading", reading, + "Activation", activation, "DwellTime", + dwellTimeStr)) + { + return false; + } + + std::optional dwellTime = + time_utils::fromDurationString(dwellTimeStr); + if (!dwellTime) + { + messages::propertyValueIncorrect(res, "DwellTime", dwellTimeStr); + return false; + } + + parsedParams.emplace_back(thresholdName, + static_cast(dwellTime->count()), + activation, reading); + } + + ctx.dbusArgs.thresholds = std::move(parsedParams); + return true; +} + +inline bool parseDiscreteTriggers( + crow::Response& res, + std::optional>& discreteTriggers, Context& ctx) +{ + std::vector parsedParams; + if (!discreteTriggers) + { + ctx.dbusArgs.thresholds = std::move(parsedParams); + return true; + } + + parsedParams.reserve(discreteTriggers->size()); + for (nlohmann::json& thresholdInfo : *discreteTriggers) + { + std::optional name; + std::string value; + std::string dwellTimeStr; + std::string severity; + + if (!json_util::readJson(thresholdInfo, res, "Name", name, "Value", + value, "DwellTime", dwellTimeStr, "Severity", + severity)) + { + return false; + } + + std::optional dwellTime = + time_utils::fromDurationString(dwellTimeStr); + if (!dwellTime) + { + messages::propertyValueIncorrect(res, "DwellTime", dwellTimeStr); + return false; + } + + if (!name) + { + name = ""; + } + + parsedParams.emplace_back( + *name, severity, static_cast(dwellTime->count()), value); + } + + ctx.dbusArgs.thresholds = std::move(parsedParams); + return true; +} + +inline bool parseTriggerThresholds( + crow::Response& res, + std::optional>& discreteTriggers, + std::optional& numericThresholds, Context& ctx) +{ + if (discreteTriggers && numericThresholds) + { + messages::mutualExclusiveProperties(res, "DiscreteTriggers", + "NumericThresholds"); + return false; + } + + if (ctx.parsedInfo.discreteCondition) + { + if (numericThresholds) + { + messages::mutualExclusiveProperties(res, "DiscreteTriggerCondition", + "NumericThresholds"); + return false; + } + } + + if (ctx.parsedInfo.metricType) + { + if (*ctx.parsedInfo.metricType == MetricType::Discrete && + numericThresholds) + { + messages::propertyValueConflict(res, "NumericThresholds", + "MetricType"); + return false; + } + if (*ctx.parsedInfo.metricType == MetricType::Numeric && + discreteTriggers) + { + messages::propertyValueConflict(res, "DiscreteTriggers", + "MetricType"); + return false; + } + if (*ctx.parsedInfo.metricType == MetricType::Numeric && + ctx.parsedInfo.discreteCondition) + { + messages::propertyValueConflict(res, "DiscreteTriggers", + "DiscreteTriggerCondition"); + return false; + } + } + + if (discreteTriggers || ctx.parsedInfo.discreteCondition || + (ctx.parsedInfo.metricType && + *ctx.parsedInfo.metricType == MetricType::Discrete)) + { + if (ctx.parsedInfo.discreteCondition) + { + if (*ctx.parsedInfo.discreteCondition == + DiscreteCondition::Specified && + !discreteTriggers) + { + messages::createFailedMissingReqProperties(res, + "DiscreteTriggers"); + return false; + } + if (discreteTriggers && ((*ctx.parsedInfo.discreteCondition == + DiscreteCondition::Specified && + discreteTriggers->empty()) || + (*ctx.parsedInfo.discreteCondition == + DiscreteCondition::Changed && + !discreteTriggers->empty()))) + { + messages::propertyValueConflict(res, "DiscreteTriggers", + "DiscreteTriggerCondition"); + return false; + } + } + if (!parseDiscreteTriggers(res, discreteTriggers, ctx)) + { + return false; + } + } + else if (numericThresholds) + { + if (!parseNumericThresholds(res, *numericThresholds, ctx)) + { + return false; + } + } + else + { + messages::createFailedMissingReqProperties( + res, "'DiscreteTriggers', 'NumericThresholds', " + "'DiscreteTriggerCondition' or 'MetricType'"); + return false; + } + return true; +} + +inline bool parseLinks(crow::Response& res, nlohmann::json& links, Context& ctx) +{ + if (links.empty()) + { + return true; + } + + std::optional> metricReportDefinitions; + if (!json_util::readJson(links, res, "MetricReportDefinitions", + metricReportDefinitions)) + { + return false; + } + + if (metricReportDefinitions) + { + ctx.dbusArgs.reportNames.reserve(metricReportDefinitions->size()); + for (std::string& reportDefinionUri : *metricReportDefinitions) + { + std::optional reportName = + getReportNameFromReportDefinitionUri(reportDefinionUri); + if (!reportName) + { + messages::propertyValueIncorrect(res, "MetricReportDefinitions", + reportDefinionUri); + return false; + } + ctx.dbusArgs.reportNames.push_back(*reportName); + } + } + return true; +} + +inline bool parseMetricProperties(crow::Response& res, Context& ctx) +{ + if (!ctx.parsedInfo.metricProperties) + { + return true; + } + + ctx.dbusArgs.sensors.reserve(ctx.parsedInfo.metricProperties->size()); + + size_t uriIdx = 0; + for (const std::string& uri : *ctx.parsedInfo.metricProperties) + { + auto el = ctx.uriToDbusMerged.find(uri); + if (el == ctx.uriToDbusMerged.end()) + { + BMCWEB_LOG_ERROR << "Failed to find DBus sensor " + "corsresponding to URI " + << uri; + messages::propertyValueNotInList( + res, uri, "MetricProperties/" + std::to_string(uriIdx)); + return false; + } + + const std::string& dbusPath = el->second; + ctx.dbusArgs.sensors.emplace_back(dbusPath, uri); + uriIdx++; + } + return true; +} + +inline bool parsePostTriggerParams(crow::Response& res, + const crow::Request& req, Context& ctx) +{ + std::optional id; + std::optional name; + std::optional metricType; + std::optional> triggerActions; + std::optional discreteTriggerCondition; + std::optional> discreteTriggers; + std::optional numericThresholds; + std::optional links; + if (!json_util::readJson( + req, res, "Id", id, "Name", name, "MetricType", metricType, + "TriggerActions", triggerActions, "DiscreteTriggerCondition", + discreteTriggerCondition, "DiscreteTriggers", discreteTriggers, + "NumericThresholds", numericThresholds, "MetricProperties", + ctx.parsedInfo.metricProperties, "Links", links)) + { + return false; + } + + ctx.dbusArgs.id = id.value_or(""); + ctx.dbusArgs.name = name.value_or(""); + + if (metricType) + { + if (!(ctx.parsedInfo.metricType = getMetricType(*metricType))) + { + messages::propertyValueIncorrect(res, "MetricType", *metricType); + return false; + } + } + + if (discreteTriggerCondition) + { + if (!(ctx.parsedInfo.discreteCondition = + getDiscreteCondition(*discreteTriggerCondition))) + { + messages::propertyValueIncorrect(res, "DiscreteTriggerCondition", + *discreteTriggerCondition); + return false; + } + } + + if (triggerActions) + { + ctx.dbusArgs.actions.reserve(triggerActions->size()); + for (const std::string& action : *triggerActions) + { + if (const std::optional& dbusAction = + redfishActionToDbusAction(action)) + { + ctx.dbusArgs.actions.emplace_back(*dbusAction); + } + else + { + messages::propertyValueNotInList(res, action, "TriggerActions"); + return false; + } + } + } + + if (!parseTriggerThresholds(res, discreteTriggers, numericThresholds, ctx)) + { + return false; + } + + if (links) + { + if (!parseLinks(res, *links, ctx)) + { + return false; + } + } + return true; +} + +inline void createTrigger(const std::shared_ptr& asyncResp, + Context& ctx) +{ + crow::connections::systemBus->async_method_call( + [aResp = asyncResp, id = ctx.dbusArgs.id]( + const boost::system::error_code ec, const std::string& dbusPath) { + if (ec == boost::system::errc::file_exists) + { + messages::resourceAlreadyExists(aResp->res, "Trigger", "Id", + id); + return; + } + if (ec == boost::system::errc::too_many_files_open) + { + messages::createLimitReachedForResource(aResp->res); + return; + } + if (ec) + { + messages::internalError(aResp->res); + BMCWEB_LOG_ERROR << "respHandler DBus error " << ec; + return; + } + + const std::optional& triggerId = + getTriggerIdFromDbusPath(dbusPath); + if (!triggerId) + { + messages::internalError(aResp->res); + BMCWEB_LOG_ERROR << "Unknown data returned by " + "AddTrigger DBus method"; + return; + } + + messages::created(aResp->res); + aResp->res.addHeader("Location", + triggerUri + std::string("/") + *triggerId); + }, + service, "/xyz/openbmc_project/Telemetry/Triggers", + "xyz.openbmc_project.Telemetry.TriggerManager", "AddTrigger", + "TelemetryService/" + ctx.dbusArgs.id, ctx.dbusArgs.name, + ctx.dbusArgs.actions, ctx.dbusArgs.sensors, ctx.dbusArgs.reportNames, + ctx.dbusArgs.thresholds); +} + +} // namespace add_trigger + +namespace get_trigger +{ + inline std::optional getRedfishFromDbusAction(const std::string& dbusAction) { @@ -270,6 +726,8 @@ inline bool fillTrigger( return true; } +} // namespace get_trigger + } // namespace telemetry inline void requestRoutesTriggerCollection(App& app) @@ -290,6 +748,62 @@ inline void requestRoutesTriggerCollection(App& app) asyncResp, telemetry::triggerUri, interfaces, "/xyz/openbmc_project/Telemetry/Triggers/TelemetryService"); }); + + BMCWEB_ROUTE(app, "/redfish/v1/TelemetryService/Triggers/") + .privileges(redfish::privileges::postTriggersCollection) + .methods(boost::beast::http::verb::post)( + [](const crow::Request& req, + const std::shared_ptr& asyncResp) { + const auto ctx = + std::make_shared(); + if (!telemetry::add_trigger::parsePostTriggerParams( + asyncResp->res, req, *ctx)) + { + return; + } + + if (!ctx->parsedInfo.metricProperties || + ctx->parsedInfo.metricProperties->empty()) + { + telemetry::add_trigger::createTrigger(asyncResp, *ctx); + return; + } + + boost::container::flat_set> + chassisSensors; + if (!telemetry::getChassisSensorNode( + asyncResp, *ctx->parsedInfo.metricProperties, + chassisSensors)) + { + return; + } + + const auto finalizer = + std::make_shared([asyncResp, ctx] { + if (!telemetry::add_trigger::parseMetricProperties( + asyncResp->res, *ctx)) + { + return; + } + telemetry::add_trigger::createTrigger(asyncResp, *ctx); + }); + + for (const auto& [chassis, sensorType] : chassisSensors) + { + retrieveUriToDbusMap( + chassis, sensorType, + [asyncResp, ctx, + finalizer](const boost::beast::http::status status, + const boost::container::flat_map< + std::string, std::string>& uriToDbus) { + if (status == boost::beast::http::status::ok) + { + ctx->uriToDbusMerged.insert(uriToDbus.begin(), + uriToDbus.end()); + } + }); + } + }); } inline void requestRoutesTrigger(App& app) @@ -320,8 +834,8 @@ inline void requestRoutesTrigger(App& app) return; } - if (!telemetry::fillTrigger(asyncResp->res.jsonValue, - id, ret)) + if (!telemetry::get_trigger::fillTrigger( + asyncResp->res.jsonValue, id, ret)) { messages::internalError(asyncResp->res); } -- 2.25.1