PEL: Watch for fan/PS hotplugs

Code is going to need to know when a fan or power supply (the only
hotpluggable redundant FRUs) are added or replaced so that it can clear
a flag in PELs that have that HW as a callout.  This is so other code
that is doing degraded mode notifications will not do any notifications
for PELs calling out HW that has been replaced.

To enable this functionality, add support to the DataInterface class to
tell subscribers via a function callback when a fan or power supply
becomes present, as indicated by the Present property.

Code will watch the Present property of fan and power supplies to change
via the PropertiesChanged signal, as well as watch for InterfacesAdded
to catch when they show up on D-Bus in the first place.

It won't start any of these watches until the BMC gets to ready state so
that the inventory has a chance to be populated first.

On the first boot when the inventory was previously empty there will be
a round of inventory InterfacesAdded signals that will wake up the
daemon when PLDM receives the processor core presence information from
hostboot, but it just checks and sees it's the wrong interface and
returns, and I'm not sure how it can be avoided.

Signed-off-by: Matt Spinler <spinler@us.ibm.com>
Change-Id: I93d0727c3082677826db4a4a02c1a30986f6099b
diff --git a/extensions/openpower-pels/data_interface.cpp b/extensions/openpower-pels/data_interface.cpp
index ead3c16..8da6d88 100644
--- a/extensions/openpower-pels/data_interface.cpp
+++ b/extensions/openpower-pels/data_interface.cpp
@@ -22,6 +22,7 @@
 #include <fmt/format.h>
 
 #include <phosphor-logging/log.hpp>
+#include <xyz/openbmc_project/State/BMC/server.hpp>
 #include <xyz/openbmc_project/State/Boot/Progress/server.hpp>
 
 #include <fstream>
@@ -100,10 +101,19 @@
 constexpr auto association = "xyz.openbmc_project.Association";
 constexpr auto biosConfigMgr = "xyz.openbmc_project.BIOSConfig.Manager";
 constexpr auto bootRawProgress = "xyz.openbmc_project.State.Boot.Raw";
+constexpr auto invItem = "xyz.openbmc_project.Inventory.Item";
+constexpr auto invFan = "xyz.openbmc_project.Inventory.Item.Fan";
+constexpr auto invPowerSupply =
+    "xyz.openbmc_project.Inventory.Item.PowerSupply";
 } // namespace interface
 
 using namespace sdbusplus::xyz::openbmc_project::State::Boot::server;
+using namespace sdbusplus::xyz::openbmc_project::State::server;
 using namespace phosphor::logging;
+namespace match_rules = sdbusplus::bus::match::rules;
+
+const DBusInterfaceList hotplugInterfaces{interface::invFan,
+                                          interface::invPowerSupply};
 
 std::pair<std::string, std::string>
     DataInterfaceBase::extractConnectorFromLocCode(
@@ -165,7 +175,15 @@
     _properties.emplace_back(std::make_unique<PropertyWatcher<DataInterface>>(
         bus, object_path::bmcState, interface::bmcState, "CurrentBMCState",
         *this, [this](const auto& value) {
-            this->_bmcState = std::get<std::string>(value);
+            const auto& state = std::get<std::string>(value);
+            this->_bmcState = state;
+
+            // Wait for BMC ready to start watching for
+            // plugs so things calm down first.
+            if (BMC::convertBMCStateFromString(state) == BMC::BMCState::Ready)
+            {
+                startFruPlugWatch();
+            }
         }));
 
     // Watch the chassis current and requested power state properties
@@ -871,5 +889,121 @@
     return std::get<1>(rawProgress);
 }
 
+void DataInterface::startFruPlugWatch()
+{
+    // Add a watch on inventory InterfacesAdded and then find all
+    // existing hotpluggable interfaces and add propertiesChanged
+    // watches on them.
+
+    _invIaMatch = std::make_unique<sdbusplus::bus::match_t>(
+        _bus, match_rules::interfacesAdded(object_path::baseInv),
+        std::bind(&DataInterface::inventoryIfaceAdded, this,
+                  std::placeholders::_1));
+    try
+    {
+        auto paths = getPaths(hotplugInterfaces);
+
+        _invPresentMatches.clear();
+
+        std::for_each(paths.begin(), paths.end(),
+                      [this](const auto& path) { addHotplugWatch(path); });
+    }
+    catch (const sdbusplus::exception_t& e)
+    {
+        log<level::WARNING>(
+            fmt::format("Failed getting FRU paths to watch: {}", e.what())
+                .c_str());
+    }
+}
+
+void DataInterface::addHotplugWatch(const std::string& path)
+{
+    if (!_invPresentMatches.contains(path))
+    {
+        _invPresentMatches.emplace(
+            path,
+            std::make_unique<sdbusplus::bus::match_t>(
+                _bus, match_rules::propertiesChanged(path, interface::invItem),
+                std::bind(&DataInterface::presenceChanged, this,
+                          std::placeholders::_1)));
+    }
+}
+
+void DataInterface::inventoryIfaceAdded(sdbusplus::message_t& msg)
+{
+    sdbusplus::message::object_path path;
+    DBusInterfaceMap interfaces;
+
+    msg.read(path, interfaces);
+
+    // Check if any of the new interfaces are for hot pluggable FRUs.
+    if (std::find_if(interfaces.begin(), interfaces.end(),
+                     [](const auto& interfacePair) {
+        return std::find(hotplugInterfaces.begin(), hotplugInterfaces.end(),
+                         interfacePair.first) != hotplugInterfaces.end();
+        }) == interfaces.end())
+    {
+        return;
+    }
+
+    addHotplugWatch(path.str);
+
+    // If an Inventory.Item interface was also added, check presence now.
+
+    // Notes:
+    // * This assumes the Inv.Item and Inv.Fan/PS are added together which
+    //   is currently the case.
+    // * If the code ever switches to something without a Present
+    //   property, then the IA signal itself would probably indicate presence.
+
+    auto itemIt = interfaces.find(interface::invItem);
+    if (itemIt != interfaces.end())
+    {
+        notifyPresenceSubsribers(path.str, itemIt->second);
+    }
+}
+
+void DataInterface::presenceChanged(sdbusplus::message_t& msg)
+{
+    DBusInterface interface;
+    DBusPropertyMap properties;
+
+    msg.read(interface, properties);
+    if (interface != interface::invItem)
+    {
+        return;
+    }
+
+    std::string path = msg.get_path();
+    notifyPresenceSubsribers(path, properties);
+}
+
+void DataInterface::notifyPresenceSubsribers(const std::string& path,
+                                             const DBusPropertyMap& properties)
+{
+    try
+    {
+        auto prop = properties.find("Present");
+        if (prop != properties.end())
+        {
+            if (std::get<bool>(prop->second))
+            {
+                auto locCode = getLocationCode(path);
+                log<level::INFO>(
+                    fmt::format("Detected FRU {} ({}) present ", path, locCode)
+                        .c_str());
+                // Tell the subscribers.
+                setFruPresent(locCode);
+            }
+        }
+    }
+    catch (const std::exception& e)
+    {
+        log<level::ERR>(
+            fmt::format("Failed while processing presence for {}: {}", path,
+                        e.what())
+                .c_str());
+    }
+}
 } // namespace pels
 } // namespace openpower
diff --git a/extensions/openpower-pels/data_interface.hpp b/extensions/openpower-pels/data_interface.hpp
index 396fc68..57b1fe5 100644
--- a/extensions/openpower-pels/data_interface.hpp
+++ b/extensions/openpower-pels/data_interface.hpp
@@ -93,6 +93,24 @@
         _hostChangeCallbacks.erase(name);
     }
 
