sensor SDRs: lookup Board to set IPMI info

Looks up the board object, if there is an association, and if that board
object specifies an EntityId or EntityInstance, it will use those values
in the SDR.  If the configuration of the sensor also implements one or
both of those properties, then those are used instead.

Tested: Validated if the json Board file has EntityId and
EntityInstance, those values are used in the SDR.  Validated if the
configuration for the sensor has those values, then those are used
instead.  Verified it works in cases where these values are not set,
etc.

Signed-off-by: Patrick Venture <venture@google.com>
Change-Id: I998eff9b0145ede7b386041b35f488fa6dd6714f
diff --git a/include/commandutils.hpp b/include/commandutils.hpp
index cad6e2d..e2f6523 100644
--- a/include/commandutils.hpp
+++ b/include/commandutils.hpp
@@ -18,6 +18,7 @@
 
 #include <iostream>
 #include <ipmid/api.hpp>
+#include <map>
 #include <sdbusplus/bus.hpp>
 #include <string>
 #include <tuple>
@@ -64,4 +65,6 @@
 using GetSubTreeType = std::vector<
     std::pair<std::string,
               std::vector<std::pair<std::string, std::vector<std::string>>>>>;
+
+using SensorMap = std::map<std::string, std::map<std::string, DbusVariant>>;
 } // namespace ipmi
diff --git a/include/sdrutils.hpp b/include/sdrutils.hpp
index 8969bd6..206f6af 100644
--- a/include/sdrutils.hpp
+++ b/include/sdrutils.hpp
@@ -14,12 +14,20 @@
 // limitations under the License.
 */
 
+#include "commandutils.hpp"
+
 #include <boost/algorithm/string.hpp>
 #include <boost/bimap.hpp>
 #include <boost/container/flat_map.hpp>
+#include <cstdio>
 #include <cstring>
+#include <exception>
+#include <filesystem>
+#include <map>
 #include <phosphor-logging/log.hpp>
 #include <sdbusplus/bus/match.hpp>
+#include <string>
+#include <vector>
 
 #pragma once
 
@@ -249,3 +257,230 @@
         return std::string();
     }
 }
