Create objects for running PSUs

On service startup, create activation and version objects for running
PSUs, and set related active, functional associations.

If multiple PSUs are running with the same software version, they share
the same DBus object, and the object will be associated to multiple PSU
inventories.

Tested: Verify the software objects are created on Witherspoon, and the
        active, functional associations are created.

Signed-off-by: Lei YU <mine260309@gmail.com>
Change-Id: Ia8372aba8299818baccfdf37e98fdbc99f747b7c
diff --git a/meson.build b/meson.build
index 569eab9..a630564 100644
--- a/meson.build
+++ b/meson.build
@@ -17,6 +17,7 @@
 
 # Common configurations for src and test
 cdata = configuration_data()
+cdata.set_quoted('ITEM_IFACE', 'xyz.openbmc_project.Inventory.Item')
 cdata.set_quoted('VERSION_IFACE', 'xyz.openbmc_project.Software.Version')
 cdata.set_quoted('FILEPATH_IFACE', 'xyz.openbmc_project.Common.FilePath')
 cdata.set_quoted('BUSNAME_UPDATER', 'xyz.openbmc_project.Software.Psu.Updater')
@@ -35,6 +36,7 @@
 phosphor_dbus_interfaces = dependency('phosphor-dbus-interfaces')
 phosphor_logging = dependency('phosphor-logging')
 sdbusplus = dependency('sdbusplus')
+ssl = dependency('openssl')
 
 add_project_link_arguments(['-lstdc++fs'], language: 'cpp')
 
diff --git a/src/item_updater.cpp b/src/item_updater.cpp
index b670735..a257a6b 100644
--- a/src/item_updater.cpp
+++ b/src/item_updater.cpp
@@ -2,6 +2,8 @@
 
 #include "item_updater.hpp"
 
+#include "utils.hpp"
+
 #include <filesystem>
 #include <phosphor-logging/elog-errors.hpp>
 #include <phosphor-logging/log.hpp>
@@ -18,11 +20,11 @@
 
 using namespace sdbusplus::xyz::openbmc_project::Common::Error;
 using namespace phosphor::logging;
+using SVersion = server::Version;
+using VersionPurpose = SVersion::VersionPurpose;
 
 void ItemUpdater::createActivation(sdbusplus::message::message& m)
 {
-    using SVersion = server::Version;
-    using VersionPurpose = SVersion::VersionPurpose;
     namespace msg = sdbusplus::message;
     namespace variant_ns = msg::variant_ns;
 
@@ -157,21 +159,8 @@
     associations(assocs);
 }
 
-void ItemUpdater::updateFunctionalAssociation(const std::string& versionId)
+void ItemUpdater::addFunctionalAssociation(const std::string& path)
 {
-    std::string path = std::string{SOFTWARE_OBJPATH} + '/' + versionId;
-    // remove all functional associations
-    for (auto iter = assocs.begin(); iter != assocs.end();)
-    {
-        if ((std::get<0>(*iter)).compare(FUNCTIONAL_FWD_ASSOCIATION) == 0)
-        {
-            iter = assocs.erase(iter);
-        }
-        else
-        {
-            ++iter;
-        }
-    }
     assocs.emplace_back(std::make_tuple(FUNCTIONAL_FWD_ASSOCIATION,
                                         FUNCTIONAL_REV_ASSOCIATION, path));
     associations(assocs);
@@ -204,6 +193,45 @@
                                         activationStatus, assocs);
 }
 
+void ItemUpdater::createPsuObject(const std::string& psuInventoryPath,
+                                  const std::string& psuVersion)
+{
+    auto versionId = utils::getVersionId(psuVersion);
+    auto path = std::string(SOFTWARE_OBJPATH) + "/" + versionId;
+
+    auto it = activations.find(versionId);
+    if (it != activations.end())
+    {
+        // The versionId is already created, associate the path
+        auto associations = it->second->associations();
+        associations.emplace_back(std::make_tuple(ACTIVATION_FWD_ASSOCIATION,
+                                                  ACTIVATION_REV_ASSOCIATION,
+                                                  psuInventoryPath));
+        it->second->associations(associations);
+    }
+    else
+    {
+        // Create a new object for running PSU inventory
+        AssociationList associations;
+        auto activationState = server::Activation::Activations::Active;
+
+        associations.emplace_back(std::make_tuple(ACTIVATION_FWD_ASSOCIATION,
+                                                  ACTIVATION_REV_ASSOCIATION,
+                                                  psuInventoryPath));
+
+        auto activation = createActivationObject(path, versionId, "",
+                                                 activationState, associations);
+        activations.emplace(versionId, std::move(activation));
+
+        auto versionPtr = createVersionObject(path, versionId, psuVersion,
+                                              VersionPurpose::PSU, "");
+        versions.emplace(versionId, std::move(versionPtr));
+
+        createActiveAssociation(path);
+        addFunctionalAssociation(path);
+    }
+}
+
 std::unique_ptr<Version> ItemUpdater::createVersionObject(
     const std::string& objPath, const std::string& versionId,
     const std::string& versionString,
@@ -217,6 +245,34 @@
     return version;
 }
 
