version-handler: implement json parser

version-handler adds the following capability

- ipmi-flash blobs can add a "version" along with the required
nested properties. This enables versioning support for that
flash blob.
For instance,
{
    "blob" : "/flash/sink_seq",
    "version":{ //now a /version/sink_seq blob will appear
        "handler": {
            "type: : "file", //only file type is supported
            "path" : "/tmp/version_info" // file contents
                   // contain textual version.
                   // Content returned on read
        },
        "actions":{
            "open" : { //on open launch a systemd target
                "type" : "systemd",
                "unit" : "version.target"
            }
        }
    }
}

In the above example (pretend that the rest of the required
firmware-handler fields are present) firmware handler will create the
normal ipmi-flash blobs needed for updating firmware.
The addition of the "version" section activates the version-handler
which exposes a blob named /version/sink_seq.

Opening /version/sink_seq will kick off the action specified. In this
case the systemd target version.target will be launched. This service
should retrieve the version information and then place the contents in
the file specified by handler - path (/tmp/version_info).

The user at this point can poll (stat) the blob until the meta-data
indicates that read data is ready.

A read can be issued for /version/sink_seq. What is returned will be the
contents of /tmp/version_info.

Signed-off-by: Jason Ling <jasonling@google.com>
Change-Id: I7cc7bec2e08b013c6b77f8dfd2a502272ad2d3bd
diff --git a/Makefile.am b/Makefile.am
index 187e221..f3a1997 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -51,6 +51,7 @@
 if BUILD_BMC_HANDLER
 SUBDIRS += bmc
 SUBDIRS += bmc/firmware-handler
+SUBDIRS += bmc/version-handler
 endif
 
 if BUILD_HOST_TOOL