+    using FRUPresentFunc =
+        std::function<void(const std::string& /* locationCode */)>;
+
+    /**
+     * @brief Register a callback function that will get
+     *        called when certain FRUs become present.
+     *
+     * The void(std::string) function will get passed the
+     * location code of the FRU.
+     *
+     * @param[in] name - The subscription name
+     * @param[in] func - The function to run
+     */
+    void subscribeToFruPresent(const std::string& name, FRUPresentFunc func)
+    {
+        _fruPresentCallbacks[name] = std::move(func);
+    }
+
     /**
      * @brief Returns the BMC firmware version
      *
@@ -498,6 +516,26 @@
     }
 
     /**
+     * @brief Runs the callback functions registered when
+     *        FRUs become present.
+     */
+    void setFruPresent(const std::string& locationCode)
+    {
+        for (const auto& [_, func] : _fruPresentCallbacks)
+        {
+            try
+            {
+                func(locationCode);
+            }
+            catch (const std::exception& e)
+            {
+                using namespace phosphor::logging;
+                log<level::ERR>("A FRU present callback threw an exception");
+            }
+        }
+    }
+
+    /**
      * @brief The hardware management console status.  Always kept
      *        up to date.
      */
@@ -515,6 +553,12 @@
     std::map<std::string, HostStateChangeFunc> _hostChangeCallbacks;
 
     /**
+     * @brief The map of FRU present subscriber
+     *        names to callback functions.
+     */
+    std::map<std::string, FRUPresentFunc> _fruPresentCallbacks;
+
+    /**
      * @brief The BMC firmware version string
      */
     std::string _bmcFWVersion;
@@ -834,6 +878,49 @@
     void motherboardIfaceAdded(sdbusplus::message_t& msg);
 
     /**
+     * @brief Start watching for the hotpluggable FRUs to become
+     *        present.
+     */
+    void startFruPlugWatch();
+
+    /**
+     * @brief Create a D-Bus match object for the Present property
+     *        to change on the path passed in.
+     * @param[in] path - The path to watch.
+     */
+    void addHotplugWatch(const std::string& path);
+
+    /**
+     * @brief Callback when an inventory interface was added.
+     *
+     * Only does something if it's one of the hotpluggable FRUs,
+     * in which case it will treat it as a hotplug if the
+     * Present property is true.
+     *
+     * @param[in] msg - The InterfacesAdded signal contents.
+     */
+    void inventoryIfaceAdded(sdbusplus::message_t& msg);
+
+    /**
+     * @brief Callback when the Present property changes.
+     *
+     * If present, will run the registered callbacks.
+     *
+     * @param[in] msg - The PropertiesChanged signal contents.
+     */
+    void presenceChanged(sdbusplus::message_t& msg);
+
+    /**
+     * @brief If the Present property is in the properties map
+     *        passed in and it is true, notify the subscribers.
+     *
+     * @param[in] path - The object path of the inventory item.
+     * @param[in] properties - The properties map
+     */
+    void notifyPresenceSubsribers(const std::string& path,
+                                  const DBusPropertyMap& properties);
+
+    /**
      * @brief Adds the Ufcs- prefix to the location code passed in
      *        if necessary.
      *
@@ -853,18 +940,20 @@
      */
     std::vector<std::unique_ptr<DBusWatcher>> _properties;
 
+    std::unique_ptr<sdbusplus::bus::match_t> _invIaMatch;
+
+    /**
+     * @brief The matches for watching for hotplugs.
+     *
+     * A map so we can check that we never get duplicates.
+     */
+    std::map<std::string, std::unique_ptr<sdbusplus::bus::match_t>>
+        _invPresentMatches;
+
     /**
      * @brief The sdbusplus bus object for making D-Bus calls.
      */
     sdbusplus::bus_t& _bus;
-
-    /**
-     * @brief The interfacesAdded match object used to wait for inventory
-     *        interfaces to show up, so that the object with the motherboard
-     *        interface can be found.  After it is found, this object is
-     *        deleted.
-     */
-    std::unique_ptr<sdbusplus::bus::match_t> _inventoryIfacesAddedMatch;
 };
 
 } // namespace pels