monitor: Create PowerOffRules class

This class contains a PowerOffCause and a PowerOffAction.  It provides a
check() method that takes the FanHealth map which it then checks against
the cause.  If the cause is satisfied, it then starts the power off
action.  It provides a cancel method that will force cancel a running
action in the case that the object owner detects a system power off and
so doesn't need to run this power off anymore.

The class's configuration data is read from the JSON config file.

Signed-off-by: Matt Spinler <spinler@us.ibm.com>
Change-Id: I5c0c168591d6d62c894c4d036ec762797fd759af
diff --git a/monitor/json_parser.cpp b/monitor/json_parser.cpp
index 51df226..bd239c6 100644
--- a/monitor/json_parser.cpp
+++ b/monitor/json_parser.cpp
@@ -18,8 +18,12 @@
 #include "conditions.hpp"
 #include "json_config.hpp"
 #include "nonzero_speed_trust.hpp"
+#include "power_interface.hpp"
+#include "power_off_rule.hpp"
 #include "types.hpp"
 
+#include <fmt/format.h>
+
 #include <nlohmann/json.hpp>
 #include <phosphor-logging/log.hpp>
 
@@ -239,4 +243,152 @@
     return fanDefs;
 }
 
+PowerRuleState getPowerOffPowerRuleState(const json& powerOffConfig)
+{
+    // The state is optional and defaults to runtime
+    PowerRuleState ruleState{PowerRuleState::runtime};
+
+    if (powerOffConfig.contains("state"))
+    {
+        auto state = powerOffConfig.at("state").get<std::string>();
+        if (state == "at_pgood")
+        {
+            ruleState = PowerRuleState::atPgood;
+        }
+        else if (state != "runtime")
+        {
+            auto msg = fmt::format("Invalid power off state entry {}", state);
+            log<level::ERR>(msg.c_str());
+            throw std::runtime_error(msg.c_str());
+        }
+    }
+
+    return ruleState;
+}
+
+std::unique_ptr<PowerOffCause> getPowerOffCause(const json& powerOffConfig)
+{
+    std::unique_ptr<PowerOffCause> cause;
+
+    if (!powerOffConfig.contains("count") || !powerOffConfig.contains("cause"))
+    {
+        const auto msg =
+            "Missing 'count' or 'cause' entries in power off config";
+        log<level::ERR>(msg);
+        throw std::runtime_error(msg);
+    }
+
+    auto count = powerOffConfig.at("count").get<size_t>();
+    auto powerOffCause = powerOffConfig.at("cause").get<std::string>();
+
+    const std::map<std::string, std::function<std::unique_ptr<PowerOffCause>()>>
+        causes{
+            {"missing_fan_frus",
+             [count]() { return std::make_unique<MissingFanFRUCause>(count); }},
+            {"nonfunc_fan_rotors", [count]() {
+                 return std::make_unique<NonfuncFanRotorCause>(count);
+             }}};
+
+    auto it = causes.find(powerOffCause);
+    if (it != causes.end())
+    {
+        cause = it->second();
+    }
+    else
+    {
+        auto msg =
+            fmt::format("Invalid power off cause {} in power off config JSON",
+                        powerOffCause);
+        log<level::ERR>(msg.c_str());
+        throw std::runtime_error(msg.c_str());
+    }
+
+    return cause;
+}
+
+std::unique_ptr<PowerOffAction>
+    getPowerOffAction(const json& powerOffConfig,
+                      std::shared_ptr<PowerInterfaceBase>& powerInterface)
+{
+    std::unique_ptr<PowerOffAction> action;
+    if (!powerOffConfig.contains("type"))
+    {
+        const auto msg = "Missing 'type' entry in power off config";
+        log<level::ERR>(msg);
+        throw std::runtime_error(msg);
+    }
+
+    auto type = powerOffConfig.at("type").get<std::string>();
+
+    if (((type == "hard") || (type == "soft")) &&
+        !powerOffConfig.contains("delay"))
+    {
+        const auto msg = "Missing 'delay' entry in power off config";
+        log<level::ERR>(msg);
+        throw std::runtime_error(msg);
+    }
+    else if ((type == "epow") &&
+             (!powerOffConfig.contains("service_mode_delay") ||
+              !powerOffConfig.contains("meltdown_delay")))
+    {
+        const auto msg = "Missing 'service_mode_delay' or 'meltdown_delay' "
+                         "entry in power off config";
+        log<level::ERR>(msg);
+        throw std::runtime_error(msg);
+    }
+
+    if (type == "hard")
+    {
+        action = std::make_unique<HardPowerOff>(
+            powerOffConfig.at("delay").get<uint32_t>(), powerInterface);
+    }
+    else if (type == "soft")
+    {
+        action = std::make_unique<SoftPowerOff>(
+            powerOffConfig.at("delay").get<uint32_t>(), powerInterface);
+    }
+    else if (type == "epow")
+    {
+        action = std::make_unique<EpowPowerOff>(
+            powerOffConfig.at("service_mode_delay").get<uint32_t>(),
+            powerOffConfig.at("meltdown_delay").get<uint32_t>(),
+            powerInterface);
+    }
+    else
+    {
+        auto msg =
+            fmt::format("Invalid 'type' entry {} in power off config", type);
+        log<level::ERR>(msg.c_str());
+        throw std::runtime_error(msg.c_str());
+    }
+
+    return action;
+}
+
+std::vector<std::unique_ptr<PowerOffRule>>
+    getPowerOffRules(const json& obj,
+                     std::shared_ptr<PowerInterfaceBase>& powerInterface)
+{
+    std::vector<std::unique_ptr<PowerOffRule>> rules;
+
+    if (!(obj.contains("fault_handling") &&
+          obj.at("fault_handling").contains("power_off_config")))
+    {
+        return rules;
+    }
+
+    for (const auto& config : obj.at("fault_handling").at("power_off_config"))
+    {
+        auto state = getPowerOffPowerRuleState(config);
+        auto cause = getPowerOffCause(config);
+        auto action = getPowerOffAction(config, powerInterface);
+
+        auto rule = std::make_unique<PowerOffRule>(
+            std::move(state), std::move(cause), std::move(action));
+        rules.push_back(std::move(rule));
+    }
+
+    return rules;
+}
+
 } // namespace phosphor::fan::monitor
