EventService:persistent config & subscription info

This commit is to persist EventService configuration and subscription
information across the bmcweb service restart or bmc reboot by
reading/writing config store data to the json file
(location: /var/lib/bmcweb/eventservice_config.json) and loads this
while initializing bmcweb EventService.

URI's:
/redfish/v1/EventService
/redfish/v1/EventService/Subscriptions
/redfish/v1/EventService/Subscriptions/<id>

Tested:
 - Validated initialization and reading of config and subscription info
   from persist store.
 - Validated updation and writing of config and subscription info to the
   persist store:
      - Added new subscription using POST and validated using GET.
      - Validated delete subscription.
 - Validated subscription list is persistent after multiple bmc reboots
      - Verified by GET req on subscription collection and getting
        specific subscription id's.
 - Ran redfish validator successfully
      - Created some subscriptions
      - Rebooted BMC
      - Previous subscriptions were intact
      - Ran validator and verified.

Change-Id: I9f044887b0c5b7559be58a6564b04585dc384be2
Signed-off-by: Ayushi Smriti <smriti.ayushi@linux.intel.com>
Signed-off-by: AppaRao Puli <apparao.puli@linux.intel.com>
diff --git a/redfish-core/include/event_service_manager.hpp b/redfish-core/include/event_service_manager.hpp
index b9779b6..e94bbed 100644
--- a/redfish-core/include/event_service_manager.hpp
+++ b/redfish-core/include/event_service_manager.hpp
@@ -25,6 +25,7 @@
 #include <cstdlib>
 #include <ctime>
 #include <error_messages.hpp>
+#include <fstream>
 #include <http_client.hpp>
 #include <memory>
 #include <utils/json_utils.hpp>
@@ -40,6 +41,9 @@
 static constexpr const char* eventFormatType = "Event";
 static constexpr const char* metricReportFormatType = "MetricReport";
 
+static constexpr const char* eventServiceFile =
+    "/var/lib/bmcweb/eventservice_config.json";
+
 #ifndef BMCWEB_ENABLE_REDFISH_DBUS_LOG_ENTRIES
 constexpr const char* redfishEventLogFile = "/var/log/redfish";
 constexpr const uint32_t inotifyFileAction = IN_MODIFY;
@@ -447,6 +451,13 @@
     std::shared_ptr<crow::HttpClient> conn;
 };
 
+static constexpr const bool defaultEnabledState = true;
+static constexpr const uint32_t defaultRetryAttempts = 3;
+static constexpr const uint32_t defaultRetryInterval = 30;
+static constexpr const char* defaulEventFormatType = "Event";
+static constexpr const char* defaulSubscriptionType = "RedfishEvent";
+static constexpr const char* defaulRetryPolicy = "TerminateAfterRetries";
+
 class EventServiceManager
 {
   private:
@@ -462,11 +473,8 @@
     EventServiceManager() :
         noOfEventLogSubscribers(0), noOfMetricReportSubscribers(0)
     {
-        // TODO: Read the persistent data from store and populate.
-        // Populating with default.
-        serviceEnabled = true;
-        retryAttempts = 3;
-        retryTimeoutInterval = 30; // seconds
+        // Load config from persist store.
+        initConfig();
     }
 
     std::string lastEventTStr;
@@ -483,12 +491,180 @@
         return handler;
     }
 
