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());
+ }
+}