msl_verify: Initial commit

Create a new binary that checks the msl (minimum ship level)
of the PNOR and logs an error message if the version on the
system is older. The msl can be specified via a config flag.

Change-Id: I6f477400f7a8cf56557bd0caf5d6e08d73320028
Signed-off-by: Adriana Kobylak <anoo@us.ibm.com>
diff --git a/Makefile.am b/Makefile.am
index 0f05061..e012ae5 100755
--- a/Makefile.am
+++ b/Makefile.am
@@ -3,7 +3,8 @@
 ACLOCAL_AMFLAGS = -Im4
 
 sbin_PROGRAMS = \
-	openpower-update-manager
+	openpower-update-manager \
+	openpower-pnor-msl
 
 openpower_update_manager_SOURCES = \
 	activation.cpp \
@@ -22,6 +23,10 @@
 nodist_openpower_update_manager_SOURCES = \
 	org/openbmc/Associations/server.cpp
 
+openpower_pnor_msl_SOURCES = \
+	msl_verify.cpp \
+	msl_verify_main.cpp
+
 CLEANFILES = \
 	org/openbmc/Associations/server.cpp \
 	org/openbmc/Associations/server.hpp
@@ -55,5 +60,7 @@
 
 openpower_update_manager_CXXFLAGS = $(generic_cxxflags)
 openpower_update_manager_LDFLAGS = $(generic_ldflags)
+openpower_pnor_msl_CXXFLAGS = $(generic_cxxflags)
+openpower_pnor_msl_LDFLAGS = $(generic_ldflags)
 
 SUBDIRS = test
diff --git a/configure.ac b/configure.ac
index 7153727..95b70e3 100755
--- a/configure.ac
+++ b/configure.ac
@@ -151,5 +151,14 @@
 AS_IF([test "x$ACTIVE_PNOR_MAX_ALLOWED" == "x"], [ACTIVE_PNOR_MAX_ALLOWED=2])
 AC_DEFINE_UNQUOTED([ACTIVE_PNOR_MAX_ALLOWED], [$ACTIVE_PNOR_MAX_ALLOWED], [The maximum allowed active pnor versions])
 
+AC_ARG_VAR(PNOR_MSL, [The PNOR minimum ship level])
+AS_IF([test "x$PNOR_MSL" == "x"], [PNOR_MSL=""])
+AC_DEFINE_UNQUOTED([PNOR_MSL], ["$PNOR_MSL"], [The PNOR minimum ship level])
+
+AC_ARG_VAR(PNOR_VERSION_PARTITION, [The name of the PNOR version partition])
+AS_IF([test "x$PNOR_VERSION_PARTITION" == "x"], [PNOR_VERSION_PARTITION="VERSION"])
+AC_DEFINE_UNQUOTED([PNOR_VERSION_PARTITION], ["$PNOR_VERSION_PARTITION"],
+    [The name of the PNOR version partition])
+
 AC_CONFIG_FILES([Makefile test/Makefile])
 AC_OUTPUT
diff --git a/msl_verify.cpp b/msl_verify.cpp
new file mode 100644
index 0000000..1a6234c
--- /dev/null
+++ b/msl_verify.cpp
@@ -0,0 +1,137 @@
+#include "config.h"
+
+#include "msl_verify.hpp"
+
+#include <experimental/filesystem>
+#include <fstream>
+#include <phosphor-logging/log.hpp>
+#include <regex>
+
+namespace openpower
+{
+namespace software
+{
+namespace image
+{
+
+namespace fs = std::experimental::filesystem;
+using namespace phosphor::logging;
+
+int MinimumShipLevel::compare(const Version& a, const Version& b)
+{
+    if (a.major < b.major)
+    {
+        return -1;
+    }
+    else if (a.major > b.major)
+    {
+        return 1;
+    }
+
+    if (a.minor < b.minor)
+    {
+        return -1;
+    }
+    else if (a.minor > b.minor)
+    {
+        return 1;
+    }
+
+    if (a.rev < b.rev)
+    {
+        return -1;
+    }
+    else if (a.rev > b.rev)
+    {
+        return 1;
+    }
+
+    return 0;
+}
+
+void MinimumShipLevel::parse(const std::string& versionStr, Version& version)
+{
+    std::smatch match;
+    version = {0, 0, 0};
+
+    // Match for vX.Y.Z
+    std::regex regex{"v([0-9]+)\\.([0-9]+)\\.([0-9]+)", std::regex::extended};
+
+    if (!std::regex_search(versionStr, match, regex))
+    {
+        // Match for vX.Y
+        std::regex regexShort{"v([0-9]+)\\.([0-9]+)", std::regex::extended};
+        if (!std::regex_search(versionStr, match, regexShort))
+        {
+            log<level::ERR>("Unable to parse PNOR version",
+                            entry("VERSION=%s", versionStr.c_str()));
+            return;
+        }
+    }
+    else
+    {
+        // Populate Z
+        version.rev = std::stoi(match[3]);
+    }
+    version.major = std::stoi(match[1]);
+    version.minor = std::stoi(match[2]);
+}
+
+std::string MinimumShipLevel::getFunctionalVersion()
+{
+    if (!fs::exists(PNOR_RO_ACTIVE_PATH))
+    {
+        return {};
+    }
+
+    fs::path versionPath(PNOR_RO_ACTIVE_PATH);
+    versionPath /= PNOR_VERSION_PARTITION;
+    if (!fs::is_regular_file(versionPath))
+    {
+        return {};
+    }
+
+    std::ifstream versionFile(versionPath);
+    std::string versionStr;
+    std::getline(versionFile, versionStr);
+
+    return versionStr;
+}
+
+bool MinimumShipLevel::verify()
+{
+    if (minShipLevel.empty())
+    {
+        return true;
+    }
+
+    auto actual = getFunctionalVersion();
+    if (actual.empty())
+    {
+        return true;
+    }
+
+    Version minVersion = {0, 0, 0};
+    parse(minShipLevel, minVersion);
+
+    Version actualVersion = {0, 0, 0};
+    parse(actual, actualVersion);
+
+    auto rc = compare(actualVersion, minVersion);
+    if (rc < 0)
+    {
+        log<level::ERR>(
+            "PNOR Mininum Ship Level NOT met",
+            entry("MIN_VERSION=%s", minShipLevel.c_str()),
+            entry("ACTUAL_VERSION=%s", actual.c_str()),
+            entry("VERSION_PURPOSE=%s",
+                  "xyz.openbmc_project.Software.Version.VersionPurpose.Host"));
+        return false;
+    }
+
+    return true;
+}
+
+} // namespace image
+} // namespace software
+} // namespace openpower
diff --git a/msl_verify.hpp b/msl_verify.hpp
new file mode 100644
index 0000000..59f4017
--- /dev/null
+++ b/msl_verify.hpp
@@ -0,0 +1,77 @@
+#pragma once
+
+#include <string>
+
+namespace openpower
+{
+namespace software
+{
+namespace image
+{
+
+/** @class MinimumShipLevel
+ *  @brief Contains minimum ship level verification functions.
+ */
+class MinimumShipLevel
+{
+  public:
+    MinimumShipLevel() = delete;
+    MinimumShipLevel(const MinimumShipLevel&) = delete;
+    MinimumShipLevel& operator=(const MinimumShipLevel&) = delete;
+    MinimumShipLevel(MinimumShipLevel&&) = default;
+    MinimumShipLevel& operator=(MinimumShipLevel&&) = default;
+    ~MinimumShipLevel() = default;
+
+    /** @brief Constructs MinimumShipLevel.
+     *  @param[in] minShipLevel - Minimum Ship Level string
+     */
+    MinimumShipLevel(const std::string& minShipLevel) :
+        minShipLevel(minShipLevel){};
+
+    /** @brief Verify if the current PNOR version meets the min ship level
+     *  @return true if the verification succeeded, false otherwise
+     */
+    bool verify();
+
+    /** @brief Version components */
+    struct Version
+    {
+        uint8_t major;
+        uint8_t minor;
+        uint8_t rev;
+    };
+
+    /** @brief Get the functional PNOR version on the system
+     *  @details If the PNOR version file is not found, don't log an error since
+     *           all PNOR images may had been deleted. If there is an issue with
+     *           the VERSION partition, it should be caught by the host fw code.
+     *  @return The populated or empty version string
+     */
+    std::string getFunctionalVersion();
+
+    /** @brief Parse the version components into a struct
+     *  @details Version format follows a git tag convention: vX.Y[.Z]
+     *          Reference:
+     *          https://github.com/open-power/op-build/blob/master/openpower/package/VERSION.readme
+     * @param[in]  versionStr - The version string to be parsed
+     * @param[out] version    - The version struct to be populated
+     */
+    void parse(const std::string& versionStr, Version& version);
+
+    /** @brief Compare the versions provided
+     *  @param[in] a - The first version to compare
+     *  @param[in] b - The second version to compare
+     *  @return 1 if a > b
+     *          0 if a = b
+     *         -1 if a < b
+     */
+    int compare(const Version& a, const Version& b);
+
+  private:
+    /** Minimum Ship Level to compare against */
+    std::string minShipLevel;
+};
+
+} // namespace image
+} // namespace software
+} // namespace openpower
diff --git a/msl_verify_main.cpp b/msl_verify_main.cpp
new file mode 100644
index 0000000..85911dd
--- /dev/null
+++ b/msl_verify_main.cpp
@@ -0,0 +1,16 @@
+#include "config.h"
+
+#include "msl_verify.hpp"
+
+int main(int argc, char* argv[])
+{
+    using MinimumShipLevel = openpower::software::image::MinimumShipLevel;
+    MinimumShipLevel minimumShipLevel(PNOR_MSL);
+
+    if (!minimumShipLevel.verify())
+    {
+        // TODO Create error log
+    }
+
+    return 0;
+}
diff --git a/test/Makefile.am b/test/Makefile.am
index 6dcba30..3dc6960 100755
--- a/test/Makefile.am
+++ b/test/Makefile.am
@@ -27,7 +27,9 @@
 	-lssl \
 	-lcrypto
 