+    void loadDefaultConfig()
+    {
+        serviceEnabled = defaultEnabledState;
+        retryAttempts = defaultRetryAttempts;
+        retryTimeoutInterval = defaultRetryInterval;
+    }
+
+    void initConfig()
+    {
+        std::ifstream eventConfigFile(eventServiceFile);
+        if (!eventConfigFile.good())
+        {
+            BMCWEB_LOG_DEBUG << "EventService config not exist";
+            loadDefaultConfig();
+            return;
+        }
+        auto jsonData = nlohmann::json::parse(eventConfigFile, nullptr, false);
+        if (jsonData.is_discarded())
+        {
+            BMCWEB_LOG_ERROR << "EventService config parse error.";
+            loadDefaultConfig();
+            return;
+        }
+
+        nlohmann::json jsonConfig;
+        if (json_util::getValueFromJsonObject(jsonData, "Configuration",
+                                              jsonConfig))
+        {
+            if (!json_util::getValueFromJsonObject(jsonConfig, "ServiceEnabled",
+                                                   serviceEnabled))
+            {
+                serviceEnabled = defaultEnabledState;
+            }
+            if (!json_util::getValueFromJsonObject(
+                    jsonConfig, "DeliveryRetryAttempts", retryAttempts))
+            {
+                retryAttempts = defaultRetryAttempts;
+            }
+            if (!json_util::getValueFromJsonObject(
+                    jsonConfig, "DeliveryRetryIntervalSeconds",
+                    retryTimeoutInterval))
+            {
+                retryTimeoutInterval = defaultRetryInterval;
+            }
+        }
+        else
+        {
+            loadDefaultConfig();
+        }
+
+        nlohmann::json subscriptionsList;
+        if (!json_util::getValueFromJsonObject(jsonData, "Subscriptions",
+                                               subscriptionsList))
+        {
+            BMCWEB_LOG_DEBUG << "EventService: Subscriptions not exist.";
+            return;
+        }
+
+        for (nlohmann::json& jsonObj : subscriptionsList)
+        {
+            std::string protocol;
+            if (!json_util::getValueFromJsonObject(jsonObj, "Protocol",
+                                                   protocol))
+            {
+                BMCWEB_LOG_DEBUG << "Invalid subscription Protocol exist.";
+                continue;
+            }
+            std::string destination;
+            if (!json_util::getValueFromJsonObject(jsonObj, "Destination",
+                                                   destination))
+            {
+                BMCWEB_LOG_DEBUG << "Invalid subscription destination exist.";
+                continue;
+            }
+            std::string host;
+            std::string urlProto;
+            std::string port;
+            std::string path;
+            bool status =
+                validateAndSplitUrl(destination, urlProto, host, port, path);
+
+            if (!status)
+            {
+                BMCWEB_LOG_ERROR
+                    << "Failed to validate and split destination url";
+                continue;
+            }
+            std::shared_ptr<Subscription> subValue =
+                std::make_shared<Subscription>(host, port, path, urlProto);
+
+            subValue->destinationUrl = destination;
+            subValue->protocol = protocol;
+            if (!json_util::getValueFromJsonObject(
+                    jsonObj, "DeliveryRetryPolicy", subValue->retryPolicy))
+            {
+                subValue->retryPolicy = defaulRetryPolicy;
+            }
+            if (!json_util::getValueFromJsonObject(jsonObj, "EventFormatType",
+                                                   subValue->eventFormatType))
+            {
+                subValue->eventFormatType = defaulEventFormatType;
+            }
+            if (!json_util::getValueFromJsonObject(jsonObj, "SubscriptionType",
+                                                   subValue->subscriptionType))
+            {
+                subValue->subscriptionType = defaulSubscriptionType;
+            }
+
+            json_util::getValueFromJsonObject(jsonObj, "Context",
+                                              subValue->customText);
+            json_util::getValueFromJsonObject(jsonObj, "MessageIds",
+                                              subValue->registryMsgIds);
+            json_util::getValueFromJsonObject(jsonObj, "RegistryPrefixes",
+                                              subValue->registryPrefixes);
+            json_util::getValueFromJsonObject(jsonObj, "HttpHeaders",
+                                              subValue->httpHeaders);
+            json_util::getValueFromJsonObject(
+                jsonObj, "MetricReportDefinitions",
+                subValue->metricReportDefinitions);
+
+            std::string id = addSubscription(subValue, false);
+            if (id.empty())
+            {
+                BMCWEB_LOG_ERROR << "Failed to add subscription";
+            }
+        }
+        return;
+    }
+
     void updateSubscriptionData()
     {
         // Persist the config and subscription data.
-        // TODO: subscriptionsMap & configData need to be
-        // written to Persist store.
-        return;
+        nlohmann::json jsonData;
+
+        nlohmann::json& configObj = jsonData["Configuration"];
+        configObj["ServiceEnabled"] = serviceEnabled;
+        configObj["DeliveryRetryAttempts"] = retryAttempts;
+        configObj["DeliveryRetryIntervalSeconds"] = retryTimeoutInterval;
+
+        nlohmann::json& subListArray = jsonData["Subscriptions"];
+        subListArray = nlohmann::json::array();
+
+        for (const auto& it : subscriptionsMap)
+        {
+            nlohmann::json entry;
+            std::shared_ptr<Subscription> subValue = it.second;
+
+            entry["Context"] = subValue->customText;
+            entry["DeliveryRetryPolicy"] = subValue->retryPolicy;
+            entry["Destination"] = subValue->destinationUrl;
+            entry["EventFormatType"] = subValue->eventFormatType;
+            entry["HttpHeaders"] = subValue->httpHeaders;
+            entry["MessageIds"] = subValue->registryMsgIds;
+            entry["Protocol"] = subValue->protocol;
+            entry["RegistryPrefixes"] = subValue->registryPrefixes;
+            entry["SubscriptionType"] = subValue->subscriptionType;
+            entry["MetricReportDefinitions"] =
+                subValue->metricReportDefinitions;
+
+            subListArray.push_back(entry);
+        }
+
+        const std::string tmpFile(std::string(eventServiceFile) + "_tmp");
+        std::ofstream ofs(tmpFile, std::ios::out);
+        const auto& writeData = jsonData.dump();
+        ofs << writeData;
+        ofs.close();
+
+        BMCWEB_LOG_DEBUG << "EventService config updated to file.";
+        if (std::rename(tmpFile.c_str(), eventServiceFile) != 0)
+        {
+            BMCWEB_LOG_ERROR << "Error in renaming temporary file: "
+                             << tmpFile.c_str();
+        }
     }
 
     EventServiceConfig getEventServiceConfig()
