Add SEL delete method and track record IDs

Add SEL delete method which removes the line associated with the target
entry from the SEL files. Upcoming record IDs to be used will now be
stored into a vector to keep track of the next available record ID. The
vector will be backed up to a file in case the phosphor-sel-logger
service restarts. In the case of SEL entry deletion, the record ID with
that associated entry can now be reused and will take priority in being
used when a new SEL entry is added.

If multiple entries are deleted, then the Record IDs will be reused in
a LIFO fashion. Timestamps are updated for "Last Del Time" as specified
by the spec.

The SEL delete method is behind the "sel-delete" build option to
preserve backwards stability since it requires a departure from the
previous way that the next record ID is calculated.

Tested:
* Generated SEL entries.
* Successfully removed a SEL entry using a dbus call:
  busctl call xyz.openbmc_project.Logging.IPMI \
  /xyz/openbmc_project/Logging/IPMI xyz.openbmc_project.Logging.IPMI \
  IpmiSelDelete q <RecordID>
* The next SEL entry added uses the same Record ID associated with the
  entry that was previously deleted.

Change-Id: If51f6bf2d7c61b73b5568874488cdd61031dc6ca
Signed-off-by: Jonico Eustaquio <jonico.eustaquio@fii-na.com>
diff --git a/include/sel_logger.hpp b/include/sel_logger.hpp
index 15d53fc..a51a01c 100644
--- a/include/sel_logger.hpp
+++ b/include/sel_logger.hpp
@@ -36,6 +36,9 @@
 
 static const std::filesystem::path selLogDir = "/var/log";
 static const std::string selLogFilename = "ipmi_sel";
+#ifdef SEL_LOGGER_ENABLE_SEL_DELETE
+static const std::string nextRecordFilename = "next_records";
+#endif
 
 template <typename... T>
 static uint16_t
diff --git a/meson.build b/meson.build
index e916549..2332607 100644
--- a/meson.build
+++ b/meson.build
@@ -37,6 +37,9 @@
 
   deps += dependency('phosphor-logging')
 endif
+if get_option('sel-delete')
+  cpp_args += '-DSEL_LOGGER_ENABLE_SEL_DELETE'
+endif
 
 executable(
   'sel-logger',
diff --git a/meson.options b/meson.options
index 1c0866c..39f082d 100644
--- a/meson.options
+++ b/meson.options
@@ -10,3 +10,5 @@
        description: 'Automatically log SEL records for host error events')
 option('send-to-logger', type: 'boolean',
        description: 'Automatically log events to Redfish for pulse type assert-deassert sensor events')
+option('sel-delete', type: 'boolean',
+       description: 'Enables ability to delete SEL entries given a record ID')
\ No newline at end of file
diff --git a/src/sel_logger.cpp b/src/sel_logger.cpp
index 6df8e44..95a4602 100644
--- a/src/sel_logger.cpp
+++ b/src/sel_logger.cpp
@@ -81,6 +81,194 @@
     return !selLogFiles.empty();
 }
 