-utest_SOURCES = utest.cpp
+utest_SOURCES = \
+	utest.cpp \
+	msl_verify.cpp
 utest_LDADD = \
 	$(top_builddir)/openpower_update_manager-activation.o \
 	$(top_builddir)/openpower_update_manager-version.o \
@@ -36,4 +38,5 @@
 	$(top_builddir)/openpower_update_manager-item_updater.o \
 	$(top_builddir)/org/openbmc/Associations/openpower_update_manager-server.o \
 	$(top_builddir)/image_verify.cpp \
+	$(top_builddir)/msl_verify.cpp \
 	-lstdc++fs
diff --git a/test/msl_verify.cpp b/test/msl_verify.cpp
new file mode 100644
index 0000000..5fdf505
--- /dev/null
+++ b/test/msl_verify.cpp
@@ -0,0 +1,78 @@
+#include "msl_verify.hpp"
+
+#include <gtest/gtest.h>
+
+namespace openpower
+{
+namespace software
+{
+namespace image
+{
+
+class MinimumShipLevelTest : public testing::Test
+{
+  protected:
+    std::string minShipLevel = "v.2.2";
+    std::unique_ptr<MinimumShipLevel> minimumShipLevel;
+
+    virtual void SetUp()
+    {
+        minimumShipLevel = std::make_unique<MinimumShipLevel>(minShipLevel);
+    }
+};
+
+TEST_F(MinimumShipLevelTest, compare)
+{
+    MinimumShipLevel::Version min;
+    MinimumShipLevel::Version actual;
+
+    min = {3, 5, 7};
+
+    // actual = min
+    actual = {3, 5, 7};
+    EXPECT_EQ(0, minimumShipLevel->compare(actual, min));
+
+    // actual < min
+    actual = {3, 5, 6};
+    EXPECT_EQ(-1, minimumShipLevel->compare(actual, min));
+    actual = {3, 4, 7};
+    EXPECT_EQ(-1, minimumShipLevel->compare(actual, min));
+    actual = {2, 5, 7};
+    EXPECT_EQ(-1, minimumShipLevel->compare(actual, min));
+
+    // actual > min
+    actual = {3, 5, 8};
+    EXPECT_EQ(1, minimumShipLevel->compare(actual, min));
+    actual = {3, 6, 7};
+    EXPECT_EQ(1, minimumShipLevel->compare(actual, min));
+    actual = {4, 5, 7};
+    EXPECT_EQ(1, minimumShipLevel->compare(actual, min));
+}
+
+TEST_F(MinimumShipLevelTest, parse)
+{
+    MinimumShipLevel::Version version;
+    std::string versionStr;
+
+    versionStr = "nomatch-1.2.3-abc";
+    minimumShipLevel->parse(versionStr, version);
+    EXPECT_EQ(0, version.major);
+    EXPECT_EQ(0, version.minor);
+    EXPECT_EQ(0, version.rev);
+
+    versionStr = "xyzformat-v1.2.3-4.5abc";
+    minimumShipLevel->parse(versionStr, version);
+    EXPECT_EQ(1, version.major);
+    EXPECT_EQ(2, version.minor);
+    EXPECT_EQ(3, version.rev);
+
+    versionStr = "xyformat-system-v6.7-abc";
+    minimumShipLevel->parse(versionStr, version);
+    EXPECT_EQ(6, version.major);
+    EXPECT_EQ(7, version.minor);
+    EXPECT_EQ(0, version.rev);
+}
+
+} // namespace image
+} // namespace software
+} // namespace openpower