blob: 43e3ed8bd562a34f4f6b2e4008be97b73250198a [file]
// 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)]
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
}]);
}
}