Add serialization to the Callout class

Adding serialize() and deserialize() APIs to the callout
class to save and restore the class data persistently.

The APIs take the directory to store the files in, and the
filename itself will be the ID, which is the callout number.

For example, a path could be:
/var/lib/ibm-logging/errors/5/callouts/0.

Tested: Passes the testcases in future commit.

Change-Id: I526f6483df71dbceac3a33f7ce8872f6914bcd9d
Signed-off-by: Matt Spinler <spinler@us.ibm.com>
diff --git a/callout.cpp b/callout.cpp
index ef72414..7c0310e 100644
--- a/callout.cpp
+++ b/callout.cpp
@@ -12,13 +12,72 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+#include <cereal/archives/binary.hpp>
+#include <cereal/types/string.hpp>
+#include <cereal/types/vector.hpp>
+#include <cereal/types/tuple.hpp>
+#include <experimental/filesystem>
+#include <fstream>
+#include <phosphor-logging/log.hpp>
 #include "callout.hpp"
+#include "config.h"
+#include "dbus.hpp"
+
+CEREAL_CLASS_VERSION(ibm::logging::Callout, CALLOUT_CLASS_VERSION);
 
 namespace ibm
 {
 namespace logging
 {
 
+using namespace phosphor::logging;
+
+/**
+ * Function required by Cereal for saving data
+ *
+ * @param[in] archive - the Cereal archive object
+ * @param[in] callout - the object to save
+ * @param[in] version - the version of the persisted data
+ */
+template <class Archive>
+void save(Archive& archive, const Callout& callout, const std::uint32_t version)
+{
+    archive(callout.id(), callout.ts(), callout.path(), callout.buildDate(),
+            callout.manufacturer(), callout.model(), callout.partNumber(),
+            callout.serialNumber());
+}
+
+/**
+ * Function required by Cereal for restoring data into an object
+ *
+ * @param[in] archive - the Cereal archive object
+ * @param[in] callout - the callout object to restore
+ * @param[in] version - the version of the persisted data
+ */
+template <class Archive>
+void load(Archive& archive, Callout& callout, const std::uint32_t version)
+{
+    size_t id;
+    uint64_t timestamp;
+    std::string inventoryPath;
+    std::string build;
+    std::string mfgr;
+    std::string model;
+    std::string pn;
+    std::string sn;
+
+    archive(id, timestamp, inventoryPath, build, mfgr, model, pn, sn);
+
+    callout.id(id);
+    callout.ts(timestamp);
+    callout.path(inventoryPath);
+    callout.buildDate(build);
+    callout.manufacturer(mfgr);
+    callout.model(model);
+    callout.partNumber(pn);
+    callout.serialNumber(sn);
+}
+
 Callout::Callout(sdbusplus::bus::bus& bus, const std::string& objectPath,
                  size_t id, uint64_t timestamp) :
     CalloutObject(bus, objectPath.c_str(), true),
@@ -66,5 +125,61 @@
 
     emit_object_added();
 }
+
+void Callout::serialize(const fs::path& dir)
+{
+    auto path = getFilePath(dir);
+    std::ofstream stream(path.c_str(), std::ios::binary);
+    cereal::BinaryOutputArchive oarchive(stream);
+
+    oarchive(*this);
+}
+
+bool Callout::deserialize(const fs::path& dir)
+{
+    auto path = getFilePath(dir);
+
+    if (!fs::exists(path))
+    {
+        return false;
+    }
+
+    // Save the current ID and timestamp and then use them after
+    // deserialization to check that the data we are restoring
+    // is for the correct error log.
+
+    auto originalID = entryID;
+    auto originalTS = timestamp;
+
+    try
+    {
+        std::ifstream stream(path.c_str(), std::ios::binary);
+        cereal::BinaryInputArchive iarchive(stream);
+
+        iarchive(*this);
+    }
+    catch (std::exception& e)
+    {
+        log<level::ERR>(e.what());
+        log<level::ERR>("Failed trying to restore a Callout object",
+                        entry("PATH=%s", path.c_str()));
+        fs::remove(path);
+        return false;
+    }
+
+    if ((entryID != originalID) || (timestamp != originalTS))
+    {
+        log<level::INFO>(
+            "Timestamp or ID mismatch in persisted Callout. Discarding",
+            entry("PATH=%s", path.c_str()), entry("PERSISTED_ID=%lu", entryID),
+            entry("EXPECTED_ID=%lu", originalID),
+            entry("PERSISTED_TS=%llu", timestamp),
+            entry("EXPECTED_TS=%llu", originalTS));
+        fs::remove(path);
+        return false;
+    }
+
+    return true;
+}
 }
 }
