fw update: implement example device
Introduce example-device and example-code-updater, which are used as
examples for how to implement devices and code updaters according to the
design [1]
Tested:
The example code updater allows us to perform manual testing without
a dependency on any specific device.
Running the example code updater results in follow dbus output:
```
busctl --full --no-pager tree xyz.openbmc_project.ExampleCodeUpdater
└─ /xyz
└─ /xyz/openbmc_project
└─ /xyz/openbmc_project/software
└─ /xyz/openbmc_project/software/ExampleSoftware_4081
busctl --full --no-pager introspect xyz.openbmc_project.ExampleCodeUpdater /xyz/openbmc_project/software/ExampleSoftware_4081
CodeUpdater /xyz/openbmc_project/software/ExampleSoftware_4081
NAME TYPE SIGNATURE RESULT/VALUE FLAGS
...
xyz.openbmc_project.Software.Activation interface - - -
.Activation property s "xyz.openbmc_project.Software.Activation.Activations.Active" emits-change writable
.RequestedActivation property s "xyz.openbmc_project.Software.Activation.RequestedActivations.None" emits-change writable
xyz.openbmc_project.Software.Update interface - - -
.StartUpdate method hs o -
.AllowedApplyTimes property as 1 "xyz.openbmc_project.Software.ApplyTime.RequestedApplyTimes.OnReset" emits-change
xyz.openbmc_project.Software.Version interface - - -
.Purpose property s "xyz.openbmc_project.Software.Version.VersionPurpose.Unknown" emits-change writable
.Version property s "v1.0"
```
References:
[1] https://github.com/openbmc/docs/blob/master/designs/code-update.md
Change-Id: I2bad241b3102e58eda5139174791adda82f6ca95
Signed-off-by: Alexander Hansen <alexander.hansen@9elements.com>
diff --git a/common/README.md b/common/README.md
index 9ac5054..584f9cb 100644
--- a/common/README.md
+++ b/common/README.md
@@ -9,6 +9,17 @@
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 'ExampleDevice' and
+'ExampleCodeUpdater'.
+
+The ExampleCodeUpdater implements the classes from common firmware library and
+serves as a demonstration & testing tool.
+
+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
diff --git a/test/common/exampledevice/example_device.cpp b/test/common/exampledevice/example_device.cpp
new file mode 100644
index 0000000..59acd39
--- /dev/null
+++ b/test/common/exampledevice/example_device.cpp
@@ -0,0 +1,96 @@
+#include "example_device.hpp"
+
+#include "common/include/device.hpp"
+#include "common/include/software_config.hpp"
+#include "common/include/software_manager.hpp"
+
+#include <phosphor-logging/lg2.hpp>
+#include <sdbusplus/asio/connection.hpp>
+#include <sdbusplus/asio/object_server.hpp>
+#include <sdbusplus/async.hpp>
+#include <sdbusplus/server.hpp>
+#include <xyz/openbmc_project/Association/Definitions/server.hpp>
+#include <xyz/openbmc_project/Software/Update/server.hpp>
+
+#include <memory>
+
+PHOSPHOR_LOG2_USING;
+
+using namespace phosphor::software;
+using namespace phosphor::software::config;
+using namespace phosphor::software::manager;
+using namespace phosphor::software::device;
+using namespace phosphor::software::example_device;
+
+SoftwareConfig ExampleDevice::defaultConfig =
+ SoftwareConfig(exampleInvObjPath, exampleVendorIANA,
+ exampleCompatibleHardware, "Nop", exampleName);
+
+long ExampleCodeUpdater::getRandomId()
+{
+ struct timespec ts;
+ clock_gettime(CLOCK_REALTIME, &ts);
+ unsigned int seed = ts.tv_nsec ^ getpid();
+ srandom(seed);
+ return random() % 10000;
+}
+
+// nop code updater needs unique suffix on dbus for parallel unit testing
+ExampleCodeUpdater::ExampleCodeUpdater(sdbusplus::async::context& ctx,
+ long uniqueSuffix) :
+ SoftwareManager(ctx, "ExampleUpdater" + std::to_string(uniqueSuffix))
+{}
+
+// NOLINTBEGIN(readability-static-accessed-through-instance)
+sdbusplus::async::task<bool> ExampleCodeUpdater::initDevice(
+ const std::string& /*unused*/, const std::string& /*unused*/,
+ SoftwareConfig& /*unused*/)
+// NOLINTEND(readability-static-accessed-through-instance)
+{
+ auto device = std::make_unique<ExampleDevice>(ctx, this);
+
+ device->softwareCurrent = std::make_unique<Software>(ctx, *device);
+
+ device->softwareCurrent->setVersion("v1.0");
+ device->softwareCurrent->setActivation(
+ SoftwareActivation::Activations::Active);
+
+ auto applyTimes = {RequestedApplyTimes::OnReset};
+ device->softwareCurrent->enableUpdate(applyTimes);
+
+ devices.insert({exampleInvObjPath, std::move(device)});
+
+ co_return true;
+}
+
+ExampleDevice::ExampleDevice(sdbusplus::async::context& ctx,
+ SoftwareManager* parent,
+ const SoftwareConfig& config) :
+ Device(ctx, config, parent,
+ {RequestedApplyTimes::Immediate, RequestedApplyTimes::OnReset})
+{}
+
+// NOLINTBEGIN(readability-static-accessed-through-instance)
+sdbusplus::async::task<bool> ExampleDevice::updateDevice(
+ const uint8_t* /*unused*/, size_t compImageSize)
+// NOLINTEND(readability-static-accessed-through-instance)
+{
+ debug("Called device specific update function with image size {SIZE}",
+ "SIZE", compImageSize);
+
+ deviceSpecificUpdateFunctionCalled = true;
+
+ // Setting this property for demonstration purpose.
+ // For a real device, this could represent the
+ // percentage completion of writing the firmware,
+ // and any progress made in the update process within this function.
+ // There is no hard constraint on the values here,
+ // we do not have to reach any specific percentage.
+ // The percentage should be monotonic and increasing.
+ for (auto progress = 0; progress <= 100; progress += 20)
+ {
+ setUpdateProgress(90);
+ }
+
+ co_return true;
+}
diff --git a/test/common/exampledevice/example_device.hpp b/test/common/exampledevice/example_device.hpp
new file mode 100644
index 0000000..928af3b
--- /dev/null
+++ b/test/common/exampledevice/example_device.hpp
@@ -0,0 +1,57 @@
+#pragma once
+
+#include "common/include/device.hpp"
+#include "common/include/software_manager.hpp"
+
+#include <phosphor-logging/lg2.hpp>
+#include <sdbusplus/async.hpp>
+#include <sdbusplus/server.hpp>
+#include <xyz/openbmc_project/Association/Definitions/server.hpp>
+#include <xyz/openbmc_project/Software/Update/server.hpp>
+
+namespace phosphor::software::example_device
+{
+
+class ExampleCodeUpdater : public phosphor::software::manager::SoftwareManager
+{
+ public:
+ ExampleCodeUpdater(sdbusplus::async::context& ctx,
+ long uniqueSuffix = getRandomId());
+
+ sdbusplus::async::task<bool> initDevice(const std::string& service,
+ const std::string& path,
+ SoftwareConfig& config) final;
+
+ private:
+ static long getRandomId();
+};
+
+const std::string exampleName = "ExampleSoftware";
+
+const uint32_t exampleVendorIANA = 0x0000a015;
+const std::string exampleCompatibleHardware = "com.example.CompatibleDevice";
+
+const std::string exampleInvObjPath =
+ "/xyz/openbmc_project/inventory/system/board/ExampleBoard/ExampleDevice";
+
+class ExampleDevice : public Device
+{
+ public:
+ using Device::softwarePending;
+ using phosphor::software::device::Device::softwareCurrent;
+
+ static SoftwareConfig defaultConfig;
+
+ ExampleDevice(sdbusplus::async::context& ctx,
+ phosphor::software::manager::SoftwareManager* parent,
+ const SoftwareConfig& config = defaultConfig);
+
+ // NOLINTBEGIN(readability-static-accessed-through-instance)
+ sdbusplus::async::task<bool> updateDevice(const uint8_t* image,
+ size_t image_size) override;
+ // NOLINTEND(readability-static-accessed-through-instance)
+
+ bool deviceSpecificUpdateFunctionCalled = false;
+};
+
+} // namespace phosphor::software::example_device
diff --git a/test/common/exampledevice/example_updater_main.cpp b/test/common/exampledevice/example_updater_main.cpp
new file mode 100644
index 0000000..425ab35
--- /dev/null
+++ b/test/common/exampledevice/example_updater_main.cpp
@@ -0,0 +1,40 @@
+#include "example_device.hpp"
+
+#include <sdbusplus/async/context.hpp>
+
+using namespace phosphor::software::example_device;
+
+// NOLINTBEGIN(readability-static-accessed-through-instance)
+sdbusplus::async::task<void> init(ExampleCodeUpdater& updater)
+// NOLINTEND(readability-static-accessed-through-instance)
+{
+ co_await updater.initDevice("", "", ExampleDevice::defaultConfig);
+
+ co_return;
+}
+
+int main()
+{
+ sdbusplus::async::context ctx;
+
+ ExampleCodeUpdater updater(ctx);
+
+ /*
+ * In Concrete updaters, the initDevices() function needs to be called,
+ * which in turn invokes the virtual initDevice() function implemented here.
+ * However, in ExampleUpdater, the initDevice() function is called directly
+ * because there is no example configuration from EM to consume, which would
+ * otherwise cause the initDevices() API to throw an error. Therefore,
+ * calling initDevice() directly in this case.
+ */
+
+ // NOLINTNEXTLINE(clang-analyzer-core.uninitialized.Branch)
+ ctx.spawn(init(updater));
+
+ std::string busName = "xyz.openbmc_project.Software.ExampleDevice";
+ ctx.get_bus().request_name(busName.c_str());
+
+ ctx.run();
+
+ return 0;
+}
diff --git a/test/common/exampledevice/meson.build b/test/common/exampledevice/meson.build
new file mode 100644
index 0000000..eebead1
--- /dev/null
+++ b/test/common/exampledevice/meson.build
@@ -0,0 +1,30 @@
+libexampledevice = static_library('example_device',
+ 'example_device.cpp',
+ include_directories: ['.', common_include],
+ dependencies: [
+ pdi_dep,
+ phosphor_logging_dep,
+ sdbusplus_dep,
+ libpldm_dep,
+ ],
+ link_with: [
+ software_common_lib,
+ ],
+)
+
+executable(
+ 'example-code-updater',
+ 'example_updater_main.cpp',
+ include_directories: ['.', common_include],
+ dependencies: [
+ pdi_dep,
+ phosphor_logging_dep,
+ sdbusplus_dep,
+ libpldm_dep,
+ ],
+ link_with: [
+ libpldmutil,
+ software_common_lib,
+ libexampledevice
+ ],
+)
diff --git a/test/common/meson.build b/test/common/meson.build
new file mode 100644
index 0000000..964f769
--- /dev/null
+++ b/test/common/meson.build
@@ -0,0 +1 @@
+subdir('exampledevice')
diff --git a/test/meson.build b/test/meson.build
index 20b6879..b7d75f8 100644
--- a/test/meson.build
+++ b/test/meson.build
@@ -1 +1,2 @@
subdir('create_package')
+subdir('common')