updater: add process-host-firmware subcommand

Add general support for subcommands.  Do this by bringing in the the
CLI11 library, and by skipping over dbus service initialization if a
subcommand is specified on the command line.

Implement the first subcommand: process-host-firmware.  If no subcommand
is specified on the command line, openpower-update-manager continues to
launch as a dbus service.

Introduce functions.cpp/functions.hpp.  functions.cpp is intended to be
a module in which process-hostfirmware and future openpower-item-updater
subcommands can be implemented.  functions.hpp should be used to export
declarations to unit tests or item_updater_main.cpp.

The IBM POWER host firmware runtime looks for data and/or additional
code while bootstraping in locations with well-known names.
Additionally, the host firmware runtime relies on an external party to
map platform specific data and/or code to the well known names.  The
process-host-firmware command maintains that mapping on behalf of the
host firmware and ensures the correct blob files exist in the well-known
locations prior to starting the host firmware runtime.

The process-host-firmware subcommand registers for callbacks from entity
manager when entity manager adds new DBus interfaces.  Interfaces other
than IBMCompatibleSystem are ignored.  Entity manager is started with
dbus activation if it is not already running.  If IBMCompatibleSystem is
found and a set of host firmware blob filename extensions are mapped,
they are used to write "well-known" names into the filesystem for use by
the host firmware runtime.

Once the well-known names are written to the filesystem the program will
exit.

If entity manager does not publish an IBMCompatibleSystem interface the
command will wait indefinitely for it to appear.  For this reason it is
not recommended to run the process-host-fimrware subcommand if
IBMCompatibleSystem is not implemented.

If IBMCompatibleSystem is implemented but no host firmware blob filename
extensions are mapped, the program will exit without doing anything.

Testcases are provided.

