Sync PSU images on service startup

On service startup, it shall check the PSU images, find a latest
version, and update to the PSUs that are not with this version.

Tested: With dummy image and service, test on Witherspoon with 2
        different running PSU software:
* When startup without stored image, the serive tries to update an older
PSU but it does not have PSU image, so it's skipped with below journal
log:
    Automatically update PSU
    No image for the activation, skipped
* When startup with an older image stored in BMC, it behaves the same as
above.
* When startup with a new image stored in BMC, it updates the PSUs, with
below example journal log:
    Automatically update PSU
    Starting Update PSU
    /xyz/openbmc_project/inventory/system/chassis/motherboard/powersupply0
    /var/lib/obmc/psu/2B1D...
    Started Update PSU
    /xyz/openbmc_project/inventory/system/chassis/motherboard/powersupply0
    /var/lib/obmc/psu/2B1D.
    Starting Update PSU
    /xyz/openbmc_project/inventory/system/chassis/motherboard/powersupply1
    /var/lib/obmc/psu/2B1D...
    Started Update PSU
    /xyz/openbmc_project/inventory/system/chassis/motherboard/powersupply1
    /var/lib/obmc/psu/2B1D.

Signed-off-by: Lei YU <mine260309@gmail.com>
Change-Id: I6d676c5a4441685fb2b5920455f439c00f6097af
diff --git a/src/activation.cpp b/src/activation.cpp
index efff61b..cab9acd 100644
--- a/src/activation.cpp
+++ b/src/activation.cpp
@@ -49,7 +49,11 @@
         (SoftwareActivation::requestedActivation() !=
          SoftwareActivation::RequestedActivations::Active))
     {
-        if ((activation() == Status::Ready) || (activation() == Status::Failed))
+        // PSU image could be activated even when it's in active,
+        // e.g. in case a PSU is replaced and has a older image, it will be
+        // updated with the running PSU image that is stored in BMC.
+        if ((activation() == Status::Ready) ||
+            (activation() == Status::Failed) || activation() == Status::Active)
         {
             activation(Status::Activating);
         }
@@ -141,6 +145,13 @@
 
 Activation::Status Activation::startActivation()
 {
+    // Check if the activation has file path
+    if (path().empty())
+    {
+        log<level::WARNING>("No image for the activation, skipped",
+                            entry("VERSION_ID=%s", versionId.c_str()));
+        return activation(); // Return the previous activation status
+    }
     if (!activationProgress)
     {
         activationProgress = std::make_unique<ActivationProgress>(bus, objPath);
@@ -162,6 +173,12 @@
     {
         if (isCompatible(p))
         {
+            if (utils::isAssociated(p, associations()))
+            {
+                log<level::NOTICE>("PSU already running the image, skipping",
+                                   entry("PSU=%s", p.c_str()));
+                continue;
+            }
             psuQueue.push(p);
         }
         else
@@ -173,8 +190,8 @@
 
     if (psuQueue.empty())
     {
-        log<level::ERR>("No PSU compatible with the software");
-        return Status::Failed;
+        log<level::WARNING>("No PSU compatible with the software");
+        return activation(); // Return the previous activation status
     }
 
     // The progress to be increased for each successful update of PSU
diff --git a/src/activation.hpp b/src/activation.hpp
index be373c5..669686e 100644
--- a/src/activation.hpp
+++ b/src/activation.hpp
@@ -176,6 +176,12 @@
     RequestedActivations
         requestedActivation(RequestedActivations value) override;
 
+    /** @brief Get the object path */
+    const std::string& getObjectPath() const
+    {
+        return objPath;
+    }
+
     /** @brief Get the version ID */
     const std::string& getVersionId() const
     {
diff --git a/src/item_updater.cpp b/src/item_updater.cpp
index 13e6bf4..acbc943 100644
--- a/src/item_updater.cpp
+++ b/src/item_updater.cpp
@@ -435,6 +435,40 @@
     return versionId;
 }
 
+void ItemUpdater::syncToLatestImage()
+{
+    auto latestVersionId = getLatestVersionId();
+    if (!latestVersionId)
+    {
+        return;
+    }
+    const auto& it = activations.find(*latestVersionId);
+    assert(it != activations.end());
+    const auto& activation = it->second;
+    const auto& assocs = activation->associations();
+
+    auto paths = utils::getPSUInventoryPath(bus);
+    for (const auto& p : paths)
+    {
+        // As long as there is a PSU is not associated with the latest image,
+        // run the activation so that all PSUs are running the same latest
+        // image.
+        if (!utils::isAssociated(p, assocs))
+        {
+            log<level::INFO>("Automatically update PSU",
+                             entry("VERSION_ID=%s", latestVersionId->c_str()));
+            invokeActivation(activation);
+            break;
+        }
+    }
+}
+
+void ItemUpdater::invokeActivation(
+    const std::unique_ptr<Activation>& activation)
+{
+    activation->requestedActivation(Activation::RequestedActivations::Active);
+}
+
 } // namespace updater
 } // namespace software
 } // namespace phosphor
diff --git a/src/item_updater.hpp b/src/item_updater.hpp
index 2f23b1e..b713454 100644
--- a/src/item_updater.hpp
+++ b/src/item_updater.hpp
@@ -55,7 +55,7 @@
     {
         processPSUImage();
         processStoredImage();
-        getLatestVersionId();
+        syncToLatestImage();
     }
 
     /** @brief Deletes version
@@ -154,6 +154,12 @@
     /** @brief Get the versionId of the latest PSU version */
     std::optional<std::string> getLatestVersionId();
 
+    /** @brief Update PSUs to the latest version */
+    void syncToLatestImage();
+
+    /** @brief Invoke the activation via DBus */
+    void invokeActivation(const std::unique_ptr<Activation>& activation);
+
     /** @brief Persistent sdbusplus D-Bus bus connection. */
     sdbusplus::bus::bus& bus;