+static void saveClearSelTimestamp()
+{
+    int fd = open("/var/lib/ipmi/sel_erase_time",
+                  O_WRONLY | O_CREAT | O_CLOEXEC, 0644);
+    if (fd < 0)
+    {
+        std::cerr << "Failed to open file\n";
+        return;
+    }
+
+    if (futimens(fd, NULL) < 0)
+    {
+        std::cerr << "Failed to update SEL cleared timestamp: "
+                  << std::string(strerror(errno));
+    }
+    close(fd);
+}
+
+#ifdef SEL_LOGGER_ENABLE_SEL_DELETE
+std::vector<uint16_t> nextRecordsCache;
+
+static void backupCacheToFile()
+{
+    std::ofstream nextRecordStream(selLogDir / nextRecordFilename);
+    for (auto recordIds : nextRecordsCache)
+    {
+        nextRecordStream << recordIds << '\n';
+    }
+}
+
+static uint16_t getNewRecordId()
+{
+    uint16_t nextRecordId = nextRecordsCache.back();
+    // Check if SEL is full
+    if (nextRecordId == selInvalidRecID)
+    {
+        return nextRecordId;
+    }
+    nextRecordsCache.pop_back();
+    if (nextRecordsCache.empty())
+    {
+        nextRecordsCache.push_back(nextRecordId + 1);
+    }
+    backupCacheToFile();
+    return nextRecordId;
+}
+
+static void initializeRecordId()
+{
+    std::ifstream nextRecordStream(selLogDir / nextRecordFilename);
+    if (!nextRecordStream.is_open())
+    {
+        std::ofstream newStream(selLogDir / nextRecordFilename);
+        newStream << '1' << '\n';
+        newStream.close();
+        nextRecordStream.open(selLogDir / nextRecordFilename);
+    }
+    std::string line;
+    while (std::getline(nextRecordStream, line))
+    {
+        nextRecordsCache.push_back(std::stoi(line));
+    }
+}
+
+void clearSelLogFiles()
+{
+    saveClearSelTimestamp();
+
+    // Clear the SEL by deleting the log files
+    std::vector<std::filesystem::path> selLogFiles;
+    if (getSELLogFiles(selLogFiles))
+    {
+        for (const std::filesystem::path& file : selLogFiles)
+        {
+            std::error_code ec;
+            std::filesystem::remove(file, ec);
+        }
+    }
+    // Reload rsyslog so it knows to start new log files
+    boost::asio::io_context io;
+    auto dbus = std::make_shared<sdbusplus::asio::connection>(io);
+    sdbusplus::message_t rsyslogReload = dbus->new_method_call(
+        "org.freedesktop.systemd1", "/org/freedesktop/systemd1",
+        "org.freedesktop.systemd1.Manager", "ReloadUnit");
+    rsyslogReload.append("rsyslog.service", "replace");
+    try
+    {
+        sdbusplus::message_t reloadResponse = dbus->call(rsyslogReload);
+    }
+    catch (const sdbusplus::exception_t& e)
+    {
+        std::cerr << e.what() << "\n";
+    }
+    // Set next record to 1
+    nextRecordsCache.clear();
+    nextRecordsCache.push_back(1);
+    // Update backup file as well
+    std::ofstream nextRecordStream(selLogDir / nextRecordFilename);
+    nextRecordStream << '1' << '\n';
+}
+
+static bool selDeleteTargetRecord(const uint16_t& targetId)
+{
+    bool targetEntryFound = false;
+    // Check if the ipmi_sel exist and save the path
+    std::vector<std::filesystem::path> selLogFiles;
+    if (!getSELLogFiles(selLogFiles))
+    {
+        return targetEntryFound;
+    }
+
+    // Go over all the ipmi_sel files to remove the entry with the target ID
+    for (const std::filesystem::path& file : selLogFiles)
+    {
+        std::fstream logStream(file, std::ios::in);
+        std::fstream tempFile(selLogDir / "temp", std::ios::out);
+        if (!logStream.is_open())
+        {
+            return targetEntryFound;
+        }
+        std::string line;
+        while (std::getline(logStream, line))
+        {
+            // Get the recordId of the current entry
+            int left = line.find(" ");
+            int right = line.find(",");
+            int recordLen = right - left;
+            std::string recordId = line.substr(left, recordLen);
+            int newRecordId = std::stoi(recordId);
+
+            if (newRecordId != targetId)
+            {
+                // Copy the entry from the original ipmi_sel to the temp file
+                tempFile << line << '\n';
+            }
+            else
+            {
+                // Skip copying the target entry
+                targetEntryFound = true;
+            }
+        }
+        logStream.close();
+        tempFile.close();
+        if (targetEntryFound)
+        {
+            std::fstream logStream(file, std::ios::out);
+            std::fstream tempFile(selLogDir / "temp", std::ios::in);
+            while (std::getline(tempFile, line))
+            {
+                logStream << line << '\n';
+            }
+            logStream.close();
+            tempFile.close();
+            std::error_code ec;
+            if (!std::filesystem::remove(selLogDir / "temp", ec))
+            {
+                std::cerr << ec.message() << std::endl;
+            }
+            break;
+        }
+    }
+    return targetEntryFound;
+}
+
+static uint16_t selDeleteRecord(const uint16_t& recordId)
+{
+    std::filesystem::file_time_type prevAddTime =
+        std::filesystem::last_write_time(selLogDir / selLogFilename);
+    bool targetEntryFound = selDeleteTargetRecord(recordId);
+
+    // Check if the Record Id was found
+    if (!targetEntryFound)
+    {
+        return selInvalidRecID;
+    }
+    // Add to next record cache for reuse
+    nextRecordsCache.push_back(recordId);
+    // Add to backup file
+    std::ofstream nextRecordStream(selLogDir / nextRecordFilename,
+                                   std::ios::app);
+    nextRecordStream << recordId << '\n';
+    // Keep Last Add Time the same
+    std::filesystem::last_write_time(selLogDir / selLogFilename, prevAddTime);
+    // Update Last Del Time
+    saveClearSelTimestamp();
+    return recordId;
+}
+#else
 static unsigned int initializeRecordId()
 {
     std::vector<std::filesystem::path> selLogFiles;
@@ -113,22 +301,13 @@
 
 static unsigned int recordId = initializeRecordId();
 
-static void saveClearSelTimestamp()
+static unsigned int getNewRecordId()
 {
-    int fd = open("/var/lib/ipmi/sel_erase_time",
-                  O_WRONLY | O_CREAT | O_CLOEXEC, 0644);
-    if (fd < 0)
+    if (++recordId >= selInvalidRecID)
     {
-        std::cerr << "Failed to open file\n";
-        return;
+        recordId = selInvalidRecID;
     }
-
-    if (futimens(fd, NULL) < 0)
-    {
-        std::cerr << "Failed to update SEL cleared timestamp: "
-                  << std::string(strerror(errno));
-    }
-    close(fd);
+    return recordId;
 }
 
 void clearSelLogFiles()
@@ -164,15 +343,7 @@
         std::cerr << e.what() << "\n";
     }
 }
-
-static unsigned int getNewRecordId()
-{
-    if (++recordId >= selInvalidRecID)
-    {
-        recordId = selInvalidRecID;
-    }
-    return recordId;
-}
+#endif
 #endif
 
 static void toHexStr(const std::vector<uint8_t>& data, std::string& hexStr)
@@ -287,6 +458,11 @@
 
 int main(int, char*[])
 {
+#ifndef SEL_LOGGER_SEND_TO_LOGGING_SERVICE
+#ifdef SEL_LOGGER_ENABLE_SEL_DELETE
+    initializeRecordId();
+#endif
+#endif
     // setup connection to dbus
     boost::asio::io_context io;
     auto conn = std::make_shared<sdbusplus::asio::connection>(io);
@@ -318,6 +494,12 @@
 #ifndef SEL_LOGGER_SEND_TO_LOGGING_SERVICE
     // Clear SEL entries
     ifaceAddSel->register_method("Clear", []() { clearSelLogFiles(); });
+#ifdef SEL_LOGGER_ENABLE_SEL_DELETE
+    // Delete a SEL entry
+    ifaceAddSel->register_method("IpmiSelDelete", [](const uint16_t& recordId) {
+        return selDeleteRecord(recordId);
+    });
+#endif
 #endif
     ifaceAddSel->initialize();