diff --git a/services/orchestrator/driver/src/board.rs b/services/orchestrator/driver/src/board.rs index 78ca53d3..1363fee0 100644 --- a/services/orchestrator/driver/src/board.rs +++ b/services/orchestrator/driver/src/board.rs @@ -5,6 +5,7 @@ //! Boards (or test mocks) implement these. use openprot_orchestrator_sm::{ComponentId, ComponentKind}; +use orchestrator_capabilities::Updatable; pub use orchestrator_capabilities::{BootControl, BootWatch}; @@ -101,7 +102,9 @@ pub trait BoardCapabilities { type BootControl: BootControl; /// Boot-checkpoint supervision for the managed components. type BootWatch: BootWatch; - // Later seams: Recovery, Staging. + /// Stages and activates update payloads on the managed components. + type Updatable: Updatable; + // Later seams: Recovery. } /// Everything the board supplies, built once at bring-up and handed to @@ -115,6 +118,7 @@ pub trait BoardCapabilities { /// type Verifier = ManifestVerifier; // signature + SVN via the crypto engine /// type BootControl = ExtrstGpio; // per-component reset line /// type BootWatch = CheckpointWalk; // GPIO checkpoint walk over the boot window +/// type Updatable = PldmDevice; // device pulls its own chunks /// } /// let board = Board:: { /// images: [bmc_image, cpld_image], @@ -122,6 +126,7 @@ pub trait BoardCapabilities { /// boot_controls: [bmc_reset, cpld_reset], /// boot_watches: [bmc_walk, cpld_walk], /// component_kinds: [ComponentKind::Active, ComponentKind::Passive], +/// updatables: [bmc_update, cpld_update], /// }; /// ``` pub struct Board { @@ -140,5 +145,9 @@ pub struct Board { /// `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], + /// `updatables[i]` stages updates for `ComponentId(i)`, same indexing + /// as `images`. A device without an update path wires an adapter whose + /// `poll_stage` errors. + pub updatables: [B::Updatable; N], // Later seams add fields, e.g. recovery: [B::Recovery; N]. } diff --git a/services/orchestrator/driver/src/driver.rs b/services/orchestrator/driver/src/driver.rs index 5f3d9cc2..8e8929dc 100644 --- a/services/orchestrator/driver/src/driver.rs +++ b/services/orchestrator/driver/src/driver.rs @@ -4,7 +4,9 @@ //! The [`PlatformDriver`]: one executor method per [`Effect`] variant, routed from //! the SM through the [`Platform`] impl. -use openprot_orchestrator_sm::{ComponentId, ComponentKind, Effect, EffectError, Event, Platform}; +use openprot_orchestrator_sm::{ + ComponentId, ComponentKind, Effect, EffectError, Event, Orchestrator, Platform, +}; use crate::board::{Board, BoardCapabilities, ImageSource, Verdict, Verifier}; use orchestrator_capabilities::{BootControl, BootWatch, WalkVerdict}; @@ -23,6 +25,9 @@ pub enum DriverError { VerifierFault, /// The component's boot control could not actuate the reset line. BootControlFault, + /// An update is already in flight; the frontend answers the requester + /// over its own protocol, the SM never sees the refused request. + UpdateBusy, } impl core::fmt::Display for DriverError { @@ -33,6 +38,7 @@ impl core::fmt::Display for DriverError { DriverError::NotStaged => "no image staged for this component", DriverError::VerifierFault => "verifier could not perform the check", DriverError::BootControlFault => "boot control could not actuate the reset", + DriverError::UpdateBusy => "an update is already in flight", }) } } @@ -50,6 +56,15 @@ pub struct PlatformDriver { /// terminal verdict. Only watched walks are polled, so a finished or /// quiesced walk emits no stale event. watching: [bool; N], + /// The update job submitted by the frontend, target only for now; the + /// pump state joins it when the executors land. Held until the update + /// is activated or discarded. + pending_update: Option, +} + +/// One in-flight update, recorded by [`PlatformDriver::submit_update`]. +struct UpdateJob { + target: ComponentId, } impl PlatformDriver { @@ -60,9 +75,33 @@ impl PlatformDriver { board, staged: None, watching: [false; N], + pending_update: None, } } + /// The frontend half of the update handshake: record `target` as the + /// component the staged candidate is for. Must succeed BEFORE + /// [`Event::UpdateRequest`] is dispatched; `StageUpdate` with no stored + /// job fails closed. Refuses an unknown id and a second submit while + /// one update is in flight; nothing is stored on refusal, so a refused + /// request can never surface as an update event. + pub fn submit_update(&mut self, target: ComponentId) -> Result<(), DriverError> { + self.board + .updatables + .get(target.get() as usize) + .ok_or(DriverError::UnknownComponent)?; + if self.pending_update.is_some() { + return Err(DriverError::UpdateBusy); + } + self.pending_update = Some(UpdateJob { target }); + Ok(()) + } + + /// Target of the in-flight update, if one was submitted. + pub fn pending_update(&self) -> Option { + self.pending_update.as_ref().map(|job| job.target) + } + /// `id`'s image source. Takes the array rather than `&mut self` so the /// caller can borrow `board.verifier` alongside the returned image. fn source(images: &mut [B::Image; N], id: ComponentId) -> Result<&mut B::Image, DriverError> { @@ -240,3 +279,19 @@ impl Platform for PlatformDriver { .map_err(|_| EffectError) } } + +/// 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 +/// [`Event::UpdateRequest`]; that order is load-bearing, `StageUpdate` can +/// never run without a target. On refusal no event is injected and the +/// frontend answers the requester over its own protocol. +pub fn request_update( + orchestrator: &mut Orchestrator, + driver: &mut PlatformDriver, + target: ComponentId, +) -> Result<(), DriverError> { + driver.submit_update(target)?; + orchestrator.dispatch(driver, Event::UpdateRequest); + Ok(()) +} diff --git a/services/orchestrator/driver/src/lib.rs b/services/orchestrator/driver/src/lib.rs index d251b463..33f57512 100644 --- a/services/orchestrator/driver/src/lib.rs +++ b/services/orchestrator/driver/src/lib.rs @@ -35,4 +35,4 @@ mod driver; mod tests; pub use board::{Board, BoardCapabilities, ImageSource, Verdict, Verifier}; -pub use driver::{BootWalkPoll, DriverError, PlatformDriver}; +pub use driver::{request_update, BootWalkPoll, DriverError, PlatformDriver}; diff --git a/services/orchestrator/driver/src/tests.rs b/services/orchestrator/driver/src/tests.rs index f98851af..d094dbf7 100644 --- a/services/orchestrator/driver/src/tests.rs +++ b/services/orchestrator/driver/src/tests.rs @@ -224,6 +224,46 @@ impl BootWatch for MockWalk { } } +/// 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. +struct MockUpdatable { + ready: bool, + active: bool, +} + +impl MockUpdatable { + fn new() -> Self { + Self { + ready: false, + active: false, + } + } +} + +impl orchestrator_capabilities::Updatable for MockUpdatable { + fn poll_stage( + &mut self, + _payload: &dyn orchestrator_capabilities::PayloadSource, + ) -> Result + { + self.ready = true; + Ok(orchestrator_capabilities::StageProgress::Ready) + } + + fn abandon(&mut self) { + self.ready = false; + } + + fn activate(&mut self) -> Result<(), orchestrator_capabilities::UpdateError> { + if !self.ready { + return Err(orchestrator_capabilities::UpdateError::NothingStaged); + } + self.active = true; + Ok(()) + } +} + /// The test board's type choices. struct MockBoard; @@ -232,6 +272,7 @@ impl BoardCapabilities for MockBoard { type Verifier = XorVerifier; type BootControl = MockReset; type BootWatch = MockWalk; + type Updatable = MockUpdatable; } fn driver(images: [MemImage; 1]) -> PlatformDriver { @@ -241,6 +282,7 @@ fn driver(images: [MemImage; 1]) -> PlatformDriver { boot_controls: [MockReset::new()], boot_watches: [MockWalk::idle()], component_kinds: [ComponentKind::Passive], + updatables: [MockUpdatable::new()], }) } @@ -324,6 +366,7 @@ fn verifier_fault_fails_closed() { boot_controls: [MockReset::new()], boot_watches: [MockWalk::idle()], component_kinds: [ComponentKind::Passive], + updatables: [MockUpdatable::new()], }); orch.dispatch(&mut driver, Event::PowerGood(PowerOnResult::Provisioned)); @@ -344,6 +387,7 @@ fn verify_for_a_different_component_is_refused() { boot_controls: [MockReset::new(), MockReset::new()], boot_watches: [MockWalk::idle(), MockWalk::idle()], component_kinds: [ComponentKind::Passive, ComponentKind::Passive], + updatables: [MockUpdatable::new(), MockUpdatable::new()], }); driver.stage_firmware(C0).unwrap(); @@ -382,6 +426,7 @@ fn reset_release_and_assert_reach_the_boot_control() { boot_controls: [control], boot_watches: [MockWalk::idle()], component_kinds: [ComponentKind::Passive], + updatables: [MockUpdatable::new()], }); driver.release_reset(C0).unwrap(); @@ -415,6 +460,7 @@ fn reset_line_fault_is_reported() { boot_controls: [control], boot_watches: [MockWalk::idle()], component_kinds: [ComponentKind::Passive], + updatables: [MockUpdatable::new()], }); assert_eq!(driver.release_reset(C0), Err(DriverError::BootControlFault)); @@ -477,6 +523,7 @@ impl BoardCapabilities for WatchBoard { type Verifier = LineWatchingVerifier; type BootControl = MockReset; type BootWatch = MockWalk; + type Updatable = MockUpdatable; } // The at-rest guarantee end to end: the component is still held while its @@ -496,6 +543,7 @@ fn release_follows_verification() { boot_controls: [control], boot_watches: [MockWalk::idle()], component_kinds: [ComponentKind::Passive], + updatables: [MockUpdatable::new()], }); let mut orch = orchestrator(); @@ -522,6 +570,7 @@ fn failed_release_fails_closed() { boot_controls: [control], boot_watches: [MockWalk::idle()], component_kinds: [ComponentKind::Passive], + updatables: [MockUpdatable::new()], }); let mut orch = orchestrator(); @@ -550,6 +599,7 @@ fn walk_driver( boot_controls: [MockReset::new(), MockReset::new()], boot_watches: walks, component_kinds, + updatables: [MockUpdatable::new(), MockUpdatable::new()], }) } @@ -733,6 +783,7 @@ fn booted_walk_settles_in_ready() { boot_controls: [MockReset::new()], boot_watches: [MockWalk::scripted(std::vec![WalkVerdict::Complete])], component_kinds: [ComponentKind::Passive], + updatables: [MockUpdatable::new()], }); orch.dispatch(&mut driver, Event::PowerGood(PowerOnResult::Provisioned)); @@ -761,6 +812,7 @@ fn boot_timeout_fails_closed_without_recovery() { cause: FailureCause::TimedOut, }])], component_kinds: [ComponentKind::Passive], + updatables: [MockUpdatable::new()], }); orch.dispatch(&mut driver, Event::PowerGood(PowerOnResult::Provisioned)); @@ -772,3 +824,97 @@ fn boot_timeout_fails_closed_without_recovery() { assert_eq!(orch.state(), State::Locked); } + +#[test] +fn submit_update_refuses_an_unknown_component() { + let mut driver = driver([MemImage::holding(valid_image())]); + + assert_eq!( + driver.submit_update(ComponentId::new(9)), + Err(DriverError::UnknownComponent) + ); + assert_eq!(driver.pending_update(), None); +} + +// Single update in flight by construction: a second submit is refused and +// the first job's target survives untouched. +#[test] +fn submit_update_refuses_a_second_in_flight() { + let mut driver = driver([MemImage::holding(valid_image())]); + + driver.submit_update(C0).unwrap(); + assert_eq!(driver.submit_update(C0), Err(DriverError::UpdateBusy)); + assert_eq!(driver.pending_update(), Some(C0)); +} + +// The frontend connection end to end: request_update records the job and +// the SM receives UpdateRequest. Ready accepts it and enters Updating, +// whose entry effects (AuthenticateUpdate, StageUpdate) have no executors +// yet, so the machine latches Locked — that latch is the proof the event +// arrived. Flips to an Updating/Ready assertion when the pump lands. +#[test] +fn request_update_reaches_the_sm() { + let mut orch = orchestrator(); + let mut driver = driver([MemImage::holding(valid_image())]); + orch.dispatch(&mut driver, Event::PowerGood(PowerOnResult::Provisioned)); + assert_eq!(orch.state(), State::Ready); + + request_update(&mut orch, &mut driver, C0).unwrap(); + + assert_eq!(driver.pending_update(), Some(C0)); + assert_eq!(orch.state(), State::Locked); +} + +// A refused submit injects nothing: no job, no event, the SM stays Ready. +#[test] +fn refused_request_update_injects_no_event() { + let mut orch = orchestrator(); + let mut driver = driver([MemImage::holding(valid_image())]); + orch.dispatch(&mut driver, Event::PowerGood(PowerOnResult::Provisioned)); + + assert_eq!( + request_update(&mut orch, &mut driver, ComponentId::new(9)), + Err(DriverError::UnknownComponent) + ); + + assert_eq!(driver.pending_update(), None); + assert_eq!(orch.state(), State::Ready); +} + +// The Updatable seam is wired but not yet driven: no executor exists until +// the update pump lands. This pins the mock against the trait's ordering +// rule so the wiring cannot rot in the meantime. +#[test] +fn updatable_seam_is_satisfiable_by_the_mock() { + use orchestrator_capabilities::{PayloadReadError, PayloadSource, StageProgress, Updatable}; + + struct SlicePayload(&'static [u8]); + + impl PayloadSource for SlicePayload { + fn len(&self) -> u64 { + self.0.len() as u64 + } + + fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), PayloadReadError> { + let start = usize::try_from(offset).map_err(|_| PayloadReadError::OutOfRange)?; + let end = start + .checked_add(buf.len()) + .ok_or(PayloadReadError::OutOfRange)?; + buf.copy_from_slice(self.0.get(start..end).ok_or(PayloadReadError::OutOfRange)?); + Ok(()) + } + } + + let mut dev = MockUpdatable::new(); + + assert_eq!( + dev.activate(), + Err(orchestrator_capabilities::UpdateError::NothingStaged) + ); + assert_eq!( + dev.poll_stage(&SlicePayload(b"image")), + Ok(StageProgress::Ready) + ); + dev.activate().expect("activate failed"); + assert!(dev.active); +}