diff --git a/monitor/json_parser.hpp b/monitor/json_parser.hpp
index ac91463..84876ff 100644
--- a/monitor/json_parser.hpp
+++ b/monitor/json_parser.hpp
@@ -26,6 +26,8 @@
 {
 
 using json = nlohmann::json;
+class PowerOffRule;
+class PowerInterfaceBase;
 
 constexpr auto confAppName = "monitor";
 constexpr auto confFileName = "config.json";
@@ -77,4 +79,18 @@
  */
 const std::vector<FanDefinition> getFanDefs(const json& obj);
 
+/**
+ * @brief Get the configured power off rules
+ *
+ * @param[in] obj - JSON object to parse from
+ *
+ * @param[in] powerInterface - The power interface object to use
+ *
+ * @return std::vector<std::unique_ptr<PowerOffRule>> -
+ *     The PowerOffRule objects
+ */
+std::vector<std::unique_ptr<PowerOffRule>>
+    getPowerOffRules(const json& obj,
+                     std::shared_ptr<PowerInterfaceBase>& powerInterface);
+
 } // namespace phosphor::fan::monitor
diff --git a/monitor/power_off_rule.hpp b/monitor/power_off_rule.hpp
new file mode 100644
index 0000000..f28dce6
--- /dev/null
+++ b/monitor/power_off_rule.hpp
@@ -0,0 +1,144 @@
+#pragma once
+
+#include "logging.hpp"
+#include "power_off_action.hpp"
+#include "power_off_cause.hpp"
+
+#include <memory>
+
+namespace phosphor::fan::monitor
+{
+
+/**
+ * @brief Describes when the rule should be checked.
+ *        either right at PGOOD, or anytime at runtime.
+ */
+enum class PowerRuleState
+{
+    atPgood, // Only at the moment when PGOOD switches on.
+    runtime  // Anytime that power is on.
+};
+
+/**
+ * @class PowerOffRule
+ *
+ * This class implements a power off rule, which has a cause
+ * that is based on fan status, and an action which is the type of
+ * power off that will occur when the cause is satisfied.
+ *
+ * The user of this class calls the 'check()' method when fan
+ * status may have changed, and then the power off action may
+ * be started.
+ */
+class PowerOffRule
+{
+  public:
+    PowerOffRule() = delete;
+    ~PowerOffRule() = default;
+    PowerOffRule(const PowerOffRule&) = delete;
+    PowerOffRule& operator=(const PowerOffRule&) = delete;
+    PowerOffRule(PowerOffRule&&) = delete;
+    PowerOffRule& operator=(PowerOffRule&&) = delete;
+
+    /**
+     * @brief Constructor
+     *
+     * @param[in] validState - What state the rule is valid for
+     * @param[in] cause - The power off cause to use
+     * @param[in] action - The power off action to use
+     */
+    PowerOffRule(PowerRuleState validState,
+                 std::unique_ptr<PowerOffCause> cause,
+                 std::unique_ptr<PowerOffAction> action) :
+        _validState(validState),
+        _cause(std::move(cause)), _action(std::move(action))
+    {}
+
+    /**
+     * @brief Used to cancel a delay based power off when
+     *        there is still time left.
+     */
+    void cancel()
+    {
+        _active = false;
+
+        // force the cancel
+        _action->cancel(true);
+    }
+
+    /**
+     * @brief Checks the cause against the passed in fan health
+     *        and starts the power off action if the cause
+     *        is satisfied.
+     *
+     * @param[in] state - The state to check the rule at
+     * @param[in] fanHealth - The fan health map
+     */
+    void check(PowerRuleState state, const FanHealth& fanHealth)
+    {
+        if (state == _validState)
+        {
+            auto satisfied = _cause->satisfied(fanHealth);
+
+            if (!_active && satisfied)
+            {
+                // Start the action
+                _active = true;
+                _action->start();
+                getLogger().log(fmt::format(
+                    "Starting shutdown action '{}' due to cause '{}'",
+                    _action->name(), _cause->name()));
+            }
+            else if (_active && !satisfied)
+            {
+                // Attempt to cancel the action, but don't force it
+                if (_action->cancel(false))
+                {
+                    getLogger().log(fmt::format("Stopped shutdown action '{}'",
+                                                _action->name()));
+                    _active = false;
+                }
+                else
+                {
+                    getLogger().log(
+                        fmt::format("Could not stop shutdown action '{}'",
+                                    _action->name()));
+                }
+            }
+        }
+    }
+
+    /**
+     * @brief Says if there is an active power off in progress due to
+     *        this rule.
+     *
+     * @return bool - If the rule is active or not
+     */
+    bool active() const
+    {
+        return _active;
+    }
+
+  private:
+    /**
+     * @brief The state the rule is valid for.
+     */
+    PowerRuleState _validState;
+
+    /**
+     * @brief If there is an active power off in progress.
+     */
+    bool _active{false};
+
+    /**
+     * @brief Base class pointer to the power off cause class
+     */
+    std::unique_ptr<PowerOffCause> _cause;
+
+    /**
+     * @brief Base class pointer to the power off action class
+     */
+    std::unique_ptr<PowerOffAction> _action;
+};
+
+} // namespace phosphor::fan::monitor
diff --git a/monitor/test/Makefile.am b/monitor/test/Makefile.am
index 32b8c3b..e6d0f9b 100644
--- a/monitor/test/Makefile.am
+++ b/monitor/test/Makefile.am
@@ -7,7 +7,8 @@
 TESTS = $(check_PROGRAMS)
 
 check_PROGRAMS += \
