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();
     }