Add alias and option to support migration

The main goal of the change is to allow us to change the name of the
blobId while making sure the existing data is still valid during the
migration. Added an alias option which allow us to have two possible
valid blobId and will use the `baseBlobId` to commit the data even if
the current data match the alish ID. This allow us to change the blobId
of the binary store without loosing the data.

Also added an option to revert back to the alias blobId with the
`blobtool` to support backward compatibility when we need to downgrade
to a version without alias support. Created the base service that gets
trigger to start the blob id revert.

Tested:

Tested backward compatibility and working fine. The blob data are not
lost between versions.

Change-Id: I75bcbce3aff20c6ee8a491bef36fac260595d6f7
Signed-off-by: Willy Tu <wltu@google.com>
diff --git a/include/binarystore.hpp b/include/binarystore.hpp
index 792de4a..aa83714 100644
--- a/include/binarystore.hpp
+++ b/include/binarystore.hpp
@@ -74,6 +74,7 @@
     BinaryStore& operator=(BinaryStore&&) = default;
 
     std::string getBaseBlobId() const override;
+    bool setBaseBlobId(const std::string& baseBlobId) override;
     std::vector<std::string> getBlobIds() const override;
     bool openOrCreateBlob(const std::string& blobId, uint16_t flags) override;
     bool deleteBlob(const std::string& blobId) override;
@@ -91,10 +92,10 @@
      * @returns unique_ptr to constructed BinaryStore. Caller should take
      *     ownership of the instance.
      */
-    static std::unique_ptr<BinaryStoreInterface>
-        createFromConfig(const std::string& baseBlobId,
-                         std::unique_ptr<SysFile> file,
-                         std::optional<uint32_t> maxSize = std::nullopt);
+    static std::unique_ptr<BinaryStoreInterface> createFromConfig(
+        const std::string& baseBlobId, std::unique_ptr<SysFile> file,
+        std::optional<uint32_t> maxSize = std::nullopt,
+        std::optional<std::string> aliasBlobBaseId = std::nullopt);
 
     /**
      * Helper factory method to create a BinaryStore instance
@@ -111,7 +112,8 @@
   private:
     /* Load the serialized data from sysfile if commit state is dirty.
      * Returns False if encountered error when loading */
-    bool loadSerializedData();
+    bool loadSerializedData(
+        std::optional<std::string> aliasBlobBaseId = std::nullopt);
 
     std::string baseBlobId_;
     binaryblobproto::BinaryBlobBase blob_;
diff --git a/include/binarystore_interface.hpp b/include/binarystore_interface.hpp
index ba6dca3..3760305 100644
--- a/include/binarystore_interface.hpp
+++ b/include/binarystore_interface.hpp
@@ -30,6 +30,13 @@
     virtual std::string getBaseBlobId() const = 0;
 
     /**
+     * Set the base blob id
+     * @param baseBlobId: The blob id to update to.
+     * @returns true if the binary store is updated with the new baseBlobId
+     */
+    virtual bool setBaseBlobId(const std::string& baseBlobId) = 0;
+
+    /**
      * @returns List of all open blob IDs, plus the base.
      */
     virtual std::vector<std::string> getBlobIds() const = 0;
diff --git a/include/binarystore_mock.hpp b/include/binarystore_mock.hpp
index 9360dd3..30442a0 100644
--- a/include/binarystore_mock.hpp
+++ b/include/binarystore_mock.hpp
@@ -45,6 +45,7 @@
     }
     MOCK_CONST_METHOD0(getBaseBlobId, std::string());
     MOCK_CONST_METHOD0(getBlobIds, std::vector<std::string>());
+    MOCK_METHOD1(setBaseBlobId, bool(const std::string&));
     MOCK_METHOD2(openOrCreateBlob, bool(const std::string&, uint16_t));
     MOCK_METHOD1(deleteBlob, bool(const std::string&));
     MOCK_METHOD2(read, std::vector<uint8_t>(uint32_t, uint32_t));