diff --git a/bmc/version-handler/Makefile.am b/bmc/version-handler/Makefile.am
new file mode 100644
index 0000000..8370806
--- /dev/null
+++ b/bmc/version-handler/Makefile.am
@@ -0,0 +1,24 @@
+AM_DEFAULT_SOURCE_EXT = .cpp
+
+pkgdatadir = $(datadir)/phosphor-ipmi-flash
+dist_pkgdata_DATA =
+
+noinst_LTLIBRARIES = libversionblob_common.la
+libversionblob_common_la_SOURCES = \
+	version_handlers_builder.cpp
+
+libversionblob_common_la_CXXFLAGS = \
+	-I$(top_srcdir) \
+	-I$(top_srcdir)/bmc \
+	$(SDBUSPLUS_CFLAGS) \
+        $(PHOSPHOR_LOGGING_CFLAGS) \
+        $(CODE_COVERAGE_CXXFLAGS) \
+        -flto
+libversionblob_common_la_LDFLAGS = \
+        $(SDBUSPLUS_LIBS) \
+        $(PHOSPHOR_LOGGING_LIBS) \
+        $(CODE_COVERAGE_LIBS) \
+        -lstdc++fs
+libversionblob_common_la_LIBADD = $(top_builddir)/libfirmware_common.la
+libversionblob_common_la_LIBADD += $(top_builddir)/bmc/libbmc_common.la
+SUBDIRS = . test
diff --git a/bmc/version-handler/test/Makefile.am b/bmc/version-handler/test/Makefile.am
new file mode 100644
index 0000000..94c398d
--- /dev/null
+++ b/bmc/version-handler/test/Makefile.am
@@ -0,0 +1,30 @@
+@VALGRIND_CHECK_RULES@
+
+AM_CPPFLAGS = \
+	-I$(top_srcdir)/ \
+	-I$(top_srcdir)/bmc/ \
+	-I$(top_srcdir)/bmc/version-handler \
+	$(GTEST_CFLAGS) \
+	$(GMOCK_CFLAGS) \
+	$(CODE_COVERAGE_CPPFLAGS)
+AM_CXXFLAGS = \
+	$(SDBUSPLUS_CFLAGS) \
+	$(PHOSPHOR_LOGGING_CFLAGS) \
+	$(CODE_COVERAGE_CXXFLAGS)
+AM_LDFLAGS = \
+	$(GTEST_LIBS) \
+	$(GMOCK_LIBS) \
+	-lgmock_main \
+	$(OESDK_TESTCASE_FLAGS) \
+	$(SDBUSPLUS_LIBS) \
+	$(PHOSPHOR_LOGGING_LIBS) \
+	$(CODE_COVERAGE_LIBS)
+
+# Run all 'check' test programs
+check_PROGRAMS = \
+	version_json_unittest
+
+TESTS = $(check_PROGRAMS)
+
+version_json_unittest_SOURCES = version_json_unittest.cpp
+version_json_unittest_LDADD = $(top_builddir)/bmc/version-handler/libversionblob_common.la
diff --git a/bmc/version-handler/test/version_json_unittest.cpp b/bmc/version-handler/test/version_json_unittest.cpp
new file mode 100644
index 0000000..2f583a0
--- /dev/null
+++ b/bmc/version-handler/test/version_json_unittest.cpp
@@ -0,0 +1,300 @@
+#include "general_systemd.hpp"
+#include "skip_action.hpp"
+#include "version_handlers_builder.hpp"
+
+#include <nlohmann/json.hpp>
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+namespace ipmi_flash
+{
+namespace
+{
+using ::testing::IsEmpty;
+
+using json = nlohmann::json;
+
+TEST(VersionJsonTest, ValidConfigurationNoVersionHandler)
+{
+    auto j2 = R"(
+        [{
+            "blob" : "/flash/sink_seq",
+            "version":{
+                "handler": {
+                   "type" : "file",
+                   "path" : "/tmp/version_info"
+                 },
+                "actions":{
+                    "open" :{
+                    "type" : "systemd",
+                    "unit" : "absolute"
+                    }
+                 }
+            }
+         }]
+    )"_json;
+    auto h = VersionHandlersBuilder().buildHandlerFromJson(j2);
+    ASSERT_THAT(h, ::testing::SizeIs(1));
+    EXPECT_THAT(h[0].blobId, "/version/sink_seq");
+    EXPECT_FALSE(h[0].actions == nullptr);
+    EXPECT_FALSE(h[0].handler == nullptr);
+}
+
+TEST(VersionJsonTest, ValidConfigurationVersionBlobName)
+{
+    auto j2 = R"(
+        [{
+            "blob" : "/version/sink_seq",
+            "version":{
+                "handler": {
+                   "type" : "file",
+                   "path" : "/tmp/version_info"
+                 },
+                "actions": {
+                    "open" : {
+                    "type" : "systemd",
+                    "unit" : "phosphor-ipmi-flash-version-sink-sequencer.target"
+                    }
+                 }
+            }
+         }]
+    )"_json;
+    auto h = VersionHandlersBuilder().buildHandlerFromJson(j2);
+    ASSERT_THAT(h, ::testing::SizeIs(1));
+    EXPECT_THAT(h[0].blobId, "/version/sink_seq");
+    EXPECT_FALSE(h[0].actions == nullptr);
+    EXPECT_FALSE(h[0].handler == nullptr);
+}
+
+TEST(VersionJsonTest, MissingHandlerType)
+{
+    auto j2 = R"(
+        [{
+            "blob" : "/flash/image",
+            "version":{
+                "handler": {
+                   "path" : "/tmp/version_info"
+                 },
+                "actions": {
+                  "open" : {
+                  "type" : "systemd",
+                  "unit" : "absolute"}
+                 }
+            }
+         }]
+    )"_json;
+    EXPECT_THAT(VersionHandlersBuilder().buildHandlerFromJson(j2), IsEmpty());
+}
+
+TEST(VersionJsonTest, BadBlobName)
+{
+    auto j2 = R"(
+        [{
+            "blob" : "/bad/image",
+            "version":{
+                "handler": {
+                   "type" : "file",
+                   "path" : "/tmp/version_info"
+                 },
+                "actions": {
+                  "open" : {
+                  "type" : "systemd",
+                  "unit" : "absolute"}
+                 }
+            }
+         }]
+    )"_json;
+    EXPECT_THAT(VersionHandlersBuilder().buildHandlerFromJson(j2), IsEmpty());
+}
+
+TEST(VersionJsonTest, MissingActions)
+{
+    auto j2 = R"(
+        [{
+            "blob" : "/flash/image",
+            "version":{
+                "handler": {
+                   "type" : "file",
+                   "path" : "/tmp/version_info"
+                 }
+            }
+         }]
+    )"_json;
+    EXPECT_THAT(VersionHandlersBuilder().buildHandlerFromJson(j2), IsEmpty());
+}
+
+TEST(VersionJsonTest, MissingOpenAction)
+{
+    auto j2 = R"(
+        [{
+            "blob" : "/flash/image",
+            "version":{
+                "handler": {
+                   "type" : "file",
+                   "path" : "/tmp/version_info"
+                 },
+                "actions": {}
+            }
+         }]
+    )"_json;
+    EXPECT_THAT(VersionHandlersBuilder().buildHandlerFromJson(j2), IsEmpty());
+}
+
+TEST(VersionJsonTest, OneInvalidTwoValidSucceeds)
+{
+    auto j2 = R"(
+        [{
+            "blob" : "/flash/sink_seq0",
+            "version":{
+                "handler": {
+                   "type" : "file",
+                   "path" : "/tmp/version_info"
+                 },
+                "actions":{
+                    "open" :{
+                    "type" : "systemd",
+                    "unit" : "absolute"
+                    }
+                 }
+            }
+         },
+         {
+            "blob" : "/version/sink_seq1",
+            "version":{
+                "handler": {
+                   "type" : "file",
+                   "path" : "/tmp/version_info"
+                 },
+                "actions":{
+                    "open" :{
+                    "type" : "systemd",
+                    "unit" : "absolute"
+                    }
+                 }
+            }
+         },
+         {
+            "blob" : "/bad/sink_seq",
+            "version":{
+                "handler": {
+                   "type" : "file",
+                   "path" : "/tmp/version_info"
+                 },
+                "actions":{
+                    "open" :{
+                    "type" : "systemd",
+                    "unit" : "absolute"
+                    }
+                 }
+            }
+         }
+         ]
+    )"_json;
+    auto h = VersionHandlersBuilder().buildHandlerFromJson(j2);
+    ASSERT_THAT(h, ::testing::SizeIs(2));
+    EXPECT_THAT(h[0].blobId, "/version/sink_seq0");
+    EXPECT_THAT(h[1].blobId, "/version/sink_seq1");
+}
+
+TEST(VersionJsonTest, BlobNameIsTooShort)
+{
+    auto j2 = R"(
+        [{
+            "blob" : "/flash/",
+            "version":{
+                "handler": {
+                   "type" : "file",
+                   "path" : "/tmp/version_info"
+                 },
+                "actions":{
+                    "open" :{
+                    "type" : "systemd",
+                    "unit" : "absolute"
+                    }
+                 }
+            }
+         }]
+    )"_json;
+    EXPECT_THAT(VersionHandlersBuilder().buildHandlerFromJson(j2), IsEmpty());
+}
+
+TEST(VersionJsonTest, OpenSkipAction)
+{
+    auto j2 = R"(
+        [{
+            "blob" : "/flash/sink_seqs",
+            "version":{
+                "handler": {
+                   "type" : "file",
+                   "path" : "/tmp/version_info"
+                 },
+                "actions":{
+                    "open" :{
+                    "type" : "skip"
+                    }
+                 }
+            }
+         }]
+    )"_json;
+    auto h = VersionHandlersBuilder().buildHandlerFromJson(j2);
+    EXPECT_THAT(h, ::testing::SizeIs(1));
+    EXPECT_TRUE(h[0].blobId == "/version/sink_seqs");
+    ASSERT_FALSE(h[0].actions == nullptr);
+    EXPECT_FALSE(h[0].actions->onOpen == nullptr);
+}
+
+TEST(VersionJsonTest, OpenActionsWithDifferentModes)
+{
+    auto j2 = R"(
+        [{
+            "blob" : "/flash/blob1",
+            "version":{
+                "handler": {
+                   "type" : "file",
+                   "path" : "/tmp/version_info"
+                 },
+                "actions":{
+                    "open" :{
+                    "type" : "systemd",
+                    "unit" : "absolute",
+                    "mode" : "replace-nope"
+                    }
+                 }
+            }
+         },
+         {
+            "blob" : "/flash/blob2",
+            "version":{
+                "handler": {
+                   "type" : "file",
+                   "path" : "/tmp/version_info"
+                 },
+                "actions":{
+                    "open" :{
+                    "type" : "systemd",
+                    "unit" : "absolute",
+                    "mode" : "replace-fake"
+                    }
+                 }
+            }
+         }
+         ]
+    )"_json;
+    auto h = VersionHandlersBuilder().buildHandlerFromJson(j2);
+    ASSERT_THAT(h, ::testing::SizeIs(2));
+
+    EXPECT_FALSE(h[0].handler == nullptr);
+    EXPECT_FALSE(h[0].actions == nullptr);
+    EXPECT_THAT(h[0].blobId, "/version/blob1");
+    auto onOpen0 = reinterpret_cast<SystemdNoFile*>(h[0].actions->onOpen.get());
+    EXPECT_THAT(onOpen0->getMode(), "replace-nope");
+
+    EXPECT_FALSE(h[1].handler == nullptr);
+    EXPECT_FALSE(h[1].actions == nullptr);
+    EXPECT_THAT(h[1].blobId, "/version/blob2");
+    auto onOpen1 = reinterpret_cast<SystemdNoFile*>(h[1].actions->onOpen.get());
+    EXPECT_THAT(onOpen1->getMode(), "replace-fake");
+}
+} // namespace
+} // namespace ipmi_flash
diff --git a/bmc/version-handler/version_handler.hpp b/bmc/version-handler/version_handler.hpp
new file mode 100644
index 0000000..aefd3c4
--- /dev/null
+++ b/bmc/version-handler/version_handler.hpp
@@ -0,0 +1,19 @@
+#pragma once
+#include "buildjson.hpp"
+#include "status.hpp"
+
+#include <blobs-ipmid/blobs.hpp>
+
+#include <memory>
+namespace ipmi_flash
+{
+struct VersionActionPack
+{
+  public:
+    VersionActionPack(std::unique_ptr<TriggerableActionInterface> openAction) :
+        onOpen(std::move(openAction)){};
+    VersionActionPack() = default;
+    /** Only file operation action supported currently */
+    std::unique_ptr<TriggerableActionInterface> onOpen;
+};
+} // namespace ipmi_flash
diff --git a/bmc/version-handler/version_handlers_builder.cpp b/bmc/version-handler/version_handlers_builder.cpp
new file mode 100644
index 0000000..d386128
--- /dev/null
+++ b/bmc/version-handler/version_handlers_builder.cpp
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2019 Google Inc.
+ *
+ * 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.
+ */
+#include "version_handlers_builder.hpp"
+
+#include "file_handler.hpp"
+#include "fs.hpp"
+#include "skip_action.hpp"
+
+#include <nlohmann/json.hpp>
+
+#include <algorithm>
+#include <cstdio>
+#include <exception>
+#include <fstream>
+#include <memory>
+#include <regex>
+#include <string>
+#include <vector>
+
+namespace ipmi_flash
+{
+std::vector<HandlerConfig<VersionActionPack>>
+    VersionHandlersBuilder::buildHandlerFromJson(const nlohmann::json& data)
+{
+    std::vector<HandlerConfig<VersionActionPack>> handlers;
+
+    for (const auto& item : data)
+    {
+        try
+        {
+            HandlerConfig<VersionActionPack> output;
+
+            /* at() throws an exception when the key is not present. */
+            item.at("blob").get_to(output.blobId);
+
+            /* name must be: /flash/... or /version/...*/
+            std::regex regexpr("^\\/(?:flash|version)\\/(.+)");
+            std::smatch matches;
+            if (!std::regex_search(output.blobId, matches, regexpr))
+            {
+                throw std::runtime_error(
+                    "Invalid blob name: '" + output.blobId +
+                    "' must start with /flash/ or /version/");
+            }
+            output.blobId = "/version/" + matches[1].str();
+            /* version is required. */
+            const auto& v = item.at("version");
+            /* version must have handler */
+            const auto& h = v.at("handler");
+
+            const std::string handlerType = h.at("type");
+            if (handlerType == "file")
+            {
+                const auto& path = h.at("path");
+                output.handler = std::make_unique<FileHandler>(path);
+            }
+            else
+            {
+                throw std::runtime_error("Invalid handler type: " +
+                                         handlerType);
+            }
+
+            /* actions are required (presently). */
+            const auto& a = v.at("actions");
+            std::unique_ptr<VersionActionPack> pack =
+                std::make_unique<VersionActionPack>();
+
+            /* to make an action optional, assign type "skip" */
+            const auto& onOpen = a.at("open");
+            const std::string onOpenType = onOpen.at("type");
+            if (onOpenType == "systemd")
+            {
+                pack->onOpen = std::move(buildSystemd(onOpen));
+            }
+            else if (onOpenType == "skip")
+            {
+                pack->onOpen = SkipAction::CreateSkipAction();
+            }
+            else
+            {
+                throw std::runtime_error("Invalid preparation type: " +
+                                         onOpenType);
+            }
+
+            output.actions = std::move(pack);
+            handlers.push_back(std::move(output));
+        }
+        catch (const std::exception& e)
+        {
+            /* TODO: Once phosphor-logging supports unit-test injection, fix
+             * this to log.
+             */
+            std::fprintf(stderr,
+                         "Excepted building HandlerConfig from json: %s\n",
+                         e.what());
+        }
+    }
+
+    return handlers;
+}
+} // namespace ipmi_flash
diff --git a/bmc/version-handler/version_handlers_builder.hpp b/bmc/version-handler/version_handlers_builder.hpp
new file mode 100644
index 0000000..5055098
--- /dev/null
+++ b/bmc/version-handler/version_handlers_builder.hpp
@@ -0,0 +1,21 @@
+#pragma once
+#include "buildjson.hpp"
+#include "version_handler.hpp"
+
+#include <nlohmann/json.hpp>
+
+#include <vector>
+
+namespace ipmi_flash
+{
+/**
+ * provide the method to parse and validate blob entries from json and produce
+ * something that is usable by the version handler.
+ */
+class VersionHandlersBuilder : public HandlersBuilderIfc<VersionActionPack>
+{
+  public:
+    std::vector<HandlerConfig<VersionActionPack>>
+        buildHandlerFromJson(const nlohmann::json& data) override;
+};
+} // namespace ipmi_flash
diff --git a/configure.ac b/configure.ac
index 837180d..7195890 100644
--- a/configure.ac
+++ b/configure.ac
@@ -641,6 +641,7 @@
 AC_CONFIG_FILES([Makefile])
 AC_CONFIG_FILES([bmc/Makefile])
 AC_CONFIG_FILES([bmc/firmware-handler/Makefile bmc/firmware-handler/test/Makefile])
+AC_CONFIG_FILES([bmc/version-handler/Makefile bmc/version-handler/test/Makefile])
 AC_CONFIG_FILES([tools/Makefile tools/test/Makefile])
 AC_CONFIG_FILES([cleanup/Makefile cleanup/test/Makefile])
 AC_CONFIG_FILES([bmc/firmware-handler/phosphor-ipmi-flash-bmc-prepare.target])