+
+namespace ipmi
+{
+
+static inline std::map<std::string, std::vector<std::string>>
+    getObjectInterfaces(const char* path)
+{
+    std::map<std::string, std::vector<std::string>> interfacesResponse;
+    std::vector<std::string> interfaces;
+    std::shared_ptr<sdbusplus::asio::connection> dbus = getSdBus();
+
+    sdbusplus::message::message getObjectMessage =
+        dbus->new_method_call("xyz.openbmc_project.ObjectMapper",
+                              "/xyz/openbmc_project/object_mapper",
+                              "xyz.openbmc_project.ObjectMapper", "GetObject");
+    getObjectMessage.append(path, interfaces);
+
+    try
+    {
+        sdbusplus::message::message response = dbus->call(getObjectMessage);
+        response.read(interfacesResponse);
+    }
+    catch (const std::exception& e)
+    {
+        phosphor::logging::log<phosphor::logging::level::ERR>(
+            "Failed to GetObject", phosphor::logging::entry("PATH=%s", path),
+            phosphor::logging::entry("WHAT=%s", e.what()));
+    }
+
+    return interfacesResponse;
+}
+
+static inline std::map<std::string, DbusVariant>
+    getEntityManagerProperties(const char* path, const char* interface)
+{
+    std::map<std::string, DbusVariant> properties;
+    std::shared_ptr<sdbusplus::asio::connection> dbus = getSdBus();
+
+    sdbusplus::message::message getProperties =
+        dbus->new_method_call("xyz.openbmc_project.EntityManager", path,
+                              "org.freedesktop.DBus.Properties", "GetAll");
+    getProperties.append(interface);
+
+    try
+    {
+        sdbusplus::message::message response = dbus->call(getProperties);
+        response.read(properties);
+    }
+    catch (const std::exception& e)
+    {
+        phosphor::logging::log<phosphor::logging::level::ERR>(
+            "Failed to GetAll", phosphor::logging::entry("PATH=%s", path),
+            phosphor::logging::entry("INTF=%s", interface),
+            phosphor::logging::entry("WHAT=%s", e.what()));
+    }
+
+    return properties;
+}
+
+static inline const std::string* getSensorConfigurationInterface(
+    const std::map<std::string, std::vector<std::string>>&
+        sensorInterfacesResponse)
+{
+    auto entityManagerService =
+        sensorInterfacesResponse.find("xyz.openbmc_project.EntityManager");
+    if (entityManagerService == sensorInterfacesResponse.end())
+    {
+        return nullptr;
+    }
+
+    // Find the fan configuration first (fans can have multiple configuration
+    // interfaces).
+    for (const auto& entry : entityManagerService->second)
+    {
+        if (entry == "xyz.openbmc_project.Configuration.AspeedFan" ||
+            entry == "xyz.openbmc_project.Configuration.I2CFan" ||
+            entry == "xyz.openbmc_project.Configuration.NuvotonFan")
+        {
+            return &entry;
+        }
+    }
+
+    for (const auto& entry : entityManagerService->second)
+    {
+        if (boost::algorithm::starts_with(entry,
+                                          "xyz.openbmc_project.Configuration."))
+        {
+            return &entry;
+        }
+    }
+
+    return nullptr;
+}
+
+// Follow Association properties for Sensor back to the Board dbus object to
+// check for an EntityId and EntityInstance property.
+static inline void updateIpmiFromAssociation(const std::string& path,
+                                             const SensorMap& sensorMap,
+                                             uint8_t& entityId,
+                                             uint8_t& entityInstance)
+{
+    namespace fs = std::filesystem;
+
+    auto sensorAssociationObject =
+        sensorMap.find("xyz.openbmc_project.Association.Definitions");
+    if (sensorAssociationObject == sensorMap.end())
+    {
+        if constexpr (debug)
+        {
+            std::fprintf(stderr, "path=%s, no association interface found\n",
+                         path.c_str());
+        }
+
+        return;
+    }
+
+    auto associationObject =
+        sensorAssociationObject->second.find("Associations");
+    if (associationObject == sensorAssociationObject->second.end())
+    {
+        if constexpr (debug)
+        {
+            std::fprintf(stderr, "path=%s, no association records found\n",
+                         path.c_str());
+        }
+
+        return;
+    }
+
+    std::vector<Association> associationValues =
+        std::get<std::vector<Association>>(associationObject->second);
+
+    // loop through the Associations looking for the right one:
+    for (const auto& entry : associationValues)
+    {
+        // forward, reverse, endpoint
+        const std::string& forward = std::get<0>(entry);
+        const std::string& reverse = std::get<1>(entry);
+        const std::string& endpoint = std::get<2>(entry);
+
+        // We only currently concern ourselves with chassis+all_sensors.
+        if (!(forward == "chassis" && reverse == "all_sensors"))
+        {
+            continue;
+        }
+
+        // the endpoint is the board entry provided by
+        // Entity-Manager. so let's grab its properties if it has
+        // the right interface.
+
+        // just try grabbing the properties first.
+        std::map<std::string, DbusVariant> ipmiProperties =
+            getEntityManagerProperties(
+                endpoint.c_str(),
+                "xyz.openbmc_project.Inventory.Decorator.Ipmi");
+
+        auto entityIdProp = ipmiProperties.find("EntityId");
+        auto entityInstanceProp = ipmiProperties.find("EntityInstance");
+        if (entityIdProp != ipmiProperties.end())
+        {
+            entityId =
+                static_cast<uint8_t>(std::get<uint64_t>(entityIdProp->second));
+        }
+        if (entityInstanceProp != ipmiProperties.end())
+        {
+            entityInstance = static_cast<uint8_t>(
+                std::get<uint64_t>(entityInstanceProp->second));
+        }
+
+        // Now check the entity-manager entry for this sensor to see
+        // if it has its own value and use that instead.
+        //
+        // In theory, checking this first saves us from checking
+        // both, except in most use-cases identified, there won't be
+        // a per sensor override, so we need to always check both.
+        std::string sensorNameFromPath = fs::path(path).filename();
+
+        std::string sensorConfigPath = endpoint + "/" + sensorNameFromPath;
+
+        // Download the interfaces for the sensor from
+        // Entity-Manager to find the name of the configuration
+        // interface.
+        std::map<std::string, std::vector<std::string>>
+            sensorInterfacesResponse =
+                getObjectInterfaces(sensorConfigPath.c_str());
+
+        const std::string* configurationInterface =
+            getSensorConfigurationInterface(sensorInterfacesResponse);
+
+        // We didnt' find a configuration interface for this sensor, but we
+        // followed the Association property to get here, so we're done
+        // searching.
+        if (!configurationInterface)
+        {
+            break;
+        }
+
+        // We found a configuration interface.
+        std::map<std::string, DbusVariant> configurationProperties =
+            getEntityManagerProperties(sensorConfigPath.c_str(),
+                                       configurationInterface->c_str());
+
+        entityIdProp = configurationProperties.find("EntityId");
+        entityInstanceProp = configurationProperties.find("EntityInstance");
+        if (entityIdProp != configurationProperties.end())
+        {
+            entityId =
+                static_cast<uint8_t>(std::get<uint64_t>(entityIdProp->second));
+        }
+        if (entityInstanceProp != configurationProperties.end())
+        {
+            entityInstance = static_cast<uint8_t>(
+                std::get<uint64_t>(entityInstanceProp->second));
+        }
+
+        // stop searching Association records.
+        break;
+    } // end for Association vectors.
+
+    if constexpr (debug)
+    {
+        std::fprintf(stderr, "path=%s, entityId=%d, entityInstance=%d\n",
+                     path.c_str(), entityId, entityInstance);
+    }
+}
+
+} // namespace ipmi