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..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;
+}