diff --git a/callout.hpp b/callout.hpp
index 7eec934..b1e3b53 100644
--- a/callout.hpp
+++ b/callout.hpp
@@ -100,8 +100,44 @@
         timestamp = ts;
     }
 
+    /**
+     * Serializes the class instance into a file in the
+     * directory passed in.  The filename will match the
+     * ID value passed into the constructor.
+     *
+     * @param[in] - the directory to save the file  in.
+     */
+    void serialize(const fs::path& dir);
+
+    /**
+     * Loads the class members in from a file written by a previous
+     * call to serialize().  The filename it uses is the ID
+     * value passed into the constructor in the directory
+     * passed to this function.
+     *
+     * @param[in] dir - the directory to look for the file in
+     *
+     * @return bool - true if the deserialization was successful,
+     *                false if it wasn't
+     */
+    bool deserialize(const fs::path& dir);
+
   private:
     /**
+     * Returns the fully qualified filename to use for the serialization
+     * data.  The file is the ID value, like "0", in the base directory
+     * passed in.
+     *
+     * @param[in] baseDir - the directory the file will be in
+     *
+     * @return path - the filename
+     */
+    inline auto getFilePath(const fs::path& baseDir)
+    {
+        return baseDir / std::to_string(entryID);
+    }
+
+    /**
      * The unique identifier for the callout, as error logs can have
      * multiple callouts.  They start at 0.
      */
diff --git a/configure.ac b/configure.ac
index 07abbe0..574bd1a 100644
--- a/configure.ac
+++ b/configure.ac
@@ -22,6 +22,9 @@
 AC_CHECK_HEADER(nlohmann/json.hpp, ,
                 [AC_MSG_ERROR([Could not find nlohmann/json.hpp... nlohmann/json package required])])
 
+AC_CHECK_HEADER(cereal/archives/binary.hpp, ,
+                [AC_MSG_ERROR([Could not find cereal/archives/binary.hpp... cereal package required])])
+
 # Checks for typedefs, structures, and compiler characteristics.
 AX_CXX_COMPILE_STDCXX_14([noext])
 AX_APPEND_COMPILE_FLAGS([-Wall -Werror], [CXXFLAGS])
@@ -83,5 +86,12 @@
 AS_IF([test "x$POLICY_JSON_PATH" == "x"], [POLICY_JSON_PATH="/usr/share/ibm-logging/policy.json"])
 AC_DEFINE_UNQUOTED([POLICY_JSON_PATH], ["$POLICY_JSON_PATH"], [The path to the policy json file on the BMC])
 
+AC_ARG_VAR(CALLOUT_CLASS_VERSION,
+           [Callout class version to register with Cereal])
+AS_IF([test "x$CALLOUT_CLASS_VERSION" == "x"],
+      [CALLOUT_CLASS_VERSION=1])
+AC_DEFINE_UNQUOTED([CALLOUT_CLASS_VERSION], [$CALLOUT_CLASS_VERSION],
+                   [Callout Class version to register with Cereal])
+
 AC_CONFIG_FILES([Makefile test/Makefile])
 AC_OUTPUT