From 19963f157f87998c3241c4144c178e253d345058 Mon Sep 17 00:00:00 2001 From: Christina Quast Date: Thu, 1 Oct 2026 22:02:09 +0200 Subject: [PATCH] config: Declare a component once, in the device table ComponentKind was written out twice: once in ComponentAttrs on the orchestrator's chain, once in Board::component_kinds. Nothing checked they matched. Disagree and the machine waits in AwaitingReady for a readiness the driver never sends, the boot watchdog fires, and the component goes round the recovery loop until its retries run out. DeviceConfig now carries the ComponentAttrs for its device, and chain_of turns a table into the chain the machine runs on. Board no longer holds the kinds at all: the driver reads them off the chain entries it was built with, so the second copy is gone rather than hidden. Holding ComponentAttrs whole rather than copying its four fields keeps one definition of what a component's policy is. A field added there reaches the table with no change here. chain_of returns a ChainEntries newtype with a private field. The only way to get one is through chain_of, and boards declare it as a const item, so the validation (nonempty, at most u8::MAX, no forward or self deps) runs at compile time. A bad table is a build error, never a runtime panic. bring_up takes &'static ChainEntries and is infallible: the const fence already guarantees everything Chain::try_from would check, so the conversion uses an expect rather than returning a Result. The old bring_up_reports_a_chain_the_machine_refuses test is gone because the scenario is now unrepresentable. Table order assigns the ids: devices[i] is ComponentId(i), which is also how the driver indexes every per-component array. A table that came through chain_of is in position by construction; the check in PlatformDriver::new catches synthetic entries in the driver's own tests. chain_of also checks the depends_on links while it has the whole table, because a dependency the chain does not contain would silently never cascade. Orchestrator::new and Chain::try_from stay public: the state machine's own tests and the ast10x0 runtime harness build synthetic chains, and narrowing that to win an encapsulation point would cost more than it buys. config gains a dependency on sm for the vocabulary. driver gains a dependency on config for ChainEntries. Assisted-by: Claude --- .../orchestrator/adapters/walk/BUILD.bazel | 1 + .../orchestrator/adapters/walk/src/walk.rs | 9 +- services/orchestrator/config/BUILD.bazel | 7 + services/orchestrator/config/src/chain.rs | 138 ++++++++ services/orchestrator/config/src/device.rs | 47 ++- services/orchestrator/config/src/lib.rs | 3 + services/orchestrator/driver/BUILD.bazel | 1 + services/orchestrator/driver/src/board.rs | 48 +-- services/orchestrator/driver/src/driver.rs | 63 +++- services/orchestrator/driver/src/lib.rs | 2 +- services/orchestrator/driver/src/tests.rs | 323 ++++++++++++------ services/orchestrator/sm/src/model.rs | 9 +- services/orchestrator/test/BUILD.bazel | 5 +- services/orchestrator/test/devices.rs | 21 +- target/ast10x0/board/BUILD.bazel | 2 + target/ast10x0/board/src/bmc.rs | 22 +- target/ast10x0/boot_evidence/BUILD.bazel | 1 + target/ast10x0/boot_evidence/src/lib.rs | 1 + 18 files changed, 539 insertions(+), 164 deletions(-) create mode 100644 services/orchestrator/config/src/chain.rs diff --git a/services/orchestrator/adapters/walk/BUILD.bazel b/services/orchestrator/adapters/walk/BUILD.bazel index cf86946ef..919bff503 100644 --- a/services/orchestrator/adapters/walk/BUILD.bazel +++ b/services/orchestrator/adapters/walk/BUILD.bazel @@ -14,6 +14,7 @@ rust_library( deps = [ "//services/orchestrator/capabilities:orchestrator_capabilities", "//services/orchestrator/config:orchestrator_config", + "//services/orchestrator/sm:orchestrator_sm", ], ) diff --git a/services/orchestrator/adapters/walk/src/walk.rs b/services/orchestrator/adapters/walk/src/walk.rs index e19e4422f..0d1a762e8 100644 --- a/services/orchestrator/adapters/walk/src/walk.rs +++ b/services/orchestrator/adapters/walk/src/walk.rs @@ -130,14 +130,19 @@ impl, P> BootWatch for CheckpointWalk { mod tests { use super::*; use core::time::Duration; + use openprot_orchestrator_sm::ComponentAttrs; use orchestrator_config::DeviceConfig; + const ATTRS: ComponentAttrs = ComponentAttrs::passive_required(); + const BL1: BootCheckpoint = BootCheckpoint::new("bl1", 1, Duration::from_millis(100)); const KERNEL: BootCheckpoint = BootCheckpoint::new("kernel", 2, Duration::from_millis(200)); const CHECKPOINTS: &[BootCheckpoint] = &[BL1, KERNEL]; - static DEVICE: DeviceConfig = DeviceConfig::new("test-dev", 0, CHECKPOINTS, None); - static ONE_CP_DEVICE: DeviceConfig = DeviceConfig::new("one-cp-dev", 0, &[BL1], None); + static DEVICE: DeviceConfig = + DeviceConfig::new("test-dev", 0, CHECKPOINTS, None, ATTRS); + static ONE_CP_DEVICE: DeviceConfig = + DeviceConfig::new("one-cp-dev", 0, &[BL1], None, ATTRS); // A progress-register reader: probe N is Booted once progress >= N. // Mirrors the SocReader archetype in the evidence tests. diff --git a/services/orchestrator/config/BUILD.bazel b/services/orchestrator/config/BUILD.bazel index 5628f96d5..830a5040a 100644 --- a/services/orchestrator/config/BUILD.bazel +++ b/services/orchestrator/config/BUILD.bazel @@ -6,6 +6,7 @@ load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test") rust_library( name = "orchestrator_config", srcs = [ + "src/chain.rs", "src/checkpoint.rs", "src/device.rs", "src/layout.rs", @@ -14,10 +15,16 @@ rust_library( ], edition = "2024", visibility = ["//visibility:public"], + deps = [ + "//services/orchestrator/sm:orchestrator_sm", + ], ) # Host tests: build on the host platform, no kernel/QEMU. rust_test( name = "orchestrator_config_test", crate = ":orchestrator_config", + deps = [ + "@rust_crates//:heapless", + ], ) diff --git a/services/orchestrator/config/src/chain.rs b/services/orchestrator/config/src/chain.rs new file mode 100644 index 000000000..4c97484e9 --- /dev/null +++ b/services/orchestrator/config/src/chain.rs @@ -0,0 +1,138 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! The views the orchestrator and the platform driver take of the device +//! table. Both are derived here, so the table is the only place a board +//! states what its components are. + +use crate::device::DeviceConfig; +use openprot_orchestrator_sm::{ComponentAttrs, ComponentId}; + +/// The entries `chain_of` validated. The field is private, so the only +/// constructor is the const-validated path and holders can trust the +/// invariants without rechecking. +pub struct ChainEntries([(ComponentId, ComponentAttrs); N]); + +impl ChainEntries { + /// The validated entries, one per device in table order. + pub const fn entries(&self) -> &[(ComponentId, ComponentAttrs); N] { + &self.0 + } +} + +/// Table order assigns the ids: `devices[i]` is `ComponentId::new(i)`. The +/// board's per-component arrays are indexed the same way, so the table is +/// also what keeps those arrays lined up with the chain. +/// +/// # Panics +/// +/// Panics, a build error in const context, if the table is empty or holds +/// more than `u8::MAX` devices (`Chain` takes neither), or if a `depends_on` +/// names anything other than an earlier device in the table. +#[must_use] +pub const fn chain_of(devices: &[DeviceConfig; N]) -> ChainEntries { + assert!(N > 0, "a device table needs at least one device"); + assert!( + N <= u8::MAX as usize, + "a device table holds at most u8::MAX devices" + ); + let mut i = 0; + while i < N { + if let Some(on) = devices[i].attrs().depends_on { + let on = on.get() as usize; + assert!( + on < i, + "depends_on must name an earlier device in the table" + ); + } + i += 1; + } + + // Const arrays need a default; the loop below overwrites every element. + let mut chain = [(ComponentId::new(0), ComponentAttrs::passive_required()); N]; + let mut i = 0; + while i < N { + chain[i] = (ComponentId::new(i as u8), devices[i].attrs()); + i += 1; + } + ChainEntries(chain) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::checkpoint::BootCheckpoint; + use core::time::Duration; + use openprot_orchestrator_sm::{Chain, FailurePolicy}; + + const CP: BootCheckpoint = BootCheckpoint::new("up", 1, Duration::from_millis(10)); + + const fn dev(attrs: ComponentAttrs) -> DeviceConfig { + DeviceConfig::new("dev", 0, &[CP], None, attrs) + } + + #[test] + fn ids_come_from_table_order() { + let table = [ + dev(ComponentAttrs::passive_required()), + dev(ComponentAttrs::active_isolable()), + ]; + let validated = chain_of(&table); + let entries = validated.entries(); + + assert_eq!(entries[0].0, ComponentId::new(0)); + assert_eq!(entries[1].0, ComponentId::new(1)); + assert_eq!(entries[1].1.failure_policy, FailurePolicy::Isolable); + } + + #[test] + fn the_derived_chain_is_one_the_machine_accepts() { + let table = [ + dev(ComponentAttrs::passive_required()), + dev(ComponentAttrs::active_isolable()), + ]; + let validated = chain_of(&table); + let entries: heapless::Vec<_, 2> = + heapless::Vec::from_slice(validated.entries()).expect("same length as the table"); + + assert!(Chain::<2>::try_from(entries).is_ok()); + } + + #[test] + #[should_panic(expected = "at least one device")] + fn rejects_an_empty_table() { + let table: [DeviceConfig; 0] = []; + let _ = chain_of(&table); + } + + #[test] + #[should_panic(expected = "at most u8::MAX")] + fn rejects_a_table_longer_than_the_chain_takes() { + let table = [dev(ComponentAttrs::passive_required()); 256]; + let _ = chain_of(&table); + } + + #[test] + #[should_panic(expected = "depends_on must name an earlier device")] + fn rejects_a_dependency_outside_the_table() { + let mut attrs = ComponentAttrs::passive_required(); + attrs.depends_on = Some(ComponentId::new(4)); + let _ = chain_of(&[dev(attrs)]); + } + + #[test] + #[should_panic(expected = "depends_on must name an earlier device")] + fn rejects_a_self_dependency() { + let mut attrs = ComponentAttrs::passive_required(); + attrs.depends_on = Some(ComponentId::new(0)); + let _ = chain_of(&[dev(attrs)]); + } + + #[test] + #[should_panic(expected = "depends_on must name an earlier device")] + fn rejects_a_forward_dependency() { + let mut first = ComponentAttrs::passive_required(); + first.depends_on = Some(ComponentId::new(1)); + let _ = chain_of(&[dev(first), dev(ComponentAttrs::passive_required())]); + } +} diff --git a/services/orchestrator/config/src/device.rs b/services/orchestrator/config/src/device.rs index cbad32a39..997ddc5ce 100644 --- a/services/orchestrator/config/src/device.rs +++ b/services/orchestrator/config/src/device.rs @@ -5,6 +5,7 @@ use crate::checkpoint::BootCheckpoint; use crate::layout::ImageLayout; +use openprot_orchestrator_sm::ComponentAttrs; /// Fails the build if `max_retry` is too small for a device to boot every /// image. Recovery restores one image per attempt, and the orchestrator @@ -48,10 +49,9 @@ pub const fn assert_retry_reaches_every_image(max_retry: u8, devices: &[De /// implementation) and its boot-probe vocabulary `P`, for the same /// reason: probes are board-specific. /// -/// Deliberately says nothing about attestation or commit requirements: -/// those follow from what kind of device this is (iRoT-backed or -/// symbiont, the orchestrator's `ComponentKind`), not from a table -/// setting — a second knob would only let the two disagree. +/// Says nothing about attestation or commit requirements: those follow from +/// what kind of device this is, which `attrs` carries, not from a second +/// table setting that could disagree with it. /// /// Fields are private so a device entry that violates the schema is /// unrepresentable: [`new`](Self::new) is the only way in, and it checks. @@ -61,6 +61,7 @@ pub struct DeviceConfig { reset_signal: R, checkpoints: &'static [BootCheckpoint

], layout: Option, + attrs: ComponentAttrs, } impl DeviceConfig { @@ -79,6 +80,7 @@ impl DeviceConfig { reset_signal: R, checkpoints: &'static [BootCheckpoint

], layout: Option, + attrs: ComponentAttrs, ) -> Self { assert!(!name.is_empty(), "device name must not be empty"); assert!( @@ -102,9 +104,16 @@ impl DeviceConfig { reset_signal, checkpoints, layout, + attrs, } } + /// The orchestrator's per-component policy for this device. + #[must_use] + pub const fn attrs(&self) -> ComponentAttrs { + self.attrs + } + /// The device's name in reports and logs. #[must_use] pub const fn name(&self) -> &'static str { @@ -182,6 +191,7 @@ mod tests { /// The golden image, above every slot `slot` can place. const GOLDEN: Golden = Golden::new(Region::new(0xF000_0000, SLOT_LEN)); + const ATTRS: ComponentAttrs = ComponentAttrs::passive_required(); const LAYOUT: ImageLayout = ImageLayout::new(const { &[slot(0), slot(1)] }, Some(GOLDEN)); #[test] @@ -192,12 +202,13 @@ mod tests { 0u8, &[BOOT_COMPLETE, BOOT_COMPLETE_DUPLICATE_NAME], None, + ATTRS, ); } #[test] fn accepts_a_valid_table() { - let device = DeviceConfig::new("dev", 0u8, &[BOOT_COMPLETE], Some(LAYOUT)); + let device = DeviceConfig::new("dev", 0u8, &[BOOT_COMPLETE], Some(LAYOUT), ATTRS); assert_eq!(device.name(), "dev"); assert_eq!(*device.reset_signal(), 0); assert_eq!(device.checkpoints().len(), 1); @@ -219,28 +230,33 @@ mod tests { /// included: the eRoT never addresses a byte range for it. #[test] fn accepts_a_device_without_a_layout() { - let device = DeviceConfig::new("dev", 0u8, &[BOOT_COMPLETE], None); + let device = DeviceConfig::new("dev", 0u8, &[BOOT_COMPLETE], None, ATTRS); assert!(device.layout().is_none()); } #[test] #[should_panic(expected = "device name must not be empty")] fn rejects_an_empty_device_name() { - let _ = DeviceConfig::new("", 0u8, &[BOOT_COMPLETE], None); + let _ = DeviceConfig::new("", 0u8, &[BOOT_COMPLETE], None, ATTRS); } #[test] #[should_panic(expected = "at least one boot checkpoint")] fn rejects_an_empty_checkpoint_list() { - let _ = DeviceConfig::new("dev", 0u8, &[] as &[BootCheckpoint], None); + let _ = DeviceConfig::new("dev", 0u8, &[] as &[BootCheckpoint], None, ATTRS); } /// Two slots and a golden image need four attempts: three restores /// plus the one the last restore would otherwise never get. #[test] fn accepts_a_retry_budget_that_boots_the_golden_image() { - const DEVICES: &[DeviceConfig] = - &[DeviceConfig::new("dev", 0, &[BOOT_COMPLETE], Some(LAYOUT))]; + const DEVICES: &[DeviceConfig] = &[DeviceConfig::new( + "dev", + 0, + &[BOOT_COMPLETE], + Some(LAYOUT), + ATTRS, + )]; assert_retry_reaches_every_image(4, DEVICES); } @@ -249,7 +265,7 @@ mod tests { #[test] fn accepts_any_retry_budget_for_a_device_without_a_layout() { const DEVICES: &[DeviceConfig] = - &[DeviceConfig::new("dev", 0, &[BOOT_COMPLETE], None)]; + &[DeviceConfig::new("dev", 0, &[BOOT_COMPLETE], None, ATTRS)]; assert_retry_reaches_every_image(0, DEVICES); } @@ -257,8 +273,13 @@ mod tests { /// Three attempts restore the golden image and stop before booting it. #[should_panic(expected = "max_retry is too small")] fn rejects_a_retry_budget_that_never_boots_the_golden_image() { - const DEVICES: &[DeviceConfig] = - &[DeviceConfig::new("dev", 0, &[BOOT_COMPLETE], Some(LAYOUT))]; + const DEVICES: &[DeviceConfig] = &[DeviceConfig::new( + "dev", + 0, + &[BOOT_COMPLETE], + Some(LAYOUT), + ATTRS, + )]; assert_retry_reaches_every_image(3, DEVICES); } } diff --git a/services/orchestrator/config/src/lib.rs b/services/orchestrator/config/src/lib.rs index 7187479fc..509426f5f 100644 --- a/services/orchestrator/config/src/lib.rs +++ b/services/orchestrator/config/src/lib.rs @@ -12,11 +12,14 @@ #![cfg_attr(not(test), no_std)] +pub mod chain; pub mod checkpoint; pub mod device; pub mod layout; pub mod record; +#[doc(inline)] +pub use chain::{chain_of, ChainEntries}; #[doc(inline)] pub use checkpoint::BootCheckpoint; #[doc(inline)] diff --git a/services/orchestrator/driver/BUILD.bazel b/services/orchestrator/driver/BUILD.bazel index bd2399da1..b02c33b5f 100644 --- a/services/orchestrator/driver/BUILD.bazel +++ b/services/orchestrator/driver/BUILD.bazel @@ -16,6 +16,7 @@ rust_library( visibility = ["//visibility:public"], deps = [ "//services/orchestrator/capabilities:orchestrator_capabilities", + "//services/orchestrator/config:orchestrator_config", "//services/orchestrator/sm:orchestrator_sm", "//util/io", "@rust_crates//:heapless", diff --git a/services/orchestrator/driver/src/board.rs b/services/orchestrator/driver/src/board.rs index a16c05595..8f9650788 100644 --- a/services/orchestrator/driver/src/board.rs +++ b/services/orchestrator/driver/src/board.rs @@ -4,7 +4,7 @@ //! What the board supplies to the driver: traits and wiring data only. //! Boards (or test mocks) implement these. -use openprot_orchestrator_sm::{BootFailureKind, ComponentId, ComponentKind}; +use openprot_orchestrator_sm::{BootFailureKind, ComponentId}; use orchestrator_capabilities::Updatable; use util_io::ByteSource; @@ -202,8 +202,10 @@ pub enum SvnFloorBinding { } /// Everything the board supplies, built once at bring-up and handed to -/// `PlatformDriver::new`. Fields are public: executors may need two parts at once -/// (disjoint borrows). +/// [`bring_up`](crate::bring_up). Holds only what a board composes by hand: +/// anything the chain already states, the driver derives instead of taking it +/// here. Fields are public because a board writes this as a literal, and +/// because executors may need two parts at once (disjoint borrows). /// /// ```ignore /// struct Ast1060Board; @@ -217,20 +219,30 @@ pub enum SvnFloorBinding { /// type Updatable = PldmDevice; // device pulls its own chunks /// type Recovery = SlotRecovery; // A/B + golden, attempt-indexed /// type Staging = StagingFlash; // where the update source writes +/// type SelfUpdate = SelfUpdateSession; // session record in the eRoT's own flash /// } -/// let board = Board:: { -/// images: [bmc_image, cpld_image], -/// verifier, -/// boot_controls: [bmc_reset, cpld_reset], -/// boot_watches: [bmc_walk, cpld_walk], -/// component_kinds: [ComponentKind::Active, ComponentKind::Passive], -/// svn_floors: [SvnFloorBinding::Erot(bmc_floor), SvnFloorBinding::SelfManaged], -/// report_sink, -/// updatables: [bmc_update, cpld_update], -/// recovery: [bmc_recovery, cpld_recovery], -/// update_staging, -/// update_stall_budget_millis: 30_000, -/// }; +/// +/// // One chain, from the board's device table, for both halves. The const +/// // item is load-bearing: it forces chain_of's validation at build time. +/// const CHAIN: orchestrator_config::ChainEntries<2> = orchestrator_config::chain_of(&DEVICES); +/// let (orchestrator, driver) = bring_up::( +/// &CHAIN, +/// Board { +/// images: [bmc_image, cpld_image], +/// verifier, +/// boot_controls: [bmc_reset, cpld_reset], +/// boot_watches: [bmc_walk, cpld_walk], +/// svn_floors: [SvnFloorBinding::Erot(bmc_floor), SvnFloorBinding::SelfManaged], +/// report_sink, +/// updatables: [bmc_update, cpld_update], +/// recovery: [bmc_recovery, cpld_recovery], +/// update_staging, +/// update_stall_budget_millis: 30_000, +/// self_update, +/// self_svn_floor, +/// }, +/// MAX_RETRY, +/// ); /// ``` pub struct Board { /// `images[i]` belongs to `ComponentId(i)` — device index = chain @@ -244,10 +256,6 @@ pub struct Board { /// `boot_watches[i]` supervises `ComponentId(i)`'s boot walk, same /// indexing as `images`. pub boot_watches: [B::BootWatch; N], - /// `component_kinds[i]` classifies `ComponentId(i)`: a completed walk becomes - /// `ComponentReady` for `Active`, `Booted` for `Passive`. Comes from - /// the same board table as the SM's chain, so both sides agree. - pub component_kinds: [ComponentKind; N], /// `svn_floors[i]` says who keeps `ComponentId(i)`'s anti-rollback /// floor, same indexing as `images`. pub svn_floors: [SvnFloorBinding; N], diff --git a/services/orchestrator/driver/src/driver.rs b/services/orchestrator/driver/src/driver.rs index 1f341b953..1be728381 100644 --- a/services/orchestrator/driver/src/driver.rs +++ b/services/orchestrator/driver/src/driver.rs @@ -5,9 +5,12 @@ //! the SM through the [`Platform`] impl. use openprot_orchestrator_sm::{ - BootFailureKind, ComponentId, ComponentKind, Effect, EffectError, Event, Orchestrator, Platform, + BootFailureKind, Chain, ComponentAttrs, ComponentId, ComponentKind, Effect, EffectError, Event, + Orchestrator, Platform, }; +use orchestrator_config::ChainEntries; + use crate::board::{ Board, BoardCapabilities, ImageSource, Report, ReportSink, SvnFloorBinding, Verdict, Verifier, }; @@ -92,6 +95,10 @@ impl core::error::Error for DriverError {} /// the driver's own fields are bookkeeping. pub struct PlatformDriver { board: Board, + /// `kinds[i]` classifies `ComponentId(i)`, derived from the chain the + /// state machine runs on. A completed walk becomes `ComponentReady` for + /// an `Active` component and `Booted` for a `Passive` one. + kinds: [ComponentKind; N], /// Component whose image is staged (source opened) for verification. staged: Option, /// `watching[i]`: `ComponentId(i)` is out of reset with a walk in @@ -162,11 +169,31 @@ enum UpdatePhase { } impl PlatformDriver { - pub fn new(board: Board) -> Self { - // ComponentId is a u8, so ids for N > 256 components would wrap. - const { assert!(N <= 256) }; + /// Derives what the chain already states instead of taking it twice: the + /// component kinds come from `entries`, the same entries the state machine + /// is built from. + /// + /// # Panics + /// + /// Panics if an entry's id is not its position. The driver indexes every + /// per-component array by `id.get()`, so an entry out of position would + /// address the wrong component's reset line, flash and floor. + pub(crate) fn new(entries: &[(ComponentId, ComponentAttrs); N], board: Board) -> Self { + // ComponentId is a u8; Chain rejects more than u8::MAX entries. + const { assert!(N <= u8::MAX as usize) }; + let mut kinds = [ComponentKind::Passive; N]; + let mut i = 0; + while i < N { + assert!( + entries[i].0.get() as usize == i, + "a chain entry's id must be its position in the chain" + ); + kinds[i] = entries[i].1.kind; + i += 1; + } Self { board, + kinds, staged: None, watching: [false; N], verified_svn: [None; N], @@ -532,7 +559,7 @@ impl PlatformDriver { } WalkVerdict::Complete => { self.watching[idx] = false; - let event = match self.board.component_kinds[idx] { + let event = match self.kinds[idx] { ComponentKind::Active => Event::ComponentReady(id), ComponentKind::Passive => Event::Booted(id), }; @@ -690,6 +717,32 @@ impl Platform for PlatformDriver { } } +/// Brings a platform up from one chain: the state machine that decides and the +/// driver that acts, built from the same entries so neither can be holding a +/// different list of components than the other. +/// +/// Takes a [`ChainEntries`] returned by `orchestrator_config::chain_of`, +/// which validates the table at const time. Boards declare the result as a +/// `const` item, so an invalid table is a build error, not a runtime panic. +pub fn bring_up( + chain_entries: &'static ChainEntries, + board: Board, + max_retry: u8, +) -> (Orchestrator, PlatformDriver) { + let entries = chain_entries.entries(); + // chain_of validated: nonempty, at most u8::MAX, ids are positions, + // deps strictly earlier. Chain::try_from rechecks the same invariants + // at runtime, so the expect cannot fire. + let chain: Chain = heapless::Vec::from_slice(entries) + .expect("same length as the capacity") + .try_into() + .expect("chain_of validated the entries"); + ( + Orchestrator::new(chain, max_retry), + PlatformDriver::new(entries, board), + ) +} + /// The connection between an update frontend and the SM: called (by the /// event loop, on the frontend's behalf) once a complete candidate for /// `target` sits in the staging region. Records the job first, then injects diff --git a/services/orchestrator/driver/src/lib.rs b/services/orchestrator/driver/src/lib.rs index c26e8b903..b19145e87 100644 --- a/services/orchestrator/driver/src/lib.rs +++ b/services/orchestrator/driver/src/lib.rs @@ -38,4 +38,4 @@ mod tests; pub use board::{ Board, BoardCapabilities, ImageSource, Report, ReportSink, SvnFloorBinding, Verdict, Verifier, }; -pub use driver::{request_update, BootWalkPoll, DriverError, PlatformDriver, UpdatePoll}; +pub use driver::{bring_up, request_update, BootWalkPoll, DriverError, PlatformDriver, UpdatePoll}; diff --git a/services/orchestrator/driver/src/tests.rs b/services/orchestrator/driver/src/tests.rs index ea2a26b70..ebeec55a7 100644 --- a/services/orchestrator/driver/src/tests.rs +++ b/services/orchestrator/driver/src/tests.rs @@ -276,6 +276,28 @@ impl MockFloor { } } +/// Chain entries for an all-passive chain of `N` components, which is what +/// most of these tests want. Ids are positions, as the driver requires. +fn passive_entries() -> [(ComponentId, ComponentAttrs); N] { + core::array::from_fn(|i| { + ( + ComponentId::new(i as u8), + ComponentAttrs::passive_required(), + ) + }) +} + +/// The same, with the kinds a test cares about. +fn entries_with_kinds( + kinds: [ComponentKind; N], +) -> [(ComponentId, ComponentAttrs); N] { + core::array::from_fn(|i| { + let mut attrs = ComponentAttrs::passive_required(); + attrs.kind = kinds[i]; + (ComponentId::new(i as u8), attrs) + }) +} + /// Update adapter without a HAL. Wiring-only for now: it stages the whole /// payload in one step. The update pump replaces it with a stepping mock /// when the executors land. @@ -471,7 +493,6 @@ fn mock_board() -> Board { }, boot_controls: core::array::from_fn(|_| MockReset::new()), boot_watches: core::array::from_fn(|_| MockWalk::idle()), - component_kinds: core::array::from_fn(|_| ComponentKind::Passive), svn_floors: core::array::from_fn(|_| SvnFloorBinding::Erot(MockFloor::new())), report_sink: RecordingSink::new(), updatables: core::array::from_fn(|_| MockUpdatable::new()), @@ -482,10 +503,13 @@ fn mock_board() -> Board { } fn driver(images: [MemImage; 1]) -> PlatformDriver { - PlatformDriver::new(Board { - images, - ..mock_board() - }) + PlatformDriver::new( + &passive_entries(), + Board { + images, + ..mock_board() + }, + ) } fn orchestrator() -> Orchestrator<1, 4> { @@ -562,13 +586,16 @@ fn unreadable_source_fails_closed() { #[test] fn verifier_fault_fails_closed() { let mut orch = orchestrator(); - let mut driver = PlatformDriver::::new(Board { - verifier: XorVerifier { - fault: true, - svn: MOCK_SVN, + let mut driver = PlatformDriver::::new( + &passive_entries(), + Board { + verifier: XorVerifier { + fault: true, + svn: MOCK_SVN, + }, + ..mock_board() }, - ..mock_board() - }); + ); orch.dispatch(&mut driver, Event::PowerGood(PowerOnResult::Provisioned)); @@ -579,7 +606,7 @@ const C1: ComponentId = ComponentId::new(1); #[test] fn verify_for_a_different_component_is_refused() { - let mut driver = PlatformDriver::::new(mock_board()); + let mut driver = PlatformDriver::::new(&passive_entries(), mock_board()); driver.stage_firmware(C0).unwrap(); @@ -609,7 +636,7 @@ fn verify_of_unknown_component_is_refused() { #[test] fn reset_release_and_assert_reach_the_boot_control() { - let mut driver = PlatformDriver::::new(mock_board()); + let mut driver = PlatformDriver::::new(&passive_entries(), mock_board()); driver.release_reset(C0).unwrap(); assert!(!driver.board().boot_controls[0].held.get()); @@ -636,10 +663,13 @@ fn reset_of_unknown_component_is_refused() { fn reset_line_fault_is_reported() { let mut control = MockReset::new(); control.fail = true; - let mut driver = PlatformDriver::::new(Board { - boot_controls: [control], - ..mock_board() - }); + let mut driver = PlatformDriver::::new( + &passive_entries(), + Board { + boot_controls: [control], + ..mock_board() + }, + ); assert_eq!(driver.release_reset(C0), Err(DriverError::BootControlFault)); assert_eq!(driver.assert_reset(C0), Err(DriverError::BootControlFault)); @@ -716,26 +746,28 @@ fn release_follows_verification() { let control = MockReset::new(); let held = control.held.clone(); let held_during_verify = std::rc::Rc::new(core::cell::Cell::new(false)); - let mut driver = PlatformDriver::::new(Board { - images: [MemImage::holding(valid_image())], - verifier: LineWatchingVerifier { - inner: XorVerifier { - fault: false, - svn: MOCK_SVN, + let mut driver = PlatformDriver::::new( + &passive_entries(), + Board { + images: [MemImage::holding(valid_image())], + verifier: LineWatchingVerifier { + inner: XorVerifier { + fault: false, + svn: MOCK_SVN, + }, + line: held.clone(), + held_during_verify: held_during_verify.clone(), }, - line: held.clone(), - held_during_verify: held_during_verify.clone(), + boot_controls: [control], + boot_watches: [MockWalk::idle()], + svn_floors: [SvnFloorBinding::Erot(MockFloor::new())], + report_sink: (), + updatables: [MockUpdatable::new()], + recovery: [()], + update_staging: MemStaging::new(), + update_stall_budget_millis: STALL_BUDGET_MILLIS, }, - boot_controls: [control], - boot_watches: [MockWalk::idle()], - component_kinds: [ComponentKind::Passive], - svn_floors: [SvnFloorBinding::Erot(MockFloor::new())], - report_sink: (), - updatables: [MockUpdatable::new()], - recovery: [()], - update_staging: MemStaging::new(), - update_stall_budget_millis: STALL_BUDGET_MILLIS, - }); + ); let mut orch = orchestrator(); orch.dispatch(&mut driver, Event::PowerGood(PowerOnResult::Provisioned)); @@ -755,10 +787,13 @@ fn failed_release_fails_closed() { let mut control = MockReset::new(); control.fail = true; let held = control.held.clone(); - let mut driver = PlatformDriver::::new(Board { - boot_controls: [control], - ..mock_board() - }); + let mut driver = PlatformDriver::::new( + &passive_entries(), + Board { + boot_controls: [control], + ..mock_board() + }, + ); let mut orch = orchestrator(); orch.dispatch(&mut driver, Event::PowerGood(PowerOnResult::Provisioned)); @@ -777,11 +812,13 @@ fn walk_driver( walks: [MockWalk; 2], component_kinds: [ComponentKind; 2], ) -> PlatformDriver { - PlatformDriver::new(Board { - boot_watches: walks, - component_kinds, - ..mock_board() - }) + PlatformDriver::new( + &entries_with_kinds(component_kinds), + Board { + boot_watches: walks, + ..mock_board() + }, + ) } // A completed walk becomes ComponentReady for Active, Booted for Passive. @@ -981,10 +1018,13 @@ fn rerelease_arms_a_fresh_walk() { #[test] fn booted_walk_settles_in_ready() { let mut orch = orchestrator(); - let mut driver = PlatformDriver::::new(Board { - boot_watches: [MockWalk::scripted(std::vec![WalkVerdict::Complete])], - ..mock_board() - }); + let mut driver = PlatformDriver::::new( + &passive_entries(), + Board { + boot_watches: [MockWalk::scripted(std::vec![WalkVerdict::Complete])], + ..mock_board() + }, + ); orch.dispatch(&mut driver, Event::PowerGood(PowerOnResult::Provisioned)); assert_eq!(orch.state(), State::Ready); @@ -1002,13 +1042,16 @@ fn booted_walk_settles_in_ready() { #[test] fn boot_failure_locks_when_recovery_is_exhausted() { let mut orch = orchestrator(); - let mut driver = PlatformDriver::::new(Board { - boot_watches: [MockWalk::scripted(std::vec![WalkVerdict::Failed { - checkpoint: "heartbeat", - cause: FailureCause::TimedOut, - }])], - ..mock_board() - }); + let mut driver = PlatformDriver::::new( + &passive_entries(), + Board { + boot_watches: [MockWalk::scripted(std::vec![WalkVerdict::Failed { + checkpoint: "heartbeat", + cause: FailureCause::TimedOut, + }])], + ..mock_board() + }, + ); orch.dispatch(&mut driver, Event::PowerGood(PowerOnResult::Provisioned)); assert_eq!(orch.state(), State::Ready); @@ -1092,7 +1135,6 @@ fn recoverable_board(sources: u8) -> Board }, boot_controls: core::array::from_fn(|_| MockReset::new()), boot_watches: core::array::from_fn(|_| MockWalk::idle()), - component_kinds: core::array::from_fn(|_| ComponentKind::Passive), svn_floors: core::array::from_fn(|_| SvnFloorBinding::Erot(MockFloor::new())), report_sink: RecordingSink::new(), updatables: core::array::from_fn(|_| MockUpdatable::new()), @@ -1108,7 +1150,8 @@ fn recoverable_board(sources: u8) -> Board // RecoverComponent with a successful restore returns Restored(id). #[test] fn recover_component_returns_restored() { - let mut driver = PlatformDriver::::new(recoverable_board(2)); + let mut driver = + PlatformDriver::::new(&passive_entries(), recoverable_board(2)); assert_eq!(driver.recover_component(C0, 0), Ok(Event::Restored(C0))); assert_eq!(driver.recover_component(C0, 1), Ok(Event::Restored(C0))); @@ -1117,7 +1160,8 @@ fn recover_component_returns_restored() { // RecoverComponent past the last source returns RecoveryUnavailable(id). #[test] fn recover_component_returns_unavailable_when_exhausted() { - let mut driver = PlatformDriver::::new(recoverable_board(1)); + let mut driver = + PlatformDriver::::new(&passive_entries(), recoverable_board(1)); assert_eq!(driver.recover_component(C0, 0), Ok(Event::Restored(C0))); assert_eq!( @@ -1130,13 +1174,16 @@ fn recover_component_returns_unavailable_when_exhausted() { // succeeds, so a fault does not poison the path. #[test] fn recovery_fault_is_reported() { - let mut driver = PlatformDriver::::new(Board { - recovery: [MockRecovery { - sources: 2, - fail_on: Some(0), - }], - ..recoverable_board(2) - }); + let mut driver = PlatformDriver::::new( + &passive_entries(), + Board { + recovery: [MockRecovery { + sources: 2, + fail_on: Some(0), + }], + ..recoverable_board(2) + }, + ); assert_eq!( driver.recover_component(C0, 0), @@ -1149,7 +1196,8 @@ fn recovery_fault_is_reported() { // An unknown component is refused before the mechanism is consulted. #[test] fn recover_unknown_component_is_refused() { - let mut driver = PlatformDriver::::new(recoverable_board(2)); + let mut driver = + PlatformDriver::::new(&passive_entries(), recoverable_board(2)); assert_eq!( driver.recover_component(ComponentId::new(9), 0), @@ -1160,7 +1208,7 @@ fn recover_unknown_component_is_refused() { // The `()` impl reports exhaustion on every attempt: no sources exist. #[test] fn unit_recovery_always_exhausted() { - let mut driver = PlatformDriver::::new(mock_board()); + let mut driver = PlatformDriver::::new(&passive_entries(), mock_board()); assert_eq!( driver.recover_component(C0, 0), @@ -1174,20 +1222,24 @@ fn unit_recovery_always_exhausted() { fn execute_routes_recover_component() { use openprot_orchestrator_sm::{Effect, EffectError, Platform}; - let mut driver = PlatformDriver::::new(recoverable_board(2)); + let mut driver = + PlatformDriver::::new(&passive_entries(), recoverable_board(2)); assert_eq!( driver.execute(Effect::RecoverComponent { id: C0, attempt: 0 }), Ok(Some(Event::Restored(C0))) ); - let mut faulting_driver = PlatformDriver::::new(Board { - recovery: [MockRecovery { - sources: 2, - fail_on: Some(0), - }], - ..recoverable_board(2) - }); + let mut faulting_driver = PlatformDriver::::new( + &passive_entries(), + Board { + recovery: [MockRecovery { + sources: 2, + fail_on: Some(0), + }], + ..recoverable_board(2) + }, + ); assert_eq!( faulting_driver.execute(Effect::RecoverComponent { id: C0, attempt: 0 }), @@ -1258,7 +1310,7 @@ fn every_report_reaches_a_sink() { // after a verification has passed — the two halves of the commit contract. #[test] fn commit_advances_the_floor_to_the_verified_svn() { - let mut driver = PlatformDriver::::new(mock_board()); + let mut driver = PlatformDriver::::new(&passive_entries(), mock_board()); driver .execute(Effect::ReadFirmware(C0)) @@ -1284,10 +1336,13 @@ fn commit_advances_the_floor_to_the_verified_svn() { // mis-advance. #[test] fn commit_without_an_erot_floor_is_a_no_op() { - let mut driver = PlatformDriver::::new(Board { - svn_floors: [SvnFloorBinding::SelfManaged], - ..mock_board() - }); + let mut driver = PlatformDriver::::new( + &passive_entries(), + Board { + svn_floors: [SvnFloorBinding::SelfManaged], + ..mock_board() + }, + ); assert_eq!(driver.execute(Effect::CommitSvnFloor(C0)), Ok(None)); } @@ -1308,10 +1363,13 @@ fn commit_without_a_verified_image_fails_closed() { fn rejected_image_clears_the_verified_svn() { let mut corrupt = valid_image(); corrupt[7] ^= 0x01; - let mut driver = PlatformDriver::::new(Board { - images: [MemImage::holding(valid_image()).reflash_on_reopen(corrupt)], - ..mock_board() - }); + let mut driver = PlatformDriver::::new( + &passive_entries(), + Board { + images: [MemImage::holding(valid_image()).reflash_on_reopen(corrupt)], + ..mock_board() + }, + ); driver.stage_firmware(C0).expect("stage failed"); assert_eq!( @@ -1337,10 +1395,13 @@ fn rejected_image_clears_the_verified_svn() { fn floor_fault_is_reported() { let mut mock = MockFloor::new(); mock.fail = true; - let mut driver = PlatformDriver::::new(Board { - svn_floors: [SvnFloorBinding::Erot(mock)], - ..mock_board() - }); + let mut driver = PlatformDriver::::new( + &passive_entries(), + Board { + svn_floors: [SvnFloorBinding::Erot(mock)], + ..mock_board() + }, + ); driver.stage_firmware(C0).expect("stage failed"); driver.verify_firmware(C0).expect("verify failed"); @@ -1352,13 +1413,16 @@ fn floor_fault_is_reported() { // hands back an error for the SM to fail closed on. #[test] fn reports_reach_the_board_sink() { - let mut driver = PlatformDriver::::new(Board { - verifier: XorVerifier { - fault: false, - svn: 0, + let mut driver = PlatformDriver::::new( + &passive_entries(), + Board { + verifier: XorVerifier { + fault: false, + svn: 0, + }, + ..mock_board() }, - ..mock_board() - }); + ); for effect in [ Effect::ReportIsolated(C0), @@ -1382,13 +1446,16 @@ fn reports_reach_the_board_sink() { // fail-closed path. #[test] fn reporting_an_isolated_component_does_not_lock_the_platform() { - let mut driver = PlatformDriver::::new(Board { - verifier: XorVerifier { - fault: false, - svn: 0, + let mut driver = PlatformDriver::::new( + &passive_entries(), + Board { + verifier: XorVerifier { + fault: false, + svn: 0, + }, + ..mock_board() }, - ..mock_board() - }); + ); let mut chain = heapless::Vec::<_, 2>::new(); chain .push((C0, ComponentAttrs::passive_required())) @@ -1551,10 +1618,13 @@ fn aborted_update_clears_pending_update() { } fn update_driver(updatable: MockUpdatable) -> PlatformDriver { - PlatformDriver::new(Board { - updatables: [updatable], - ..mock_board() - }) + PlatformDriver::new( + &passive_entries(), + Board { + updatables: [updatable], + ..mock_board() + }, + ) } /// Submits a job and runs the entry executor, as entry to `Updating` @@ -1793,6 +1863,36 @@ fn a_rejected_update_returns_the_platform_to_ready() { assert!(!driver.board().updatables[0].active); } +// bring_up takes a ChainEntries, the validated output of chain_of. The +// machine and the driver cannot be holding different component lists +// because the entries come from one const-validated device table. +#[test] +fn bring_up_builds_both_halves_from_one_chain() { + use core::time::Duration; + use orchestrator_config::{chain_of, BootCheckpoint, ChainEntries, DeviceConfig}; + + const CP: BootCheckpoint = BootCheckpoint::new("up", 1, Duration::from_millis(10)); + const fn dev(attrs: ComponentAttrs) -> DeviceConfig { + DeviceConfig::new("dev", 0, &[CP], None, attrs) + } + const TABLE: [DeviceConfig; 1] = [dev(ComponentAttrs::active_required())]; + const CHAIN: ChainEntries<1> = chain_of(&TABLE); + + let (orch, driver) = bring_up::(&CHAIN, mock_board(), 3); + + assert_eq!(orch.state(), State::PowerOnReset); + assert!(driver.pending_update().is_none()); +} + +// The driver indexes every per-component array by id, so an entry out of +// position would address the wrong component's reset line and flash. +#[test] +#[should_panic(expected = "id must be its position")] +fn an_entry_out_of_position_is_refused() { + let entries = [(ComponentId::new(3), ComponentAttrs::passive_required())]; + let _ = PlatformDriver::::new(&entries, mock_board()); +} + // Before the platform is in service nothing would report the request // deferred, so it is refused and no job is recorded. #[test] @@ -1813,13 +1913,16 @@ fn a_request_before_the_platform_is_in_service_is_refused() { #[test] fn a_request_to_a_locked_platform_is_refused() { let mut orch = orchestrator(); - let mut driver = PlatformDriver::::new(Board { - verifier: XorVerifier { - fault: true, - svn: MOCK_SVN, + let mut driver = PlatformDriver::::new( + &passive_entries(), + Board { + verifier: XorVerifier { + fault: true, + svn: MOCK_SVN, + }, + ..mock_board() }, - ..mock_board() - }); + ); orch.dispatch(&mut driver, Event::PowerGood(PowerOnResult::Provisioned)); assert_eq!(orch.state(), State::Locked); diff --git a/services/orchestrator/sm/src/model.rs b/services/orchestrator/sm/src/model.rs index e561b8043..c5abb60c5 100644 --- a/services/orchestrator/sm/src/model.rs +++ b/services/orchestrator/sm/src/model.rs @@ -485,10 +485,13 @@ impl State { /// dependency is always walked before its dependents), /// - the length fits `u8`, the `cursor` index type. /// +/// Boards build it from their device table rather than entry by entry, so the +/// table stays the only place a component is declared: +/// /// ```ignore -/// let mut v = heapless::Vec::<_, 4>::new(); -/// v.push((ComponentId::new(0), ComponentAttrs::passive_required())).unwrap(); -/// let chain: Chain<4> = v.try_into()?; +/// let validated = orchestrator_config::chain_of(&DEVICES); +/// let entries = heapless::Vec::from_slice(validated.entries())?; +/// let chain: Chain<2> = entries.try_into()?; /// ``` #[derive(Clone, Debug)] pub struct Chain { diff --git a/services/orchestrator/test/BUILD.bazel b/services/orchestrator/test/BUILD.bazel index 10919bb77..fb9ea94dc 100644 --- a/services/orchestrator/test/BUILD.bazel +++ b/services/orchestrator/test/BUILD.bazel @@ -10,7 +10,10 @@ rust_library( srcs = ["devices.rs"], crate_name = "board_devices", edition = "2024", - deps = ["//services/orchestrator/config:orchestrator_config"], + deps = [ + "//services/orchestrator/config:orchestrator_config", + "//services/orchestrator/sm:orchestrator_sm", + ], ) rust_library( diff --git a/services/orchestrator/test/devices.rs b/services/orchestrator/test/devices.rs index b32b05b7a..4979ed92a 100644 --- a/services/orchestrator/test/devices.rs +++ b/services/orchestrator/test/devices.rs @@ -9,9 +9,10 @@ use core::time::Duration; +use openprot_orchestrator_sm::ComponentAttrs; use orchestrator_config::{ - assert_retry_reaches_every_image, BootCheckpoint, DeviceConfig, Golden, ImageLayout, Region, - Slot, SlotId, + assert_retry_reaches_every_image, chain_of, BootCheckpoint, ChainEntries, DeviceConfig, Golden, + ImageLayout, Region, Slot, SlotId, }; /// The mock board's boot-probe vocabulary. The schema carries these @@ -45,7 +46,7 @@ const BMC_LAYOUT: ImageLayout = ImageLayout::new( /// /// The mock board's reset controller addresses reset lines by plain index, /// so the reset id type is `u8`. -pub const MANAGED_DEVICES: &[DeviceConfig] = &[ +pub const MANAGED_DEVICES: [DeviceConfig; 2] = [ // Direct-flash SPI device (BMC archetype): the eRoT fronts its flash. // Single checkpoint: it raises a boot-complete GPIO. DeviceConfig::new( @@ -60,6 +61,9 @@ pub const MANAGED_DEVICES: &[DeviceConfig] = &[ // are counted from the start of the BMC's flash area; real // boards declare their own. Some(BMC_LAYOUT), + // No iRoT of its own, so the eRoT's check is the only one and the + // boot-complete GPIO is the only signal it sends back. + ComponentAttrs::passive_required(), ), // PLDM device (NIC archetype): self-updating, SPDM-capable. Two // checkpoints, exercising the multi-checkpoint path: transport up @@ -75,9 +79,16 @@ pub const MANAGED_DEVICES: &[DeviceConfig] = &[ // so the eRoT addresses no byte range for it and declares no // layout. None, + // SPDM-capable, so it has an iRoT that verifies itself and reports + // ready once it has. + ComponentAttrs::active_required(), ), ]; +/// The orchestrator's chain for this board, derived from the table above so +/// the two cannot name different components. +pub const CHAIN: ChainEntries<{ MANAGED_DEVICES.len() }> = chain_of(&MANAGED_DEVICES); + /// Board-local checks the schema constructors cannot do — they know the /// schema's shape, not this board's meanings. Const-fence pattern: a bad /// probe fails the build. @@ -97,7 +108,7 @@ const fn validate_probes(devices: &[DeviceConfig]) { } } -const _: () = validate_probes(MANAGED_DEVICES); +const _: () = validate_probes(&MANAGED_DEVICES); /// How many times the orchestrator restores a component before it gives up. /// Four, because the BMC has three images and the attempt that restores the @@ -105,4 +116,4 @@ const _: () = validate_probes(MANAGED_DEVICES); /// golden image. pub const MAX_RETRY: u8 = 4; -const _: () = assert_retry_reaches_every_image(MAX_RETRY, MANAGED_DEVICES); +const _: () = assert_retry_reaches_every_image(MAX_RETRY, &MANAGED_DEVICES); diff --git a/target/ast10x0/board/BUILD.bazel b/target/ast10x0/board/BUILD.bazel index 856e7b9c0..4c2c64523 100644 --- a/target/ast10x0/board/BUILD.bazel +++ b/target/ast10x0/board/BUILD.bazel @@ -23,6 +23,7 @@ rust_library( "//services/orchestrator/adapters/hal:orchestrator_hal_adapters", "//services/orchestrator/capabilities:orchestrator_capabilities", "//services/orchestrator/config:orchestrator_config", + "//services/orchestrator/sm:orchestrator_sm", "//target/ast10x0/backend/i2c:i2c_backend_ast10x0", "//target/ast10x0/peripherals", "@ast1060_pac", @@ -46,6 +47,7 @@ rust_test( "//services/orchestrator/adapters/walk:orchestrator_checkpoint_walk", "//services/orchestrator/capabilities:orchestrator_capabilities", "//services/orchestrator/config:orchestrator_config", + "//services/orchestrator/sm:orchestrator_sm", "@rust_crates//:embedded-hal", ], ) diff --git a/target/ast10x0/board/src/bmc.rs b/target/ast10x0/board/src/bmc.rs index 1951717d3..48b5d1729 100644 --- a/target/ast10x0/board/src/bmc.rs +++ b/target/ast10x0/board/src/bmc.rs @@ -15,8 +15,9 @@ use core::time::Duration; use openprot_hal_blocking::gpio_port::ActivePolarity; use openprot_hal_blocking::{DelayNs, InputPin, OutputPin}; +use openprot_orchestrator_sm::ComponentAttrs; use orchestrator_capabilities::{BootStatus, EvidenceReader}; -use orchestrator_config::{BootCheckpoint, DeviceConfig}; +use orchestrator_config::{chain_of, BootCheckpoint, ChainEntries, DeviceConfig}; use orchestrator_hal_adapters::{ GpioReadyMonitor, GpioResetControl, ReadyLineError, ResetLineError, }; @@ -49,9 +50,22 @@ const CHECKPOINTS: &[BootCheckpoint] = &[BootCheckpoint::new( READY_WINDOW, )]; -/// The mock BMC's entry in this board's device table. -pub const BMC: DeviceConfig = - DeviceConfig::new("bmc", BmcReset::Bmc, CHECKPOINTS, None); +/// The mock BMC's entry in this board's device table. Passive: it has no +/// iRoT to self-verify, so its ready line is the only post-release signal +/// it produces. Required: the platform has nothing to run without it. +pub const BMC: DeviceConfig = DeviceConfig::new( + "bmc", + BmcReset::Bmc, + CHECKPOINTS, + None, + ComponentAttrs::passive_required(), +); + +/// Every device this board manages, in the order that assigns their ids. +pub const DEVICES: [DeviceConfig; 1] = [BMC]; + +/// The orchestrator's chain, derived from [`DEVICES`]. +pub const CHAIN: ChainEntries<{ DEVICES.len() }> = chain_of(&DEVICES); /// The BMC's ready line is driven high when it has booted. /// diff --git a/target/ast10x0/boot_evidence/BUILD.bazel b/target/ast10x0/boot_evidence/BUILD.bazel index 21271a267..80385d77b 100644 --- a/target/ast10x0/boot_evidence/BUILD.bazel +++ b/target/ast10x0/boot_evidence/BUILD.bazel @@ -13,6 +13,7 @@ rust_library( "//services/orchestrator/adapters/hal:orchestrator_hal_adapters", "//services/orchestrator/capabilities:orchestrator_capabilities", "//services/orchestrator/config:orchestrator_config", + "//services/orchestrator/sm:orchestrator_sm", ], ) diff --git a/target/ast10x0/boot_evidence/src/lib.rs b/target/ast10x0/boot_evidence/src/lib.rs index ce0ebbbd3..972cc45a8 100644 --- a/target/ast10x0/boot_evidence/src/lib.rs +++ b/target/ast10x0/boot_evidence/src/lib.rs @@ -71,6 +71,7 @@ pub const BMC_DEVICE: orchestrator_config::DeviceConfig = core::time::Duration::from_millis(500), )], None, + openprot_orchestrator_sm::ComponentAttrs::passive_required(), ); #[cfg(test)]