sysd_monitor: Parse json file(s)
Parse the json file(s) into a c++ data object that can be used in later
commits to easily check for monitored targets and log appropriate
errors.
Accept a command line parameter to input file and call parsing function
Tested:
- Verified 100% code coverage of new parser cpp
Change-Id: I0bacd80b7f5330eb9cb03d8e3717742ab107bf94
Signed-off-by: Andrew Geissler <geissonator@yahoo.com>
diff --git a/Makefile.am b/Makefile.am
index 1be2555..aba2d6b 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -1,5 +1,23 @@
AM_DEFAULT_SOURCE_EXT = .cpp
+if AUTOCONF_CODE_COVERAGE_2019_01_06
+include $(top_srcdir)/aminclude_static.am
+clean-local: code-coverage-clean
+distclean-local: code-coverage-dist-clean
+else
+@CODE_COVERAGE_RULES@
+endif
+
+AM_LIBS = $(CODE_COVERAGE_LIBS)
+AM_CPPFLAGS = $(CODE_COVERAGE_CPPFLAGS) -UNDEBUG $(GTEST_CPPFLAGS)
+AM_CFLAGS = $(CODE_COVERAGE_CFLAGS)
+AM_CXXFLAGS = $(CODE_COVERAGE_CXXFLAGS) \
+ -DBOOST_SYSTEM_NO_DEPRECATED -DBOOST_ERROR_CODE_HEADER_ONLY \
+ -DBOOST_ALL_NO_LIB
+AM_LDFLAGS = $(GMOCK_LIBS) -lgmock_main \
+ $(GTEST_LIBS) $(OESDK_TESTCASE_FLAGS) $(PTHREAD_LIBS) \
+$(SDBUSPLUS_LIBS) -lboost_system
+
bin_PROGRAMS = \
phosphor-host-state-manager \
phosphor-chassis-state-manager \
@@ -32,7 +50,8 @@
host_check_main.cpp
phosphor_systemd_target_monitor_SOURCES = \
- systemd_target_monitor.cpp
+ systemd_target_monitor.cpp \
+ systemd_target_parser.cpp
generic_cxxflags = \
$(SYSTEMD_CFLAGS) \
@@ -73,3 +92,14 @@
$(generic_ldflags) \
$(SDEVENTPLUS_LIBS) \
-lstdc++fs
+
+
+check_PROGRAMS =
+XFAIL_TESTS =
+
+# Ignore system headers
+CODE_COVERAGE_IGNORE_PATTERN = '/include/*' '/usr/include/*' '$(includedir)/*'
+
+include test/Makefile.am.include
+
+TESTS = $(check_PROGRAMS)
diff --git a/configure.ac b/configure.ac
index 07ef420..e6ec283 100644
--- a/configure.ac
+++ b/configure.ac
@@ -2,7 +2,7 @@
AC_INIT([phosphor-state-manager], [1.0], [https://github.com/openbmc/phosphor-state-manager/issues])
AC_LANG([C++])
AC_CONFIG_HEADERS([config.h])
-AM_INIT_AUTOMAKE([subdir-objects -Wall -Werror foreign dist-xz])
+AM_INIT_AUTOMAKE([subdir-objects -Wall -Wno-portability -Werror foreign])
AM_SILENT_RULES([yes])
# Checks for programs
@@ -23,6 +23,8 @@
[],
[AC_MSG_ERROR([Could not find CLI11 CLI/CLI.hpp])]
)
+AC_CHECK_HEADER(nlohmann/json.hpp, ,
+ [AC_MSG_ERROR([Could not find nlohmann/json.hpp... nlohmann/json package required])])
# Checks for typedefs, structures, and compiler characteristics.
AX_CXX_COMPILE_STDCXX_17([noext])
@@ -90,5 +92,88 @@
AS_IF([test "x$CLASS_VERSION" == "x"], [CLASS_VERSION=1])
AC_DEFINE_UNQUOTED([CLASS_VERSION], [$CLASS_VERSION], [Class version to register with Cereal])
+# Make it possible for users to choose if they want test support
+# explicitly or not at all
+AC_ARG_ENABLE([tests], AC_HELP_STRING([--disable-tests],
+ [Build test cases]))
+
+ # Check/set gtest specific functions
+AS_IF([test "x$enable_tests" != "xno"], [
+ PKG_CHECK_MODULES([GTEST], [gtest], [], [true])
+ PKG_CHECK_MODULES([GMOCK], [gmock], [], [true])
+ AX_PTHREAD
+
+ AX_SAVE_FLAGS_WITH_PREFIX(OLD, [CPPFLAGS])
+ AX_APPEND_COMPILE_FLAGS([$GTEST_CFLAGS], [CPPFLAGS])
+ AC_CHECK_HEADERS([gtest/gtest.h], [
+ AS_IF([test "x$GTEST_CFLAGS" = "x"], [
+ AS_IF([test "x$PTHREAD_CFLAGS" = "x"], [
+ AX_APPEND_COMPILE_FLAGS(["-DGTEST_HAS_PTHREAD=0"], [GTEST_CFLAGS])
+ ], [
+ AX_APPEND_COMPILE_FLAGS(["-DGTEST_HAS_PTHREAD=1"], [GTEST_CFLAGS])
+ AX_APPEND_COMPILE_FLAGS([$PTHREAD_CFLAGS], [GTEST_CFLAGS])
+ ])
+ ])
+ ], [
+ AS_IF([test "x$enable_tests" = "xyes"], [
+ AC_MSG_ERROR([Testing enabled but could not find gtest/gtest.h])
+ ])
+ ])
+ AX_RESTORE_FLAGS_WITH_PREFIX(OLD, [CPPFLAGS])
+
+ AX_SAVE_FLAGS_WITH_PREFIX(OLD, [CPPFLAGS])
+ AX_APPEND_COMPILE_FLAGS([$GMOCK_CFLAGS], [CPPFLAGS])
+ AC_CHECK_HEADERS([gmock/gmock.h], [], [
+ AS_IF([test "x$enable_tests" = "xyes"], [
+ AC_MSG_ERROR([Testing enabled but could not find gmock/gmock.h])
+ ])
+ ])
+ AX_RESTORE_FLAGS_WITH_PREFIX(OLD, [CPPFLAGS])
+
+ AX_SAVE_FLAGS_WITH_PREFIX(OLD, [LDFLAGS])
+ AX_APPEND_COMPILE_FLAGS([$GTEST_LIBS], [LDFLAGS])
+ AC_CHECK_LIB([gtest], [main], [
+ AS_IF([test "x$GTEST_LIBS" = "x"], [
+ AX_APPEND_COMPILE_FLAGS([-lgtest], [GTEST_LIBS])
+ ])
+ ], [
+ AS_IF([test "x$enable_tests" = "xyes"], [
+ AC_MSG_ERROR([Testing enabled but couldn't find gtest libs])
+ ])
+ ])
+ AX_RESTORE_FLAGS_WITH_PREFIX(OLD, [LDFLAGS])
+
+ AX_SAVE_FLAGS_WITH_PREFIX(OLD, [LDFLAGS])
+ AX_APPEND_COMPILE_FLAGS([$GMOCK_LIBS], [LDFLAGS])
+ AC_CHECK_LIB([gmock], [main], [
+ AS_IF([test "x$GMOCK_LIBS" = "x"], [
+ AX_APPEND_COMPILE_FLAGS([-lgmock], [GMOCK_LIBS])
+ ])
+ ], [
+ AS_IF([test "x$enable_tests" = "xyes"], [
+ AC_MSG_ERROR([Testing enabled but couldn't find gmock libs])
+ ])
+ ])
+ AX_RESTORE_FLAGS_WITH_PREFIX(OLD, [LDFLAGS])
+])
+
+# Check for valgrind
+AS_IF([test "x$enable_tests" = "xno"], [enable_valgrind=no])
+m4_foreach([vgtool], [valgrind_tool_list],
+ [AX_VALGRIND_DFLT(vgtool, [off])])
+AX_VALGRIND_DFLT([memcheck], [on])
+AX_VALGRIND_CHECK
+AM_EXTRA_RECURSIVE_TARGETS([check-valgrind])
+m4_foreach([vgtool], [valgrind_tool_list],
+ [AM_EXTRA_RECURSIVE_TARGETS([check-valgrind-]vgtool)])
+
+# Code coverage
+AX_CODE_COVERAGE
+
+m4_ifdef([_AX_CODE_COVERAGE_RULES],
+ [AM_CONDITIONAL(AUTOCONF_CODE_COVERAGE_2019_01_06, [true])],
+ [AM_CONDITIONAL(AUTOCONF_CODE_COVERAGE_2019_01_06, [false])])
+AX_ADD_AM_MACRO_STATIC([])
+
AC_CONFIG_FILES([Makefile])
AC_OUTPUT
diff --git a/systemd_target_monitor.cpp b/systemd_target_monitor.cpp
index 5fa05fa..2c58769 100644
--- a/systemd_target_monitor.cpp
+++ b/systemd_target_monitor.cpp
@@ -3,11 +3,30 @@
#include <phosphor-logging/log.hpp>
#include <sdbusplus/bus.hpp>
#include <sdeventplus/event.hpp>
+#include <systemd_target_parser.hpp>
#include <vector>
using phosphor::logging::level;
using phosphor::logging::log;
+bool gVerbose = false;
+
+void dump_targets(const TargetErrorData& targetData)
+{
+ std::cout << "## Data Structure of Json ##" << std::endl;
+ for (const auto& [target, value] : targetData)
+ {
+ std::cout << target << " " << value.errorToLog << std::endl;
+ std::cout << " ";
+ for (auto& eToMonitor : value.errorsToMonitor)
+ {
+ std::cout << eToMonitor << ", ";
+ }
+ std::cout << std::endl;
+ }
+ std::cout << std::endl;
+}
+
void print_usage(void)
{
std::cout << "[-f <file1> -f <file2> ...] : Full path to json file(s) with "
@@ -26,6 +45,7 @@
CLI::App app{"OpenBmc systemd target monitor"};
app.add_option("-f,--file", filePaths,
"Full path to json file(s) with target/error mappings");
+ app.add_flag("-v", gVerbose, "Enable verbose output");
CLI11_PARSE(app, argc, argv);
@@ -36,7 +56,19 @@
exit(-1);
}
- // TODO - Load in json config file(s)
+ TargetErrorData targetData = parseFiles(filePaths);
+
+ if (targetData.size() == 0)
+ {
+ log<level::ERR>("Invalid input files, no targets found");
+ print_usage();
+ exit(-1);
+ }
+
+ if (gVerbose)
+ {
+ dump_targets(targetData);
+ }
// TODO - Begin monitoring for systemd unit changes and logging appropriate
// errors
diff --git a/systemd_target_parser.cpp b/systemd_target_parser.cpp
new file mode 100644
index 0000000..d2cfa86
--- /dev/null
+++ b/systemd_target_parser.cpp
@@ -0,0 +1,58 @@
+#include <cassert>
+#include <fstream>
+#include <iostream>
+#include <systemd_target_parser.hpp>
+
+void validateErrorsToMonitor(std::vector<std::string>& errorsToMonitor)
+{
+ assert(errorsToMonitor.size());
+
+ const std::vector<std::string> validErrorsToMonitor = {
+ "default", "timeout", "failed", "dependency"};
+ for (const auto& errorToMonitor : errorsToMonitor)
+ {
+ if (std::find(validErrorsToMonitor.begin(), validErrorsToMonitor.end(),
+ errorToMonitor) == validErrorsToMonitor.end())
+ {
+ throw std::out_of_range("Found invalid error to monitor");
+ }
+ }
+}
+
+TargetErrorData parseFiles(const std::vector<std::string>& filePaths)
+{
+ TargetErrorData systemdTargetMap;
+ for (const auto& jsonFile : filePaths)
+ {
+ if (gVerbose)
+ {
+ std::cout << "Parsing input file " << jsonFile << std::endl;
+ }
+ std::ifstream fileStream(jsonFile);
+ auto j = json::parse(fileStream);
+
+ for (auto it = j["targets"].begin(); it != j["targets"].end(); ++it)
+ {
+ targetEntry entry;
+ if (gVerbose)
+ {
+ std::cout << "target: " << it.key() << " | " << it.value()
+ << std::endl;
+ }
+
+ // Be unforgiving on invalid json files. Just throw or allow
+ // nlohmann to throw an exception if something is off
+ auto errorsToMonitor = it.value().find("errorsToMonitor");
+ entry.errorsToMonitor =
+ errorsToMonitor->get<std::vector<std::string>>();
+
+ validateErrorsToMonitor(entry.errorsToMonitor);
+
+ auto errorToLog = it.value().find("errorToLog");
+ entry.errorToLog = errorToLog->get<std::string>();
+
+ systemdTargetMap[it.key()] = entry;
+ }
+ }
+ return systemdTargetMap;
+}
diff --git a/systemd_target_parser.hpp b/systemd_target_parser.hpp
new file mode 100644
index 0000000..fbe3814
--- /dev/null
+++ b/systemd_target_parser.hpp
@@ -0,0 +1,35 @@
+#pragma once
+
+#include <map>
+#include <string>
+#include <vector>
+
+#include <nlohmann/json.hpp>
+
+/** @brief Stores the error to log if errors to monitor is found */
+struct targetEntry
+{
+ std::string errorToLog;
+ std::vector<std::string> errorsToMonitor;
+};
+
+/** @brief A map of the systemd target to its corresponding targetEntry*/
+using TargetErrorData = std::map<std::string, targetEntry>;
+
+using json = nlohmann::json;
+
+extern bool gVerbose;
+
+/** @brief Parse input json files
+ *
+ * Will return the parsed data in the TargetErrorData object
+ *
+ * @note This function will throw exceptions for an invalid json file
+ * @note See phosphor-target-monitor-default.json for example of json file
+ * format
+ *
+ * @param[in] filePaths - The file(s) to parse
+ *
+ * @return Map of target to error log relationships
+ */
+TargetErrorData parseFiles(const std::vector<std::string>& filePaths);
diff --git a/test/Makefile.am.include b/test/Makefile.am.include
new file mode 100644
index 0000000..95bf636
--- /dev/null
+++ b/test/Makefile.am.include
@@ -0,0 +1,4 @@
+test_systemd_parser_SOURCES = %reldir%/systemd_parser.cpp systemd_target_parser.cpp
+
+check_PROGRAMS += \
+ %reldir%/systemd_parser
diff --git a/test/systemd_parser.cpp b/test/systemd_parser.cpp
new file mode 100644
index 0000000..7ca94b9
--- /dev/null
+++ b/test/systemd_parser.cpp
@@ -0,0 +1,109 @@
+#include <systemd_target_parser.hpp>
+#include <gtest/gtest.h>
+
+#include <iostream>
+#include <cstdio>
+#include <cstdlib>
+#include <filesystem>
+
+namespace fs = std::filesystem;
+
+// Enable debug by default for debug when needed
+bool gVerbose = true;
+
+TEST(TargetJsonParser, BasicGoodPath)
+{
+ auto defaultData1 = R"(
+ {
+ "targets" : {
+ "multi-user.target" : {
+ "errorsToMonitor": ["default"],
+ "errorToLog": "xyz.openbmc_project.State.BMC.Error.MultiUserTargetFailure"},
+ "obmc-chassis-poweron@0.target" : {
+ "errorsToMonitor": ["timeout", "failed"],
+ "errorToLog": "xyz.openbmc_project.State.Chassis.Error.PowerOnTargetFailure"}
+ }
+ }
+ )"_json;
+
+ auto defaultData2 = R"(
+ {
+ "targets" : {
+ "obmc-host-start@0.target" : {
+ "errorsToMonitor": ["default"],
+ "errorToLog": "xyz.openbmc_project.State.Host.Error.HostStartFailure"},
+ "obmc-host-stop@0.target" : {
+ "errorsToMonitor": ["dependency"],
+ "errorToLog": "xyz.openbmc_project.State.Host.Error.HostStopFailure"}
+ }
+ }
+ )"_json;
+
+ std::FILE* tmpf = fopen("/tmp/good_file1.json", "w");
+ std::fputs(defaultData1.dump().c_str(), tmpf);
+ std::fclose(tmpf);
+
+ tmpf = fopen("/tmp/good_file2.json", "w");
+ std::fputs(defaultData2.dump().c_str(), tmpf);
+ std::fclose(tmpf);
+
+ std::vector<std::string> filePaths;
+ filePaths.push_back("/tmp/good_file1.json");
+ filePaths.push_back("/tmp/good_file2.json");
+
+ TargetErrorData targetData = parseFiles(filePaths);
+
+ EXPECT_EQ(targetData.size(), 4);
+ EXPECT_NE(targetData.find("multi-user.target"), targetData.end());
+ EXPECT_NE(targetData.find("obmc-chassis-poweron@0.target"),
+ targetData.end());
+ EXPECT_NE(targetData.find("obmc-host-start@0.target"), targetData.end());
+ EXPECT_NE(targetData.find("obmc-host-stop@0.target"), targetData.end());
+ targetEntry tgt = targetData["obmc-chassis-poweron@0.target"];
+ EXPECT_EQ(tgt.errorToLog,
+ "xyz.openbmc_project.State.Chassis.Error.PowerOnTargetFailure");
+ EXPECT_EQ(tgt.errorsToMonitor.size(), 2);
+
+ std::remove("/tmp/good_file1.json");
+ std::remove("/tmp/good_file2.json");
+}
+
+TEST(TargetJsonParser, InvalidErrorToMonitor)
+{
+ auto invalidDataError = R"(
+ {
+ "targets" : {
+ "obmc-chassis-poweron@0.target" : {
+ "errorsToMonitor": ["timeout", "invalid"],
+ "errorToLog": "xyz.openbmc_project.State.Chassis.Error.PowerOnTargetFailure"}
+ }
+ }
+ )"_json;
+
+ std::FILE* tmpf = fopen("/tmp/invalid_error_file.json", "w");
+ std::fputs(invalidDataError.dump().c_str(), tmpf);
+ std::fclose(tmpf);
+
+ std::vector<std::string> filePaths;
+ filePaths.push_back("/tmp/invalid_error_file.json");
+
+ // Verify exception thrown on invalid errorsToMonitor
+ EXPECT_THROW(TargetErrorData targetData = parseFiles(filePaths),
+ std::out_of_range);
+ std::remove("/tmp/invalid_error_file.json");
+}
+
+TEST(TargetJsonParser, InvalidFileFormat)
+{
+ std::FILE* tmpf = fopen("/tmp/invalid_json_file.json", "w");
+ std::fputs("{\"targets\":{\"missing closing quote}}", tmpf);
+ fclose(tmpf);
+
+ std::vector<std::string> filePaths;
+ filePaths.push_back("/tmp/invalid_json_file.json");
+
+ // Verify exception thrown on invalid json file format
+ EXPECT_THROW(TargetErrorData targetData = parseFiles(filePaths),
+ nlohmann::detail::parse_error);
+ std::remove("/tmp/invalid_json_file.json");
+}