fw update: common code

new daemons to implement the flow as described in
https://github.com/openbmc/docs/blob/master/designs/code-update.md

- common/
  common code folder
  - common update flow
  - base class for the device specific update daemons

The new daemons are all following the generic template of Code Updater
daemon as outlined in the design.

The idea is that they are separate daemons (per device, as outlined in
the design) but share all the code that's not device specific.

Tested: next patch in series

Change-Id: If2438b8506aceb8c5313ec13a0bf7cb68f3cc279
Signed-off-by: Alexander Hansen <alexander.hansen@9elements.com>
diff --git a/common/README.md b/common/README.md
new file mode 100644
index 0000000..917b133
--- /dev/null
+++ b/common/README.md
@@ -0,0 +1,34 @@
+# Common Firmware Library
+
+This library follows the code update design:
+https://github.com/openbmc/docs/blob/master/designs/code-update.md
+
+It enables implementing code updaters for different devices. Each one inherits
+from the classes found in the common folder.
+
+Device-specific class members can be added to implement the code update flow for
+different devices.
+
+## Example Code
+
+To understand the control flow, consider looking at 'NopDevice' and
+'NopDeviceCodeUpdater'.
+
+They implement a complete device-specific code updater, for the nop device,
+which is a nonexistent device used only for examples and testing.
+
+It implements everything expected of a device-specific code updater and can be
+used as a starting point.
+
+## PLDM Package Parser
+
+The PackageParser in the pldm directory currently references a following
+revision from the openbmc/pldm repository -
+
+https://github.com/openbmc/pldm/blob/f48015b39f945c2f9534e674819bdfef7b6c7054/fw-update/package_parser.cpp#L294
+
+However, this code will be deprecated and replaced with the package parsing APIs
+provided by libpldm once they become available.
+
+The PackageParser has a maximum supported PLDM firmware package revision of
+1.0.0.
diff --git a/common/include/device.hpp b/common/include/device.hpp
new file mode 100644
index 0000000..2bfcac7
--- /dev/null
+++ b/common/include/device.hpp
@@ -0,0 +1,102 @@
+#pragma once
+
+#include "common/pldm/package_parser.hpp"
+#include "device_config.hpp"
+#include "software.hpp"
+
+#include <sdbusplus/async/context.hpp>
+#include <xyz/openbmc_project/Association/Definitions/aserver.hpp>
+#include <xyz/openbmc_project/Software/Activation/aserver.hpp>
+#include <xyz/openbmc_project/Software/Update/aserver.hpp>
+#include <xyz/openbmc_project/Software/Version/aserver.hpp>
+
+#include <string>
+
+using ActivationInterface =
+    sdbusplus::common::xyz::openbmc_project::software::Activation;
+
+class SoftwareManager;
+
+class Device
+{
+  public:
+    Device(sdbusplus::async::context& ctx, bool dryRun,
+           const DeviceConfig& deviceConfig, SoftwareManager* parent);
+
+    virtual ~Device() = default;
+    Device(const Device&) = delete;
+    Device& operator=(const Device&) = delete;
+    Device(Device&&) = delete;
+    Device& operator=(Device&&) = delete;
+
+    // we provide the pointer to the dbus interface so the device specific
+    // implementation can give percentage progress updates
+    // @param image                raw fw image without pldm header
+    // @param image_size           size of 'image'
+    // @param activationProgress   dbus interface for progress reporting
+    // @returns                    true if update was applied successfully.
+    //                             Should also return true if update was applied
+    //                             successfully, but the host failed to power
+    //                             on.
+    virtual sdbusplus::async::task<bool> updateDevice(
+        const uint8_t* image, size_t image_size,
+        std::unique_ptr<SoftwareActivationProgress>& activationProgress) = 0;
+
+    // @returns                    the object path of the inventory item which
+    //                             can be associated with this device.
+    virtual sdbusplus::async::task<std::string>
+        getInventoryItemObjectPath() = 0;
+
+    // Returns the apply times for updates which are supported by the device
+    // Override this if your device deviates from the default set of apply
+    // times.
+    virtual std::set<RequestedApplyTimes> allowedApplyTimes();
+
+    // @param image                the memory fd with the pldm package
+    // @param applyTime            when the update should be applied
+    // @param swid                 the software id to use
+    // @returns                    true if update was successfull
+    sdbusplus::async::task<bool> startUpdateAsync(
+        sdbusplus::message::unix_fd image, RequestedApplyTimes applyTime,
+        std::unique_ptr<Software> softwareUpdate);
+
+    // Value of 'Type' field for the configuration in EM exposes record
+    std::string getEMConfigType() const;
+
+    // software instance, identified by its swid
+    // The specific derived class also owns its dbus interfaces,
+    // which are destroyed when the instance is deleted.
+    std::unique_ptr<Software> softwareCurrent;
+
+  protected:
+    // Resets the device, in whichever way is appropriate for the device.
+    // The reset must be capable to apply the firmware update which was done
+    // by 'deviceSpecificUpdateFunction', in case that function did not already
+    // apply it. This method is optional to implement for that reason.
+    virtual void resetDevice();
+
+    // The common configuration that all devices share.
+    // We get this from EM configuration.
+    DeviceConfig config;
+
+    SoftwareManager* parent;
+    bool dryRun;
+
+    sdbusplus::async::context& ctx;
+
+  private:
+    // @param pldm_pkg             raw pldm package
+    // @param pldm_pkg_size        size of 'pldm_pkg'
+    // @param applyTime            when the update should be applied
+    // @returns                    the return value of the device specific
+    // update function
+    sdbusplus::async::task<bool> continueUpdateWithMappedPackage(
+        void* pldm_pkg,
+        const std::shared_ptr<pldm::fw_update::PackageParser>& packageParser,
+        sdbusplus::common::xyz::openbmc_project::software::ApplyTime::
+            RequestedApplyTimes applyTime,
+        const std::unique_ptr<Software>& softwareUpdate);
+
+    friend SoftwareUpdate;
+    friend Software;
+};
diff --git a/common/include/device_config.hpp b/common/include/device_config.hpp
new file mode 100644
index 0000000..365e5bb
--- /dev/null
+++ b/common/include/device_config.hpp
@@ -0,0 +1,30 @@
+#pragma once
+
+#include <cstdint>
+#include <string>
+
+/* This class represents the device software configuration we get from
+ * entity-manager via dbus. Each Code Updater can create its own configuration
+ * class that inherits from this to store additional properties from their
+ * device configuration, like bus/address/...
+ */
+class DeviceConfig
+{
+  public:
+    DeviceConfig(uint32_t vendorIANA, const std::string& compatible,
+                 const std::string& configType, const std::string& name);
+
+    // https://gerrit.openbmc.org/c/openbmc/docs/+/74653
+    // properties from the configuration
+    const uint32_t vendorIANA; // "0x0000A015", 4 bytes as per PLDM spec
+
+    const std::string
+        compatibleHardware; // e.g.
+                            // "com.meta.Hardware.Yosemite4.MedusaBoard.CPLD.LCMX02_2000HC"
+
+    // 'Name' field from the EM config
+    const std::string configName;
+
+    // 'Type' field from the EM config
+    const std::string configType;
+};
diff --git a/common/include/software.hpp b/common/include/software.hpp
new file mode 100644
index 0000000..6b96f09
--- /dev/null
+++ b/common/include/software.hpp
@@ -0,0 +1,124 @@
+#pragma once
+
+#include "software_update.hpp"
+
+#include <sdbusplus/async/context.hpp>
+#include <xyz/openbmc_project/Association/Definitions/aserver.hpp>
+#include <xyz/openbmc_project/Software/Activation/aserver.hpp>
+#include <xyz/openbmc_project/Software/ActivationBlocksTransition/aserver.hpp>
+#include <xyz/openbmc_project/Software/ActivationProgress/aserver.hpp>
+#include <xyz/openbmc_project/Software/Version/aserver.hpp>
+
+#include <string>
+
+// Need to declare this class to initialize the protected members of our base
+// class. This prevents "Conditional jump or move depends on uninitialised
+// value(s)" when properties are updated for the first time.
+class SoftwareActivationProgress :
+    public sdbusplus::aserver::xyz::openbmc_project::software::
+        ActivationProgress<Software>
+{
+  public:
+    SoftwareActivationProgress(sdbusplus::async::context& ctx,
+                               const char* objPath);
+};
+
+using SoftwareActivationBlocksTransition = sdbusplus::aserver::xyz::
+    openbmc_project::software::ActivationBlocksTransition<Software>;
+
+using SoftwareVersion =
+    sdbusplus::aserver::xyz::openbmc_project::software::Version<Software>;
+using SoftwareActivation =
+    sdbusplus::aserver::xyz::openbmc_project::software::Activation<Software>;
+using SoftwareAssociationDefinitions =
+    sdbusplus::aserver::xyz::openbmc_project::association::Definitions<
+        Software>;
+
+class Device;
+
+// This represents a software version running on the device.
+class Software : private SoftwareActivation
+{
+  public:
+    Software(sdbusplus::async::context& ctx, const std::string& swid,
+             Device& parent);
+
+    // @returns        object path of this software
+    sdbusplus::message::object_path getObjectPath() const;
+
+    // The software id
+    const std::string swid;
+
+    // Set the activation status of this software
+    // @param activation         The activation status
+    void setActivation(sdbusplus::common::xyz::openbmc_project::software::
+                           Activation::Activations activation);
+
+    // Add / remove the 'ActivationBlocksTransition' dbus interface.
+    // This dbus interface is only needed during the update process.
+    // @param enabled       determines if the dbus interface should be there
+    void setActivationBlocksTransition(bool enabled);
+
+    // This is only required during the activation of the new fw
+    // and is deleted again afterwards.
+    // This member is public since the device specific update function
+    // needs to update the progress.
+    std::unique_ptr<SoftwareActivationProgress> optSoftwareActivationProgress =
+        nullptr;
+
+    // This should populate 'optSoftwareUpdate'
+    // @param allowedApplyTimes        When updates to this Version can be
+    // applied
+    void enableUpdate(const std::set<RequestedApplyTimes>& allowedApplyTimes);
+
+    // This should populate 'optSoftwareVersion'
+    // @param version      the version string
+    void setVersion(const std::string& versionStr);
+
+    // This should populate 'optSoftwareAssociationDefinitions'
+    // @param isRunning             if the software version is currently running
+    // on the device
+    // @param isActivating          if the software version is currently
+    // activating (not yet running) on the device
+    sdbusplus::async::task<> setAssociationDefinitionsRunningActivating(
+        bool isRunning, bool isActivating);
+
+    // @returns this->parent
+    Device& getParentDevice();
+
+  protected:
+    // @returns        a random software id (swid) for that device
+    static std::string getRandomSoftwareId(Device& parent);
+
+  private:
+    // @returns        the object path for the a software with that swid
+    static std::string getObjPathFromSwid(const std::string& swid);
+
+    // The device we are associated to, meaning this software is either running
+    // on the device, or could potentially run on that device (update).
+    Device& parent;
+
+    // Dbus interface to prevent power state transition during update.
+    std::unique_ptr<SoftwareActivationBlocksTransition>
+        optActivationBlocksTransition = nullptr;
+
+    // The software update dbus interface is not always present.
+    // It is constructed if the software version is able to be updated.
+    // For the new software version, this interface is constructed after the
+    // update has taken effect
+    std::unique_ptr<SoftwareUpdate> optSoftwareUpdate = nullptr;
+
+    // We do not know the software version until we parse the PLDM package.
+    // Since the Activation interface needs to be available
+    // before then, this is nullptr until we get to know the version.
+    std::unique_ptr<SoftwareVersion> optSoftwareVersion = nullptr;
+
+    // This represents our association to the inventory item in case
+    // this software is currently on the device.
+    std::unique_ptr<SoftwareAssociationDefinitions>
+        optSoftwareAssociationDefinitions = nullptr;
+
+    sdbusplus::async::context& ctx;
+
+    friend SoftwareUpdate;
+};
diff --git a/common/include/software_manager.hpp b/common/include/software_manager.hpp
new file mode 100644
index 0000000..fd3a467
--- /dev/null
+++ b/common/include/software_manager.hpp
@@ -0,0 +1,127 @@
+#pragma once
+
+#include "device.hpp"
+#include "sdbusplus/async/proxy.hpp"
+
+#include <boost/asio/steady_timer.hpp>
+#include <phosphor-logging/lg2.hpp>
+#include <sdbusplus/asio/connection.hpp>
+#include <sdbusplus/asio/object_server.hpp>
+#include <sdbusplus/async/context.hpp>
+#include <sdbusplus/timer.hpp>
+
+#include <string>
+
+// This is the base class for the code updater
+// Every code updater can inherit from this
+class SoftwareManager
+{
+  public:
+    SoftwareManager(sdbusplus::async::context& ctx,
+                    const std::string& busNameSuffix, bool dryRun);
+
+    // @param state   desired powerstate (true means on)
+    // @returns       true on success
+    sdbusplus::async::task<bool> setHostPowerstate(bool state);
+
+    // @returns       true when powered
+    // @returns       std::nullopt on failure to query power state
+    sdbusplus::async::task<std::optional<bool>> getHostPowerstate();
+
+    // Fetches initial configuration from dbus.
+    // This should be called once by a code updater at startup.
+    // It will call 'getInitialConfigurationSingleDevice' for each device
+    // configured
+    // @param configurationInterfaces    the dbus interfaces from which to fetch
+    // configuration
+    sdbusplus::async::task<> getInitialConfiguration(
+        const std::vector<std::string>& configurationInterfaces);
+
+    // request the bus name on dbus after all configuration has been parsed
+    // and the devices have been initialized
+    // @returns        the name on dbus
+    std::string setupBusName();
+
+    // set of devices found through configuration and probing
+    std::set<std::unique_ptr<Device>> devices;
+
+  protected:
+    const bool dryRun;
+
+    template <typename T>
+    sdbusplus::async::task<std::optional<T>>
+        dbusGetRequiredConfigurationProperty(
+            const std::string& service, const std::string& path,
+            const std::string& property, DeviceConfig& config)
+    {
+        std::string configIface =
+            "xyz.openbmc_project.Configuration." + config.configType;
+        std::optional<T> res = co_await dbusGetRequiredProperty<T>(
+            service, path, configIface, property);
+        co_return res;
+    }
+
+    // this variant also logs the error
+    template <typename T>
+    sdbusplus::async::task<std::optional<T>> dbusGetRequiredProperty(
+        const std::string& service, const std::string& path,
+        const std::string& intf, const std::string& property)
+    {
+        std::optional<T> opt =
+            co_await dbusGetProperty<T>(service, path, intf, property);
+        if (!opt.has_value())
+        {
+            lg2::error(
+                "[config] missing property {PROPERTY} on path {PATH}, interface {INTF}",
+                "PROPERTY", property, "PATH", path, "INTF", intf);
+        }
+        co_return opt;
+    }
+
+    template <typename T>
+    sdbusplus::async::task<std::optional<T>>
+        dbusGetProperty(const std::string& service, const std::string& path,
+                        const std::string& intf, const std::string& property)
+    {
+        auto client =
+            sdbusplus::async::proxy().service(service).path(path).interface(
+                "org.freedesktop.DBus.Properties");
+
+        try
+        {
+            std::variant<T> result = co_await client.call<std::variant<T>>(
+                ctx, "Get", intf, property);
+
+            T res = std::get<T>(result);
+            co_return res;
+        }
+        catch (std::exception& e)
+        {
+            co_return std::nullopt;
+        }
+    }
+
+    // This function receives a dbus name and object path for a single device,
+    // which was configured.
+    // The component code updater overrides this function and may create a
+    // device instance internally, or reject the configuration as invalid.
+    // @param service       The dbus name where our configuration is
+    // @param config        The common configuration properties which are shared
+    // by all devices.
+    //                      Also includes the object path to fetch other
+    //                      configuration properties.
+    virtual sdbusplus::async::task<> getInitialConfigurationSingleDevice(
+        const std::string& service, const std::string& path,
+        DeviceConfig& config) = 0;
+
+    sdbusplus::async::context& ctx;
+
+  private:
+    // this is appended to the common prefix to construct the dbus name
+    std::string busNameSuffix;
+
+    sdbusplus::server::manager_t manager;
+
+    friend Software;
+    friend Device;
+};
diff --git a/common/include/software_update.hpp b/common/include/software_update.hpp
new file mode 100644
index 0000000..9080005
--- /dev/null
+++ b/common/include/software_update.hpp
@@ -0,0 +1,30 @@
+#pragma once
+
+#include <sdbusplus/async/context.hpp>
+#include <xyz/openbmc_project/Software/Update/aserver.hpp>
+
+class Software;
+
+using RequestedApplyTimes = sdbusplus::common::xyz::openbmc_project::software::
+    ApplyTime::RequestedApplyTimes;
+
+class SoftwareUpdate :
+    public sdbusplus::aserver::xyz::openbmc_project::software::Update<
+        SoftwareUpdate>
+{
+  public:
+    SoftwareUpdate(sdbusplus::async::context& ctx, const char* path,
+                   Software& software,
+                   const std::set<RequestedApplyTimes>& allowedApplyTimes);
+
+    Software& software;
+
+    auto method_call(start_update_t su, auto image, auto applyTime)
+        -> sdbusplus::async::task<start_update_t::return_type>;
+
+    auto set_property(allowed_apply_times_t aat, auto value) -> bool;
+    auto get_property(allowed_apply_times_t aat) const;
+
+  private:
+    const std::set<RequestedApplyTimes> allowedApplyTimes;
+};
diff --git a/common/meson.build b/common/meson.build
index 766fbe0..b51dc7e 100644
--- a/common/meson.build
+++ b/common/meson.build
@@ -1,2 +1,18 @@
 
 subdir('pldm')
