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 S8030 and Tyan S5549 Board. Steps below.

1. Display the fw inventory

```
curl --silent $creds https://$bmc/redfish/v1/UpdateService/FirmwareInventory
```

```
{
  "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory",
  "@odata.type": "#SoftwareInventoryCollection.SoftwareInventoryCollection",
  "Members": [
    {
      "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/717f6f4d"
    },
    {
      "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/HostSPIFlash_4950"
    },
    {
      "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/bios_active"
    }
  ],
  "Members@odata.count": 3,
  "Name": "Software Inventory Collection"
}
```

2. Query BIOS version.
The version is "unknown" here since currently there is no interface
enabled via which to query it.

```
curl $creds https://$bmc/redfish/v1/UpdateService/FirmwareInventory/HostSPIFlash_4950
```

```
{
  "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/HostSPIFlash_4950",
  "@odata.type": "#SoftwareInventory.v1_1_0.SoftwareInventory",
  "Description": "Unknown image",
  "Id": "HostSPIFlash_4950",
  "Name": "Software Inventory",
  "Status": {
    "Health": "Warning",
    "HealthRollup": "OK",
    "State": "Disabled"
  },
  "Updateable": true,
  "Version": "unknown"
}
```

3. Trigger the fw update via redfish.

```
curl -k ${creds} \
	-H "Content-Type:multipart/form-data" \
	-X POST \
	-F UpdateParameters="{\"Targets\":[\"/redfish/v1/UpdateService/FirmwareInventory/HostSPIFlash_6041\"],\"@Redfish.OperationApplyTime\":\"Immediate\"};type=application/json" \
	-F "UpdateFile=@${fwpath};type=application/octet-stream" \
	https://${bmc}/redfish/v1/UpdateService/update
```

4. Task is returned

```
{
  "@odata.id": "/redfish/v1/TaskService/Tasks/0",
  "@odata.type": "#Task.v1_4_3.Task",
  "Id": "0",
  "TaskState": "Running",
  "TaskStatus": "OK"
}
```

5. Query Task status
```
curl --silent $creds https://$bmc/redfish/v1/TaskService/Tasks/0
```

```
{
  "@odata.id": "/redfish/v1/TaskService/Tasks/0",
  "@odata.type": "#Task.v1_4_3.Task",
  "EndTime": "2025-02-18T14:05:46+00:00",
  "HidePayload": false,
  "Id": "0",
  "Messages": [
    {
      "@odata.type": "#Message.v1_1_1.Message",
      "Message": "The task with Id '0' has started.",
      "MessageArgs": [
        "0"
      ],
      "MessageId": "TaskEvent.1.0.TaskStarted",
      "MessageSeverity": "OK",
      "Resolution": "None."
    },
    {
      "@odata.type": "#Message.v1_1_1.Message",
      "Message": "The task with Id '0' has changed to progress 10 percent complete.",
      "MessageArgs": [
        "0",
        "10"
      ],
      "MessageId": "TaskEvent.1.0.TaskProgressChanged",
      "MessageSeverity": "OK",
      "Resolution": "None."
    },
    {
      "@odata.type": "#Message.v1_1_1.Message",
      "Message": "The task with Id '0' has changed to progress 20 percent complete.",
      "MessageArgs": [
        "0",
        "20"
      ],
      "MessageId": "TaskEvent.1.0.TaskProgressChanged",
      "MessageSeverity": "OK",
      "Resolution": "None."
    },
    {
      "@odata.type": "#Message.v1_1_1.Message",
      "Message": "The task with Id '0' has changed to progress 70 percent complete.",
      "MessageArgs": [
        "0",
        "70"
      ],
      "MessageId": "TaskEvent.1.0.TaskProgressChanged",
      "MessageSeverity": "OK",
      "Resolution": "None."
    },
    {
      "@odata.type": "#Message.v1_1_1.Message",
      "Message": "The task with Id '0' has changed to progress 100 percent complete.",
      "MessageArgs": [
        "0",
        "100"
      ],
      "MessageId": "TaskEvent.1.0.TaskProgressChanged",
      "MessageSeverity": "OK",
      "Resolution": "None."
    },
    {
      "@odata.type": "#Message.v1_1_1.Message",
      "Message": "The task with Id '0' has completed.",
      "MessageArgs": [
        "0"
      ],
      "MessageId": "TaskEvent.1.0.TaskCompletedOK",
      "MessageSeverity": "OK",
      "Resolution": "None."
    }
  ],
  "Name": "Task 0",
  "Payload": {
    "HttpHeaders": [],
    "HttpOperation": "POST",
    "JsonBody": "null",
    "TargetUri": "/redfish/v1/UpdateService/update"
  },
  "PercentComplete": 90,
  "StartTime": "2025-02-18T14:04:47+00:00",
  "TaskMonitor": "/redfish/v1/TaskService/TaskMonitors/0",
  "TaskState": "Completed",
  "TaskStatus": "OK"
}
```