-	power_off_cause_test
+	power_off_cause_test \
+	power_off_rule_test
 
 power_off_cause_test_SOURCES = \
 	power_off_cause_test.cpp
@@ -17,3 +18,18 @@
 	$(OESDK_TESTCASE_FLAGS)
 power_off_cause_test_LDADD = \
 	$(gtest_ldadd)
+
+power_off_rule_test_SOURCES = \
+	power_off_rule_test.cpp \
+	../conditions.cpp \
+	../json_parser.cpp \
+	../logging.cpp
+power_off_rule_test_CXXFLAGS = \
+	$(gtest_cflags)
+power_off_rule_test_LDFLAGS = \
+	$(OESDK_TESTCASE_FLAGS)
+power_off_rule_test_LDADD = \
+	$(gtest_ldadd) \
+	$(FMT_LIBS) \
+	$(SDBUSPLUS_LIBS) \
+	$(SDEVENTPLUS_LIBS)
diff --git a/monitor/test/mock_power_interface.hpp b/monitor/test/mock_power_interface.hpp
new file mode 100644
index 0000000..c070182
--- /dev/null
+++ b/monitor/test/mock_power_interface.hpp
@@ -0,0 +1,17 @@
+#pragma once
+
+#include "../power_interface.hpp"
+
+#include <gmock/gmock.h>
+
+namespace phosphor::fan::monitor
+{
+
+class MockPowerInterface : public PowerInterfaceBase
+{
+  public:
+    MOCK_METHOD(void, softPowerOff, (), (override));
+    MOCK_METHOD(void, hardPowerOff, (), (override));
+};
+
+} // namespace phosphor::fan::monitor
diff --git a/monitor/test/power_off_rule_test.cpp b/monitor/test/power_off_rule_test.cpp
new file mode 100644
index 0000000..b708bb9
--- /dev/null
+++ b/monitor/test/power_off_rule_test.cpp
@@ -0,0 +1,148 @@
+#include "../json_parser.hpp"
+#include "../power_off_rule.hpp"
+#include "mock_power_interface.hpp"
+
+#include <gtest/gtest.h>
+
+using namespace phosphor::fan::monitor;
+using json = nlohmann::json;
+
+TEST(PowerOffRuleTest, TestRules)
+{
+    sd_event* event;
+    sd_event_default(&event);
+    sdeventplus::Event sdEvent{event};
+
+    const auto faultConfig = R"(
+    {
+        "fault_handling":
+        {
+            "power_off_config": [
+                {
+                    "type": "hard",
+                    "cause": "missing_fan_frus",
+                    "count": 2,
+                    "delay": 0,
+                    "state": "at_pgood"
+                },
+                {
+                    "type": "soft",
+                    "cause": "nonfunc_fan_rotors",
+                    "count": 3,
+                    "delay": 0,
+                    "state": "runtime"
+                },
+                {
+                    "type": "soft",
+                    "cause": "nonfunc_fan_rotors",
+                    "count": 4,
+                    "delay": 1,
+                    "state": "runtime"
+                },
+                {
+                    "type": "hard",
+                    "cause": "missing_fan_frus",
+                    "count": 4,
+                    "delay": 1,
+                    "state": "runtime"
+                }
+            ]
+        }
+    })"_json;
+
+    std::shared_ptr<PowerInterfaceBase> powerIface =
+        std::make_shared<MockPowerInterface>();
+
+    MockPowerInterface& mockIface =
+        static_cast<MockPowerInterface&>(*powerIface);
+
+    EXPECT_CALL(mockIface, hardPowerOff).Times(1);
+    EXPECT_CALL(mockIface, softPowerOff).Times(1);
+
+    auto rules = getPowerOffRules(faultConfig, powerIface);
+    ASSERT_EQ(rules.size(), 4);
+
+    FanHealth health{{"fan0", {false, {true, true}}},
+                     {"fan1", {false, {true, true}}}};
+
+    {
+        // Check rule 0
+
+        // wrong state, won't be active
+        rules[0]->check(PowerRuleState::runtime, health);
+        EXPECT_FALSE(rules[0]->active());
+
+        rules[0]->check(PowerRuleState::atPgood, health);
+        EXPECT_TRUE(rules[0]->active());
+
+        // Run the event loop, since the timeout is 0 it should
+        // run the power off and satisfy the EXPECT_CALL above
+        sdEvent.run(std::chrono::milliseconds(1));
+
+        // It doesn't really make much sense to cancel a rule after it
+        // powered off, but it should at least say it isn't active.
+        rules[0]->cancel();
+        EXPECT_FALSE(rules[0]->active());
+    }
+
+    {
+        // Check the second rule.
+        rules[1]->check(PowerRuleState::runtime, health);
+        EXPECT_FALSE(rules[1]->active());
+
+        // > 2 nonfunc rotors
+        health["fan0"] = {true, {true, false}};
+        health["fan1"] = {true, {false, false}};
+
+        rules[1]->check(PowerRuleState::runtime, health);
+        EXPECT_TRUE(rules[1]->active());
+
+        // Run the event loop, since the timeout is 0 it should
+        // run the power off and satisfy the EXPECT_CALL above
+        sdEvent.run(std::chrono::milliseconds(1));
+    }
+
+    {
+        // Check the third rule.  It has a timeout so long we can
+        // cancel it before it runs.
+        health["fan0"] = {true, {false, false}};
+        health["fan1"] = {true, {false, false}};
+
+        rules[2]->check(PowerRuleState::runtime, health);
+        EXPECT_TRUE(rules[2]->active());
+
+        sdEvent.run(std::chrono::milliseconds(1));
+
+        rules[2]->cancel();
+        EXPECT_FALSE(rules[2]->active());
+
+        // This will go past the timeout, it should have been canceled so the
+        // soft power off won't have run and the EXPECT_CALL above
+        // should be happy.
+        sdEvent.run(std::chrono::seconds(1));
+    }
+
+    {
+        // Check the 4th rule. Resolve it before it completes
+        health["fan0"] = {false, {true, true}};
+        health["fan1"] = {false, {true, true}};
+        health["fan2"] = {false, {true, true}};
+        health["fan3"] = {false, {true, true}};
+
+        rules[3]->check(PowerRuleState::runtime, health);
+        EXPECT_TRUE(rules[3]->active());
+
+        // Won't complete yet
+        sdEvent.run(std::chrono::milliseconds(1));
+
+        // Make them present
+        health["fan0"] = {true, {true, true}};
+        health["fan1"] = {true, {true, true}};
+        health["fan2"] = {true, {true, true}};
+        health["fan3"] = {true, {true, true}};
+
+        //  It should be inactive now
+        rules[3]->check(PowerRuleState::runtime, health);
+        EXPECT_FALSE(rules[3]->active());
+    }
+}