Skip to content
Draft
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
11 changes: 10 additions & 1 deletion services/orchestrator/driver/src/board.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -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
Expand All @@ -115,13 +118,15 @@ 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::<Ast1060Board, 2> {
/// images: [bmc_image, cpld_image],
/// verifier,
/// 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<B: BoardCapabilities, const N: usize> {
Expand All @@ -140,5 +145,9 @@ pub struct Board<B: BoardCapabilities, const N: usize> {
/// `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].
}
57 changes: 56 additions & 1 deletion services/orchestrator/driver/src/driver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -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 {
Expand All @@ -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",
})
}
}
Expand All @@ -50,6 +56,15 @@ pub struct PlatformDriver<B: BoardCapabilities, const N: usize> {
/// 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<UpdateJob>,
}

/// One in-flight update, recorded by [`PlatformDriver::submit_update`].
struct UpdateJob {
target: ComponentId,
}

impl<B: BoardCapabilities, const N: usize> PlatformDriver<B, N> {
Expand All @@ -60,9 +75,33 @@ impl<B: BoardCapabilities, const N: usize> PlatformDriver<B, N> {
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<ComponentId> {
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> {
Expand Down Expand Up @@ -240,3 +279,19 @@ impl<B: BoardCapabilities, const N: usize> Platform for PlatformDriver<B, N> {
.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<B: BoardCapabilities, const N: usize, const E: usize>(
orchestrator: &mut Orchestrator<N, E>,
driver: &mut PlatformDriver<B, N>,
target: ComponentId,
) -> Result<(), DriverError> {
driver.submit_update(target)?;
orchestrator.dispatch(driver, Event::UpdateRequest);
Ok(())
}
2 changes: 1 addition & 1 deletion services/orchestrator/driver/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
146 changes: 146 additions & 0 deletions services/orchestrator/driver/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<orchestrator_capabilities::StageProgress, orchestrator_capabilities::UpdateError>
{
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;

Expand All @@ -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<MockBoard, 1> {
Expand All @@ -241,6 +282,7 @@ fn driver(images: [MemImage; 1]) -> PlatformDriver<MockBoard, 1> {
boot_controls: [MockReset::new()],
boot_watches: [MockWalk::idle()],
component_kinds: [ComponentKind::Passive],
updatables: [MockUpdatable::new()],
})
}

Expand Down Expand Up @@ -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));
Expand All @@ -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();
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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
Expand All @@ -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();

Expand All @@ -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();

Expand Down Expand Up @@ -550,6 +599,7 @@ fn walk_driver(
boot_controls: [MockReset::new(), MockReset::new()],
boot_watches: walks,
component_kinds,
updatables: [MockUpdatable::new(), MockUpdatable::new()],
})
}

Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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));
Expand All @@ -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);
}