fwmanager: encode boot-progress checkpointing in the device table Liveness can be more than one timeout: a device may pass several boot checkpoints, and the signal may be polled or queried rather than device-pushed. Replace boot_timeout with an ordered checkpoint list, each pairing a BootSignal (boot-complete GPIO, heartbeat, MCTP ready, version query) with its own window: last checkpoint reached means booted, an expired window fails the boot. BootSignal is generic over the id its boot monitor reads a boot-complete line by, as DeviceConfig is over its reset signal. The mock nic exercises the multi-checkpoint path. Assisted-by: Claude:claude-fable-5 Signed-off-by: Christina Quast <christina.quast@9elements.com> Refs: 9elements/openprot#1
diff --git a/services/fwmanager/api/src/config.rs b/services/fwmanager/api/src/config.rs index 7149465..0a946cd 100644 --- a/services/fwmanager/api/src/config.rs +++ b/services/fwmanager/api/src/config.rs
@@ -19,6 +19,40 @@ LivenessAndAttestation, } +/// How the orchestrator observes a device's boot-progress signal. +/// +/// Generic over the id type `G` the board's boot monitor uses to read a +/// boot-complete line, for the same reason `DeviceConfig` is generic over +/// its reset signal: signal ids are board-specific. +/// +/// Intentionally exhaustive (not `#[non_exhaustive]`): adding a signal +/// kind is a breaking change, so every consumer that dispatches on it is +/// forced to handle the new kind explicitly. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BootSignal<G> { + /// The device raises a boot-complete GPIO line. + GpioBootComplete(G), + /// The device sends a heartbeat message. + Heartbeat, + /// The device's MCTP endpoint answers as ready. + MctpReady, + /// The device answers a firmware version query. + VersionQuery, +} + +/// One boot-progress checkpoint: a signal the orchestrator waits for, and +/// how long it waits. +#[derive(Debug, Clone, Copy)] +pub struct BootCheckpoint<G> { + /// Names the checkpoint in timeout reports. + pub name: &'static str, + pub signal: BootSignal<G>, + /// How long the orchestrator waits for `signal` before it declares the + /// checkpoint — and the device's boot — failed. Expiry is the + /// orchestrator's own judgment; hung devices report nothing. + pub window: core::time::Duration, +} + /// One managed downstream device, as declared by the board config. /// /// Generic over the board's reset signal type `R`, which must match the @@ -30,26 +64,39 @@ /// construct this struct by literal, which the attribute would forbid. /// Adding a field is a breaking change that updates every board table. #[derive(Debug, Clone, Copy)] -pub struct DeviceConfig<R> { +pub struct DeviceConfig<R, G: 'static> { pub name: &'static str, /// Reset signal id, passed to HalBootControl::new. pub reset_signal: R, - /// How long the orchestrator waits for this device to report Booted - /// before it declares a timeout. - pub boot_timeout: core::time::Duration, + /// Boot-progress checkpoints, in the order the device passes them. + /// The device counts as booted when the last one is reached; a + /// checkpoint whose window expires fails the boot. + pub checkpoints: &'static [BootCheckpoint<G>], pub commit_policy: CommitPolicy, } /// Checks a device table. Board configs call this in a const context so a /// bad table fails the build. -pub const fn validate<R>(devices: &[DeviceConfig<R>]) { +pub const fn validate<R, G>(devices: &[DeviceConfig<R, G>]) { let mut i = 0; while i < devices.len() { assert!(!devices[i].name.is_empty(), "device name must not be empty"); assert!( - !devices[i].boot_timeout.is_zero(), - "boot timeout must not be zero" + !devices[i].checkpoints.is_empty(), + "device must declare at least one boot checkpoint" ); + let mut c = 0; + while c < devices[i].checkpoints.len() { + assert!( + !devices[i].checkpoints[c].name.is_empty(), + "checkpoint name must not be empty" + ); + assert!( + !devices[i].checkpoints[c].window.is_zero(), + "checkpoint window must not be zero" + ); + c += 1; + } i += 1; } }
diff --git a/target/mock/devices.rs b/target/mock/devices.rs index 5f9fa17..ea5b7df 100644 --- a/target/mock/devices.rs +++ b/target/mock/devices.rs
@@ -7,26 +7,45 @@ #![no_std] -use fwmanager_api::config::{CommitPolicy, DeviceConfig}; +use core::time::Duration; + +use fwmanager_api::config::{BootCheckpoint, BootSignal, CommitPolicy, DeviceConfig}; /// Declaration order is the boot order: the orchestrator releases devices /// top to bottom, one at a time. /// -/// The mock board's reset controller addresses lines by plain index, so its -/// reset id type is `u8`. -pub const MANAGED_DEVICES: &[DeviceConfig<u8>] = &[ +/// The mock board's reset controller and boot monitor both address +/// signals by plain index, so both id types are `u8`. +pub const MANAGED_DEVICES: &[DeviceConfig<u8, u8>] = &[ // Direct-flash SPI device (BMC archetype): the eRoT fronts its flash. + // Single checkpoint: it raises a boot-complete GPIO. DeviceConfig { name: "bmc", reset_signal: 7, - boot_timeout: core::time::Duration::from_secs(90), + checkpoints: &[BootCheckpoint { + name: "boot-complete", + signal: BootSignal::GpioBootComplete(12), + window: Duration::from_secs(90), + }], commit_policy: CommitPolicy::Liveness, }, - // PLDM device (NIC archetype): self-updating, SPDM-capable. + // PLDM device (NIC archetype): self-updating, SPDM-capable. Two + // checkpoints, exercising the multi-checkpoint path. DeviceConfig { name: "nic", reset_signal: 3, - boot_timeout: core::time::Duration::from_secs(30), + checkpoints: &[ + BootCheckpoint { + name: "mctp-ready", + signal: BootSignal::MctpReady, + window: Duration::from_secs(20), + }, + BootCheckpoint { + name: "heartbeat", + signal: BootSignal::Heartbeat, + window: Duration::from_secs(10), + }, + ], commit_policy: CommitPolicy::LivenessAndAttestation, }, ];