@@ -576,7 +752,8 @@
         return subValue;
     }
 
-    std::string addSubscription(const std::shared_ptr<Subscription> subValue)
+    std::string addSubscription(const std::shared_ptr<Subscription> subValue,
+                                const bool updateFile = true)
     {
         std::srand(static_cast<uint32_t>(std::time(0)));
         std::string id;
@@ -600,7 +777,11 @@
         }
 
         updateNoOfSubscribersCount();
-        updateSubscriptionData();
+
+        if (updateFile)
+        {
+            updateSubscriptionData();
+        }
 
 #ifndef BMCWEB_ENABLE_REDFISH_DBUS_LOG_ENTRIES
         if (lastEventTStr.empty())
@@ -934,6 +1115,52 @@
                 getMetricReading(service, objPath, intf);
             });
     }
-};
+
+    bool validateAndSplitUrl(const std::string& destUrl, std::string& urlProto,
+                             std::string& host, std::string& port,
+                             std::string& path)
+    {
+        // Validate URL using regex expression
+        // Format: <protocol>://<host>:<port>/<path>
+        // protocol: http/https
+        const std::regex urlRegex(
+            "(http|https)://([^/\\x20\\x3f\\x23\\x3a]+):?([0-9]*)(/"
+            "([^\\x20\\x23\\x3f]*\\x3f?([^\\x20\\x23\\x3f])*)?)");
+        std::cmatch match;
+        if (!std::regex_match(destUrl.c_str(), match, urlRegex))
+        {
+            BMCWEB_LOG_INFO << "Dest. url did not match ";
+            return false;
+        }
+
+        urlProto = std::string(match[1].first, match[1].second);
+        if (urlProto == "http")
+        {
+#ifndef BMCWEB_INSECURE_ENABLE_HTTP_PUSH_STYLE_EVENTING
+            return false;
+#endif
+        }
+
+        host = std::string(match[2].first, match[2].second);
+        port = std::string(match[3].first, match[3].second);
+        path = std::string(match[4].first, match[4].second);
+        if (port.empty())
+        {
+            if (urlProto == "http")
+            {
+                port = "80";
+            }
+            else
+            {
+                port = "443";
+            }
+        }
+        if (path.empty())
+        {
+            path = "/";
+        }
+        return true;
+    }
+}; // namespace redfish
 
 } // namespace redfish