6. Display the fw inventory with newly updated fw.
```
curl --silent $creds https://$bmc/redfish/v1/UpdateService/FirmwareInventory
```

```
{
  "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory",
  "@odata.type": "#SoftwareInventoryCollection.SoftwareInventoryCollection",
  "Members": [
    {
      "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/717f6f4d"
    },
    {
      "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/HostSPIFlash_8728"
    },
    {
      "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/bios_active"
    }
  ],
  "Members@odata.count": 3,
  "Name": "Software Inventory Collection"
}
```

7. Query the new fw version.

The version is 'mycompversion' since that's what has been set in the
pldm fw update package for testing.

```
curl $creds https://$bmc/redfish/v1/UpdateService/FirmwareInventory/HostSPIFlash_8728
```

```
{
  "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/HostSPIFlash_8728",
  "@odata.type": "#SoftwareInventory.v1_1_0.SoftwareInventory",
  "Description": "Unknown image",
  "Id": "HostSPIFlash_8728",
  "Name": "Software Inventory",
  "Status": {
    "Health": "OK",
    "HealthRollup": "OK",
    "State": "Enabled"
  },
  "Updateable": false,
  "Version": "mycompversion"
}
```

Change-Id: I27803b7fded71af2364c2f55fad841a410603dec
Signed-off-by: Alexander Hansen <alexander.hansen@9elements.com>
diff --git a/bios/spi_device.cpp b/bios/spi_device.cpp
new file mode 100644
index 0000000..93cb9a3
--- /dev/null
+++ b/bios/spi_device.cpp
@@ -0,0 +1,519 @@
+#include "spi_device.hpp"
+
+#include "common/include/NotifyWatch.hpp"
+#include "common/include/device.hpp"
+#include "common/include/host_power.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 <xyz/openbmc_project/Association/Definitions/server.hpp>
+#include <xyz/openbmc_project/ObjectMapper/client.hpp>
+#include <xyz/openbmc_project/State/Host/client.hpp>
+
+#include <cstddef>
+#include <fstream>
+#include <random>
+
+PHOSPHOR_LOG2_USING;
+
+using namespace std::literals;
+using namespace phosphor::software;
+using namespace phosphor::software::manager;
+using namespace phosphor::software::host_power;
+
+SPIDevice::SPIDevice(sdbusplus::async::context& ctx,
+                     uint64_t spiControllerIndex, uint64_t spiDeviceIndex,
+                     bool dryRun, const std::vector<std::string>& gpioLinesIn,
+                     const std::vector<uint64_t>& gpioValuesIn,
+                     SoftwareConfig& config, SoftwareManager* parent,
+                     enum FlashLayout layout, enum FlashTool tool,
+                     const std::string& versionDirPath) :
+    Device(ctx, config, parent,
+           {RequestedApplyTimes::Immediate, RequestedApplyTimes::OnReset}),
+    NotifyWatchIntf(ctx, versionDirPath), dryRun(dryRun),
+    gpioLines(gpioLinesIn),
+    gpioValues(gpioValuesIn.begin(), gpioValuesIn.end()),
+    spiControllerIndex(spiControllerIndex), spiDeviceIndex(spiDeviceIndex),
+    layout(layout), tool(tool)
+{
+    // To probe the driver for our spi flash, we need the memory-mapped address
+    // of the spi peripheral. These values are specific to aspeed BMC.
+    // https://github.com/torvalds/linux/blob/master/arch/arm/boot/dts/aspeed/aspeed-g6.dtsi
+    std::map<uint32_t, std::string> spiDevAddr = {
+        {0, "1e620000.spi"},
+        {1, "1e630000.spi"},
+        {2, "1e631000.spi"},
+    };
+
+    if (spiControllerIndex >= spiDevAddr.size())
+    {
+        throw std::invalid_argument("SPI controller index out of bounds");
+    }
+
+    spiDev = spiDevAddr[spiControllerIndex];
+
+    ctx.spawn(readNotifyAsync());
+
+    debug(
+        "SPI Device {NAME} at {CONTROLLERINDEX}:{DEVICEINDEX} initialized successfully",
+        "NAME", config.configName, "CONTROLLERINDEX", spiControllerIndex,
+        "DEVICEINDEX", spiDeviceIndex);
+}
+
+// NOLINTBEGIN(readability-static-accessed-through-instance)
+sdbusplus::async::task<bool> SPIDevice::updateDevice(const uint8_t* image,
+                                                     size_t image_size)
+// NOLINTEND(readability-static-accessed-through-instance)
+{
+    // NOLINTBEGIN(readability-static-accessed-through-instance)
+    // NOLINTNEXTLINE(clang-analyzer-core.uninitialized.Branch)
+    auto prevPowerstate = co_await HostPower::getState(ctx);
+
+    if (prevPowerstate != stateOn && prevPowerstate != stateOff)
+    {
+        co_return false;
+    }
+
+    // NOLINTBEGIN(readability-static-accessed-through-instance)
+    bool success = co_await HostPower::setState(ctx, stateOff);
+    // NOLINTEND(readability-static-accessed-through-instance)
+    if (!success)
+    {
+        error("error changing host power state");
+        co_return false;
+    }
+    setUpdateProgress(10);
+
+    success = co_await writeSPIFlash(image, image_size);
+
+    if (success)
+    {
+        setUpdateProgress(100);
+    }
+
+    // restore the previous powerstate
+    const bool powerstate_restore =
+        co_await HostPower::setState(ctx, prevPowerstate);
+    if (!powerstate_restore)
+    {
+        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;
+    // NOLINTEND(readability-static-accessed-through-instance)
+}
+
+const std::string spiAspeedSMCPath = "/sys/bus/platform/drivers/spi-aspeed-smc";
+
+// NOLINTBEGIN(readability-static-accessed-through-instance)
+sdbusplus::async::task<bool> SPIDevice::bindSPIFlash()
+// NOLINTEND(readability-static-accessed-through-instance)
+{
+    debug("binding flash to SMC");
+
+    if (SPIDevice::isSPIFlashBound())
+    {
+        debug("flash was already bound, unbinding it now");
+        bool success = co_await SPIDevice::unbindSPIFlash();
+
+        if (!success)
+        {
+            error("error unbinding spi flash");
+            co_return false;
+        }
+    }
+
+    std::ofstream ofbind(spiAspeedSMCPath + "/bind", std::ofstream::out);
+    ofbind << spiDev;
+    ofbind.close();
+
+    const int driverBindSleepDuration = 2;
+
+    co_await sdbusplus::async::sleep_for(
+        ctx, std::chrono::seconds(driverBindSleepDuration));
+
+    const bool isBound = isSPIFlashBound();
+
+    if (!isBound)
+    {
+        error("failed to bind spi device");
+    }
+
+    co_return isBound;
+}
+
+// NOLINTBEGIN(readability-static-accessed-through-instance)
+sdbusplus::async::task<bool> SPIDevice::unbindSPIFlash()
+// NOLINTEND(readability-static-accessed-through-instance)
+{
+    debug("unbinding flash from SMC");
+    std::ofstream ofunbind(spiAspeedSMCPath + "/unbind", std::ofstream::out);
+    ofunbind << 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 + "/" + spiDev;
+
+    return std::filesystem::exists(path);
+}
+
+static std::unique_ptr<::gpiod::line_bulk> requestMuxGPIOs(
+    const std::vector<std::string>& gpioLines,
+    const std::vector<int>& gpioValues, bool inverted)
+{
+    std::vector<::gpiod::line> lines;
+
+    for (const std::string& lineName : gpioLines)
+    {
+        const ::gpiod::line line = ::gpiod::find_line(lineName);
+
+        if (line.is_used())
+        {
+            error("gpio line {LINE} was still used", "LINE", lineName);
+            return nullptr;
+        }
+
+        lines.push_back(line);
+    }
+
+    ::gpiod::line_request config{"", ::gpiod::line_request::DIRECTION_OUTPUT,
+                                 0};
+
+    debug("[gpio] requesting gpios to mux SPI to BMC");
+
+    auto lineBulk = std::make_unique<::gpiod::line_bulk>(lines);
+
+    if (inverted)
+    {
+        std::vector<int> valuesInverted;
+        valuesInverted.reserve(gpioValues.size());
+
+        for (int value : gpioValues)
+        {
+            valuesInverted.push_back(value ? 0 : 1);
+        }
+
+        lineBulk->request(config, valuesInverted);
+    }
+    else
+    {
+        lineBulk->request(config, gpioValues);
+    }
+
+    return lineBulk;
+}
+
+// NOLINTBEGIN(readability-static-accessed-through-instance)
+sdbusplus::async::task<bool> SPIDevice::writeSPIFlash(const uint8_t* image,
+                                                      size_t image_size)
+// NOLINTEND(readability-static-accessed-through-instance)
+{
+    debug("[gpio] requesting gpios to mux SPI to BMC");
+
+    std::unique_ptr<::gpiod::line_bulk> lineBulk =
+        requestMuxGPIOs(gpioLines, gpioValues, false);
+
+    if (!lineBulk)
+    {
+        co_return false;
+    }
+
+    bool success = co_await SPIDevice::bindSPIFlash();
+    if (success)
+    {
+        if (dryRun)
+        {
+            info("dry run, NOT writing to the chip");
+        }
+        else
+        {
+            if (tool == flashToolFlashrom)
+            {
+                const int status =
+                    co_await SPIDevice::writeSPIFlashWithFlashrom(image,
+                                                                  image_size);
+                if (status != 0)
+                {
+                    error(
+                        "Error writing to SPI flash {CONTROLLERINDEX}:{DEVICEINDEX}, exit code {EXITCODE}",
+                        "CONTROLLERINDEX", spiControllerIndex, "DEVICEINDEX",
+                        spiDeviceIndex, "EXITCODE", status);
+                }
+                success = (status == 0);
+            }
+            else
+            {
+                success =
+                    co_await SPIDevice::writeSPIFlashDefault(image, image_size);
+            }
+        }
+
+        success = success && co_await SPIDevice::unbindSPIFlash();
+    }
+
+    lineBulk->release();
+
+    // switch bios flash back to host via mux / GPIO
+    // (not assume there is a pull to the default value)
+    debug("[gpio] requesting gpios to mux SPI to Host");
+
+    lineBulk = requestMuxGPIOs(gpioLines, gpioValues, true);
+
+    if (!lineBulk)
+    {
+        co_return success;
+    }
+
+    lineBulk->release();
+
+    co_return success;
+}
+
+// NOLINTBEGIN(readability-static-accessed-through-instance)
+sdbusplus::async::task<int> asyncSystem(sdbusplus::async::context& ctx,
+                                        const std::string& cmd)
+// NOLINTEND(readability-static-accessed-through-instance)
+{
+    int pipefd[2];
+    if (pipe(pipefd) == -1)
+    {
+        perror("pipe");
+        co_return -1;
+    }
+
+    pid_t pid = fork();
+    if (pid == -1)
+    {
+        perror("fork");
+        close(pipefd[0]);
+        close(pipefd[1]);
+        co_return -1;
+    }
+    else if (pid == 0)
+    {
+        close(pipefd[0]);
+        int exitCode = std::system(cmd.c_str());
+
+        ssize_t status = write(pipefd[1], &exitCode, sizeof(exitCode));
+        close(pipefd[1]);
+        exit((status == sizeof(exitCode)) ? 0 : 1);
+    }
+    else
+    {
+        close(pipefd[1]);
+
+        sdbusplus::async::fdio pipe_fdio(ctx, pipefd[0]);
+
+        co_await pipe_fdio.next();
+
+        int status;
+        waitpid(pid, &status, 0);
+        close(pipefd[0]);
+
+        co_return WEXITSTATUS(status);
+    }
+}
+
+// NOLINTBEGIN(readability-static-accessed-through-instance)
+sdbusplus::async::task<int> SPIDevice::writeSPIFlashWithFlashrom(
+    const uint8_t* image, size_t image_size) const
+// NOLINTEND(readability-static-accessed-through-instance)
+{
+    // randomize the name to enable parallel updates
+    const std::string path = "/tmp/spi-device-image-" +
+                             std::to_string(Software::getRandomId()) + ".bin";
+
+    int fd = open(path.c_str(), O_CREAT | O_WRONLY | O_TRUNC, 0644);
+    if (fd < 0)
+    {
+        error("Failed to open file: {PATH}", "PATH", path);
+        co_return 1;
+    }
+
+    const ssize_t bytesWritten = write(fd, image, image_size);
+
+    close(fd);
+
+    setUpdateProgress(30);
+
+    if (bytesWritten < 0 || static_cast<size_t>(bytesWritten) != image_size)
+    {
+        error("Failed to write image to file");
+        co_return 1;
+    }
+
+    debug("wrote {SIZE} bytes to {PATH}", "SIZE", bytesWritten, "PATH", path);
+
+    auto devPath = getMTDDevicePath();
+
+    if (!devPath.has_value())
+    {
+        co_return 1;
+    }
+
+    std::string cmd = "flashrom -p linux_mtd:dev=" + devPath.value();
+
+    if (layout == flashLayoutFlat)
+    {
+        cmd += " -w " + path;
+    }
+    else
+    {
+        error("unsupported flash layout");
+
+        co_return 1;
+    }
+
+    debug("[flashrom] running {CMD}", "CMD", cmd);
+
+    const int exitCode = co_await asyncSystem(ctx, cmd);
+
+    std::filesystem::remove(path);
+
+    co_return exitCode;
+}
+
+// NOLINTBEGIN(readability-static-accessed-through-instance)
+sdbusplus::async::task<bool> SPIDevice::writeSPIFlashDefault(
+    const uint8_t* image, size_t image_size)
+// NOLINTEND(readability-static-accessed-through-instance)
+{
+    auto devPath = getMTDDevicePath();
+
+    if (!devPath.has_value())
+    {
+        co_return false;
+    }
+
+    int fd = open(devPath.value().c_str(), O_WRONLY);
+    if (fd < 0)
+    {
+        error("Failed to open device: {PATH}", "PATH", devPath.value());
+        co_return false;
+    }
+
+    // Write the image in chunks to avoid blocking for too long.
+    // Also, to provide meaningful progress updates.
+
+    const size_t chunk = static_cast<size_t>(1024 * 1024);
+    ssize_t bytesWritten = 0;
+
+    const int progressStart = 30;
+    const int progressEnd = 90;
+
+    for (size_t offset = 0; offset < image_size; offset += chunk)
+    {
+        const ssize_t written =
+            write(fd, image + offset, std::min(chunk, image_size - offset));
+
+        if (written < 0)
+        {
+            error("Failed to write to device");
+            co_return false;
+        }
+
+        bytesWritten += written;
+
+        setUpdateProgress(
+            progressStart + int((progressEnd - progressStart) *
+                                (double(offset) / double(image_size))));
+    }
+
+    close(fd);
+
+    if (static_cast<size_t>(bytesWritten) != image_size)
+    {
+        error("Incomplete write to device");
+        co_return false;
+    }
+
+    debug("Successfully wrote {NBYTES} bytes to {PATH}", "NBYTES", bytesWritten,
+          "PATH", devPath.value());
+
+    co_return true;
+}
+
+std::string SPIDevice::getVersion()
+{
+    std::string version{};
+    try
+    {
+        std::ifstream config(biosVersionPath);
+
+        config >> version;
+    }
+    catch (std::exception& e)
+    {
+        error("Failed to get version with {ERROR}", "ERROR", e.what());
+        version = versionUnknown;
+    }
+
+    if (version.empty())
+    {
+        version = versionUnknown;
+    }
+
+    return version;
+}
+
+// NOLINTNEXTLINE(readability-static-accessed-through-instance)
+auto SPIDevice::processUpdate(std::string versionFileName)
+    -> sdbusplus::async::task<>
+{
+    if (biosVersionFilename != versionFileName)
+    {
+        error(
+            "Update config file name '{NAME}' (!= '{EXPECTED}') is not expected",
+            "NAME", versionFileName, "EXPECTED", biosVersionFilename);
+        co_return;
+    }
+
+    if (softwareCurrent)
+    {
+        softwareCurrent->setVersion(getVersion());
+    }
+
+    co_return;
+}
+
+std::optional<std::string> SPIDevice::getMTDDevicePath() const
+{
+    const std::string spiPath =
+        "/sys/class/spi_master/spi" + std::to_string(spiControllerIndex) +
+        "/spi" + std::to_string(spiControllerIndex) + "." +
+        std::to_string(spiDeviceIndex) + "/mtd/";
+
+    if (!std::filesystem::exists(spiPath))
+    {
+        error("Error: SPI path not found: {PATH}", "PATH", spiPath);
+        return "";
+    }
+
+    for (const auto& entry : std::filesystem::directory_iterator(spiPath))
+    {
+        const std::string mtdName = entry.path().filename().string();
+
+        if (mtdName.starts_with("mtd") && !mtdName.ends_with("ro"))
+        {
+            return "/dev/" + mtdName;
+        }
+    }
+
+    error("Error: No MTD device found for spi {CONTROLLERINDEX}.{DEVICEINDEX}",
+          "CONTROLLERINDEX", spiControllerIndex, "DEVICEINDEX", spiDeviceIndex);
+
+    return std::nullopt;
+}