Add chassis association to sensors

Add an association to the chassis for each sensor so that the Redfish
code can find them.  The forward association is 'chassis', and the
reverse association is 'all_sensors'.  This matches what other sensor
applications do and is documented in sensors-architecture.md in the docs
repository.

Tested: Checked the associations were present in the mapper and that the
sensors now show up on the sensors page in the web UI.

Signed-off-by: Matt Spinler <spinler@us.ibm.com>
Change-Id: Iefb6d4dc2e5c9399d919e9e1b7c8dd04f20e7a3e
diff --git a/occ_dbus.cpp b/occ_dbus.cpp
index dde2e7d..0b44181 100644
--- a/occ_dbus.cpp
+++ b/occ_dbus.cpp
@@ -2,6 +2,8 @@
 
 #include "utils.hpp"
 
+#include <fmt/core.h>
+
 #include <phosphor-logging/log.hpp>
 
 #include <iostream>
@@ -14,6 +16,11 @@
 {
 
 using namespace phosphor::logging;
+using namespace std::string_literals;
+const auto defaultChassisPath =
+    "/xyz/openbmc_project/inventory/system/chassis"s;
+const auto chassisInterface = "xyz.openbmc_project.Inventory.Item.Chassis"s;
+
 bool OccDBusSensors::setMaxValue(const std::string& path, double value)
 {
     if (path.empty())
@@ -166,6 +173,68 @@
     throw std::invalid_argument("Failed to get OperationalStatus property.");
 }
 
+void OccDBusSensors::setChassisAssociation(const std::string& path)
+{
+    using AssociationsEntry = std::tuple<std::string, std::string, std::string>;
+    using AssociationsProperty = std::vector<AssociationsEntry>;
+    using PropVariant = sdbusplus::xyz::openbmc_project::Association::server::
+        Definitions::PropertiesVariant;
+
+    if (chassisPath.empty())
+    {
+        chassisPath = getChassisPath();
+    }
+
+    AssociationsProperty associations{
+        AssociationsEntry{"chassis", "all_sensors", chassisPath}};
+    PropVariant value{std::move(associations)};
+
+    std::map<std::string, PropVariant> properties;
+    properties.emplace("Associations", std::move(value));
+
+    chassisAssociations.emplace(
+        path, std::make_unique<AssociationIntf>(utils::getBus(), path.c_str(),
+                                                properties));
+}
+
+std::string OccDBusSensors::getChassisPath()
+{
+    try
+    {
+        auto paths = utils::getSubtreePaths(std::vector{chassisInterface});
+
+        // For now, support either 1 chassis, or multiple as long as one
+        // of them has the standard name, which we will use.  If this ever
+        // fails, then someone would have to figure out how to identify the
+        // chassis the OCCs are on.
+        if (paths.size() == 1)
+        {
+            return paths[0];
+        }
+        else if (std::find(paths.begin(), paths.end(), defaultChassisPath) ==
+                 paths.end())
+        {
+            log<level::ERR>(
+                fmt::format(
+                    "Could not find a chassis out of {} chassis objects",
+                    paths.size())
+                    .c_str());
+            // Can't throw an exception here, the sdeventplus timer
+            // just catches it.
+            abort();
+        }
+    }
+    catch (const std::exception& e)
+    {
+        log<level::ERR>(
+            fmt::format("Error looking up chassis objects: {}", e.what())
+                .c_str());
+        abort();
+    }
+
+    return defaultChassisPath;
+}
+
 } // namespace dbus
 } // namespace occ
 } // namespace open_power
diff --git a/occ_dbus.hpp b/occ_dbus.hpp
index d042ea8..3938464 100644
--- a/occ_dbus.hpp
+++ b/occ_dbus.hpp
@@ -1,5 +1,6 @@
 #pragma once
 
+#include <xyz/openbmc_project/Association/Definitions/server.hpp>
 #include <xyz/openbmc_project/Sensor/Value/server.hpp>
 #include <xyz/openbmc_project/State/Decorator/OperationalStatus/server.hpp>
 