+void ItemUpdater::onPsuInventoryChanged(sdbusplus::message::message&)
+{
+    // TODO
+}
+
+void ItemUpdater::processPSUImage()
+{
+    auto paths = utils::getPSUInventoryPath(bus);
+    for (const auto& p : paths)
+    {
+        // Assume the same service implement both Version and Item interface
+        auto service = utils::getService(bus, p.c_str(), VERSION_IFACE);
+        auto version = utils::getProperty<std::string>(
+            bus, service.c_str(), p.c_str(), VERSION_IFACE, "Version");
+        auto present = utils::getProperty<bool>(bus, service.c_str(), p.c_str(),
+                                                ITEM_IFACE, "Present");
+        if (present && !version.empty())
+        {
+            createPsuObject(p, version);
+        }
+        // Add matches for PSU present changes
+        psuMatches.emplace_back(bus,
+                                MatchRules::propertiesChanged(p, ITEM_IFACE),
+                                std::bind(&ItemUpdater::onPsuInventoryChanged,
+                                          this, std::placeholders::_1));
+    }
+}
+
 } // namespace updater
 } // namespace software
 } // namespace phosphor
diff --git a/src/item_updater.hpp b/src/item_updater.hpp
index aacd77f..6df7624 100644
--- a/src/item_updater.hpp
+++ b/src/item_updater.hpp
@@ -45,13 +45,7 @@
                      std::bind(std::mem_fn(&ItemUpdater::createActivation),
                                this, std::placeholders::_1))
     {
-        // TODO: create psu inventory objects based on the paths
-        using namespace phosphor::logging;
-        auto paths = utils::getPSUInventoryPath(bus);
-        for (const auto& p : paths)
-        {
-            log<level::INFO>("PSU path", entry("PATH=%s", p.c_str()));
-        }
+        processPSUImage();
     }
 
     /** @brief Deletes version
@@ -73,12 +67,12 @@
      */
     void createActiveAssociation(const std::string& path);
 
-    /** @brief Updates the functional association to the
+    /** @brief Add the functional association to the
      *  new "running" PSU images
      *
-     * @param[in]  versionId - The id of the image to update the association to.
+     * @param[in]  path - The path to add the association to.
      */
-    void updateFunctionalAssociation(const std::string& versionId);
+    void addFunctionalAssociation(const std::string& path);
 
     /** @brief Removes the associations from the provided software image path
      *
@@ -93,6 +87,13 @@
      */
     void createActivation(sdbusplus::message::message& msg);
 
+    /** @brief Callback function for PSU inventory match.
+     *  @details Update an Activation D-Bus object for PSU inventory.
+     *
+     * @param[in]  msg       - Data associated with subscribed signal
+     */
+    void onPsuInventoryChanged(sdbusplus::message::message& msg);
+
     /** @brief Create Activation object */
     std::unique_ptr<Activation> createActivationObject(
         const std::string& path, const std::string& versionId,
@@ -110,6 +111,15 @@
                                 Version::VersionPurpose versionPurpose,
                             const std::string& filePath);
 
+    /** @brief Create Activation and Version object for PSU inventory */
+    void createPsuObject(const std::string& psuInventoryPath,
+                         const std::string& psuVersion);
+
+    /**
+     * @brief Create and populate the active PSU Version.
+     */
+    void processPSUImage();
+
     /** @brief Persistent sdbusplus D-Bus bus connection. */
     sdbusplus::bus::bus& bus;
 
@@ -121,9 +131,12 @@
      * version id */
     std::map<std::string, std::unique_ptr<Version>> versions;
 
-    /** @brief sdbusplus signal match for Software.Version */
+    /** @brief sdbusplus signal match for PSU Software*/
     sdbusplus::bus::match_t versionMatch;
 
+    /** @brief sdbusplus signal matches for PSU Inventory */
+    std::vector<sdbusplus::bus::match_t> psuMatches;
+
     /** @brief This entry's associations */
     AssociationList assocs;
 };
diff --git a/src/meson.build b/src/meson.build
index 4e2e126..75ab69a 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -15,6 +15,7 @@
     phosphor_logging,
     phosphor_dbus_interfaces,
     sdbusplus,
+    ssl,
   ],
   install: true,
   install_dir: get_option('bindir'))
diff --git a/src/utils.cpp b/src/utils.cpp
index deb5eee..f80776e 100644
--- a/src/utils.cpp
+++ b/src/utils.cpp
@@ -2,6 +2,8 @@
 
 #include "utils.hpp"
 
+#include <openssl/sha.h>
+
 #include <fstream>
 
 namespace utils
@@ -28,4 +30,62 @@
     return paths;
 }
 