Change-Id: Icb066b0f3e164520cae312e3b03432a6ad67e0df
Signed-off-by: Brad Bishop <bradleyb@fuzziesquirrel.com>
diff --git a/test/test_functions.cpp b/test/test_functions.cpp
new file mode 100644
index 0000000..3b7d366
--- /dev/null
+++ b/test/test_functions.cpp
@@ -0,0 +1,466 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#include "../functions.hpp"
+
+#include <stdlib.h>
+
+#include <array>
+#include <cerrno>
+#include <filesystem>
+#include <fstream>
+#include <map>
+#include <string>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+using namespace std::string_literals;
+
+TEST(GetExtensionsForIbmCompatibleSystem, testSingleMatch)
+{
+    std::map<std::string, std::vector<std::string>> extensionMap{{
+        {"system-foo"s, {".EXT"s}},
+    }};
+    std::vector<std::string> compatibleSystem{"system-foo"s}, extensions;
+    auto found =
+        functions::process_hostfirmware::getExtensionsForIbmCompatibleSystem(
+            extensionMap, compatibleSystem, extensions);
+    EXPECT_TRUE(found);
+    EXPECT_EQ(extensions, std::vector<std::string>{".EXT"s});
+}
+
+TEST(GetExtensionsForIbmCompatibleSystem, testSingleNoMatchNoModify)
+{
+    std::map<std::string, std::vector<std::string>> extensionMap{{
+        {"system-bar"s, {".EXT"s}},
+    }};
+    std::vector<std::string> compatibleSystem{"system-foo"s},
+        extensions{"foo"s};
+    auto found =
+        functions::process_hostfirmware::getExtensionsForIbmCompatibleSystem(
+            extensionMap, compatibleSystem, extensions);
+    EXPECT_FALSE(found);
+    EXPECT_EQ(extensions, std::vector<std::string>{"foo"s});
+}
+
+TEST(GetExtensionsForIbmCompatibleSystem, testMatchModify)
+{
+    std::map<std::string, std::vector<std::string>> extensionMap{{
+        {"system-bar"s, {".BAR"s}},
+        {"system-foo"s, {".FOO"s}},
+    }};
+    std::vector<std::string> compatibleSystem{"system-foo"s},
+        extensions{"foo"s};
+    auto found =
+        functions::process_hostfirmware::getExtensionsForIbmCompatibleSystem(
+            extensionMap, compatibleSystem, extensions);
+    EXPECT_TRUE(found);
+    EXPECT_EQ(extensions, std::vector<std::string>{".FOO"s});
+}
+
+TEST(GetExtensionsForIbmCompatibleSystem, testEmpty)
+{
+    std::map<std::string, std::vector<std::string>> extensionMap;
+    std::vector<std::string> compatibleSystem{"system-foo"s},
+        extensions{"foo"s};
+    auto found =
+        functions::process_hostfirmware::getExtensionsForIbmCompatibleSystem(
+            extensionMap, compatibleSystem, extensions);
+    EXPECT_FALSE(found);
+    EXPECT_EQ(extensions, std::vector<std::string>{"foo"s});
+}
+
+TEST(GetExtensionsForIbmCompatibleSystem, testEmptyEmpty)
+{
+    std::map<std::string, std::vector<std::string>> extensionMap;
+    std::vector<std::string> compatibleSystem, extensions{"foo"s};
+    auto found =
+        functions::process_hostfirmware::getExtensionsForIbmCompatibleSystem(
+            extensionMap, compatibleSystem, extensions);
+    EXPECT_FALSE(found);
+    EXPECT_EQ(extensions, std::vector<std::string>{"foo"s});
+}
+
+TEST(GetExtensionsForIbmCompatibleSystem, testMatchMultiCompat)
+{
+    std::map<std::string, std::vector<std::string>> extensionMap{{
+        {"system-bar"s, {".BAR"s}},
+        {"system-foo"s, {".FOO"s}},
+    }};
+    std::vector<std::string> compatibleSystem{"system-foo"s, "system"},
+        extensions;
+    auto found =
+        functions::process_hostfirmware::getExtensionsForIbmCompatibleSystem(
+            extensionMap, compatibleSystem, extensions);
+    EXPECT_TRUE(found);
+    EXPECT_EQ(extensions, std::vector<std::string>{".FOO"s});
+}
+
+TEST(GetExtensionsForIbmCompatibleSystem, testMultiMatchMultiCompat)
+{
+    std::map<std::string, std::vector<std::string>> extensionMap{{
+        {"system-bar"s, {".BAR"s}},
+        {"system-foo"s, {".FOO"s}},
+    }};
+    std::vector<std::string> compatibleSystem{"system-foo"s, "system-bar"},
+        extensions;
+    auto found =
+        functions::process_hostfirmware::getExtensionsForIbmCompatibleSystem(
+            extensionMap, compatibleSystem, extensions);
+    EXPECT_TRUE(found);
+    EXPECT_EQ(extensions, std::vector<std::string>{".FOO"s});
+}
+
+TEST(GetExtensionsForIbmCompatibleSystem, testMultiMatchMultiCompat2)
+{
+    std::map<std::string, std::vector<std::string>> extensionMap{{
+        {"system-foo"s, {".FOO"s}},
+        {"system-bar"s, {".BAR"s}},
+    }};
+    std::vector<std::string> compatibleSystem{"system-foo"s, "system-bar"},
+        extensions;
+    auto found =
+        functions::process_hostfirmware::getExtensionsForIbmCompatibleSystem(
+            extensionMap, compatibleSystem, extensions);
+    EXPECT_TRUE(found);
+    EXPECT_EQ(extensions, std::vector<std::string>{".FOO"s});
+}
+
+TEST(GetExtensionsForIbmCompatibleSystem, testMultiMatchMultiCompat3)
+{
+    std::map<std::string, std::vector<std::string>> extensionMap{{
+        {"system-bar"s, {".BAR"s}},
+        {"system-foo"s, {".FOO"s}},
+    }};
+    std::vector<std::string> compatibleSystem{"system-bar", "system-foo"s},
+        extensions;
+    auto found =
+        functions::process_hostfirmware::getExtensionsForIbmCompatibleSystem(
+            extensionMap, compatibleSystem, extensions);
+    EXPECT_TRUE(found);
+    EXPECT_EQ(extensions, std::vector<std::string>{".BAR"s});
+}
+
+TEST(GetExtensionsForIbmCompatibleSystem, testMultiMatchMultiCompat4)
+{
+    std::map<std::string, std::vector<std::string>> extensionMap{{
+        {"system-foo"s, {".FOO"s}},
+        {"system-bar"s, {".BAR"s}},
+    }};
+    std::vector<std::string> compatibleSystem{"system-bar", "system-foo"s},
+        extensions;
+    auto found =
+        functions::process_hostfirmware::getExtensionsForIbmCompatibleSystem(
+            extensionMap, compatibleSystem, extensions);
+    EXPECT_TRUE(found);
+    EXPECT_EQ(extensions, std::vector<std::string>{".BAR"s});
+}
+
+TEST(MaybeCall, noMatch)
+{
+    bool called = false;
+    auto callback = [&called](const auto&) { called = true; };
+    std::map<std::string,
+             std::map<std::string, std::variant<std::vector<std::string>>>>
+        interfaces{{
+            {"foo"s, {{"bar"s, std::vector<std::string>{"foo"s}}}},
+        }};
+    auto found = functions::process_hostfirmware::maybeCall(
+        interfaces, std::move(callback));
+    EXPECT_FALSE(found);
+    EXPECT_FALSE(called);
+}
+
+TEST(MaybeCall, match)
+{
+    bool called = false;
+    std::vector<std::string> sys;
+    auto callback = [&called, &sys](const auto& system) {
+        sys = system;
+        called = true;
+    };
+    std::map<std::string,
+             std::map<std::string, std::variant<std::vector<std::string>>>>
+        interfaces{{
+            {"xyz.openbmc_project.Configuration.IBMCompatibleSystem"s,
+             {{"Names"s, std::vector<std::string>{"foo"s}}}},
+        }};
+    auto found = functions::process_hostfirmware::maybeCall(
+        interfaces, std::move(callback));
+    EXPECT_TRUE(found);
+    EXPECT_TRUE(called);
+    EXPECT_EQ(sys, std::vector<std::string>{"foo"s});
+}
+
+TEST(MaybeCall, missingNames)
+{
+    bool called = false;
+    auto callback = [&called](const auto&) { called = true; };
+    std::map<std::string,
+             std::map<std::string, std::variant<std::vector<std::string>>>>
+        interfaces{{
+            {"xyz.openbmc_project.Configuration.IBMCompatibleSystem"s, {}},
+        }};
+    auto found = functions::process_hostfirmware::maybeCall(
+        interfaces, std::move(callback));
+    EXPECT_TRUE(found);
+    EXPECT_FALSE(called);
+}
+
+TEST(MaybeCall, emptyCallbackFound)
+{
+    std::map<std::string,
+             std::map<std::string, std::variant<std::vector<std::string>>>>
+        interfaces{{
+            {"xyz.openbmc_project.Configuration.IBMCompatibleSystem"s,
+             {{"Names"s, std::vector<std::string>{"foo"s}}}},
+        }};
+    auto found = functions::process_hostfirmware::maybeCall(
+        interfaces, std::function<void(std::vector<std::string>)>());
+    EXPECT_TRUE(found);
+}
+
+TEST(MaybeCall, emptyCallbackNotFound)
+{
+    std::map<std::string,
+             std::map<std::string, std::variant<std::vector<std::string>>>>
+        interfaces{{
+            {"foo"s, {{"Names"s, std::vector<std::string>{"foo"s}}}},
+        }};
+    auto found = functions::process_hostfirmware::maybeCall(
+        interfaces, std::function<void(std::vector<std::string>)>());
+    EXPECT_FALSE(found);
+}
+
+TEST(MaybeCall, emptyInterfaces)
+{
+    bool called = false;
+    auto callback = [&called](const auto&) { called = true; };
+    std::map<std::string,
+             std::map<std::string, std::variant<std::vector<std::string>>>>
+        interfaces;
+    auto found = functions::process_hostfirmware::maybeCall(
+        interfaces, std::move(callback));
+    EXPECT_FALSE(found);
+    EXPECT_FALSE(called);
+}
+
+TEST(MaybeCall, emptyInterfacesEmptyCallback)
+{
+    std::map<std::string,
+             std::map<std::string, std::variant<std::vector<std::string>>>>
+        interfaces;
+    auto found = functions::process_hostfirmware::maybeCall(
+        interfaces, std::function<void(std::vector<std::string>)>());
+    EXPECT_FALSE(found);
+}
+
+TEST(WriteLink, testLinkNoDelete)
+{
+    std::array<char, 15> tmpl{"/tmp/tmpXXXXXX"};
+    std::filesystem::path workdir = mkdtemp(&tmpl[0]);
+    bool called = false;
+    auto callback = [&called](const auto&, auto&) { called = true; };
+    std::filesystem::path linkPath = workdir / "link";
+    std::filesystem::path targetPath = workdir / "target";
+    std::ofstream link{linkPath};
+    functions::process_hostfirmware::writeLink(linkPath.filename(), targetPath,
+                                               callback);
+    std::filesystem::remove_all(workdir);
+    EXPECT_FALSE(called);
+}
+
+TEST(WriteLink, testLinkDelete)
+{
+    std::array<char, 15> tmpl{"/tmp/tmpXXXXXX"};
+    std::filesystem::path workdir = mkdtemp(&tmpl[0]);
+    bool called = false;
+    auto callback = [&called](const auto&, auto&) { called = true; };
+    auto linkPath = workdir / "link";
+    auto targetPath = workdir / "target";
+    std::ofstream link{linkPath}, target{targetPath};
+    functions::process_hostfirmware::writeLink(linkPath.filename(), targetPath,
+                                               callback);
+    std::filesystem::remove_all(workdir);
+    EXPECT_FALSE(called);
+}
+
+TEST(WriteLink, testLinkFailDeleteDir)
+{
+    std::array<char, 15> tmpl{"/tmp/tmpXXXXXX"};
+    std::filesystem::path workdir = mkdtemp(&tmpl[0]);
+    std::error_code ec;
+    std::filesystem::path callbackPath;
+    auto callback = [&ec, &callbackPath](const auto& p, auto& _ec) {
+        ec = _ec;
+        callbackPath = p;
+    };
+    auto targetPath = workdir / "target";
+    std::filesystem::create_directory(targetPath);
+    auto linkPath = workdir / "link";
+    auto filePath = targetPath / "file";
+    std::ofstream link{linkPath}, file{filePath};
+    functions::process_hostfirmware::writeLink(linkPath.filename(), targetPath,
+                                               callback);
+    std::filesystem::remove_all(workdir);
+    EXPECT_EQ(ec.value(), ENOTEMPTY);
+    EXPECT_EQ(callbackPath, targetPath);
+}
+
+TEST(WriteLink, testLinkPathNotExist)
+{
+    std::array<char, 15> tmpl{"/tmp/tmpXXXXXX"};
+    std::filesystem::path workdir = mkdtemp(&tmpl[0]);
+    std::error_code ec;
+    std::filesystem::path callbackPath;
+    auto callback = [&ec, &callbackPath](const auto& p, auto& _ec) {
+        ec = _ec;
+        callbackPath = p;
+    };
+    auto linkPath = workdir / "baz";
+    auto targetPath = workdir / "foo/bar/foo";
+    functions::process_hostfirmware::writeLink(linkPath.filename(), targetPath,
+                                               callback);
+    std::filesystem::remove_all(workdir);
+    EXPECT_EQ(ec.value(), ENOENT);
+    EXPECT_EQ(callbackPath, targetPath);
+}
+
+TEST(FindLinks, testNoLinks)
+{
+    std::array<char, 15> tmpl{"/tmp/tmpXXXXXX"};
+    std::filesystem::path workdir = mkdtemp(&tmpl[0]);
+
+    bool callbackCalled = false, errorCallbackCalled = false;
+    auto callback = [&callbackCalled](const auto&, const auto&, const auto&) {
+        callbackCalled = true;
+    };
+    auto errorCallback = [&errorCallbackCalled](const auto&, auto&) {
+        errorCallbackCalled = true;
+    };
+
+    std::vector<std::string> extensions;
+    functions::process_hostfirmware::findLinks(workdir, extensions,
+                                               errorCallback, callback);
+    std::filesystem::remove_all(workdir);
+    EXPECT_FALSE(errorCallbackCalled);
+    EXPECT_FALSE(callbackCalled);
+}
+
+TEST(FindLinks, testOneFound)
+{
+    std::array<char, 15> tmpl{"/tmp/tmpXXXXXX"};
+    std::filesystem::path workdir = mkdtemp(&tmpl[0]);
+    std::filesystem::path callbackPath, callbackLink;
+
+    bool errorCallbackCalled = false;
+    auto callback = [&callbackPath, &callbackLink](
+                        const auto& p1, const auto& p2, const auto&) {
+        callbackPath = p1;
+        callbackLink = p2;
+    };
+    auto errorCallback = [&errorCallbackCalled](const auto&, auto&) {
+        errorCallbackCalled = true;
+    };
+
+    auto filePath = workdir / "foo.foo";
+    std::ofstream file{filePath};
+    std::vector<std::string> extensions{".foo"s};
+    functions::process_hostfirmware::findLinks(workdir, extensions,
+                                               errorCallback, callback);
+    std::filesystem::remove_all(workdir);
+    EXPECT_FALSE(errorCallbackCalled);
+    EXPECT_EQ(callbackLink, workdir / "foo");
+    EXPECT_EQ(callbackPath, filePath.filename());
+}
+
+TEST(FindLinks, testNoExtensions)
+{
+    std::array<char, 15> tmpl{"/tmp/tmpXXXXXX"};
+    std::filesystem::path workdir = mkdtemp(&tmpl[0]);
+    std::filesystem::path callbackPath, callbackLink;
+
+    bool errorCallbackCalled = false, callbackCalled = false;
+    auto callback = [&callbackCalled](const auto&, const auto&, const auto&) {
+        callbackCalled = true;
+    };
+    auto errorCallback = [&errorCallbackCalled](const auto&, auto&) {
+        errorCallbackCalled = true;
+    };
+
+    auto filePath = workdir / "foo.foo";
+    std::ofstream file{filePath};
+    std::vector<std::string> extensions;
+    functions::process_hostfirmware::findLinks(workdir, extensions,
+                                               errorCallback, callback);
+    std::filesystem::remove_all(workdir);
+    EXPECT_FALSE(errorCallbackCalled);
+    EXPECT_FALSE(callbackCalled);
+}
+
+TEST(FindLinks, testEnoent)
+{
+    std::array<char, 15> tmpl{"/tmp/tmpXXXXXX"};
+    std::filesystem::path workdir = mkdtemp(&tmpl[0]);
+
+    std::error_code ec;
+    bool called = false;
+    std::filesystem::path callbackPath;
+    auto callback = [&called](const auto&, const auto&, const auto&) {
+        called = true;
+    };
+    auto errorCallback = [&ec, &callbackPath](const auto& p, auto& _ec) {
+        ec = _ec;
+        callbackPath = p;
+    };
+
+    std::vector<std::string> extensions;
+    auto dir = workdir / "baz";
+    functions::process_hostfirmware::findLinks(dir, extensions, errorCallback,
+                                               callback);
+    std::filesystem::remove_all(workdir);
+    EXPECT_EQ(ec.value(), ENOENT);
+    EXPECT_EQ(callbackPath, dir);
+    EXPECT_FALSE(called);
+}
+
+TEST(FindLinks, testEmptyCallback)
+{
+    std::array<char, 15> tmpl{"/tmp/tmpXXXXXX"};
+    std::filesystem::path workdir = mkdtemp(&tmpl[0]);
+
+    bool called = false;
+    std::filesystem::path callbackPath;
+    auto errorCallback = [&called](const auto&, auto&) { called = true; };
+
+    auto filePath = workdir / "foo.foo";
+    std::ofstream file{filePath};
+
+    std::vector<std::string> extensions{".foo"s};
+    functions::process_hostfirmware::findLinks(
+        workdir, extensions, errorCallback,
+        functions::process_hostfirmware::LinkCallbackType());
+    std::filesystem::remove_all(workdir);
+    EXPECT_FALSE(called);
+    EXPECT_NO_THROW();
+}
+
+TEST(FindLinks, testEmptyErrorCallback)
+{
+    std::array<char, 15> tmpl{"/tmp/tmpXXXXXX"};
+    std::filesystem::path workdir = mkdtemp(&tmpl[0]);
+
+    bool called = false;
+    auto callback = [&called](const auto&, const auto&, const auto&) {
+        called = true;
+    };
+
+    std::vector<std::string> extensions;
+    auto dir = workdir / "baz";
+    functions::process_hostfirmware::findLinks(
+        dir, extensions, functions::process_hostfirmware::ErrorCallbackType(),
+        callback);
+    std::filesystem::remove_all(workdir);
+    EXPECT_FALSE(called);
+    EXPECT_NO_THROW();
+}