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/src/device.cpp b/common/src/device.cpp
new file mode 100644
index 0000000..471697c
--- /dev/null
+++ b/common/src/device.cpp
@@ -0,0 +1,213 @@
+#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>
+
+PHOSPHOR_LOG2_USING;
+
+using namespace phosphor::software::device;
+
+const auto applyTimeImmediate = sdbusplus::common::xyz::openbmc_project::
+    software::ApplyTime::RequestedApplyTimes::Immediate;
+
+const auto ActivationInvalid = ActivationInterface::Activations::Invalid;
+const auto ActivationFailed = ActivationInterface::Activations::Failed;
+
+Device::Device(sdbusplus::async::context& ctx, const SoftwareConfig& config,
+               manager::SoftwareManager* parent,
+               std::set<RequestedApplyTimes> allowedApplyTimes =
+                   {RequestedApplyTimes::Immediate,
+                    RequestedApplyTimes::OnReset}) :
+    allowedApplyTimes(std::move(allowedApplyTimes)), config(config),
+    parent(parent), ctx(ctx)
+{}
+
+// NOLINTBEGIN(readability-static-accessed-through-instance)
+sdbusplus::async::task<bool> Device::getImageInfo(
+    std::unique_ptr<void, std::function<void(void*)>>& pldmPackage,
+    size_t pldmPackageSize, uint8_t** matchingComponentImage,
+    size_t* componentImageSize, std::string& componentVersion)
+
+// NOLINTEND(readability-static-accessed-through-instance)
+{
+    std::shared_ptr<PackageParser> packageParser =
+        pldm_package_util::parsePLDMPackage(
+            static_cast<uint8_t*>(pldmPackage.get()), pldmPackageSize);
+
+    if (packageParser == nullptr)
+    {
+        error("could not parse PLDM package");
+        co_return false;
+    }
+
+    uint32_t componentOffset = 0;
+    const int status = pldm_package_util::extractMatchingComponentImage(
+        packageParser, config.compatibleHardware, config.vendorIANA,
+        &componentOffset, componentImageSize, componentVersion);
+
+    if (status != 0)
+    {
+        error("could not extract matching component image");
+        co_return false;
+    }
+
+    *matchingComponentImage =
+        static_cast<uint8_t*>(pldmPackage.get()) + componentOffset;
+
+    co_return true;
+}
+
+// NOLINTBEGIN(readability-static-accessed-through-instance)
+sdbusplus::async::task<bool> Device::startUpdateAsync(
+    sdbusplus::message::unix_fd image, RequestedApplyTimes applyTime,
+    std::unique_ptr<Software> softwarePendingIn)
+// NOLINTEND(readability-static-accessed-through-instance)
+{
+    debug("starting the async update with memfd {FD}", "FD", image.fd);
+
+    size_t pldm_pkg_size = 0;
+    auto pldm_pkg = pldm_package_util::mmapImagePackage(image, &pldm_pkg_size);
+
+    if (pldm_pkg == nullptr)
+    {
+        softwarePendingIn->setActivation(ActivationInvalid);
+        co_return false;
+    }
+
+    uint8_t* componentImage;
+    size_t componentImageSize = 0;
+    std::string componentVersion;
+
+    if (!co_await getImageInfo(pldm_pkg, pldm_pkg_size, &componentImage,
+                               &componentImageSize, componentVersion))
+    {
+        error("could not extract matching component image");
+        softwarePendingIn->setActivation(ActivationInvalid);
+        co_return false;
+    }
+
+    const bool success = co_await continueUpdateWithMappedPackage(
+        componentImage, componentImageSize, componentVersion, applyTime,
+        softwarePendingIn);
+
+    if (success)
+    {
+        if (applyTime == RequestedApplyTimes::Immediate)
+        {
+            softwareCurrent = std::move(softwarePendingIn);
+
+            // In case an immediate update is triggered after an update for
+            // onReset.
+            softwarePending = nullptr;
+
+            debug("Successfully updated to software version {SWID}", "SWID",
+                  softwareCurrent->swid);
+        }
+        else if (applyTime == RequestedApplyTimes::OnReset)
+        {
+            softwarePending = std::move(softwarePendingIn);
+        }
+    }
+    else
+    {
+        softwarePendingIn->setActivation(ActivationFailed);
+        error("Failed to update the software for {SWID}", "SWID",
+              softwareCurrent->swid);
+    }
+
+    co_return success;
+}
+
+std::string Device::getEMConfigType() const
+{
+    return config.configType;
+}
+
+// NOLINTBEGIN(readability-static-accessed-through-instance)
+sdbusplus::async::task<bool> Device::resetDevice()
+// NOLINTEND(readability-static-accessed-through-instance)
+{
+    debug("Default implementation for device reset");
+
+    co_return true;
+}
+
+bool Device::setUpdateProgress(uint8_t progress) const
+{
+    if (!softwarePending || !softwarePending->softwareActivationProgress)
+    {
+        return false;
+    }
+
+    softwarePending->softwareActivationProgress->setProgress(progress);
+
+    return true;
+}
+
+// NOLINTBEGIN(readability-static-accessed-through-instance)
+sdbusplus::async::task<bool> Device::continueUpdateWithMappedPackage(
+    const uint8_t* matchingComponentImage, size_t componentImageSize,
+    const std::string& componentVersion, RequestedApplyTimes applyTime,
+    const std::unique_ptr<Software>& softwarePendingIn)
+// NOLINTEND(readability-static-accessed-through-instance)
+{
+    softwarePendingIn->setActivation(ActivationInterface::Activations::Ready);
+
+    softwarePendingIn->setVersion(componentVersion);
+
+    std::string objPath = softwarePendingIn->objectPath;
+
+    softwarePendingIn->softwareActivationProgress =
+        std::make_unique<SoftwareActivationProgress>(ctx, objPath.c_str());
+
+    softwarePendingIn->setActivationBlocksTransition(true);
+
+    softwarePendingIn->setActivation(
+        ActivationInterface::Activations::Activating);
+
+    bool success =
+        co_await updateDevice(matchingComponentImage, componentImageSize);
+
+    if (success)
+    {
+        softwarePendingIn->setActivation(
+            ActivationInterface::Activations::Active);
+    }
+
+    softwarePendingIn->setActivationBlocksTransition(false);
+
+    softwarePendingIn->softwareActivationProgress = nullptr;
+
+    if (!success)
+    {
+        // do not apply the update, it has failed.
+        // We can delete the new software version.
+
+        co_return false;
+    }
+
+    if (applyTime == applyTimeImmediate)
+    {
+        co_await resetDevice();
+
+        co_await softwarePendingIn->createInventoryAssociations(true);
+
+        softwarePendingIn->enableUpdate(allowedApplyTimes);
+    }
+    else
+    {
+        co_await softwarePendingIn->createInventoryAssociations(false);
+    }
+
+    co_return true;
+}
diff --git a/common/src/software.cpp b/common/src/software.cpp
new file mode 100644
index 0000000..03e9199
--- /dev/null
+++ b/common/src/software.cpp
@@ -0,0 +1,174 @@
+#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>
+
+PHOSPHOR_LOG2_USING;
+
+using namespace phosphor::software;
+using namespace phosphor::software::device;
+using namespace phosphor::software::config;
+using namespace phosphor::software::update;
+
+const static std::string baseObjPathSoftware = "/xyz/openbmc_project/software/";
+
+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;
+}
+
+void SoftwareActivationProgress::setProgress(int progressArg)
+{
+    progress(progressArg);
+}
+
+Software::Software(sdbusplus::async::context& ctx, Device& parent) :
+    Software(ctx, parent, getRandomSoftwareId(parent))
+{}
+
+Software::Software(sdbusplus::async::context& ctx, Device& parent,
+                   const std::string& swid) :
+    SoftwareActivation(ctx, (baseObjPathSoftware + swid).c_str()),
+    objectPath(baseObjPathSoftware + swid), parentDevice(parent), swid(swid),
+    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 = baseObjPathSoftware + swid;
+
+    debug("{SWID}: created dbus interfaces on path {OBJPATH}", "SWID", swid,
+          "OBJPATH", objPath);
+};
+
+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)
+{
+    return std::format("{}_{}", parent.config.configName, getRandomId());
+}
+
+// NOLINTBEGIN(readability-static-accessed-through-instance)
+sdbusplus::async::task<> Software::createInventoryAssociations(bool isRunning)
+// NOLINTEND(readability-static-accessed-through-instance)
+{
+    debug("{SWID}: setting association definitions", "SWID", swid);
+
+    std::string endpoint = "";
+
+    try
+    {
+        endpoint = co_await parentDevice.config.getInventoryItemObjectPath(ctx);
+    }
+    catch (std::exception& e)
+    {
+        error(e.what());
+    }
+
+    if (!associationDefinitions)
+    {
+        std::string path = objectPath;
+        associationDefinitions =
+            std::make_unique<SoftwareAssociationDefinitions>(ctx, path.c_str());
+    }
+
+    std::vector<std::tuple<std::string, std::string, std::string>> assocs;
+
+    if (endpoint.empty())
+    {
+        associationDefinitions->associations(assocs);
+        co_return;
+    }
+
+    if (isRunning)
+    {
+        debug("{SWID}: creating 'running' association to {OBJPATH}", "SWID",
+              swid, "OBJPATH", endpoint);
+        std::tuple<std::string, std::string, std::string> assocRunning = {
+            "running", "ran_on", endpoint};
+        assocs.push_back(assocRunning);
+    }
+    else
+    {
+        debug("{SWID}: creating 'activating' association to {OBJPATH}", "SWID",
+              swid, "OBJPATH", endpoint);
+        std::tuple<std::string, std::string, std::string> assocActivating = {
+            "activating", "activated_on", endpoint};
+        assocs.push_back(assocActivating);
+    }
+
+    associationDefinitions->associations(assocs);
+
+    co_return;
+}
+
+void Software::setVersion(const std::string& versionStr)
+{
+    debug("{SWID}: set version {VERSION}", "SWID", swid, "VERSION", versionStr);
+
+    if (version)
+    {
+        error("{SWID}: version was already set", "SWID", swid);
+        return;
+    }
+
+    version = std::make_unique<SoftwareVersion>(ctx, objectPath.str.c_str());
+    version->version(versionStr);
+}
+
+void Software::setActivationBlocksTransition(bool enabled)
+{
+    if (!enabled)
+    {
+        activationBlocksTransition = nullptr;
+        return;
+    }
+
+    std::string path = objectPath;
+    activationBlocksTransition =
+        std::make_unique<SoftwareActivationBlocksTransition>(ctx, path.c_str());
+}
+
+void Software::setActivation(SoftwareActivation::Activations act)
+{
+    activation(act);
+}
+
+void Software::enableUpdate(
+    const std::set<RequestedApplyTimes>& allowedApplyTimes)
+{
+    if (updateIntf != nullptr)
+    {
+        error("[Software] update of {OBJPATH} has already been enabled",
+              "OBJPATH", objectPath);
+        return;
+    }
+
+    debug(
+        "[Software] enabling update of {OBJPATH} (adding the update interface)",
+        "OBJPATH", objectPath);
+
+    updateIntf = std::make_unique<SoftwareUpdate>(ctx, objectPath.str.c_str(),
+                                                  *this, allowedApplyTimes);
+}
diff --git a/common/src/software_config.cpp b/common/src/software_config.cpp
new file mode 100644
index 0000000..8fb3162
--- /dev/null
+++ b/common/src/software_config.cpp
@@ -0,0 +1,70 @@
+#include "common/include/software_config.hpp"
+
+#include <phosphor-logging/lg2.hpp>
+#include <xyz/openbmc_project/ObjectMapper/client.hpp>
+
+#include <regex>
+#include <stdexcept>
+
+PHOSPHOR_LOG2_USING;
+
+using namespace phosphor::software::config;
+
+SoftwareConfig::SoftwareConfig(const std::string& objPath, uint32_t vendorIANA,
+                               const std::string& compatible,
+                               const std::string& configType,
+                               const std::string& name) :
+    objectPath(objPath), configName(name), configType(configType),
+    vendorIANA(vendorIANA), compatibleHardware(compatible)
+{
+    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 + "'");
+    }
+}
+
+// NOLINTBEGIN(readability-static-accessed-through-instance)
+sdbusplus::async::task<std::string> SoftwareConfig::getInventoryItemObjectPath(
+    sdbusplus::async::context& ctx)
+// NOLINTEND(readability-static-accessed-through-instance)
+{
+    std::vector<std::string> allInterfaces = {
+        "xyz.openbmc_project.Inventory.Item.Board",
+        "xyz.openbmc_project.Inventory.Item.Chassis",
+    };
+
+    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/system", 0, allInterfaces);
+
+    for (auto& [path, v] : res)
+    {
+        debug("inventory item at path {PATH}", "PATH", path);
+
+        // check if their path is a parent of our path
+        if (objectPath.starts_with(path))
+        {
+            debug("found associated inventory item for {NAME}: {PATH}", "NAME",
+                  configName, "PATH", path);
+            co_return path;
+        }
+    }
+
+    error("could not find associated inventory item for {NAME}", "NAME",
+          configName);
+
+    co_return "";
+}
diff --git a/common/src/software_manager.cpp b/common/src/software_manager.cpp
new file mode 100644
index 0000000..9c80e62
--- /dev/null
+++ b/common/src/software_manager.cpp
@@ -0,0 +1,136 @@
+#include "software_manager.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>
+
+#include <cstdint>
+
+PHOSPHOR_LOG2_USING;
+
+using namespace phosphor::software::manager;
+
+SoftwareManager::SoftwareManager(sdbusplus::async::context& ctx,
+                                 const std::string& serviceNameSuffix) :
+    ctx(ctx), serviceNameSuffix(serviceNameSuffix), manager(ctx, "/")
+{
+    debug("initialized SoftwareManager");
+}
+
+std::string SoftwareManager::setupBusName()
+{
+    const std::string serviceNameFull =
+        "xyz.openbmc_project.Software." + serviceNameSuffix;
+
+    debug("requesting dbus name {BUSNAME}", "BUSNAME", serviceNameFull);
+
+    ctx.get_bus().request_name(serviceNameFull.c_str());
+
+    return serviceNameFull;
+}
+
+// NOLINTBEGIN(readability-static-accessed-through-instance)
+sdbusplus::async::task<> SoftwareManager::initDevices(
+    const std::vector<std::string>& configurationInterfaces)
+// NOLINTEND(readability-static-accessed-through-instance)
+{
+    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)
+    {
+        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;
+            }
+
+            debug(
+                "[config] found configuration interface at {SERVICE}, {OBJPATH}",
+                "SERVICE", service, "OBJPATH", path);
+
+            auto client =
+                sdbusplus::async::proxy().service(service).path(path).interface(
+                    "org.freedesktop.DBus.Properties");
+
+            uint64_t vendorIANA = 0;
+            std::string compatible{};
+            std::string emConfigType{};
+            std::string emConfigName{};
+
+            try
+            {
+                {
+                    auto propVendorIANA =
+                        co_await client.call<std::variant<uint64_t>>(
+                            ctx, "Get", interfaceFound, "VendorIANA");
+
+                    vendorIANA = std::get<uint64_t>(propVendorIANA);
+                }
+                {
+                    auto propCompatible =
+                        co_await client.call<std::variant<std::string>>(
+                            ctx, "Get", interfaceFound, "Compatible");
+
+                    compatible = std::get<std::string>(propCompatible);
+                }
+                {
+                    auto propEMConfigType =
+                        co_await client.call<std::variant<std::string>>(
+                            ctx, "Get", interfaceFound, "Type");
+
+                    emConfigType = std::get<std::string>(propEMConfigType);
+                }
+                {
+                    auto propEMConfigName =
+                        co_await client.call<std::variant<std::string>>(
+                            ctx, "Get", interfaceFound, "Name");
+
+                    emConfigName = std::get<std::string>(propEMConfigName);
+                }
+            }
+            catch (std::exception& e)
+            {
+                error(e.what());
+                continue;
+            }
+
+            SoftwareConfig config(path, vendorIANA, compatible, emConfigType,
+                                  emConfigName);
+
+            co_await initDevice(service, path, config);
+        }
+    }
+
+    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..26a4602
--- /dev/null
+++ b/common/src/software_update.cpp
@@ -0,0 +1,103 @@
+#include "software_update.hpp"
+
+#include "device.hpp"
+#include "software.hpp"
+
+#include <phosphor-logging/elog-errors.hpp>
+#include <phosphor-logging/elog.hpp>
+#include <phosphor-logging/lg2.hpp>
+#include <sdbusplus/async/context.hpp>
+#include <xyz/openbmc_project/Software/Update/aserver.hpp>
+
+PHOSPHOR_LOG2_USING;
+
+using Unavailable = sdbusplus::xyz::openbmc_project::Common::Error::Unavailable;
+
+using namespace phosphor::logging;
+using namespace phosphor::software::update;
+using namespace phosphor::software::device;
+using namespace phosphor::software;
+
+namespace SoftwareLogging = phosphor::logging::xyz::openbmc_project::software;
+namespace SoftwareErrors =
+    sdbusplus::error::xyz::openbmc_project::software::image;
+
+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>
+{
+    debug("Requesting Image update with {FD}", "FD", image.fd);
+
+    Device& device = software.parentDevice;
+
+    if (device.updateInProgress)
+    {
+        error("An update is already in progress, cannot update.");
+        report<Unavailable>();
+        co_return sdbusplus::message::object_path();
+    }
+
+    device.updateInProgress = true;
+
+    // check if the apply time is allowed by our device
+    if (!allowedApplyTimes.contains(applyTime))
+    {
+        error(
+            "the selected apply time {APPLYTIME} is not allowed by the device",
+            "APPLYTIME", applyTime);
+        device.updateInProgress = false;
+        report<Unavailable>();
+        co_return sdbusplus::message::object_path();
+    }
+
+    debug("started asynchronous update with fd {FD}", "FD", image.fd);
+
+    int imageDup = dup(image.fd);
+
+    if (imageDup < 0)
+    {
+        error("ERROR calling dup on fd: {ERR}", "ERR", strerror(errno));
+        device.updateInProgress = false;
+        co_return software.objectPath;
+    }
+
+    debug("starting async update with FD: {FD}\n", "FD", imageDup);
+
+    std::unique_ptr<Software> softwareInstance =
+        std::make_unique<Software>(ctx, device);
+
+    softwareInstance->setActivation(ActivationInterface::Activations::NotReady);
+
+    std::string newObjPath = softwareInstance->objectPath;
+
+    // NOLINTBEGIN(readability-static-accessed-through-instance)
+    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));
+            device.updateInProgress = false;
+            close(imageDup);
+            co_return;
+        }(device, imageDup, applyTime, std::move(softwareInstance)));
+    // 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 newObjPath;
+}
+
+auto SoftwareUpdate::get_property(allowed_apply_times_t /*unused*/) const
+{
+    return allowedApplyTimes;
+}