Scan directories that store PSU images on start

When the service starts, scan the directories that store PSU images,
including the built-in images, and the saved images during PSU update.

When the scanned image is different than the running images, create
activation/version object;
When the scanned image is the same as the running images, update the
version object's path to indicate the PSU image path, so it could be
used for future update in case a PSU is replaced with a different
software.

Tested: On Witherspoon, fake create a dummy PSU image with a different
        version than running PSU, verify a new object is created on
        restart;
        fake creating a dummy PSU image with a same version as a running
        PSU, verify no new object is created, but the "Path" property is
        set to the PSU image directory.

Signed-off-by: Lei YU <mine260309@gmail.com>
Change-Id: I860b978250a718eb82d948a1c88bd8f41bb2b2e3
diff --git a/meson_options.txt b/meson_options.txt
index 4e61922..4212459 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -46,5 +46,5 @@
 
 option('IMG_DIR_BUILTIN',
        type: 'string',
-       value: '/usr/local/obmc/psu',
+       value: '/usr/share/obmc/psu',
        description: 'The read-only directory where the built-in PSU images are stored')
diff --git a/src/activation.cpp b/src/activation.cpp
index 4d190f9..680cdd9 100644
--- a/src/activation.cpp
+++ b/src/activation.cpp
@@ -33,6 +33,9 @@
 std::string getUpdateService(const std::string& psuInventoryPath,
                              const std::string& versionId)
 {
+    // TODO: get image path from the related version
+    // because it could be in either IMG_DIR, or IMG_DIR_PERSIST, or
+    // IMG_DIR_BUILTIN
     fs::path imagePath(IMG_DIR);
     imagePath /= versionId;
 
diff --git a/src/item_updater.cpp b/src/item_updater.cpp
index 8d5695c..6472fe1 100644
--- a/src/item_updater.cpp
+++ b/src/item_updater.cpp
@@ -11,8 +11,9 @@
 
 namespace
 {
-constexpr auto EXTENDED_VERSION = "extended_version";
-}
+constexpr auto MANIFEST_VERSION = "version";
+constexpr auto MANIFEST_EXTENDED_VERSION = "extended_version";
+} // namespace
 
 namespace phosphor
 {
@@ -21,7 +22,6 @@
 namespace updater
 {
 namespace server = sdbusplus::xyz::openbmc_project::Software::server;
-namespace fs = std::filesystem;
 
 using namespace sdbusplus::xyz::openbmc_project::Common::Error;
 using namespace phosphor::logging;
@@ -95,7 +95,7 @@
     {
         // Determine the Activation state by processing the given image dir.
         AssociationList associations;
-        auto activationState = server::Activation::Activations::Ready;
+        auto activationState = Activation::Status::Ready;
 
         associations.emplace_back(std::make_tuple(ACTIVATION_FWD_ASSOCIATION,
                                                   ACTIVATION_REV_ASSOCIATION,
@@ -103,14 +103,8 @@
 
         fs::path manifestPath(filePath);
         manifestPath /= MANIFEST_FILE;
-        std::string extendedVersion;
-        auto values =
-            Version::getValues(manifestPath.string(), {EXTENDED_VERSION});
-        const auto it = values.find(EXTENDED_VERSION);
-        if (it != values.end())
-        {
-            extendedVersion = it->second;
-        }
+        std::string extendedVersion =
+            Version::getValue(manifestPath, {MANIFEST_EXTENDED_VERSION});
 
         auto activation =
             createActivationObject(path, versionId, extendedVersion,
@@ -186,9 +180,7 @@
 
 std::unique_ptr<Activation> ItemUpdater::createActivationObject(
     const std::string& path, const std::string& versionId,
-    const std::string& extVersion,
-    sdbusplus::xyz::openbmc_project::Software::server::Activation::Activations
-        activationStatus,
+    const std::string& extVersion, Activation::Status activationStatus,
     const AssociationList& assocs, const std::string& filePath)
 {
     return std::make_unique<Activation>(bus, path, versionId, extVersion,
@@ -217,7 +209,7 @@
     {
         // Create a new object for running PSU inventory
         AssociationList associations;
-        auto activationState = server::Activation::Activations::Active;
+        auto activationState = Activation::Status::Active;
 
         associations.emplace_back(std::make_tuple(ACTIVATION_FWD_ASSOCIATION,
                                                   ACTIVATION_REV_ASSOCIATION,
@@ -346,6 +338,80 @@
     }
 }
 
+void ItemUpdater::processStoredImage()
+{
+    scanDirectory(IMG_DIR_BUILTIN);
+    scanDirectory(IMG_DIR_PERSIST);
+}
+
+void ItemUpdater::scanDirectory(const fs::path& dir)
+{
+    // The directory shall put PSU images in directories named with model
+    if (!fs::exists(dir))
+    {
+        // Skip
+        return;
+    }
+    if (!fs::is_directory(dir))
+    {
+        log<level::ERR>("The path is not a directory",
+                        entry("PATH=%s", dir.c_str()));
+        return;
+    }
+    for (const auto& d : fs::directory_iterator(dir))
+    {
+        // If the model in manifest does not match the dir name
+        // Log a warning and skip it
+        auto path = d.path();
+        auto manifest = path / MANIFEST_FILE;
+        if (fs::exists(manifest))
+        {
+            auto ret = Version::getValues(
+                manifest.string(),
+                {MANIFEST_VERSION, MANIFEST_EXTENDED_VERSION});
+            auto version = ret[MANIFEST_VERSION];
+            auto extVersion = ret[MANIFEST_EXTENDED_VERSION];
+            auto info = Version::getExtVersionInfo(extVersion);
+            auto model = info["model"];
+            if (path.stem() != model)
+            {
+                log<level::ERR>("Unmatched model",
+                                entry("PATH=%s", path.c_str()),
+                                entry("MODEL=%s", model.c_str()));
+                continue;
+            }
+            auto versionId = utils::getVersionId(version);
+            auto it = activations.find(versionId);
+            if (it == activations.end())
+            {
+                // This is a version that is different than the running PSUs
+                auto activationState = Activation::Status::Ready;
+                auto purpose = VersionPurpose::PSU;
+                auto objPath = std::string(SOFTWARE_OBJPATH) + "/" + versionId;
+
+                auto activation = createActivationObject(
+                    objPath, versionId, extVersion, activationState, {}, path);
+                activations.emplace(versionId, std::move(activation));
+
+                auto versionPtr =
+                    createVersionObject(objPath, versionId, version, purpose);
+                versions.emplace(versionId, std::move(versionPtr));
+            }
+            else
+            {
+                // This is a version that a running PSU is using, set the path
+                // on the version object
+                it->second->path(path);
+            }
+        }
+        else
+        {
+            log<level::ERR>("No MANIFEST found",
+                            entry("PATH=%s", path.c_str()));
+        }
+    }
+}
+
 } // namespace updater
 } // namespace software
 } // namespace phosphor
diff --git a/src/item_updater.hpp b/src/item_updater.hpp
index 33db8bc..2cccf68 100644
--- a/src/item_updater.hpp
+++ b/src/item_updater.hpp
@@ -8,6 +8,7 @@
 #include "utils.hpp"
 #include "version.hpp"
 
+#include <filesystem>
 #include <phosphor-logging/log.hpp>
 #include <sdbusplus/server.hpp>
 #include <xyz/openbmc_project/Association/Definitions/server.hpp>
@@ -29,6 +30,8 @@
 
 namespace MatchRules = sdbusplus::bus::match::rules;
 
+namespace fs = std::filesystem;
+
 /** @class ItemUpdater
  *  @brief Manages the activation of the PSU version items.
  */
@@ -51,6 +54,7 @@
                                this, std::placeholders::_1))
     {
         processPSUImage();
+        processStoredImage();
     }
 
     /** @brief Deletes version
@@ -109,9 +113,7 @@
     /** @brief Create Activation object */
     std::unique_ptr<Activation> createActivationObject(
         const std::string& path, const std::string& versionId,
-        const std::string& extVersion,
-        sdbusplus::xyz::openbmc_project::Software::server::Activation::
-            Activations activationStatus,
+        const std::string& extVersion, Activation::Status activationStatus,
         const AssociationList& assocs, const std::string& filePath);
 
     /** @brief Create Version object */
@@ -142,6 +144,12 @@
      */
     void processPSUImage();
 
+    /** @brief Create PSU Version from stored images */
+    void processStoredImage();
+
+    /** @brief Scan a directory and create PSU Version from stored images */
+    void scanDirectory(const fs::path& p);
+
     /** @brief Persistent sdbusplus D-Bus bus connection. */
     sdbusplus::bus::bus& bus;
 
diff --git a/src/version.cpp b/src/version.cpp
index 02cf6af..ecefc98 100644
--- a/src/version.cpp
+++ b/src/version.cpp
@@ -53,6 +53,19 @@
     return ret;
 }
 
+std::string Version::getValue(const std::string& filePath,
+                              const std::string& key)
+{
+    std::string ret;
+    auto values = Version::getValues(filePath, {key});
+    const auto it = values.find(key);
+    if (it != values.end())
+    {
+        ret = it->second;
+    }
+    return ret;
+}
+
 std::map<std::string, std::string>
     Version::getExtVersionInfo(const std::string& extVersion)
 {
diff --git a/src/version.hpp b/src/version.hpp
index 89db260..dbc48d7 100644
--- a/src/version.hpp
+++ b/src/version.hpp
@@ -112,7 +112,7 @@
     }
 
     /**
-     * @brief Read the manifest file to get the value of the key.
+     * @brief Read the manifest file to get the values of the keys.
      *
      * @param[in] filePath - The path to the file which contains the value
      *                       of keys.
@@ -124,6 +124,18 @@
         getValues(const std::string& filePath,
                   const std::vector<std::string>& keys);
 
+    /**
+     * @brief Read the manifest file to get the value of the key.
+     *
+     * @param[in] filePath - The path to the file which contains the value
+     *                       of keys.
+     * @param[in] key      - The string of the key.
+     *
+     * @return The string of the value.
+     **/
+    static std::string getValue(const std::string& filePath,
+                                const std::string& key);
+
     /** @brief Get information from extVersion
      *
      * @param[in] extVersion - The extended version string that contains
diff --git a/test/meson.build b/test/meson.build
index 2de2b21..7a6a9e7 100644
--- a/test/meson.build
+++ b/test/meson.build
@@ -58,4 +58,5 @@
   ])
 
 test('util', test_util)
-test('phosphor_psu_manager', test_phosphor_psu_manager)
+#test('phosphor_psu_manager', test_phosphor_psu_manager)
+test('phosphor_psu_manager', test_phosphor_psu_manager, workdir: meson.current_source_dir())
diff --git a/test/psu-images-one-valid-one-invalid/model-1/MANIFEST b/test/psu-images-one-valid-one-invalid/model-1/MANIFEST
new file mode 100644
index 0000000..09ceb08
--- /dev/null
+++ b/test/psu-images-one-valid-one-invalid/model-1/MANIFEST
@@ -0,0 +1,4 @@
+purpose=xyz.openbmc_project.Software.Version.VersionPurpose.PSU
+version=psu-test.v0.4
+extended_version=model=model-1,manufacture=test-manu
+
diff --git a/test/psu-images-one-valid-one-invalid/model-2/MANIFEST b/test/psu-images-one-valid-one-invalid/model-2/MANIFEST
new file mode 100644
index 0000000..f4dc679
--- /dev/null
+++ b/test/psu-images-one-valid-one-invalid/model-2/MANIFEST
@@ -0,0 +1,3 @@
+purpose=xyz.openbmc_project.Software.Version.VersionPurpose.PSU
+version=psu-test.v0.5
+extended_version=model=unmatch
diff --git a/test/psu-images-valid-version0/model-3/MANIFEST b/test/psu-images-valid-version0/model-3/MANIFEST
new file mode 100644
index 0000000..4e9c33c
--- /dev/null
+++ b/test/psu-images-valid-version0/model-3/MANIFEST
@@ -0,0 +1,3 @@
+purpose=xyz.openbmc_project.Software.Version.VersionPurpose.PSU
+version=version0
+extended_version=model=model-3
diff --git a/test/test_activation.cpp b/test/test_activation.cpp
index 088e3ba..0c45a37 100644
--- a/test/test_activation.cpp
+++ b/test/test_activation.cpp
@@ -72,6 +72,14 @@
         &mockedAssociationInterface, filePath);
 }
 
+TEST_F(TestActivation, ctorWithInvalidExtVersion)
+{
+    extVersion = "invalid text";
+    activation = std::make_unique<Activation>(
+        mockedBus, dBusPath, versionId, extVersion, status, associations,
+        &mockedAssociationInterface, filePath);
+}
+
 namespace phosphor::software::updater::internal
 {
 extern std::string getUpdateService(const std::string& psuInventoryPath,
diff --git a/test/test_item_updater.cpp b/test/test_item_updater.cpp
index b020c28..fb6ea93 100644
--- a/test/test_item_updater.cpp
+++ b/test/test_item_updater.cpp
@@ -8,6 +8,7 @@
 
 using namespace phosphor::software::updater;
 using ::testing::_;
+using ::testing::Pointee;
 using ::testing::Return;
 using ::testing::ReturnArg;
 using ::testing::StrEq;
@@ -49,6 +50,11 @@
         itemUpdater->onPsuInventoryChanged(psuPath, properties);
     }
 
+    void scanDirectory(const fs::path& p)
+    {
+        itemUpdater->scanDirectory(p);
+    }
+
     static constexpr auto dBusPath = SOFTWARE_OBJPATH;
     sdbusplus::SdBusMock sdbusMock;
     sdbusplus::bus::bus mockedBus = sdbusplus::get_mocked_new(&sdbusMock);
@@ -408,3 +414,77 @@
     EXPECT_CALL(sdbusMock, sd_bus_emit_object_removed(_, StrEq(dBusPath)))
         .Times(1);
 }
+
+TEST_F(TestItemUpdater, scanDirOnNoPSU)
+{
+    constexpr auto psuPath = "/com/example/inventory/psu0";
+    constexpr auto service = "com.example.Software.Psu";
+    constexpr auto version = "version0";
+    std::string objPath = getObjPath(version);
+    EXPECT_CALL(mockedUtils, getPSUInventoryPath(_))
+        .WillOnce(Return(std::vector<std::string>({psuPath})));
+    EXPECT_CALL(mockedUtils, getService(_, StrEq(psuPath), _))
+        .WillOnce(Return(service));
+    EXPECT_CALL(mockedUtils, getVersion(StrEq(psuPath)))
+        .WillOnce(Return(std::string(version)));
+    EXPECT_CALL(mockedUtils, getPropertyImpl(_, StrEq(service), StrEq(psuPath),
+                                             _, StrEq(PRESENT)))
+        .WillOnce(Return(any(PropertyType(false)))); // not present
+
+    // The item updater itself
+    EXPECT_CALL(sdbusMock, sd_bus_emit_object_added(_, StrEq(dBusPath)))
+        .Times(1);
+
+    // No activation/version objects are created
+    EXPECT_CALL(sdbusMock, sd_bus_emit_object_added(_, StrEq(objPath)))
+        .Times(0);
+    itemUpdater = std::make_unique<ItemUpdater>(mockedBus, dBusPath);
+
+    // The valid image in test/psu-images-one-valid-one-invalid/model-1/
+    auto objPathValid = getObjPath("psu-test.v0.4");
+    auto objPathInvalid = getObjPath("psu-test.v0.5");
+    // activation and version object will be added on scan dir
+    EXPECT_CALL(sdbusMock, sd_bus_emit_object_added(_, StrEq(objPathValid)))
+        .Times(2);
+    EXPECT_CALL(sdbusMock, sd_bus_emit_object_added(_, StrEq(objPathInvalid)))
+        .Times(0);
+    scanDirectory("./psu-images-one-valid-one-invalid");
+}
+
+TEST_F(TestItemUpdater, scanDirOnSamePSUVersion)
+{
+    constexpr auto psuPath = "/com/example/inventory/psu0";
+    constexpr auto service = "com.example.Software.Psu";
+    constexpr auto version = "version0";
+    std::string objPath = getObjPath(version);
+    EXPECT_CALL(mockedUtils, getPSUInventoryPath(_))
+        .WillOnce(Return(std::vector<std::string>({psuPath})));
+    EXPECT_CALL(mockedUtils, getService(_, StrEq(psuPath), _))
+        .WillOnce(Return(service));
+    EXPECT_CALL(mockedUtils, getVersion(StrEq(psuPath)))
+        .WillOnce(Return(std::string(version)));
+    EXPECT_CALL(mockedUtils, getPropertyImpl(_, StrEq(service), StrEq(psuPath),
+                                             _, StrEq(PRESENT)))
+        .WillOnce(Return(any(PropertyType(true)))); // present
+
+    // The item updater itself
+    EXPECT_CALL(sdbusMock, sd_bus_emit_object_added(_, StrEq(dBusPath)))
+        .Times(1);
+
+    // activation and version object will be added
+    EXPECT_CALL(sdbusMock, sd_bus_emit_object_added(_, StrEq(objPath)))
+        .Times(2);
+    itemUpdater = std::make_unique<ItemUpdater>(mockedBus, dBusPath);
+
+    // The valid image in test/psu-images-valid-version0/model-3/ has the same
+    // version as the running PSU, so no objects will be created, but only the
+    // path will be set to the version object
+    EXPECT_CALL(sdbusMock, sd_bus_emit_object_added(_, StrEq(objPath)))
+        .Times(0);
+    EXPECT_CALL(sdbusMock, sd_bus_emit_properties_changed_strv(
+                               _, StrEq(objPath),
+                               StrEq("xyz.openbmc_project.Common.FilePath"),
+                               Pointee(StrEq("Path"))))
+        .Times(1);
+    scanDirectory("./psu-images-valid-version0");
+}