+
+software_common_lib = static_library('fwupdate',
+  'src/software_manager.cpp',
+  'src/device.cpp',
+  'src/device_config.cpp',
+  'src/software.cpp',
+  'src/software_update.cpp',
+  include_directories: ['.','include/', common_include],
+  dependencies: [
+    pdi_dep,
+    phosphor_logging_dep,
+    sdbusplus_dep,
+    libpldm_dep,
+  ],
+  install: false,
+)
diff --git a/common/src/device.cpp b/common/src/device.cpp
new file mode 100644
index 0000000..49c9b1e
--- /dev/null
+++ b/common/src/device.cpp
@@ -0,0 +1,182 @@
+#include "device.hpp"
+
+#include "common/pldm/pldm_package_util.hpp"
+#include "software.hpp"
+#include "software_manager.hpp"
+
+#include <phosphor-logging/lg2.hpp>
+#include <sdbusplus/asio/object_server.hpp>
+#include <sdbusplus/async/context.hpp>
+#include <sdbusplus/bus.hpp>
+#include <xyz/openbmc_project/Association/Definitions/server.hpp>
+#include <xyz/openbmc_project/State/Host/client.hpp>
+
+#include <utility>
+
+const auto applyTimeImmediate = sdbusplus::common::xyz::openbmc_project::
+    software::ApplyTime::RequestedApplyTimes::Immediate;
+
+Device::Device(sdbusplus::async::context& ctx, bool isDryRun,
+               const DeviceConfig& config, SoftwareManager* parent) :
+    config(config), parent(parent), dryRun(isDryRun), ctx(ctx)
+{}
+
+// NOLINTBEGIN
+sdbusplus::async::task<bool> Device::startUpdateAsync(
+    sdbusplus::message::unix_fd image, RequestedApplyTimes applyTime,
+    std::unique_ptr<Software> softwareUpdate)
+// NOLINTEND
+{
+    lg2::debug("starting the async update with memfd {FD}", "FD", image.fd);
+
+    size_t pldm_pkg_size;
+
+    void* pldm_pkg = pldm_package_util::mmapImagePackage(image, &pldm_pkg_size);
+
+    if (pldm_pkg == NULL)
+    {
+        co_return false;
+    }
+
+    lg2::debug("[Device] mmapped the pldm update package");
+
+    std::shared_ptr<PackageParser> pp = pldm_package_util::parsePLDMPackage(
+        static_cast<uint8_t*>(pldm_pkg), pldm_pkg_size);
+
+    if (pp == nullptr)
+    {
+        lg2::error("could not parse PLDM package");
+        co_return false;
+    }
+
+    const bool success = co_await continueUpdateWithMappedPackage(
+        pldm_pkg, pp, applyTime, softwareUpdate);
+
+    if (success)
+    {
+        lg2::info("deleting old sw version {SWID}", "SWID",
+                  this->softwareCurrent->swid);
+
+        this->softwareCurrent = std::move(softwareUpdate);
+
+        lg2::info("new current sw version: {SWID}", "SWID",
+                  this->softwareCurrent->swid);
+    }
+    else
+    {
+        lg2::info("update failed, deleting sw update version {SWID}", "SWID",
+                  softwareUpdate->swid);
+    }
+
+    softwareUpdate = nullptr;
+
+    if (munmap(pldm_pkg, pldm_pkg_size) != 0)
+    {
+        lg2::error("[Device] failed to munmap the pldm package");
+    }
+
+    if (close(image.fd) != 0)
+    {
+        lg2::error("[Device] failed to close file descriptor {FD}", "FD",
+                   image.fd);
+    }
+
+    co_return success;
+}
+
+std::string Device::getEMConfigType() const
+{
+    return this->config.configType;
+}
+
+void Device::resetDevice()
+{
+    lg2::info("[Device] default implementation for reset device (nop)");
+}
+
+std::set<RequestedApplyTimes> Device::allowedApplyTimes()
+{
+    return {RequestedApplyTimes::Immediate, RequestedApplyTimes::OnReset};
+}
+
+// NOLINTBEGIN
+sdbusplus::async::task<bool> Device::continueUpdateWithMappedPackage(
+    void* pldm_pkg, const std::shared_ptr<PackageParser>& packageParser,
+    sdbusplus::common::xyz::openbmc_project::software::ApplyTime::
+        RequestedApplyTimes applyTime,
+    const std::unique_ptr<Software>& softwareUpdate)
+// NOLINTEND
+{
+    int status = 0;
+
+    // extract the component image for the specific device
+    size_t matchingComponentImageSize;
+    uint32_t matchingComponentOffset;
+    status = pldm_package_util::extractMatchingComponentImage(
+        packageParser, config.compatibleHardware, config.vendorIANA,
+        &matchingComponentOffset, &matchingComponentImageSize);
+
+    if (status != 0)
+    {
+        lg2::error("could not extract matching component image");
+
+        softwareUpdate->setActivation(
+            ActivationInterface::Activations::Invalid);
+
+        co_return false;
+    }
+
+    const uint8_t* matchingComponentImage =
+        static_cast<uint8_t*>(pldm_pkg) + matchingComponentOffset;
+
+    softwareUpdate->setActivation(ActivationInterface::Activations::Ready);
+
+    softwareUpdate->setVersion(packageParser->pkgVersion);
+
+    std::string objPath = softwareUpdate->getObjectPath();
+
+    softwareUpdate->optSoftwareActivationProgress =
+        std::make_unique<SoftwareActivationProgress>(ctx, objPath.c_str());
+
+    softwareUpdate->setActivationBlocksTransition(true);
+
+    softwareUpdate->setActivation(ActivationInterface::Activations::Activating);
+
+    bool success = co_await updateDevice(
+        matchingComponentImage, matchingComponentImageSize,
+        softwareUpdate->optSoftwareActivationProgress);
+
+    if (success)
+    {
+        softwareUpdate->setActivation(ActivationInterface::Activations::Active);
+    }
+
+    softwareUpdate->setActivationBlocksTransition(false);
+
+    softwareUpdate->optSoftwareActivationProgress = nullptr;
+
+    if (!success)
+    {
+        // do not apply the update, it has failed.
+        // We can delete the new software version.
+
+        co_return false;
+    }
+
+    if (applyTime == applyTimeImmediate)
+    {
+        this->resetDevice();
+
+        co_await softwareUpdate->setAssociationDefinitionsRunningActivating(
+            true, false);
+
+        softwareUpdate->enableUpdate(this->allowedApplyTimes());
+    }
+    else
+    {
+        co_await softwareUpdate->setAssociationDefinitionsRunningActivating(
+            false, true);
+    }
+
+    co_return true;
+}
diff --git a/common/src/device_config.cpp b/common/src/device_config.cpp
new file mode 100644
index 0000000..e0e7f50
--- /dev/null
+++ b/common/src/device_config.cpp
@@ -0,0 +1,28 @@
+
+#include "common/include/device_config.hpp"
+
+#include <regex>
+#include <stdexcept>
+
+DeviceConfig::DeviceConfig(uint32_t vendorIANA, const std::string& compatible,
+                           const std::string& configType,
+                           const std::string& name) :
+    vendorIANA(vendorIANA), compatibleHardware(compatible), configName(name),
+    configType(configType)
+{
+    std::regex reCompatible("([a-zA-Z0-9])+(\\.([a-zA-Z0-9])+)+");
+    std::cmatch m;
+
+    if (name.empty())
+    {
+        throw std::invalid_argument(
+            "invalid EM config 'Name' string: '" + name + "'");
+    }
+
+    // check compatible string with regex
+    if (!std::regex_match(compatible.c_str(), m, reCompatible))
+    {
+        throw std::invalid_argument(
+            "invalid compatible string: '" + compatible + "'");
+    }
+}
diff --git a/common/src/software.cpp b/common/src/software.cpp
new file mode 100644
index 0000000..473e377
--- /dev/null
+++ b/common/src/software.cpp
@@ -0,0 +1,196 @@
+#include "software.hpp"
+
+#include "device.hpp"
+#include "software_update.hpp"
+
+#include <phosphor-logging/lg2.hpp>
+#include <sdbusplus/async/context.hpp>
+#include <xyz/openbmc_project/Association/Definitions/server.hpp>
+#include <xyz/openbmc_project/Software/Activation/aserver.hpp>
+#include <xyz/openbmc_project/Software/Update/aserver.hpp>
+#include <xyz/openbmc_project/State/Host/client.hpp>
+
+SoftwareActivationProgress::SoftwareActivationProgress(
+    sdbusplus::async::context& ctx, const char* objPath) :
+    ActivationProgress(ctx, objPath)
+{
+    // This prevents "Conditional jump or move depends on uninitialised
+    // value(s)"
+    // when properties are updated for the first time
+    progress_ = 0;
+}
+
+Software::Software(sdbusplus::async::context& ctx, const std::string& swid,
+                   Device& parent) :
+    sdbusplus::aserver::xyz::openbmc_project::software::Activation<Software>(
+        ctx, Software::getObjPathFromSwid(swid).c_str()),
+    swid(swid), parent(parent), ctx(ctx)
+{
+    // initialize the members of our base class to prevent
+    // "Conditional jump or move depends on uninitialised value(s)"
+    activation_ = Activations::NotReady;
+    requested_activation_ = RequestedActivations::None;
+
+    std::string objPath = Software::getObjPathFromSwid(swid);
+
+    if (!objPath.starts_with("/"))
+    {
+        throw std::invalid_argument(objPath + " is not an object path");
+    }
+
+    lg2::debug("{SWID}: created dbus interfaces on path {OBJPATH}", "SWID",
+               swid, "OBJPATH", objPath);
+};
+
+std::string Software::getObjPathFromSwid(const std::string& swid)
+{
+    std::string basepath = "/xyz/openbmc_project/software/";
+    return basepath + swid;
+}
+
+static long int getRandomId()
+{
+    struct timespec ts;
+    clock_gettime(CLOCK_REALTIME, &ts);
+    unsigned int seed = ts.tv_nsec ^ getpid();
+    srandom(seed);
+    return random() % 10000;
+}
+
+std::string Software::getRandomSoftwareId(Device& parent)
+{
+    // Swid = <DeviceX>_<RandomId>
+    // For same type devices, extend the Dbus path to specify device
+    // instance, for example,
+    // /xyz/openbmc_project/Software/<deviceX>_<InstanceNum>_<SwId>
+
+    // The problem here is that InstanceNum needs to always stay the same,
+    // so that the device can be identified in the redfish fw inventory.
+
+    // Since 'Name' property is already provided in EM config, we can insert
+    // that in place of 'InstanceNum'.
+
+    const std::string configType = parent.getEMConfigType();
+
+    // 'Name' property in EM config
+    const std::string nameEM = parent.config.configName;
+
+    return std::format("{}_{}_{}", configType, nameEM, getRandomId());
+}
+
+Device& Software::getParentDevice()
+{
+    return this->parent;
+}
+
+// NOLINTBEGIN
+sdbusplus::async::task<> Software::setAssociationDefinitionsRunningActivating(
+    bool isRunning, bool isActivating)
+// NOLINTEND
+{
+    lg2::debug("{SWID}: setting association definitions", "SWID", this->swid);
+
+    std::string endpoint = co_await parent.getInventoryItemObjectPath();
+
+    if (!this->optSoftwareAssociationDefinitions)
+    {
+        std::string path = this->getObjectPath();
+        this->optSoftwareAssociationDefinitions =
+            std::make_unique<SoftwareAssociationDefinitions>(ctx, path.c_str());
+    }
+
+    std::string forward;
+    std::string reverse;
+    std::vector<std::tuple<std::string, std::string, std::string>> assocs;
+
+    if (isRunning)
+    {
+        lg2::debug("{SWID}: creating 'running' association to {OBJPATH}",
+                   "SWID", this->swid, "OBJPATH", endpoint);
+        forward = "running";
+        reverse = "ran_on";
+        std::tuple<std::string, std::string, std::string> assocRunning = {
+            forward, reverse, endpoint};
+        assocs.push_back(assocRunning);
+    }
+
+    if (isActivating)
+    {
+        lg2::debug("{SWID}: creating 'activating' association to {OBJPATH}",
+                   "SWID", this->swid, "OBJPATH", endpoint);
+        forward = "activating";
+        reverse = "activated_on";
+        std::tuple<std::string, std::string, std::string> assocActivating = {
+            forward, reverse, endpoint};
+        assocs.push_back(assocActivating);
+    }
+
+    this->optSoftwareAssociationDefinitions->associations(assocs);
+
+    co_return;
+}
+
+void Software::setVersion(const std::string& versionStr)
+{
+    lg2::debug("{SWID}: set version {VERSION}", "SWID", this->swid, "VERSION",
+               versionStr);
+
+    if (this->optSoftwareVersion)
+    {
+        lg2::error("{SWID}: version was already set", "SWID", this->swid);
+        return;
+    }
+
+    std::string path = this->getObjectPath();
+    this->optSoftwareVersion =
+        std::make_unique<SoftwareVersion>(ctx, path.c_str());
+    this->optSoftwareVersion->version(versionStr);
+}
+
+void Software::setActivationBlocksTransition(bool enabled)
+{
+    if (!enabled)
+    {
+        this->optActivationBlocksTransition = nullptr;
+        return;
+    }
+
+    std::string path = this->getObjectPath();
+    this->optActivationBlocksTransition =
+        std::make_unique<sdbusplus::aserver::xyz::openbmc_project::software::
+                             ActivationBlocksTransition<Software>>(
+            this->ctx, path.c_str());
+}
+
+void Software::setActivation(
+    sdbusplus::common::xyz::openbmc_project::software::Activation::Activations
+        act)
+{
+    this->activation(act);
+}
+
+sdbusplus::message::object_path Software::getObjectPath() const
+{
+    std::string objPathStr = Software::getObjPathFromSwid(swid);
+    return sdbusplus::message::object_path(objPathStr.c_str());
+}
+
+void Software::enableUpdate(
+    const std::set<RequestedApplyTimes>& allowedApplyTimes)
+{
+    std::string objPath = getObjectPath();
+
+    if (this->optSoftwareUpdate != nullptr)
+    {
+        lg2::error("[Software] update of {OBJPATH} has already been enabled",
+                   "OBJPATH", objPath);
+        return;
+    }
+
+    lg2::info(
+        "[Software] enabling update of {OBJPATH} (adding the update interface)",
+        "OBJPATH", objPath);
+
+    optSoftwareUpdate = std::make_unique<SoftwareUpdate>(
+        this->ctx, objPath.c_str(), *this, allowedApplyTimes);
+}
diff --git a/common/src/software_manager.cpp b/common/src/software_manager.cpp
new file mode 100644
index 0000000..487f63e
--- /dev/null
+++ b/common/src/software_manager.cpp
@@ -0,0 +1,194 @@
+#include "software_manager.hpp"
+
+#include "sdbusplus/async/timer.hpp"
+
+#include <phosphor-logging/lg2.hpp>
+#include <sdbusplus/asio/object_server.hpp>
+#include <sdbusplus/async.hpp>
+#include <sdbusplus/async/context.hpp>
+#include <sdbusplus/bus.hpp>
+#include <xyz/openbmc_project/Association/Definitions/server.hpp>
+#include <xyz/openbmc_project/ObjectMapper/client.hpp>
+#include <xyz/openbmc_project/State/Host/client.hpp>
+
+SoftwareManager::SoftwareManager(sdbusplus::async::context& ctx,
+                                 const std::string& busNameSuffix,
+                                 bool isDryRun) :
+    dryRun(isDryRun), ctx(ctx), busNameSuffix(busNameSuffix), manager(ctx, "/")
+{
+    lg2::debug("initialized SoftwareManager");
+}
+
+std::string SoftwareManager::setupBusName()
+{
+    const std::string serviceNameFull =
+        "xyz.openbmc_project.Software." + this->busNameSuffix;
+
+    lg2::debug("requesting dbus name {BUSNAME}", "BUSNAME", serviceNameFull);
+
+    ctx.get_bus().request_name(serviceNameFull.c_str());
+
+    return serviceNameFull;
+}
+
+// NOLINTBEGIN
+sdbusplus::async::task<bool> SoftwareManager::setHostPowerstate(bool state)
+// NOLINTEND
+{
+    auto proxy = sdbusplus::async::proxy()
+                     .service("xyz.openbmc_project.State.Host")
+                     .path("/xyz/openbmc_project/state/host0")
+                     .interface("xyz.openbmc_project.State.Host");
+
+    lg2::info("[PWR] changing host power state to {STATE}", "STATE",
+              (state) ? "ON" : "OFF");
+
+    std::string voff = "xyz.openbmc_project.State.Host.Transition.Off";
+    std::string von = "xyz.openbmc_project.State.Host.Transition.On";
+    std::string targetState;
+    if (state)
+    {
+        co_await proxy.set_property(ctx, "RequestedHostTransition", von);
+        targetState = "xyz.openbmc_project.State.Host.HostState.Running";
+    }
+    else
+    {
+        co_await proxy.set_property(ctx, "RequestedHostTransition", voff);
+        targetState = "xyz.openbmc_project.State.Host.HostState.Off";
+    }
+
+    lg2::debug("[PWR] requested host transition to {STATE}", "STATE",
+               targetState);
+
+    lg2::debug("[PWR] async sleep to wait for state transition");
+    co_await sdbusplus::async::sleep_for(ctx, std::chrono::seconds(10));
+
+    auto actualOpt = co_await getHostPowerstate();
+
+    if (actualOpt == std::nullopt)
+    {
+        co_return false;
+    }
+
+    const bool actual = actualOpt.value();
+
+    if (actual == state)
+    {
+        lg2::debug("[PWR] successfully achieved state {STATE}", "STATE",
+                   targetState);
+        co_return true;
+    }
+    else
+    {
+        lg2::debug("[PWR] failed to achieve state {STATE}", "STATE",
+                   targetState);
+        co_return false;
+    }
+}
+
+// NOLINTBEGIN
+sdbusplus::async::task<std::optional<bool>> SoftwareManager::getHostPowerstate()
+// NOLINTEND
+{
+    auto proxy = sdbusplus::async::proxy()
+                     .service("xyz.openbmc_project.State.Host")
+                     .path("/xyz/openbmc_project/state/host0")
+                     .interface("xyz.openbmc_project.State.Host");
+
+    std::string stateOn = "xyz.openbmc_project.State.Host.HostState.Running";
+    std::string stateOff = "xyz.openbmc_project.State.Host.HostState.Off";
+
+    std::string res =
+        co_await proxy.get_property<std::string>(ctx, "CurrentHostState");
+
+    if (res == stateOn)
+    {
+        co_return true;
+    }
+    else if (res == stateOff)
+    {
+        co_return false;
+    }
+
+    lg2::error("[PWR] unexpected power state: {STATE}", "STATE", res);
+
+    co_return true;
+}
+
+// NOLINTBEGIN
+sdbusplus::async::task<> SoftwareManager::getInitialConfiguration(
+    const std::vector<std::string>& configurationInterfaces)
+// NOLINTEND
+{
+    auto client = sdbusplus::client::xyz::openbmc_project::ObjectMapper<>(ctx)
+                      .service("xyz.openbmc_project.ObjectMapper")
+                      .path("/xyz/openbmc_project/object_mapper");
+
+    auto res =
+        co_await client.get_sub_tree("/xyz/openbmc_project/inventory", 0, {});
+
+    for (auto& iface : configurationInterfaces)
+    {
+        lg2::debug("[config] looking for dbus interface {INTF}", "INTF", iface);
+    }
+
+    for (auto& [path, v] : res)
+    {
+        for (auto& [service, interfaceNames] : v)
+        {
+            std::string interfaceFound;
+
+            for (std::string& interfaceName : interfaceNames)
+            {
+                for (auto& iface : configurationInterfaces)
+                {
+                    if (interfaceName == iface)
+                    {
+                        interfaceFound = interfaceName;
+                    }
+                }
+            }
+
+            if (interfaceFound.empty())
+            {
+                continue;
+            }
+
+            lg2::debug(
+                "[config] found configuration interface at {SERVICE}, {OBJPATH}",
+                "SERVICE", service, "OBJPATH", path);
+
+            std::optional<uint64_t> optVendorIANA =
+                co_await SoftwareManager::dbusGetRequiredProperty<uint64_t>(
+                    service, path, interfaceFound, "VendorIANA");
+
+            std::optional<std::string> optCompatible =
+                co_await SoftwareManager::dbusGetRequiredProperty<std::string>(
+                    service, path, interfaceFound, "Compatible");
+
+            std::optional<std::string> optEMConfigType =
+                co_await SoftwareManager::dbusGetRequiredProperty<std::string>(
+                    service, path, interfaceFound, "Type");
+
+            std::optional<std::string> optEMConfigName =
+                co_await SoftwareManager::dbusGetRequiredProperty<std::string>(
+                    service, path, interfaceFound, "Name");
+
+            if (!optVendorIANA.has_value() || !optCompatible.has_value() ||
+                !optEMConfigType.has_value() || !optEMConfigName.has_value())
+            {
+                continue;
+            }
+
+            DeviceConfig config(optVendorIANA.value(), optCompatible.value(),
+                                optEMConfigType.value(),
+                                optEMConfigName.value());
+
+            co_await getInitialConfigurationSingleDevice(service, path, config);
+        }
+    }
+
+    lg2::debug("[config] done with initial configuration");
+
+    setupBusName();
+}
diff --git a/common/src/software_update.cpp b/common/src/software_update.cpp
new file mode 100644
index 0000000..33bf03b
--- /dev/null
+++ b/common/src/software_update.cpp
@@ -0,0 +1,85 @@
+#include "software_update.hpp"
+
+#include "device.hpp"
+#include "software.hpp"
+
+#include <phosphor-logging/lg2.hpp>
+#include <sdbusplus/async/context.hpp>
+#include <xyz/openbmc_project/Software/Update/aserver.hpp>
+
+SoftwareUpdate::SoftwareUpdate(
+    sdbusplus::async::context& ctx, const char* path, Software& software,
+    const std::set<RequestedApplyTimes>& allowedApplyTimes) :
+    sdbusplus::aserver::xyz::openbmc_project::software::Update<SoftwareUpdate>(
+        ctx, path),
+    software(software), allowedApplyTimes(allowedApplyTimes)
+{}
+
+auto SoftwareUpdate::method_call(start_update_t /*unused*/, auto image,
+                                 auto applyTime)
+    -> sdbusplus::async::task<start_update_t::return_type>
+{
+    lg2::info("requesting Device update");
+
+    // check if the apply time is allowed by our device
+
+    if (!this->allowedApplyTimes.contains(applyTime))
+    {
+        lg2::error(
+            "the selected apply time {APPLYTIME} is not allowed by the device",
+            "APPLYTIME", applyTime);
+        co_return this->software.getObjectPath();
+    }
+
+    lg2::info("started asynchronous update with fd {FD}", "FD", image.fd);
+
+    Device& device = this->software.getParentDevice();
+
+    int imageDup = dup(image.fd);
+
+    if (imageDup < 0)
+    {
+        lg2::error("ERROR calling dup on fd: {ERR}", "ERR", strerror(errno));
+        co_return this->software.getObjectPath();
+    }
+
+    lg2::debug("starting async update with FD: {FD}\n", "FD", imageDup);
+
+    const std::string newSwid = Software::getRandomSoftwareId(device);
+
+    // Swid = <DeviceX>_<RandomId>
+    // This new swid will then be used for the object path for the new image.
+    std::unique_ptr<Software> softwareUpdate =
+        std::make_unique<Software>(ctx, newSwid, device);
+
+    softwareUpdate->setActivation(ActivationInterface::Activations::NotReady);
+
+    // NOLINTBEGIN
+    ctx.spawn(
+        [](Device& device, int imageDup, RequestedApplyTimes applyTime,
+           std::unique_ptr<Software> swupdate) -> sdbusplus::async::task<> {
+            co_await device.startUpdateAsync(imageDup, applyTime,
+                                             std::move(swupdate));
+            co_return;
+        }(device, imageDup, applyTime, std::move(softwareUpdate)));
+    // NOLINTEND
+
+    // We need the object path for the new software here.
+    // It must be the same as constructed during the update process.
+    // This is so that bmcweb and redfish clients can keep track of the update
+    // process.
+    co_return Software::getObjPathFromSwid(newSwid);
+}
+
+auto SoftwareUpdate::set_property(allowed_apply_times_t /*unused*/,
+                                  auto /*unused*/) -> bool
+{
+    // we do not implement this since the allowed apply times are
+    // defined by the device type and cannot be changed via dbus.
+    return false;
+}
+
+auto SoftwareUpdate::get_property(allowed_apply_times_t /*unused*/) const
+{
+    return this->allowedApplyTimes;
+}