fw update: spi device code updater

This code updater is for updating spi flash devices.
It can for example update the host firmware on different server boards
and has following features:

- power down the host before update
- set mux gpios to access spi flash
- (very limited) communication with ME (Management Engine)
- use flashrom to utilize fw with IFD (Intel Flash Descriptor)
- otherwise directly write to the flash chip.

The behavior of this code updater can be configured via EM.

Tested: on Tyan S5549 Board

Change-Id: I27803b7fded71af2364c2f55fad841a410603dec
Signed-off-by: Alexander Hansen <alexander.hansen@9elements.com>
diff --git a/bios-spi/spi_device.cpp b/bios-spi/spi_device.cpp
new file mode 100644
index 0000000..46fe1dc
--- /dev/null
+++ b/bios-spi/spi_device.cpp
@@ -0,0 +1,405 @@
+#include "spi_device.hpp"
+
+#include "common/include/device.hpp"
+#include "common/include/software_manager.hpp"
+
+#include <gpiod.hpp>
+#include <phosphor-logging/lg2.hpp>
+#include <sdbusplus/async.hpp>
+#include <sdbusplus/async/context.hpp>
+
+#include <fstream>
+
+using namespace std::literals;
+
+SPIDevice::SPIDevice(
+    sdbusplus::async::context& ctx, const std::string& spiDevName, bool dryRun,
+    bool hasME, const std::vector<std::string>& gpioLines,
+    const std::vector<uint8_t>& gpioValues, DeviceConfig& config,
+    SoftwareManager* parent, bool layoutFlat, bool toolFlashrom, bool debug) :
+    Device(ctx, dryRun, config, parent), hasManagementEngine(hasME),
+    gpioLines(gpioLines), gpioValues(gpioValues), spiDev(spiDevName),
+    layoutFlat(layoutFlat), toolFlashrom(toolFlashrom), debug(debug)
+{
+    lg2::debug("initialized SPI Device instance on dbus");
+}
+
+// NOLINTBEGIN
+sdbusplus::async::task<std::string> SPIDevice::getInventoryItemObjectPath()
+// NOLINTEND
+{
+    // TODO: we currently do not know how to access the object path of the
+    // inventory item here
+    co_return "";
+}
+
+// NOLINTBEGIN
+sdbusplus::async::task<bool> SPIDevice::updateDevice(
+    const uint8_t* image, size_t image_size,
+    std::unique_ptr<SoftwareActivationProgress>& activationProgress)
+// NOLINTEND
+{
+    // NOLINTBEGIN
+    bool success =
+        co_await this->writeSPIFlash(image, image_size, activationProgress);
+    // NOLINTEND
+    co_return success;
+}
+
+constexpr const char* IPMB_SERVICE = "xyz.openbmc_project.Ipmi.Channel.Ipmb";
+constexpr const char* IPMB_PATH = "/xyz/openbmc_project/Ipmi/Channel/Ipmb";
+constexpr const char* IPMB_INTF = "org.openbmc.Ipmb";
+
+// NOLINTBEGIN
+sdbusplus::async::task<> SPIDevice::setManagementEngineRecoveryMode()
+// NOLINTEND
+{
+    lg2::info("[ME] setting Management Engine to recovery mode");
+    auto m = ctx.get_bus().new_method_call(IPMB_SERVICE, IPMB_PATH, IPMB_INTF,
+                                           "sendRequest");
+
+    // me address, 0x2e oen, 0x00 - lun, 0xdf - force recovery
+    uint8_t cmd_recover[] = {0x1, 0x2e, 0x0, 0xdf};
+    for (unsigned int i = 0; i < sizeof(cmd_recover); i++)
+    {
+        m.append(cmd_recover[i]);
+    }
+    std::vector<uint8_t> remainder = {0x04, 0x57, 0x01, 0x00, 0x01};
+    m.append(remainder);
+
+    m.call();
+
+    co_await sdbusplus::async::sleep_for(ctx, std::chrono::seconds(5));
+
+    co_return;
+}
+
+// NOLINTBEGIN
+sdbusplus::async::task<> SPIDevice::resetManagementEngine()
+// NOLINTEND
+{
+    lg2::info("[ME] resetting Management Engine");
+    auto m = ctx.get_bus().new_method_call(IPMB_SERVICE, IPMB_PATH, IPMB_INTF,
+                                           "sendRequest");
+
+    // me address, 0x6 App Fn, 0x00 - lun, 0x2 - cold reset
+    uint8_t cmd_recover[] = {0x1, 0x6, 0x0, 0x2};
+    for (unsigned int i = 0; i < sizeof(cmd_recover); i++)
+    {
+        m.append(cmd_recover[i]);
+    }
+    std::vector<uint8_t> remainder;
+    m.append(remainder);
+
+    m.call();
+
+    co_await sdbusplus::async::sleep_for(ctx, std::chrono::seconds(5));
+
+    co_return;
+}
+
+const std::string spiAspeedSMCPath = "/sys/bus/platform/drivers/spi-aspeed-smc";
+
+// NOLINTBEGIN
+sdbusplus::async::task<bool> SPIDevice::bindSPIFlash()
+// NOLINTEND
+{
+    lg2::info("[SPI] binding flash to SMC");
+    std::ofstream ofbind(spiAspeedSMCPath + "/bind", std::ofstream::out);
+    ofbind << this->spiDev;
+    ofbind.close();
+
+    // wait for kernel
+    co_await sdbusplus::async::sleep_for(ctx, std::chrono::seconds(2));
+
+    co_return isSPIFlashBound();
+}
+
+// NOLINTBEGIN
+sdbusplus::async::task<bool> SPIDevice::unbindSPIFlash()
+// NOLINTEND
+{
+    lg2::info("[SPI] unbinding flash from SMC");
+    std::ofstream ofunbind(spiAspeedSMCPath + "/unbind", std::ofstream::out);
+    ofunbind << this->spiDev;
+    ofunbind.close();
+
+    // wait for kernel
+    co_await sdbusplus::async::sleep_for(ctx, std::chrono::seconds(2));
+
+    co_return !isSPIFlashBound();
+}
+
+bool SPIDevice::isSPIFlashBound()
+{
+    std::string path = spiAspeedSMCPath + "/" + this->spiDev;
+    lg2::debug("[SPI] checking {PATH}", "PATH", path);
+
+    return std::filesystem::exists(path);
+}
+
+// NOLINTBEGIN
+sdbusplus::async::task<bool> SPIDevice::writeSPIFlash(
+    const uint8_t* image, size_t image_size,
+    const std::unique_ptr<SoftwareActivationProgress>& activationProgress)
+// NOLINTEND
+{
+    auto currentPowerstateOpt = co_await parent->getHostPowerstate();
+
+    if (!currentPowerstateOpt.has_value())
+    {
+        co_return false;
+    }
+
+    const bool prevPowerstate = currentPowerstateOpt.value();
+
+    // NOLINTBEGIN
+    bool success = co_await parent->setHostPowerstate(false);
+    // NOLINTEND
+    if (!success)
+    {
+        lg2::error("error changing host power state");
+        co_return false;
+    }
+    activationProgress->progress(10);
+
+    if (hasManagementEngine)
+    {
+        co_await setManagementEngineRecoveryMode();
+    }
+    activationProgress->progress(20);
+
+    success = co_await writeSPIFlashHostOff(image, image_size);
+
+    if (success)
+    {
+        activationProgress->progress(70);
+    }
+
+    if (hasManagementEngine)
+    {
+        co_await resetManagementEngine();
+    }
+
+    if (success)
+    {
+        activationProgress->progress(90);
+    }
+
+    // restore the previous powerstate
+    const bool powerstate_restore =
+        co_await parent->setHostPowerstate(prevPowerstate);
+    if (!powerstate_restore)
+    {
+        lg2::error("error changing host power state");
+        co_return false;
+    }
+
+    // return value here is only describing if we successfully wrote to the
+    // SPI flash. Restoring powerstate can still fail.
+    co_return success;
+}
+
+// NOLINTBEGIN
+sdbusplus::async::task<bool>
+    SPIDevice::writeSPIFlashHostOff(const uint8_t* image, size_t image_size)
+// NOLINTEND
+{
+    gpiod::chip chip;
+    try
+    {
+        // TODO: make it work for multiple chips
+        chip = gpiod::chip("/dev/gpiochip0");
+    }
+    catch (std::exception& e)
+    {
+        lg2::error(e.what());
+        co_return false;
+    }
+
+    std::vector<unsigned int> offsets;
+
+    for (const std::string& lineName : gpioLines)
+    {
+        const ::gpiod::line line = ::gpiod::find_line(lineName);
+
+        if (line.is_used())
+        {
+            lg2::error("gpio line {LINE} was still used", "LINE", lineName);
+            co_return false;
+        }
+        offsets.push_back(line.offset());
+    }
+
+    auto lines = chip.get_lines(offsets);
+
+    ::gpiod::line_request config{"", ::gpiod::line_request::DIRECTION_OUTPUT,
+                                 0};
+    std::vector<int> values;
+    std::vector<int> valuesInverted;
+    values.reserve(gpioValues.size());
+
+    for (uint8_t value : gpioValues)
+    {
+        values.push_back(value);
+        valuesInverted.push_back(value ? 0 : 1);
+    }
+
+    lg2::debug("[gpio] requesting gpios to mux SPI to BMC");
+    lines.request(config, values);
+
+    co_await writeSPIFlashHostOffGPIOSet(image, image_size);
+
+    lines.release();
+
+    // switch bios flash back to host via mux / GPIO
+    // (not assume there is a pull to the default value)
+    lg2::debug("[gpio] requesting gpios to mux SPI to Host");
+    lines.request(config, valuesInverted);
+
+    lines.release();
+
+    co_return true;
+}
+
+// NOLINTBEGIN
+sdbusplus::async::task<bool> SPIDevice::writeSPIFlashHostOffGPIOSet(
+    const uint8_t* image, size_t image_size)
+// NOLINTEND
+{
+    bool success = true;
+
+    if (SPIDevice::isSPIFlashBound())
+    {
+        lg2::debug("[SPI] flash was already bound, unbinding it now");
+        success = co_await SPIDevice::unbindSPIFlash();
+
+        if (!success)
+        {
+            lg2::error("[SPI] error unbinding spi flash");
+            co_return false;
+        }
+    }
+
+    success = co_await SPIDevice::bindSPIFlash();
+
+    if (!success)
+    {
+        lg2::error("[SPI] failed to bind spi device");
+        co_await SPIDevice::unbindSPIFlash();
+        co_return false;
+    }
+
+    if (dryRun)
+    {
+        lg2::info("[SPI] dry run, NOT writing to the chip");
+    }
+    else
+    {
+        if (this->toolFlashrom)
+        {
+            co_await SPIDevice::writeSPIFlashFlashromHostOffGPIOSetDeviceBound(
+                image, image_size);
+        }
+        else
+        {
+            co_await SPIDevice::writeSPIFlashHostOffGPIOSetDeviceBound(
+                image, image_size);
+        }
+    }
+
+    success = co_await SPIDevice::unbindSPIFlash();
+
+    co_return success;
+}
+
+// NOLINTBEGIN
+sdbusplus::async::task<bool>
+    SPIDevice::writeSPIFlashFlashromHostOffGPIOSetDeviceBound(
+        const uint8_t* image, size_t image_size)
+// NOLINTEND
+{
+    // TODO: randomize the name to enable parallel updates
+    std::string path = "/tmp/spi-device-image.bin";
+    int fd = open(path.c_str(), O_CREAT | O_WRONLY | O_TRUNC, 0644);
+    if (fd < 0)
+    {
+        lg2::error("[SPI] Failed to open file: {PATH}", "PATH", path);
+        co_return false;
+    }
+
+    const ssize_t bytesWritten = write(fd, image, image_size);
+
+    close(fd);
+
+    if (bytesWritten < 0 || static_cast<size_t>(bytesWritten) != image_size)
+    {
+        lg2::error("[SPI] Failed to write image to file");
+        co_return false;
+    }
+
+    // TODO: do not hardcode the mtd device
+    std::string cmd = "flashrom -p linux_mtd:dev=6 ";
+
+    if (this->layoutFlat)
+    {
+        cmd += "-w " + path;
+    }
+    else
+    {
+        cmd += "--ifd -i fd -i bios -i me -w " + path;
+    }
+
+    lg2::info("[flashrom] running {CMD}", "CMD", cmd);
+
+    const int exitCode = std::system(cmd.c_str());
+
+    if (exitCode != 0)
+    {
+        lg2::error("[SPI] error running flaashrom");
+    }
+
+    // in debug mode we do not delete the raw component image
+    if (!this->debug)
+    {
+        std::filesystem::remove(path);
+    }
+
+    co_return exitCode;
+}
+
+// NOLINTBEGIN
+sdbusplus::async::task<bool> SPIDevice::writeSPIFlashHostOffGPIOSetDeviceBound(
+    const uint8_t* image, size_t image_size)
+// NOLINTEND
+{
+    // TODO: not hardcode the mtd device
+    std::string devPath = "/dev/mtd6";
+    int fd = open(devPath.c_str(), O_WRONLY);
+    if (fd < 0)
+    {
+        lg2::error("[SPI] Failed to open device: {PATH}", "PATH", devPath);
+        co_return false;
+    }
+
+    ssize_t bytesWritten = write(fd, image, image_size);
+
+    close(fd);
+
+    if (bytesWritten < 0)
+    {
+        lg2::error("[SPI] Failed to write to device");
+        co_return false;
+    }
+
+    if (static_cast<size_t>(bytesWritten) != image_size)
+    {
+        lg2::error("[SPI] Incomplete write to device");
+        co_return false;
+    }
+
+    lg2::info("[SPI] Successfully wrote {NBYTES} bytes to {PATH}", "NBYTES",
+              bytesWritten, "PATH", devPath);
+
+    co_return true;
+}