@@ -18,6 +19,10 @@
     sdbusplus::server::object::object<sdbusplus::xyz::openbmc_project::State::
                                           Decorator::server::OperationalStatus>;
 
+// Note: Not using object<> so the PropertiesVariant ctor is available.
+using AssociationIntf =
+    sdbusplus::xyz::openbmc_project::Association::server::Definitions;
+
 /** @class OccDBusSensors
  *  @brief This is a custom D-Bus object, used to add D-Bus interface and update
  *         the corresponding properties value.
@@ -127,11 +132,27 @@
      */
     bool getOperationalStatus(const std::string& path) const;
 
+    /** @brief Returns the Chassis inventory path
+     *
+     * @return path       - The chassis D-Bus path
+     */
+    std::string getChassisPath();
+
+    /** @brief Set the association to the chassis
+     *
+     *  @param[in] path   - The object path
+     */
+    void setChassisAssociation(const std::string& path);
+
   private:
     std::map<ObjectPath, std::unique_ptr<SensorIntf>> sensors;
 
     std::map<ObjectPath, std::unique_ptr<OperationalStatusIntf>>
         operationalStatus;
+
+    std::map<ObjectPath, std::unique_ptr<AssociationIntf>> chassisAssociations;
+
+    std::string chassisPath;
 };
 
 } // namespace dbus
diff --git a/occ_manager.cpp b/occ_manager.cpp
index 304a764..ea2020c 100644
--- a/occ_manager.cpp
+++ b/occ_manager.cpp
@@ -399,6 +399,13 @@
             continue;
         }
 
+        // At this point, the sensor will be created for sure.
+        if (existingSensors.find(sensorPath) == existingSensors.end())
+        {
+            open_power::occ::dbus::OccDBusSensors::getOccDBus()
+                .setChassisAssociation(sensorPath);
+        }
+
         if (faultValue != 0)
         {
             open_power::occ::dbus::OccDBusSensors::getOccDBus().setValue(
@@ -528,6 +535,12 @@
         open_power::occ::dbus::OccDBusSensors::getOccDBus()
             .setOperationalStatus(sensorPath, true);
 
+        if (existingSensors.find(sensorPath) == existingSensors.end())
+        {
+            open_power::occ::dbus::OccDBusSensors::getOccDBus()
+                .setChassisAssociation(sensorPath);
+        }
+
         existingSensors[sensorPath] = id;
     }
     return;
diff --git a/utils.cpp b/utils.cpp
index 73ee3a7..a067663 100644
--- a/utils.cpp
+++ b/utils.cpp
@@ -76,6 +76,22 @@
     return value;
 }
 
+std::vector<std::string>
+    getSubtreePaths(const std::vector<std::string>& interfaces,
+                    const std::string& path)
+{
+    std::vector<std::string> paths;
+
+    auto& bus = getBus();
+    auto method = bus.new_method_call(MAPPER_BUSNAME, MAPPER_OBJ_PATH,
+                                      MAPPER_IFACE, "GetSubTreePaths");
+    method.append(path, 0, interfaces);
+
+    auto reply = bus.call(method);
+    reply.read(paths);
+
+    return paths;
+}
 } // namespace utils
 } // namespace occ
 } // namespace open_power
diff --git a/utils.hpp b/utils.hpp
index 9b78cd1..67d7a31 100644
--- a/utils.hpp
+++ b/utils.hpp
@@ -54,6 +54,19 @@
                                 const std::string& interface,
                                 const std::string& propertyName);
 
+/** @brief Get subtree paths
+ *
+ *  @param[in] interfaces -   D-Bus interfaces
+ *  @param[in] path       -   D-Bus object path
+ *
+ *  @return The D-Bus paths from the GetSubTree method
+ *
+ *  @throw sdbusplus::exception::exception when it fails
+ */
+std::vector<std::string>
+    getSubtreePaths(const std::vector<std::string>& interfaces,
+                    const std::string& path = "/");
+
 } // namespace utils
 } // namespace occ
 } // namespace open_power