diff --git a/include/parse_config.hpp b/include/parse_config.hpp
index 766830c..4771e2e 100644
--- a/include/parse_config.hpp
+++ b/include/parse_config.hpp
@@ -13,10 +13,12 @@
 
 struct BinaryBlobConfig
 {
-    std::string blobBaseId;               // Required
-    std::string sysFilePath;              // Required
-    std::optional<uint32_t> offsetBytes;  // Optional
-    std::optional<uint32_t> maxSizeBytes; // Optional
+    std::string blobBaseId;                     // Required
+    std::string sysFilePath;                    // Required
+    std::optional<uint32_t> offsetBytes;        // Optional
+    std::optional<uint32_t> maxSizeBytes;       // Optional
+    std::optional<std::string> aliasBlobBaseId; // Optional
+    bool migrateToAlias = false;                // Optional
 };
 
 /**
@@ -38,6 +40,16 @@
     {
         j.at("maxSizeBytes").get_to(config.maxSizeBytes.emplace());
     }
+
+    if (j.contains("aliasBlobBaseId"))
+    {
+        j.at("aliasBlobBaseId").get_to(config.aliasBlobBaseId.emplace());
+    }
+
+    if (j.contains("migrateToAlias"))
+    {
+        config.migrateToAlias = j.at("migrateToAlias");
+    }
 }
 
 } // namespace conf
diff --git a/meson.build b/meson.build
index dba4b57..8da9f47 100644
--- a/meson.build
+++ b/meson.build
@@ -13,6 +13,7 @@
 subdir('proto')
 subdir('include')
 subdir('src')
+subdir('service')
 
 if not get_option('tests').disabled()
   subdir('test')
diff --git a/service/binarystore-migration.service b/service/binarystore-migration.service
new file mode 100644
index 0000000..76fb3bc
--- /dev/null
+++ b/service/binarystore-migration.service
@@ -0,0 +1,7 @@
+[Unit]
+Description=Migrate Binary Stroage Data
+
+[Service]
+Type=oneshot
+ExecStart=/usr/bin/blobtool -m
+
diff --git a/service/meson.build b/service/meson.build
new file mode 100644
index 0000000..e7a6307
--- /dev/null
+++ b/service/meson.build
@@ -0,0 +1,10 @@
+if get_option('blobtool').allowed()
+  systemd_dep = dependency('systemd')
+  if systemd_dep.found()
+    import('fs').copyfile(
+      'binarystore-migration.service',
+      install: true,
+      install_dir: systemd_dep.get_variable(pkgconfig: 'systemdsystemunitdir'),
+    )
+  endif
+endif
diff --git a/src/binarystore.cpp b/src/binarystore.cpp
index 7c1bc42..c69a653 100644
--- a/src/binarystore.cpp
+++ b/src/binarystore.cpp
@@ -29,10 +29,9 @@
 
 using namespace phosphor::logging;
 
-std::unique_ptr<BinaryStoreInterface>
-    BinaryStore::createFromConfig(const std::string& baseBlobId,
-                                  std::unique_ptr<SysFile> file,
-                                  std::optional<uint32_t> maxSize)
+std::unique_ptr<BinaryStoreInterface> BinaryStore::createFromConfig(
+    const std::string& baseBlobId, std::unique_ptr<SysFile> file,
+    std::optional<uint32_t> maxSize, std::optional<std::string> aliasBlobBaseId)
 {
     if (baseBlobId.empty() || !file)
     {
@@ -44,7 +43,7 @@
     auto store =
         std::make_unique<BinaryStore>(baseBlobId, std::move(file), maxSize);
 
-    if (!store->loadSerializedData())
+    if (!store->loadSerializedData(aliasBlobBaseId))
     {
         return nullptr;
     }
@@ -73,7 +72,7 @@
     return store;
 }
 
-bool BinaryStore::loadSerializedData()
+bool BinaryStore::loadSerializedData(std::optional<std::string> aliasBlobBaseId)
 {
     /* Load blob from sysfile if we know it might not match what we have.
      * Note it will overwrite existing unsaved data per design. */
@@ -128,6 +127,16 @@
         return true;
     }
 
+    std::string alias = aliasBlobBaseId.value_or("");
+    if (blob_.blob_base_id() == alias)
+    {
+        log<level::WARNING>("Alias blob id, rename blob id...",
+                            entry("LOADED=%s", alias.c_str()),
+                            entry("RENAMED=%s", baseBlobId_.c_str()));
+        std::string tmpBlobId = baseBlobId_;
+        baseBlobId_ = alias;
+        return setBaseBlobId(tmpBlobId);
+    }
     if (blob_.blob_base_id() != baseBlobId_ && !readOnly_)
     {
         /* Uh oh, stale data loaded. Clean it and commit. */
@@ -154,6 +163,29 @@
     return blob_.blob_base_id();
 }
 
+bool BinaryStore::setBaseBlobId(const std::string& baseBlobId)
+{
+    if (baseBlobId_.empty())
+    {
+        baseBlobId_ = blob_.blob_base_id();
+    }
+
+    std::string oldBlobId = baseBlobId_;
+    size_t oldPrefixIndex = baseBlobId_.size();
+    baseBlobId_ = baseBlobId;
+    blob_.set_blob_base_id(baseBlobId_);
+    auto blobsPtr = blob_.mutable_blobs();
+    for (auto blob = blobsPtr->begin(); blob != blobsPtr->end(); blob++)
+    {
+        const std::string& blodId = blob->blob_id();
+        if (blodId.starts_with(oldBlobId))
+        {
+            blob->set_blob_id(baseBlobId_ + blodId.substr(oldPrefixIndex));
+        }
+    }
+    return this->commit();
+}
+
 std::vector<std::string> BinaryStore::getBlobIds() const
 {
     std::vector<std::string> result;
diff --git a/src/blobtool.cpp b/src/blobtool.cpp
index 2f45aab..c1e2e65 100644
--- a/src/blobtool.cpp
+++ b/src/blobtool.cpp
@@ -23,6 +23,7 @@
         HELP,
         LIST,
         READ,
+        MIGRATE,
     } action = Action::LIST;
 } toolConfig;
 
@@ -34,6 +35,8 @@
                    "\t--list\t\tList all supported blobs. This is a default.\n"
                    "\t--read\t\tRead blob specified in --blob argument (which "
                    "becomes mandatory).\n"
+                   "\t--migrate\tUpdate all binary stores to use the alias "
+                   "blob id if enabled.\n"
                    "\t--config\tFILENAME\tPath to the configuration file. The "
                    "default is /usr/share/binaryblob/config.json.\n"
                    "\t--binary-store\tFILENAME\tPath to the binary storage. If "
@@ -53,6 +56,7 @@
         {"help", no_argument, 0, 'h'},
         {"list", no_argument, 0, 'l'},
         {"read", no_argument, 0, 'r'},
+        {"migrate", no_argument, 0, 'm'},
         {"config", required_argument, 0, 'c'},
         {"binary-store", required_argument, 0, 's'},
         {"blob", required_argument, 0, 'b'},
@@ -61,7 +65,6 @@
     };
 
     int optionIndex = 0;
-    std::string configPath = defaultBlobConfigPath;
     bool res = true;
     while (1)
     {
@@ -81,6 +84,9 @@
             case 'r':
                 cfg.action = BlobToolConfig::Action::READ;
                 break;
+            case 'm':
+                cfg.action = BlobToolConfig::Action::MIGRATE;
+                break;
             case 'c':
                 cfg.configPath = optarg;
                 break;
@@ -170,11 +176,30 @@
                 config.sysFilePath, config.offsetBytes);
 
             auto store = binstore::BinaryStore::createFromConfig(
-                config.blobBaseId, std::move(file));
-            stores.push_back(std::move(store));
+                config.blobBaseId, std::move(file), config.maxSizeBytes,
+                config.aliasBlobBaseId);
+
+            if (toolConfig.action == BlobToolConfig::Action::MIGRATE)
+            {
+                if (config.migrateToAlias && config.aliasBlobBaseId.has_value())
+                {
+                    store->setBaseBlobId(config.aliasBlobBaseId.value());
+                }
+            }
+            else
+            {
+                stores.push_back(std::move(store));
+            }
         }
     }
 
+    if (toolConfig.action == BlobToolConfig::Action::MIGRATE)
+    {
+        stdplus::print(stderr,
+                       "Migrated all BinaryStore back to configured Alias\n");
+        return 0;
+    }
+
     if (toolConfig.action == BlobToolConfig::Action::LIST)
     {
         stdplus::print(stderr, "Supported Blobs: \n");
@@ -185,8 +210,9 @@
                 blobIds.begin(), blobIds.end(),
                 std::ostream_iterator<decltype(blobIds[0])>(std::cout, "\n"));
         }
+        return 0;
     }
-    else if (toolConfig.action == BlobToolConfig::Action::READ)
+    if (toolConfig.action == BlobToolConfig::Action::READ)
     {
         if (toolConfig.blobName.empty())
         {
@@ -231,6 +257,7 @@
             stdplus::print(stderr, "Blob {} not found.\n", toolConfig.blobName);
             return 1;
         }
+        return 0;
     }
 
     return 0;
diff --git a/src/main.cpp b/src/main.cpp
index 8e3d244..20858a4 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -72,7 +72,8 @@
                                                             config.offsetBytes);
 
         handler->addNewBinaryStore(binstore::BinaryStore::createFromConfig(
-            config.blobBaseId, std::move(file), config.maxSizeBytes));
+            config.blobBaseId, std::move(file), config.maxSizeBytes,
+            config.aliasBlobBaseId));
     }
 
     return handler;
diff --git a/src/meson.build b/src/meson.build
index 0e20dcd..cc1a7e9 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -30,7 +30,7 @@
   install: true,
   install_dir: get_option('libdir') / 'blob-ipmid')
 
-if not get_option('blobtool').disabled()
+if get_option('blobtool').allowed()
   executable(
     'blobtool',
     'blobtool.cpp',
diff --git a/test/binarystore_unittest.cpp b/test/binarystore_unittest.cpp
index 5d82f81..5b618e3 100644
--- a/test/binarystore_unittest.cpp
+++ b/test/binarystore_unittest.cpp
@@ -9,6 +9,7 @@
 #include <iterator>
 #include <memory>
 #include <sstream>
+#include <stdplus/print.hpp>
 #include <vector>
 
 #include <gmock/gmock.h>
@@ -119,6 +120,20 @@
     EXPECT_EQ(initialData, blobDataStorage);
 }
 
+TEST_F(BinaryStoreTest, SimpleLoadWithAlias)
+{
+    auto testDataFile = createBlobStorage(inputProto);
+    auto initialData = blobDataStorage;
+    auto store = binstore::BinaryStore::createFromConfig(
+        "/blob/my-test-2", std::move(testDataFile), std::nullopt,
+        "/blob/my-test");
+    EXPECT_THAT(store->getBlobIds(),
+                UnorderedElementsAre("/blob/my-test-2", "/blob/my-test-2/0",
+                                     "/blob/my-test-2/1", "/blob/my-test-2/2",
+                                     "/blob/my-test-2/3"));
+    EXPECT_NE(initialData, blobDataStorage);
+}
+
 TEST_F(BinaryStoreTest, TestCreateFromFile)
 {
     auto testDataFile = createBlobStorage(inputProto);
@@ -135,6 +150,24 @@
     EXPECT_EQ(initialData, blobDataStorage);
 }
 
+TEST_F(BinaryStoreTest, TestSetBaseBlobId)
+{
+    auto testDataFile = createBlobStorage(inputProto);
+    auto initialData = blobDataStorage;
+    auto store = binstore::BinaryStore::createFromConfig(
+        "/blob/my-test", std::move(testDataFile), std::nullopt);
+    ASSERT_TRUE(store);
+    EXPECT_EQ("/blob/my-test", store->getBaseBlobId());
+    EXPECT_TRUE(store->setBaseBlobId("/blob/my-test-1"));
+    EXPECT_EQ("/blob/my-test-1", store->getBaseBlobId());
+    EXPECT_THAT(store->getBlobIds(),
+                UnorderedElementsAre("/blob/my-test-1", "/blob/my-test-1/0",
+                                     "/blob/my-test-1/1", "/blob/my-test-1/2",
+                                     "/blob/my-test-1/3"));
+    // Check that the storage has changed
+    EXPECT_NE(initialData, blobDataStorage);
+}
+
 TEST_F(BinaryStoreTest, TestReadBlob)
 {
     auto testDataFile = createBlobStorage(inputProto);
diff --git a/test/meson.build b/test/meson.build
index 66a964c..fe4c0f2 100644
--- a/test/meson.build
+++ b/test/meson.build
@@ -6,6 +6,7 @@
   dependencies: [gtest, gmock])
 
 tests = [
+  'binarystore_unittest',
   'parse_config_unittest',
   'sys_file_unittest',
   'handler_unittest',
diff --git a/test/parse_config_unittest.cpp b/test/parse_config_unittest.cpp
index 772dcc8..b0f1ac2 100644
--- a/test/parse_config_unittest.cpp
+++ b/test/parse_config_unittest.cpp
@@ -26,7 +26,9 @@
       "blobBaseId": "/test/",
       "sysFilePath": "/sys/fake/path",
       "offsetBytes": 32,
-      "maxSizeBytes": 2
+      "maxSizeBytes": 2,
+      "aliasBlobBaseId": "/test2/",
+      "migrateToAlias": true
     }
   )"_json;
 
@@ -37,6 +39,8 @@
     EXPECT_EQ(config.sysFilePath, "/sys/fake/path");
     EXPECT_EQ(config.offsetBytes, 32);
     EXPECT_EQ(config.maxSizeBytes, 2);
+    EXPECT_EQ(config.aliasBlobBaseId, "/test2/");
+    EXPECT_TRUE(config.migrateToAlias);
 }
 
 TEST(ParseConfigTest, TestConfigArray)