Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions services/fwmanager/api/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ rust_library(
name = "fwmanager_api",
srcs = [
"src/boot_control.rs",
"src/config.rs",
"src/lib.rs",
],
edition = "2024",
Expand Down
173 changes: 173 additions & 0 deletions services/fwmanager/api/src/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
// Licensed under the Apache-2.0 license
// SPDX-License-Identifier: Apache-2.0

//! Schema for the per-board device table. Board device tables
//! (`target/<board>/devices.rs`) declare the values; no concrete line or
//! device is named here.

/// What the orchestrator requires before it commits a staged image.
///
/// Intentionally exhaustive (not `#[non_exhaustive]`): adding a variant is
/// a breaking change, so the compiler forces every match on the policy —
/// in particular the orchestrator's commit decision — to handle the new
/// variant explicitly instead of falling into a wildcard arm.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
Comment thread
rusty1968 marked this conversation as resolved.
pub enum CommitPolicy {
/// The device reports it came up.
Liveness,
/// Liveness plus SPDM re-attestation of the running image.
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
/// `ResetId` of the reset controller behind the board's `BootControl`
/// implementation — the compiler rejects a table whose ids the controller
/// cannot accept.
///
/// Intentionally exhaustive (not `#[non_exhaustive]`): board tables
/// 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, G: 'static> {
pub name: &'static str,
/// Reset signal id, passed to HalBootControl::new.
pub reset_signal: R,
/// 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, 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].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;
}
}

#[cfg(test)]
mod tests {
use super::*;
use core::time::Duration;

// Board tables run validate() at compile time, where a rejection is a
// build error nobody can assert on. These tests call it at runtime to
// prove the reject paths actually fire — a vacuous loop would pass
// every `const _` check silently.

const CHECKPOINT: BootCheckpoint<u8> = BootCheckpoint {
name: "boot-complete",
signal: BootSignal::GpioBootComplete(0),
window: Duration::from_secs(1),
};

const DEVICE: DeviceConfig<u8, u8> = DeviceConfig {
name: "dev",
reset_signal: 0,
checkpoints: &[CHECKPOINT],
commit_policy: CommitPolicy::Liveness,
};

#[test]
fn accepts_a_valid_table() {
validate(&[DEVICE]);
}

#[test]
#[should_panic(expected = "device name must not be empty")]
fn rejects_an_empty_device_name() {
validate(&[DEVICE, DeviceConfig { name: "", ..DEVICE }]);
}

#[test]
#[should_panic(expected = "at least one boot checkpoint")]
fn rejects_an_empty_checkpoint_list() {
validate(&[DeviceConfig {
checkpoints: &[],
..DEVICE
}]);
}

#[test]
#[should_panic(expected = "checkpoint name must not be empty")]
fn rejects_an_empty_checkpoint_name() {
validate(&[DeviceConfig {
checkpoints: &[BootCheckpoint {
name: "",
..CHECKPOINT
}],
..DEVICE
}]);
}

#[test]
#[should_panic(expected = "checkpoint window must not be zero")]
fn rejects_a_zero_checkpoint_window() {
validate(&[DeviceConfig {
checkpoints: &[
CHECKPOINT,
BootCheckpoint {
window: Duration::ZERO,
..CHECKPOINT
},
],
..DEVICE
}]);
}
}
15 changes: 9 additions & 6 deletions services/fwmanager/api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,18 @@
//! single managed device's reset without knowing which controller line it
//! maps to.
//!
//! This crate is a dependency-free leaf: it holds only the capability
//! contracts, and everything depends downward on it. Concrete adapters bind a
//! trait to a signal source and live in their own crates, so naming a
//! capability never drags in the stack behind it — the HAL-backed
//! `HalBootControl` is in `fwmanager-hal-adapters`; other backends implement
//! the same trait from their own transport crate.
//! This crate is a dependency-free leaf: it holds the capability contracts
//! and the schema for the per-board device table, and everything depends
//! downward on it. Concrete adapters bind a trait to a signal source and
//! live in their own crates, so naming a capability never drags in the stack
//! behind it — the HAL-backed `HalBootControl` is in
//! `fwmanager-hal-adapters`; other backends implement the same trait from
//! their own transport crate. Config values live in the board device tables
//! (`target/<board>/devices.rs`).

#![cfg_attr(not(test), no_std)]

mod boot_control;
pub mod config;

pub use boot_control::BootControl;
14 changes: 14 additions & 0 deletions target/mock/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Licensed under the Apache-2.0 license
# SPDX-License-Identifier: Apache-2.0

load("@rules_rust//rust:defs.bzl", "rust_library")

package(default_visibility = ["//visibility:public"])

rust_library(
name = "devices",
srcs = ["devices.rs"],
crate_name = "board_devices",
edition = "2024",
deps = ["//services/fwmanager/api:fwmanager_api"],
)
53 changes: 53 additions & 0 deletions target/mock/devices.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Licensed under the Apache-2.0 license
// SPDX-License-Identifier: Apache-2.0

//! Mock board: a device table exercising every device archetype the
//! orchestrator manages. Not a real board — consumed by host tests and QEMU
//! runs until a hardware target declares its own table.

#![no_std]

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 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,
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. Two
// checkpoints, exercising the multi-checkpoint path.
DeviceConfig {
name: "nic",
reset_signal: 3,
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,
},
];

const _: () = fwmanager_api::config::validate(MANAGED_DEVICES);