control: Add init trigger support

Add init trigger support to the available triggers where init triggered
event actions are run immediately when fan control starts. Initially the
`getProperties` and `nameHasOwner` methods are supported to be triggered
when fan control starts. The `getProperties` method populates the dbus
object cache for each group member's property configured. The
`nameHasOwner` method populates the service owned state from dbus for
each group member. Each of these attempts to do bulk
reads(getManagedObjects for `getProperties` and getSubTree for
`nameHasOwner`) from dbus when populating the caches to minimize dbus
calls.

Change-Id: Ib5d89eceadd26db1bf7610113ea30fc8ba69ab12
Signed-off-by: Matthew Barth <msbarth@us.ibm.com>
diff --git a/control/Makefile.am b/control/Makefile.am
index 5a70b07..9408358 100644
--- a/control/Makefile.am
+++ b/control/Makefile.am
@@ -32,6 +32,7 @@
 	json/event.cpp \
 	json/triggers/timer.cpp \
 	json/triggers/signal.cpp \
+	json/triggers/init.cpp \
 	json/actions/default_floor.cpp \
 	json/actions/request_target_base.cpp \
 	json/actions/missing_owner_target.cpp \
diff --git a/control/json/triggers/init.cpp b/control/json/triggers/init.cpp
new file mode 100644
index 0000000..160b725
--- /dev/null
+++ b/control/json/triggers/init.cpp
@@ -0,0 +1,165 @@
+/**
+ * Copyright © 2021 IBM Corporation
+ *
+ * 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 "init.hpp"
+
+#include "../manager.hpp"
+#include "action.hpp"
+#include "group.hpp"
+#include "sdbusplus.hpp"
+
+#include <fmt/format.h>
+
+#include <nlohmann/json.hpp>
+#include <phosphor-logging/log.hpp>
+
+#include <algorithm>
+#include <iterator>
+#include <memory>
+#include <numeric>
+#include <utility>
+#include <vector>
+
+namespace phosphor::fan::control::json::trigger::init
+{
+
+using json = nlohmann::json;
+using namespace phosphor::logging;
+
+void getProperties(Manager* mgr, const Group& group)
+{
+    for (const auto& member : group.getMembers())
+    {
+        try
+        {
+            // Check if property already cached
+            auto value = mgr->getProperty(member, group.getInterface(),
+                                          group.getProperty());
+            if (value == std::nullopt)
+            {
+                // Property not in cache, attempt to add it
+                mgr->addObjects(member, group.getInterface(),
+                                group.getProperty());
+            }
+        }
+        catch (const util::DBusMethodError& dme)
+        {
+            // Configured dbus object does not exist on dbus yet?
+            // TODO How to handle this? Create timer to keep checking for
+            // object/service to appear? When to stop checking?
+        }
+    }
+}
+
+void nameHasOwner(Manager* mgr, const Group& group)
+{
+    std::string lastName = "";
+    for (const auto& member : group.getMembers())
+    {
+        std::string servName = "";
+        auto intf = group.getInterface();
+        try
+        {
+            servName = mgr->getService(member, intf);
+            if (!servName.empty() && lastName != servName)
+            {
+                // Member not provided by same service as last group member
+                lastName = servName;
+                auto hasOwner = util::SDBusPlus::callMethodAndRead<bool>(
+                    mgr->getBus(), "org.freedesktop.DBus",
+                    "/org/freedesktop/DBus", "org.freedesktop.DBus",
+                    "NameHasOwner", servName);
+                // Update service name owner state of group object
+                mgr->setOwner(member, servName, intf, hasOwner);
+            }
+            if (servName.empty())
+            {
+                // Path and/or interface configured does not exist on dbus?
+                // TODO How to handle this? Create timer to keep checking for
+                // object/service to appear? When to stop checking?
+                log<level::ERR>(
+                    fmt::format(
+                        "Unable to get service name for path {}, interface {}",
+                        member, intf)
+                        .c_str());
+            }
+        }
+        catch (const util::DBusMethodError& dme)
+        {
+            if (!servName.empty())
+            {
+                // Failed to get service name owner state
+                mgr->setOwner(member, servName, intf, false);
+            }
+            else
+            {
+                // Path and/or interface configured does not exist on dbus?
+                // TODO How to handle this? Create timer to keep checking for
+                // object/service to appear? When to stop checking?
+                log<level::ERR>(
+                    fmt::format(
+                        "Unable to get service name for path {}, interface {}",
+                        member, intf)
+                        .c_str());
+                throw dme;
+            }
+        }
+    }
+}
+
+void triggerInit(const json& jsonObj, const std::string& eventName,
+                 Manager* mgr,
+                 std::vector<std::unique_ptr<ActionBase>>& actions)
+{
+    // Get the method handler if configured
+    auto handler = methods.end();
+    if (jsonObj.contains("method"))
+    {
+        auto method = jsonObj["method"].get<std::string>();
+        std::transform(method.begin(), method.end(), method.begin(), tolower);
+        handler = methods.find(method);
+    }
+
+    for (auto& action : actions)
+    {
+        // Groups are optional, so a method is only required if there are groups
+        // i.e.) An init triggered event without any groups results in just
+        // running the actions
+        for (const auto& group : action->getGroups())
+        {
+            if (handler == methods.end())
+            {
+                // Construct list of available methods
+                auto availMethods = std::accumulate(
+                    std::next(methods.begin()), methods.end(),
+                    methods.begin()->first, [](auto list, auto method) {
+                        return std::move(list) + ", " + method.first;
+                    });
+                auto msg =
+                    fmt::format("Event '{}' requires a supported method given "
+                                "to be init driven, available methods: {}",
+                                eventName, availMethods);
+                log<level::ERR>(msg.c_str());
+                throw std::runtime_error(msg.c_str());
+            }
+            // Call method handler for each group in the actions
+            handler->second(mgr, group);
+        }
+        // Run the action
+        action->run();
+    }
+}
+
+} // namespace phosphor::fan::control::json::trigger::init
diff --git a/control/json/triggers/init.hpp b/control/json/triggers/init.hpp
new file mode 100644
index 0000000..6447435
--- /dev/null
+++ b/control/json/triggers/init.hpp
@@ -0,0 +1,77 @@
+/**
+ * Copyright © 2021 IBM Corporation
+ *
+ * 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.
+ */
+#pragma once
+
+#include "../manager.hpp"
+#include "action.hpp"
+#include "group.hpp"
+
+#include <nlohmann/json.hpp>
+
+#include <functional>
+#include <map>
+#include <memory>
+#include <vector>
+
+namespace phosphor::fan::control::json::trigger::init
+{
+
+using json = nlohmann::json;
+
+/**
+ * @brief An init method to get properties used in an event
+ *
+ * @param[in] mgr - Pointer to manager of the event
+ * @param[in] group - Group associated with the event
+ */
+void getProperties(Manager* mgr, const Group& group);
+
+/**
+ * @brief An init method to get the owner name of a service used in an event
+ *
+ * @param[in] mgr - Pointer to manager of the event
+ * @param[in] group - Group associated with the event
+ */
+void nameHasOwner(Manager* mgr, const Group& group);
+
+// Handler function for method calls
+using methodHandler = std::function<void(Manager*, const Group&)>;
+
+/* Supported methods to their corresponding handler functions */
+static const std::map<std::string, methodHandler> methods = {
+    {"get_properties", getProperties}, {"name_has_owner", nameHasOwner}};
+
+/**
+ * @brief Trigger to process an event immediately upon fan control starting
+ *
+ * @param[in] jsonObj - JSON object for the trigger
+ * @param[in] eventName - Name of event creating the trigger
+ * @param[in] mgr - Manager to setup the trigger on
+ * @param[in] actions - Actions associated with the trigger
+ *
+ * When fan control starts (or restarts), all events with 'init' triggers are
+ * processed immediately, per its configuration, and its corresponding actions
+ * are run.
+ *
+ * Generally, this type of trigger is paired with a 'signal' class of trigger on
+ * an event so the initial data for an event is collected, processed, and run
+ * before any signal may be received.
+ */
+void triggerInit(const json& jsonObj, const std::string& eventName,
+                 Manager* mgr,
+                 std::vector<std::unique_ptr<ActionBase>>& actions);
+
+} // namespace phosphor::fan::control::json::trigger::init
diff --git a/control/json/triggers/trigger.hpp b/control/json/triggers/trigger.hpp
index eff36c6..4e7e213 100644
--- a/control/json/triggers/trigger.hpp
+++ b/control/json/triggers/trigger.hpp
@@ -16,6 +16,7 @@
 #pragma once
 
 #include "action.hpp"
+#include "init.hpp"
 #include "manager.hpp"
 #include "signal.hpp"
 #include "timer.hpp"
@@ -39,6 +40,8 @@
 
 // Mapping of trigger class name to its creation function
 static const std::map<std::string, createTrigger> triggers = {
-    {"timer", timer::triggerTimer}, {"signal", signal::triggerSignal}};
+    {"timer", timer::triggerTimer},
+    {"signal", signal::triggerSignal},
+    {"init", init::triggerInit}};
 
 } // namespace phosphor::fan::control::json::trigger