+std::string getService(sdbusplus::bus::bus& bus, const char* path,
+                       const char* interface)
+{
+    auto mapper = bus.new_method_call(MAPPER_BUSNAME, MAPPER_PATH,
+                                      MAPPER_INTERFACE, "GetObject");
+
+    mapper.append(path, std::vector<std::string>({interface}));
+    try
+    {
+        auto mapperResponseMsg = bus.call(mapper);
+
+        std::vector<std::pair<std::string, std::vector<std::string>>>
+            mapperResponse;
+        mapperResponseMsg.read(mapperResponse);
+        if (mapperResponse.empty())
+        {
+            log<level::ERR>("Error reading mapper response");
+            throw std::runtime_error("Error reading mapper response");
+        }
+        if (mapperResponse.size() < 1)
+        {
+            return "";
+        }
+        return mapperResponse[0].first;
+    }
+    catch (const sdbusplus::exception::SdBusError& ex)
+    {
+        log<level::ERR>("Mapper call failed", entry("METHOD=%d", "GetObject"),
+                        entry("PATH=%s", path),
+                        entry("INTERFACE=%s", interface));
+        throw std::runtime_error("Mapper call failed");
+    }
+}
+
+std::string getVersionId(const std::string& version)
+{
+    if (version.empty())
+    {
+        log<level::ERR>("Error version is empty");
+        return {};
+    }
+
+    unsigned char digest[SHA512_DIGEST_LENGTH];
+    SHA512_CTX ctx;
+    SHA512_Init(&ctx);
+    SHA512_Update(&ctx, version.c_str(), strlen(version.c_str()));
+    SHA512_Final(digest, &ctx);
+    char mdString[SHA512_DIGEST_LENGTH * 2 + 1];
+    for (int i = 0; i < SHA512_DIGEST_LENGTH; i++)
+    {
+        snprintf(&mdString[i * 2], 3, "%02x", (unsigned int)digest[i]);
+    }
+
+    // Only need 8 hex digits.
+    std::string hexId = std::string(mdString);
+    return (hexId.substr(0, 8));
+}
+
 } // namespace utils
diff --git a/src/utils.hpp b/src/utils.hpp
index 5203509..fd111ff 100644
--- a/src/utils.hpp
+++ b/src/utils.hpp
@@ -1,5 +1,6 @@
 #pragma once
 
+#include <phosphor-logging/log.hpp>
 #include <sdbusplus/bus.hpp>
 #include <string>
 #include <vector>
@@ -7,9 +8,67 @@
 namespace utils
 {
 
+using namespace phosphor::logging;
+
 /**
  * @brief Get PSU inventory object path from DBus
  */
 std::vector<std::string> getPSUInventoryPath(sdbusplus::bus::bus& bus);
 
+/** @brief Get service name from object path and interface
+ *
+ * @param[in] bus          - The Dbus bus object
+ * @param[in] path         - The Dbus object path
+ * @param[in] interface    - The Dbus interface
+ *
+ * @return The name of the service
+ */
+std::string getService(sdbusplus::bus::bus& bus, const char* path,
+                       const char* interface);
+
+/** @brief The template function to get property from the requested dbus path
+ *
+ * @param[in] bus          - The Dbus bus object
+ * @param[in] service      - The Dbus service name
+ * @param[in] path         - The Dbus object path
+ * @param[in] interface    - The Dbus interface
+ * @param[in] propertyName - The property name to get
+ *
+ * @return The value of the property
+ */
+template <typename T>
+T getProperty(sdbusplus::bus::bus& bus, const char* service, const char* path,
+              const char* interface, const char* propertyName)
+{
+    auto method = bus.new_method_call(service, path,
+                                      "org.freedesktop.DBus.Properties", "Get");
+    method.append(interface, propertyName);
+    try
+    {
+        sdbusplus::message::variant<T> value{};
+        auto reply = bus.call(method);
+        reply.read(value);
+        return sdbusplus::message::variant_ns::get<T>(value);
+    }
+    catch (const sdbusplus::exception::SdBusError& ex)
+    {
+        log<level::ERR>("GetProperty call failed", entry("PATH=%s", path),
+                        entry("INTERFACE=%s", interface),
+                        entry("PROPERTY=%s", propertyName));
+        throw std::runtime_error("GetProperty call failed");
+    }
+}
+
+/**
+ * @brief Calculate the version id from the version string.
+ *
+ * @details The version id is a unique 8 hexadecimal digit id
+ *          calculated from the version string.
+ *
+ * @param[in] version - The image version string (e.g. v1.99.10-19).
+ *
+ * @return The id.
+ */
+std::string getVersionId(const std::string& version);
+
 } // namespace utils
diff --git a/test/meson.build b/test/meson.build
index 4d644cf..6d82b5d 100644
--- a/test/meson.build
+++ b/test/meson.build
@@ -34,6 +34,7 @@
     phosphor_logging,
     phosphor_dbus_interfaces,
     sdbusplus,
+    ssl,
   ])
 
 test('all', utest)
diff --git a/test/test_utils.cpp b/test/test_utils.cpp
index 08eecf7..a623cf5 100644
--- a/test/test_utils.cpp
+++ b/test/test_utils.cpp
@@ -60,3 +60,13 @@
     EXPECT_EQ(path0, ret[0]);
     EXPECT_EQ(path1, ret[1]);
 }
+
+TEST(Utils, GetVersionID)
+{
+
+    auto ret = utils::getVersionId("");
+    EXPECT_EQ("", ret);
+
+    ret = utils::getVersionId("some version");
+    EXPECT_EQ(8u, ret.size());
+}