Redfish EventService schema implementation
Add Redfish EventService schema support for
EventService - GET and PATCH methods.
EventDestinationCollections - GET and POST methods.
EventDestination - GET, PATCH and DELETE methods.
URI's:
/redfish/v1/EventService
/redfish/v1/EventService/Subscriptions
/redfish/v1/EventService/Subscriptions/<id>
Tested:
- Validated all default event config data using GET.
- Validated supported/unsupported properties change.
- Validated range parameters for retry and timeout.
- Added new subscription using POST and validated using GET.
- Modified subscription using PATCH and validated.
- Validated delete subscription.
- Validated negative case for eventTypes, RegistryPrefixes,
mandate properties for POST etc.
- Successfully ran the redfish validator tool.
Counter({'metadataNamespaces': 1739, 'pass': 24,
'skipOptional': 23, 'passGet': 3,
'serviceNamespaces': 3})
Validation has succeeded.
Signed-off-by: AppaRao Puli <apparao.puli@linux.intel.com>
Change-Id: I220de2cb85e73124753d95b7ee311f1c30e2cce4
diff --git a/redfish-core/include/redfish.hpp b/redfish-core/include/redfish.hpp
index aacfda0..62c7ad8 100644
--- a/redfish-core/include/redfish.hpp
+++ b/redfish-core/include/redfish.hpp
@@ -21,6 +21,7 @@
#include "../lib/chassis.hpp"
#include "../lib/cpudimm.hpp"
#include "../lib/ethernet.hpp"
+#include "../lib/event_service.hpp"
#include "../lib/log_services.hpp"
#include "../lib/managers.hpp"
#include "../lib/message_registries.hpp"
@@ -174,7 +175,9 @@
nodes.emplace_back(std::make_unique<TaskService>(app));
nodes.emplace_back(std::make_unique<TaskCollection>(app));
nodes.emplace_back(std::make_unique<Task>(app));
-
+ nodes.emplace_back(std::make_unique<EventService>(app));
+ nodes.emplace_back(std::make_unique<EventDestinationCollection>(app));
+ nodes.emplace_back(std::make_unique<EventDestination>(app));
for (const auto& node : nodes)
{
node->initPrivileges();
diff --git a/redfish-core/lib/event_service.hpp b/redfish-core/lib/event_service.hpp
new file mode 100644
index 0000000..6265491
--- /dev/null
+++ b/redfish-core/lib/event_service.hpp
@@ -0,0 +1,517 @@
+/*
+// Copyright (c) 2020 Intel Corporation
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+*/
+#pragma once
+#include "node.hpp"
+
+#include <boost/container/flat_map.hpp>
+#include <error_messages.hpp>
+#include <utils/json_utils.hpp>
+#include <variant>
+
+namespace redfish
+{
+
+static constexpr const std::array<const char*, 1> supportedEvtFormatTypes = {
+ "Event"};
+static constexpr const std::array<const char*, 3> supportedRegPrefixes = {
+ "Base", "OpenBMC", "Task"};
+static constexpr const std::array<const char*, 3> supportedRetryPolicies = {
+ "TerminateAfterRetries", "SuspendRetries", "RetryForever"};
+
+static constexpr const uint8_t maxNoOfSubscriptions = 20;
+
+struct EventSrvConfig
+{
+ bool enabled;
+ uint32_t retryAttempts;
+ uint32_t retryTimeoutInterval;
+};
+
+struct EventSrvSubscription
+{
+ std::string destinationUrl;
+ std::string protocol;
+ std::string retryPolicy;
+ std::string customText;
+ std::string eventFormatType;
+ std::string subscriptionType;
+ std::vector<std::string> registryMsgIds;
+ std::vector<std::string> registryPrefixes;
+ std::vector<nlohmann::json> httpHeaders; // key-value pair
+};
+
+EventSrvConfig configData;
+boost::container::flat_map<std::string, EventSrvSubscription> subscriptionsMap;
+
+inline void initEventSrvStore()
+{
+ // TODO: Read the persistent data from store and populate.
+ // Populating with default.
+ configData.enabled = true;
+ configData.retryAttempts = 3;
+ configData.retryTimeoutInterval = 30; // seconds
+}
+
+inline void updateSubscriptionData()
+{
+ // Persist the config and subscription data.
+ // TODO: subscriptionsMap & configData need to be
+ // written to Persist store.
+ return;
+}
+class EventService : public Node
+{
+ public:
+ EventService(CrowApp& app) : Node(app, "/redfish/v1/EventService/")
+ {
+ initEventSrvStore();
+
+ entityPrivileges = {
+ {boost::beast::http::verb::get, {{"Login"}}},
+ {boost::beast::http::verb::head, {{"Login"}}},
+ {boost::beast::http::verb::patch, {{"ConfigureManager"}}},
+ {boost::beast::http::verb::put, {{"ConfigureManager"}}},
+ {boost::beast::http::verb::delete_, {{"ConfigureManager"}}},
+ {boost::beast::http::verb::post, {{"ConfigureManager"}}}};
+ }
+
+ private:
+ void doGet(crow::Response& res, const crow::Request& req,
+ const std::vector<std::string>& params) override
+ {
+ auto asyncResp = std::make_shared<AsyncResp>(res);
+ res.jsonValue = {
+ {"@odata.type", "#EventService.v1_5_0.EventService"},
+ {"Id", "EventService"},
+ {"Name", "Event Service"},
+ {"ServerSentEventUri",
+ "/redfish/v1/EventService/Subscriptions/SSE"},
+ {"Subscriptions",
+ {{"@odata.id", "/redfish/v1/EventService/Subscriptions"}}},
+ {"@odata.id", "/redfish/v1/EventService"}};
+
+ asyncResp->res.jsonValue["Status"]["State"] = "Enabled";
+ asyncResp->res.jsonValue["ServiceEnabled"] = configData.enabled;
+ asyncResp->res.jsonValue["DeliveryRetryAttempts"] =
+ configData.retryAttempts;
+ asyncResp->res.jsonValue["DeliveryRetryIntervalSeconds"] =
+ configData.retryTimeoutInterval;
+ asyncResp->res.jsonValue["EventFormatTypes"] = supportedEvtFormatTypes;
+ asyncResp->res.jsonValue["RegistryPrefixes"] = supportedRegPrefixes;
+ }
+
+ void doPatch(crow::Response& res, const crow::Request& req,
+ const std::vector<std::string>& params) override
+ {
+ auto asyncResp = std::make_shared<AsyncResp>(res);
+
+ std::optional<bool> serviceEnabled;
+ std::optional<uint32_t> retryAttemps;
+ std::optional<uint32_t> retryInterval;
+
+ if (!json_util::readJson(req, res, "ServiceEnabled", serviceEnabled,
+ "DeliveryRetryAttempts", retryAttemps,
+ "DeliveryRetryIntervalSeconds", retryInterval))
+ {
+ return;
+ }
+
+ if (serviceEnabled)
+ {
+ configData.enabled = *serviceEnabled;
+ }
+
+ if (retryAttemps)
+ {
+ // Supported range [1-3]
+ if ((*retryAttemps < 1) || (*retryAttemps > 3))
+ {
+ messages::queryParameterOutOfRange(
+ asyncResp->res, std::to_string(*retryAttemps),
+ "DeliveryRetryAttempts", "[1-3]");
+ }
+ else
+ {
+ configData.retryAttempts = *retryAttemps;
+ }
+ }
+
+ if (retryInterval)
+ {
+ // Supported range [30 - 180]
+ if ((*retryInterval < 30) || (*retryInterval > 180))
+ {
+ messages::queryParameterOutOfRange(
+ asyncResp->res, std::to_string(*retryInterval),
+ "DeliveryRetryIntervalSeconds", "[30-180]");
+ }
+ else
+ {
+ configData.retryTimeoutInterval = *retryInterval;
+ }
+ }
+
+ updateSubscriptionData();
+ }
+};
+
+class EventDestinationCollection : public Node
+{
+ public:
+ EventDestinationCollection(CrowApp& app) :
+ Node(app, "/redfish/v1/EventService/Subscriptions/")
+ {
+ entityPrivileges = {
+ {boost::beast::http::verb::get, {{"Login"}}},
+ {boost::beast::http::verb::head, {{"Login"}}},
+ {boost::beast::http::verb::patch, {{"ConfigureManager"}}},
+ {boost::beast::http::verb::put, {{"ConfigureManager"}}},
+ {boost::beast::http::verb::delete_, {{"ConfigureManager"}}},
+ {boost::beast::http::verb::post, {{"ConfigureManager"}}}};
+ }
+
+ private:
+ void doGet(crow::Response& res, const crow::Request& req,
+ const std::vector<std::string>& params) override
+ {
+ auto asyncResp = std::make_shared<AsyncResp>(res);
+
+ res.jsonValue = {
+ {"@odata.type",
+ "#EventDestinationCollection.EventDestinationCollection"},
+ {"@odata.id", "/redfish/v1/EventService/Subscriptions"},
+ {"Name", "Event Destination Collections"}};
+
+ nlohmann::json& memberArray = asyncResp->res.jsonValue["Members"];
+ memberArray = nlohmann::json::array();
+ asyncResp->res.jsonValue["Members@odata.count"] =
+ subscriptionsMap.size();
+
+ for (auto& it : subscriptionsMap)
+ {
+ memberArray.push_back(
+ {{"@odata.id",
+ "/redfish/v1/EventService/Subscriptions/" + it.first}});
+ }
+ }
+
+ void doPost(crow::Response& res, const crow::Request& req,
+ const std::vector<std::string>& params) override
+ {
+ auto asyncResp = std::make_shared<AsyncResp>(res);
+
+ if (subscriptionsMap.size() >= maxNoOfSubscriptions)
+ {
+ messages::eventSubscriptionLimitExceeded(asyncResp->res);
+ return;
+ }
+ std::string destUrl;
+ std::string protocol;
+ std::optional<std::string> context;
+ std::optional<std::string> subscriptionType;
+ std::optional<std::string> eventFormatType;
+ std::optional<std::string> retryPolicy;
+ std::optional<std::vector<std::string>> msgIds;
+ std::optional<std::vector<std::string>> regPrefixes;
+ std::optional<std::vector<nlohmann::json>> headers;
+
+ if (!json_util::readJson(
+ req, res, "Destination", destUrl, "Context", context,
+ "Protocol", protocol, "SubscriptionType", subscriptionType,
+ "EventFormatType", eventFormatType, "HttpHeaders", headers,
+ "RegistryPrefixes", regPrefixes, "MessageIds", msgIds,
+ "DeliveryRetryPolicy", retryPolicy))
+ {
+ return;
+ }
+
+ EventSrvSubscription subValue;
+
+ // Validate the URL using regex expression
+ // Format: <protocol>://<host>:<port>/uri
+ // protocol: http/https, uri: can include params.
+ const std::regex urlRegex("(http|https)://([^/ :]+):?.*");
+ if (!std::regex_match(destUrl, urlRegex))
+ {
+ messages::propertyValueFormatError(asyncResp->res, destUrl,
+ "Destination");
+ return;
+ }
+ subValue.destinationUrl = destUrl;
+
+ if (subscriptionType)
+ {
+ if (*subscriptionType != "RedfishEvent")
+ {
+ messages::propertyValueNotInList(
+ asyncResp->res, *subscriptionType, "SubscriptionType");
+ return;
+ }
+ subValue.subscriptionType = *subscriptionType;
+ }
+ else
+ {
+ subValue.subscriptionType = "RedfishEvent"; // Default
+ }
+
+ if (protocol != "Redfish")
+ {
+ messages::propertyValueNotInList(asyncResp->res, protocol,
+ "Protocol");
+ return;
+ }
+ subValue.protocol = protocol;
+
+ if (eventFormatType)
+ {
+ if (std::find(supportedEvtFormatTypes.begin(),
+ supportedEvtFormatTypes.end(),
+ *eventFormatType) == supportedEvtFormatTypes.end())
+ {
+ messages::propertyValueNotInList(
+ asyncResp->res, *eventFormatType, "EventFormatType");
+ return;
+ }
+ subValue.eventFormatType = *eventFormatType;
+ }
+ else
+ {
+ // If not specified, use default "Event"
+ subValue.eventFormatType.assign({"Event"});
+ }
+
+ if (context)
+ {
+ subValue.customText = *context;
+ }
+
+ if (headers)
+ {
+ subValue.httpHeaders = *headers;
+ }
+
+ if (regPrefixes)
+ {
+ for (const std::string& it : *regPrefixes)
+ {
+ if (std::find(supportedRegPrefixes.begin(),
+ supportedRegPrefixes.end(),
+ it) == supportedRegPrefixes.end())
+ {
+ messages::propertyValueNotInList(asyncResp->res, it,
+ "RegistryPrefixes");
+ return;
+ }
+ }
+ subValue.registryPrefixes = *regPrefixes;
+ }
+
+ if (msgIds)
+ {
+ // Do we need to loop-up MessageRegistry and validate
+ // data for authenticity??? Not mandate, i believe.
+ subValue.registryMsgIds = *msgIds;
+ }
+
+ if (retryPolicy)
+ {
+ if (std::find(supportedRetryPolicies.begin(),
+ supportedRetryPolicies.end(),
+ *retryPolicy) == supportedRetryPolicies.end())
+ {
+ messages::propertyValueNotInList(asyncResp->res, *retryPolicy,
+ "DeliveryRetryPolicy");
+ return;
+ }
+ subValue.retryPolicy = *retryPolicy;
+ }
+ else
+ {
+ // Default "TerminateAfterRetries"
+ subValue.retryPolicy = "TerminateAfterRetries";
+ }
+
+ std::srand(static_cast<uint32_t>(std::time(0)));
+ std::string id;
+
+ int retry = 3;
+ while (retry)
+ {
+ id = std::to_string(std::rand());
+ auto inserted = subscriptionsMap.insert(std::pair(id, subValue));
+ if (inserted.second)
+ {
+ break;
+ }
+ retry--;
+ };
+
+ if (retry <= 0)
+ {
+ messages::internalError(asyncResp->res);
+ return;
+ }
+
+ updateSubscriptionData();
+
+ messages::created(asyncResp->res);
+ asyncResp->res.addHeader(
+ "Location", "/redfish/v1/EventService/Subscriptions/" + id);
+ }
+};
+
+class EventDestination : public Node
+{
+ public:
+ EventDestination(CrowApp& app) :
+ Node(app, "/redfish/v1/EventService/Subscriptions/<str>/",
+ std::string())
+ {
+ entityPrivileges = {
+ {boost::beast::http::verb::get, {{"Login"}}},
+ {boost::beast::http::verb::head, {{"Login"}}},
+ {boost::beast::http::verb::patch, {{"ConfigureManager"}}},
+ {boost::beast::http::verb::put, {{"ConfigureManager"}}},
+ {boost::beast::http::verb::delete_, {{"ConfigureManager"}}},
+ {boost::beast::http::verb::post, {{"ConfigureManager"}}}};
+ }
+
+ private:
+ void doGet(crow::Response& res, const crow::Request& req,
+ 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];
+ auto obj = subscriptionsMap.find(id);
+ if (obj == subscriptionsMap.end())
+ {
+ res.result(boost::beast::http::status::not_found);
+ res.end();
+ return;
+ }
+
+ EventSrvSubscription& subValue = obj->second;
+
+ res.jsonValue = {
+ {"@odata.type", "#EventDestination.v1_7_0.EventDestination"},
+ {"Protocol", "Redfish"}};
+ asyncResp->res.jsonValue["@odata.id"] =
+ "/redfish/v1/EventService/Subscriptions/" + id;
+ asyncResp->res.jsonValue["Id"] = id;
+ asyncResp->res.jsonValue["Name"] = "Event Destination " + id;
+ asyncResp->res.jsonValue["Destination"] = subValue.destinationUrl;
+ asyncResp->res.jsonValue["Context"] = subValue.customText;
+ asyncResp->res.jsonValue["SubscriptionType"] =
+ subValue.subscriptionType;
+ asyncResp->res.jsonValue["HttpHeaders"] = subValue.httpHeaders;
+ asyncResp->res.jsonValue["EventFormatType"] = subValue.eventFormatType;
+ asyncResp->res.jsonValue["RegistryPrefixes"] =
+ subValue.registryPrefixes;
+ asyncResp->res.jsonValue["MessageIds"] = subValue.registryMsgIds;
+ asyncResp->res.jsonValue["DeliveryRetryPolicy"] = subValue.retryPolicy;
+ }
+
+ void doPatch(crow::Response& res, const crow::Request& req,
+ 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];
+ auto obj = subscriptionsMap.find(id);
+ if (obj == subscriptionsMap.end())
+ {
+ res.result(boost::beast::http::status::not_found);
+ res.end();
+ return;
+ }
+
+ std::optional<std::string> context;
+ std::optional<std::string> retryPolicy;
+ std::optional<std::vector<nlohmann::json>> headers;
+
+ if (!json_util::readJson(req, res, "Context", context,
+ "DeliveryRetryPolicy", retryPolicy,
+ "HttpHeaders", headers))
+ {
+ return;
+ }
+
+ EventSrvSubscription& subValue = obj->second;
+
+ if (context)
+ {
+ subValue.customText = *context;
+ }
+
+ if (headers)
+ {
+ subValue.httpHeaders = *headers;
+ }
+
+ if (retryPolicy)
+ {
+ if (std::find(supportedRetryPolicies.begin(),
+ supportedRetryPolicies.end(),
+ *retryPolicy) == supportedRetryPolicies.end())
+ {
+ messages::propertyValueNotInList(asyncResp->res, *retryPolicy,
+ "DeliveryRetryPolicy");
+ return;
+ }
+ subValue.retryPolicy = *retryPolicy;
+ }
+
+ updateSubscriptionData();
+ }
+
+ void doDelete(crow::Response& res, const crow::Request& req,
+ 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];
+ auto obj = subscriptionsMap.find(id);
+ if (obj == subscriptionsMap.end())
+ {
+ res.result(boost::beast::http::status::not_found);
+ res.end();
+ return;
+ }
+
+ subscriptionsMap.erase(obj);
+
+ updateSubscriptionData();
+ }
+};
+
+} // namespace redfish
diff --git a/redfish-core/lib/service_root.hpp b/redfish-core/lib/service_root.hpp
index 1f4343e..b6bd6e0 100644
--- a/redfish-core/lib/service_root.hpp
+++ b/redfish-core/lib/service_root.hpp
@@ -67,6 +67,8 @@
res.jsonValue["CertificateService"] = {
{"@odata.id", "/redfish/v1/CertificateService"}};
res.jsonValue["Tasks"] = {{"@odata.id", "/redfish/v1/TaskService"}};
+ res.jsonValue["EventService"] = {
+ {"@odata.id", "/redfish/v1/EventService"}};
res.end();
}