psu-ng: Handle health rollup based on availability

When a PSU is set to not available, create an association between the
power supply and its chassis as a way to 'roll up' the health status to
that chassis.  It looks like:

    <chassis>/critical
        endpoints: <power supply>
    <power supply>/health_rollup
        endpoints: <chassis>

There is Redfish code that look at the endpoints in that chassis
association object to determine if the chassis health is OK or not.

Note that some systems, such as IBM's, have other code that will fill
in that association when it is called out in an event log, which is why
this code doesn't have to do it for every single fault.

Signed-off-by: Matt Spinler <spinler@us.ibm.com>
Change-Id: I262dd738ebadb72aa207011941066fc282bfe4df
diff --git a/phosphor-power-supply/power_supply.cpp b/phosphor-power-supply/power_supply.cpp
index e5fa9c8..bdfc6bb 100644
--- a/phosphor-power-supply/power_supply.cpp
+++ b/phosphor-power-supply/power_supply.cpp
@@ -168,6 +168,16 @@
         log<level::DEBUG>(
             fmt::format("presentOld: {} present: {}", presentOld, present)
                 .c_str());
+
+        auto invpath = inventoryPath.substr(strlen(INVENTORY_OBJ_PATH));
+        auto const lastSlashPos = invpath.find_last_of('/');
+        std::string prettyName = invpath.substr(lastSlashPos + 1);
+        setPresence(bus, invpath, present, prettyName);
+        updateInventory();
+
+        // Need Functional to already be correct before calling this
+        checkAvailability();
+
         if (present)
         {
             std::this_thread::sleep_for(std::chrono::milliseconds(bindDelay));
@@ -180,13 +190,6 @@
         {
             bindOrUnbindDriver(present);
         }
-
-        auto invpath = inventoryPath.substr(strlen(INVENTORY_OBJ_PATH));
-        auto const lastSlashPos = invpath.find_last_of('/');
-        std::string prettyName = invpath.substr(lastSlashPos + 1);
-        setPresence(bus, invpath, present, prettyName);
-        updateInventory();
-        checkAvailability();
     }
 }
 
@@ -921,6 +924,11 @@
     {
         auto invpath = inventoryPath.substr(strlen(INVENTORY_OBJ_PATH));
         phosphor::power::psu::setAvailable(bus, invpath, available);
+
+        // Check if the health rollup needs to change based on the
+        // new availability value.
+        phosphor::power::psu::handleChassisHealthRollup(bus, inventoryPath,
+                                                        !available);
     }
 }
 
diff --git a/phosphor-power-supply/test/mock.hpp b/phosphor-power-supply/test/mock.hpp
index 706aea4..edfe7a8 100644
--- a/phosphor-power-supply/test/mock.hpp
+++ b/phosphor-power-supply/test/mock.hpp
@@ -55,6 +55,10 @@
                 (sdbusplus::bus::bus & bus, const std::string& invpath,
                  bool available),
                 (const, override));
+    MOCK_METHOD(void, handleChassisHealthRollup,
+                (sdbusplus::bus::bus & bus, const std::string& invpath,
+                 bool addRollup),
+                (const, override));
 };
 
 class MockedGPIOInterface : public GPIOInterfaceBase
diff --git a/phosphor-power-supply/util.hpp b/phosphor-power-supply/util.hpp
index ff9f13d..92e4eeb 100644
--- a/phosphor-power-supply/util.hpp
+++ b/phosphor-power-supply/util.hpp
@@ -123,6 +123,97 @@
             throw;
         }
     }
+
+    void handleChassisHealthRollup(sdbusplus::bus::bus& bus,
+                                   const std::string& invpath,
+                                   bool addRollup) const override
+    {
+        using AssociationTuple =
+            std::tuple<std::string, std::string, std::string>;
+        using AssociationsProperty = std::vector<AssociationTuple>;
+        try
+        {
+            auto chassisPath = getChassis(bus, invpath);
+
+            auto service = phosphor::power::util::getService(
+                invpath, ASSOC_DEF_IFACE, bus);
+
+            AssociationsProperty associations;
+            phosphor::power::util::getProperty<AssociationsProperty>(
+                ASSOC_DEF_IFACE, ASSOC_PROP, invpath, service, bus,
+                associations);
+
+            AssociationTuple critAssociation{"health_rollup", "critical",
+                                             chassisPath};
+
+            auto assocIt = std::find(associations.begin(), associations.end(),
+                                     critAssociation);
+            if (addRollup)
+            {
+                if (assocIt != associations.end())
+                {
+                    // It's already there
+                    return;
+                }
+
+                associations.push_back(critAssociation);
+            }
+            else
+            {
+                if (assocIt == associations.end())
+                {
+                    // It's already been removed.
+                    return;
+                }
+
+                // If the object still isn't functional, then don't clear
+                // the association.
+                bool functional = false;
+                phosphor::power::util::getProperty<bool>(
+                    OPERATIONAL_STATE_IFACE, FUNCTIONAL_PROP, invpath, service,
+                    bus, functional);
+
+                if (!functional)
+                {
+                    return;
+                }
+
+                associations.erase(assocIt);
+            }
+
+            phosphor::power::util::setProperty(ASSOC_DEF_IFACE, ASSOC_PROP,
+                                               invpath, service, bus,
+                                               associations);
+        }
+        catch (const sdbusplus::exception::exception& e)
+        {
+            using namespace phosphor::logging;
+            log<level::INFO>(fmt::format("Error trying to handle health rollup "
+                                         "associations for {}: {}",
+                                         invpath, e.what())
+                                 .c_str());
+        }
+    }
+
+    std::string getChassis(sdbusplus::bus::bus& bus,
+                           const std::string& invpath) const
+    {
+        // Use the 'chassis' association to find the parent chassis.
+        auto assocPath = invpath + "/chassis";
+        std::vector<std::string> endpoints;
+
+        phosphor::power::util::getProperty<decltype(endpoints)>(
+            ASSOCIATION_IFACE, ENDPOINTS_PROP, assocPath,
+            "xyz.openbmc_project.ObjectMapper", bus, endpoints);
+
+        if (endpoints.empty())
+        {
+            throw std::runtime_error(
+                fmt::format("Missing chassis association for {}", invpath));
+        }
+
+        return endpoints[0];
+    }
 };
 
 std::unique_ptr<GPIOInterfaceBase> createGPIO(const std::string& namedGpio);
diff --git a/phosphor-power-supply/util_base.hpp b/phosphor-power-supply/util_base.hpp
index bb7dc3c..576a965 100644
--- a/phosphor-power-supply/util_base.hpp
+++ b/phosphor-power-supply/util_base.hpp
@@ -28,6 +28,10 @@
     virtual void setAvailable(sdbusplus::bus::bus& bus,
                               const std::string& invpath,
                               bool available) const = 0;
+
+    virtual void handleChassisHealthRollup(sdbusplus::bus::bus& bus,
+                                           const std::string& invpath,
+                                           bool addRollup) const = 0;
 };
 
 const UtilBase& getUtils();
@@ -49,6 +53,13 @@
     getUtils().setAvailable(bus, invpath, available);
 }
 
+inline void handleChassisHealthRollup(sdbusplus::bus::bus& bus,
+                                      const std::string& invpath,
+                                      bool addRollup)
+{
+    getUtils().handleChassisHealthRollup(bus, invpath, addRollup);
+}
+
 class GPIOInterfaceBase
 {
   public: