Activation: initially support psu update

Initial support for PSU update by starting a systemd unit with PSU
inventory path and image dir as arguments.

Add an example psu-update@.service that shows how the arguments are
passed to systemd unit and expanded to command line arguments.

Tested: Upload a dummy tarball, create a dummy service that only prints
        the arguments, and verify the service is invoked correctly when
        the RequestedActivation is set to Active.

Signed-off-by: Lei YU <mine260309@gmail.com>
Change-Id: I7e122f1cce234caf4951d3e3daad5bee406b507b
diff --git a/meson.build b/meson.build
index 31bffe1..a18b935 100644
--- a/meson.build
+++ b/meson.build
@@ -35,6 +35,8 @@
 cdata.set_quoted('MANIFEST_FILE', get_option('MANIFEST_FILE'))
 cdata.set_quoted('PSU_INVENTORY_PATH_BASE', get_option('PSU_INVENTORY_PATH_BASE'))
 cdata.set_quoted('PSU_VERSION_UTIL', get_option('PSU_VERSION_UTIL'))
+cdata.set_quoted('PSU_UPDATE_SERVICE', get_option('PSU_UPDATE_SERVICE'))
+cdata.set_quoted('IMG_DIR', get_option('IMG_DIR'))
 
 phosphor_dbus_interfaces = dependency('phosphor-dbus-interfaces')
 phosphor_logging = dependency('phosphor-logging')
diff --git a/meson_options.txt b/meson_options.txt
index 1509ac2..1aee013 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -17,6 +17,10 @@
        value: '/xyz/openbmc_project/inventory/system',
        description: 'The base path for PSU inventory')
 
+option('IMG_DIR',
+       type: 'string',
+       value: '/tmp/images',
+       description: 'The directory where downloaded or uploaded PSU images are placed and extracted')
 
 # The PSU_VERSION_UTIL specifies an executable that accepts the PSU
 # inventory path as input, and output the version string, e.g
@@ -27,3 +31,10 @@
        type: 'string',
        value: '/usr/bin/psutils --getversion',
        description: 'The command and arguments to get PSU version')
+
+# The PSU update service
+# It shall take a path containing the PSU image(s) as the input
+option('PSU_UPDATE_SERVICE',
+       type: 'string',
+       value: 'psu-update@.service',
+       description: 'The PSU update service')
diff --git a/src/activation.cpp b/src/activation.cpp
index 8bfed0b..df84879 100644
--- a/src/activation.cpp
+++ b/src/activation.cpp
@@ -1,5 +1,12 @@
+#include "config.h"
+
 #include "activation.hpp"
 
+#include "utils.hpp"
+
+#include <cassert>
+#include <filesystem>
+
 namespace phosphor
 {
 namespace software
@@ -7,19 +14,114 @@
 namespace updater
 {
 
+constexpr auto SYSTEMD_BUSNAME = "org.freedesktop.systemd1";
+constexpr auto SYSTEMD_PATH = "/org/freedesktop/systemd1";
+constexpr auto SYSTEMD_INTERFACE = "org.freedesktop.systemd1.Manager";
+
+namespace fs = std::filesystem;
 namespace softwareServer = sdbusplus::xyz::openbmc_project::Software::server;
 
+using SoftwareActivation = softwareServer::Activation;
+
+namespace internal
+{
+/** Construct the systemd service name */
+std::string getUpdateService(const std::string& psuInventoryPath,
+                             const std::string& versionId)
+{
+    fs::path imagePath(IMG_DIR);
+    imagePath /= versionId;
+
+    // The systemd unit shall be escaped
+    std::string args = psuInventoryPath;
+    args += "\\x20";
+    args += imagePath;
+    std::replace(args.begin(), args.end(), '/', '-');
+
+    std::string service = PSU_UPDATE_SERVICE;
+    auto p = service.find('@');
+    assert(p != std::string::npos);
+    service.insert(p + 1, args);
+    return service;
+}
+
+} // namespace internal
 auto Activation::activation(Activations value) -> Activations
 {
-    // TODO
-    return softwareServer::Activation::activation(value);
+    if (value == Status::Activating)
+    {
+        startActivation();
+    }
+    else
+    {
+        // TODO
+    }
+
+    return SoftwareActivation::activation(value);
 }
 
 auto Activation::requestedActivation(RequestedActivations value)
     -> RequestedActivations
 {
-    // TODO
-    return softwareServer::Activation::requestedActivation(value);
+    if ((value == SoftwareActivation::RequestedActivations::Active) &&
+        (SoftwareActivation::requestedActivation() !=
+         SoftwareActivation::RequestedActivations::Active))
+    {
+        if ((activation() == Status::Ready) || (activation() == Status::Failed))
+        {
+            activation(Status::Activating);
+        }
+    }
+    return SoftwareActivation::requestedActivation(value);
+}
+
+void Activation::unitStateChange(sdbusplus::message::message& msg)
+{
+    uint32_t newStateID{};
+    sdbusplus::message::object_path newStateObjPath;
+    std::string newStateUnit{};
+    std::string newStateResult{};
+
+    // Read the msg and populate each variable
+    msg.read(newStateID, newStateObjPath, newStateUnit, newStateResult);
+
+    if (newStateUnit == psuUpdateUnit)
+    {
+        if (newStateResult == "done")
+        {
+            finishActivation();
+        }
+        if (newStateResult == "failed" || newStateResult == "dependency")
+        {
+            activation(Status::Failed);
+        }
+    }
+}
+
+void Activation::startActivation()
+{
+    // TODO: for now only update one psu, future commits shall handle update
+    // multiple psus
+    auto psuPaths = utils::getPSUInventoryPath(bus);
+    if (psuPaths.empty())
+    {
+        return;
+    }
+
+    psuUpdateUnit = internal::getUpdateService(psuPaths[0], versionId);
+
+    auto method = bus.new_method_call(SYSTEMD_BUSNAME, SYSTEMD_PATH,
+                                      SYSTEMD_INTERFACE, "StartUnit");
+    method.append(psuUpdateUnit, "replace");
+    bus.call_noreply(method);
+}
+
+void Activation::finishActivation()
+{
+    // TODO: delete the interfaces created by phosphor-software-manager
+    // TODO: delete the old software object
+    // TODO: create related associations
+    activation(Status::Active);
 }
 
 } // namespace updater
diff --git a/src/activation.hpp b/src/activation.hpp
index 45aa521..cde55c5 100644
--- a/src/activation.hpp
+++ b/src/activation.hpp
@@ -16,6 +16,8 @@
 namespace updater
 {
 
+namespace sdbusRule = sdbusplus::bus::match::rules;
+
 using ActivationInherit = sdbusplus::server::object::object<
     sdbusplus::xyz::openbmc_project::Software::server::ExtendedVersion,
     sdbusplus::xyz::openbmc_project::Software::server::Activation,
@@ -29,6 +31,7 @@
 class Activation : public ActivationInherit
 {
   public:
+    using Status = Activations;
     /** @brief Constructs Activation Software Manager
      *
      * @param[in] bus    - The Dbus bus object
@@ -44,7 +47,14 @@
                    Activations activationStatus,
                const AssociationList& assocs) :
         ActivationInherit(bus, path.c_str(), true),
-        bus(bus), path(path), versionId(versionId)
+        versionId(versionId), bus(bus), path(path),
+        systemdSignals(
+            bus,
+            sdbusRule::type::signal() + sdbusRule::member("JobRemoved") +
+                sdbusRule::path("/org/freedesktop/systemd1") +
+                sdbusRule::interface("org.freedesktop.systemd1.Manager"),
+            std::bind(&Activation::unitStateChange, this,
+                      std::placeholders::_1))
     {
         // Set Properties.
         extendedVersion(extVersion);
@@ -75,14 +85,37 @@
     RequestedActivations
         requestedActivation(RequestedActivations value) override;
 
+    /** @brief Version id */
+    std::string versionId;
+
+  private:
+    /** @brief Check if systemd state change is relevant to this object
+     *
+     * Instance specific interface to handle the detected systemd state
+     * change
+     *
+     * @param[in]  msg       - Data associated with subscribed signal
+     *
+     */
+    void unitStateChange(sdbusplus::message::message& msg);
+
+    /** @brief Start PSU update */
+    void startActivation();
+
+    /** @brief Finish PSU update */
+    void finishActivation();
+
     /** @brief Persistent sdbusplus DBus bus connection */
     sdbusplus::bus::bus& bus;
 
     /** @brief Persistent DBus object path */
     std::string path;
 
-    /** @brief Version id */
-    std::string versionId;
+    /** @brief Used to subscribe to dbus systemd signals */
+    sdbusplus::bus::match_t systemdSignals;
+
+    /** @brief The PSU update systemd unit */
+    std::string psuUpdateUnit;
 };
 
 } // namespace updater
diff --git a/test/meson.build b/test/meson.build
index 4ec6c0e..0b7b217 100644
--- a/test/meson.build
+++ b/test/meson.build
@@ -37,12 +37,13 @@
     ssl,
   ])
 
-test_item_updater = executable(
-  'test_item_updater',
+test_phosphor_psu_manager = executable(
+  'test_phosphor_psu_manager',
   '../src/activation.cpp',
   '../src/item_updater.cpp',
   '../src/version.cpp',
   'test_item_updater.cpp',
+  'test_activation.cpp',
   include_directories: [psu_inc, test_inc],
   link_args: dynamic_linker,
   build_rpath: oe_sdk.enabled() ? rpath : '',
@@ -56,4 +57,4 @@
   ])
 
 test('util', test_util)
-test('item_updater', test_item_updater)
+test('phosphor_psu_manager', test_phosphor_psu_manager)
diff --git a/test/test_activation.cpp b/test/test_activation.cpp
new file mode 100644
index 0000000..0ea3640
--- /dev/null
+++ b/test/test_activation.cpp
@@ -0,0 +1,53 @@
+#include "activation.hpp"
+
+#include <sdbusplus/test/sdbus_mock.hpp>
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+using namespace phosphor::software::updater;
+
+class TestActivation : public ::testing::Test
+{
+  public:
+    using ActivationStatus = sdbusplus::xyz::openbmc_project::Software::server::
+        Activation::Activations;
+    TestActivation()
+    {
+    }
+    ~TestActivation()
+    {
+    }
+    static constexpr auto dBusPath = SOFTWARE_OBJPATH;
+    sdbusplus::SdBusMock sdbusMock;
+    sdbusplus::bus::bus mockedBus = sdbusplus::get_mocked_new(&sdbusMock);
+    std::unique_ptr<Activation> activation;
+    std::string versionId = "abcdefgh";
+    std::string extVersion = "Some Ext Version";
+    ActivationStatus status = ActivationStatus::Active;
+    AssociationList associations;
+};
+
+TEST_F(TestActivation, ctordtor)
+{
+    activation = std::make_unique<Activation>(mockedBus, dBusPath, versionId,
+                                              extVersion, status, associations);
+}
+
+namespace phosphor::software::updater::internal
+{
+extern std::string getUpdateService(const std::string& psuInventoryPath,
+                                    const std::string& versionId);
+}
+
+TEST_F(TestActivation, getUpdateService)
+{
+    std::string psuInventoryPath = "/com/example/inventory/powersupply1";
+    std::string versionId = "12345678";
+    std::string toCompare = "psu-update@-com-example-inventory-"
+                            "powersupply1\\x20-tmp-images-12345678.service";
+
+    auto service = phosphor::software::updater::internal::getUpdateService(
+        psuInventoryPath, versionId);
+    EXPECT_EQ(toCompare, service);
+}
diff --git a/vendor-example/psu-update@.service b/vendor-example/psu-update@.service
new file mode 100644
index 0000000..06635be
--- /dev/null
+++ b/vendor-example/psu-update@.service
@@ -0,0 +1,17 @@
+# This service shall be started with two arguments:
+#  * The PSU inventory DBus object
+#  * The path of the PSU images
+# E.g.
+#  "psu-update@-xyz-openbmc_project-inventory-system-chassis-motherboard-powersupply0\x20-tmp-image-abcdefg.service"
+# expands to
+#  /usr/bin/psutils --update /xyz/openbmc_project/inventory/system/chassis/motherboard/powersupply0 /tmp/image/abcdefg
+
+[Unit]
+Description=Update PSU %I
+
+[Service]
+Type=oneshot
+RemainAfterExit=no
+Environment="ARGS=%I"
+ExecStart=/usr/bin/psutils --update $ARGS
+SyslogIdentifier=psutils