orchestrator: Compose reset actuation from the board's BootControl capability release_reset and assert_reset now delegate to a per-component orchestrator_capabilities::BootControl supplied by the board, instead of returning NotImplemented. BoardCapabilities gains a BootControl associated type and Board a boot_controls array, following the composition pattern already used for ImageSource and Verifier. Signed-off-by: Christina Quast <christina.quast@9elements.com>
diff --git a/services/orchestrator/driver/BUILD.bazel b/services/orchestrator/driver/BUILD.bazel index b9ee0aa..0d3485a 100644 --- a/services/orchestrator/driver/BUILD.bazel +++ b/services/orchestrator/driver/BUILD.bazel
@@ -15,6 +15,7 @@ edition = "2024", visibility = ["//visibility:public"], deps = [ + "//services/orchestrator/capabilities:orchestrator_capabilities", "//services/orchestrator/sm:orchestrator_sm", "@rust_crates//:heapless", ],
diff --git a/services/orchestrator/driver/src/board.rs b/services/orchestrator/driver/src/board.rs index 5c16706..81e06cc 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; +use orchestrator_capabilities::BootControl; /// Access to one component's active firmware image, however it is reached — /// interposed flash, a PLDM/MCTP transfer, a RAM copy in tests. @@ -93,8 +94,9 @@ type Image: ImageSource; /// Judges images for every component. type Verifier: Verifier; - // Later seams: Reset (release/assert_reset), Evidence (checkpoint - // walk), Recovery, Staging. + /// Reset actuation for the managed components. + type BootControl: BootControl; + // Later seams: Evidence (checkpoint walk), Recovery, Staging. } /// Everything the board supplies, built once at bring-up and handed to @@ -104,12 +106,14 @@ /// ```ignore /// struct Ast1060Board; /// impl BoardCapabilities for Ast1060Board { -/// type Image = SpiFlashImage; // interposed flash, offsets from the slot layout -/// type Verifier = ManifestVerifier; // signature + SVN via the crypto engine +/// type Image = SpiFlashImage; // interposed flash, offsets from the slot layout +/// type Verifier = ManifestVerifier; // signature + SVN via the crypto engine +/// type BootControl = ExtrstGpio; // per-component reset line /// } /// let board = Board::<Ast1060Board, 2> { /// images: [bmc_image, cpld_image], /// verifier, +/// boot_controls: [bmc_reset, cpld_reset], /// }; /// ``` pub struct Board<B: BoardCapabilities, const N: usize> { @@ -118,5 +122,8 @@ pub images: [B::Image; N], /// Judges images for every component. pub verifier: B::Verifier, - // Later seams add fields, e.g. resets: [B::Reset; N]. + /// `boot_controls[i]` actuates `ComponentId(i)`'s reset, same indexing + /// as `images`. + pub boot_controls: [B::BootControl; N], + // Later seams add fields, e.g. evidence: [B::Evidence; N]. }
diff --git a/services/orchestrator/driver/src/driver.rs b/services/orchestrator/driver/src/driver.rs index d9bbdde..559f58d 100644 --- a/services/orchestrator/driver/src/driver.rs +++ b/services/orchestrator/driver/src/driver.rs
@@ -7,6 +7,7 @@ use openprot_orchestrator_sm::{ComponentId, Effect, EffectError, Event, Platform}; use crate::board::{Board, BoardCapabilities, ImageSource, Verdict, Verifier}; +use orchestrator_capabilities::BootControl; /// Queue bound. Executors produce at most one event per effect, the event loop /// drains it after every dispatch, and the largest SM effect batch today is @@ -29,6 +30,8 @@ /// The verifier could not perform the check (a failed image is a /// [`Verdict`], not an error). VerifierFault, + /// The component's boot control could not actuate the reset line. + BootControlFault, /// The event queue overflowed; dropping events breaks the SM's /// honest-feedback contract. QueueFull, @@ -42,6 +45,7 @@ DriverError::ImageUnavailable => "image source could not be opened", 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::QueueFull => "event queue full", }) } @@ -118,17 +122,29 @@ }) } - /// Release `id` from reset, then walk its boot checkpoints; feed back - /// one `Event::ComponentReady(id)` (Active) or `Event::Booted(id)` - /// (Passive), or `Event::Timeout(id)` on window expiry. - pub fn release_reset(&mut self, _id: ComponentId) -> Result<(), DriverError> { - Err(DriverError::NotImplemented) + /// `id`'s reset actuator. + fn boot_control(&mut self, id: ComponentId) -> Result<&mut B::BootControl, DriverError> { + self.board + .boot_controls + .get_mut(id.get() as usize) + .ok_or(DriverError::UnknownComponent) + } + + /// Release `id` from reset. The boot-checkpoint walk that feeds back + /// `Event::ComponentReady(id)`/`Event::Booted(id)`/`Event::Timeout(id)` + /// belongs to the BootWatch seam, not yet composed. + pub fn release_reset(&mut self, id: ComponentId) -> Result<(), DriverError> { + self.boot_control(id)? + .release() + .map_err(|_| DriverError::BootControlFault) } /// Hold `id` in reset — a durable quiesce, not a pulse; at-rest /// verification and the recovery re-walk depend on it. - pub fn assert_reset(&mut self, _id: ComponentId) -> Result<(), DriverError> { - Err(DriverError::NotImplemented) + pub fn assert_reset(&mut self, id: ComponentId) -> Result<(), DriverError> { + self.boot_control(id)? + .hold_in_reset() + .map_err(|_| DriverError::BootControlFault) } /// Restore `id` from its configured recovery source (the mechanism is
diff --git a/services/orchestrator/driver/src/lib.rs b/services/orchestrator/driver/src/lib.rs index ff93b89..9b3a3c8 100644 --- a/services/orchestrator/driver/src/lib.rs +++ b/services/orchestrator/driver/src/lib.rs
@@ -14,8 +14,9 @@ //! via [`PlatformDriver::take_event`] and dispatches each. //! //! Everything device-specific arrives through the seams in [`board`]: -//! image access ([`ImageSource`]) and image judgment ([`Verifier`]), -//! bundled in one [`Board`] built by the board's composition crate. +//! image access ([`ImageSource`]), image judgment ([`Verifier`]) and reset +//! actuation ([`orchestrator_capabilities::BootControl`]), bundled in one +//! [`Board`] built by the board's composition crate. //! //! [`Platform`]: openprot_orchestrator_sm::Platform
diff --git a/services/orchestrator/driver/src/tests.rs b/services/orchestrator/driver/src/tests.rs index cb9c0f3..07ee2e2 100644 --- a/services/orchestrator/driver/src/tests.rs +++ b/services/orchestrator/driver/src/tests.rs
@@ -128,18 +128,67 @@ } } +#[derive(Debug)] +struct ResetFault; + +impl core::fmt::Display for ResetFault { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.write_str("reset line fault") + } +} + +impl core::error::Error for ResetFault {} + +/// Reset actuation without a HAL; `held` is shared so tests can observe the +/// line after the control moves into the driver. +struct MockReset { + held: std::rc::Rc<core::cell::Cell<bool>>, + fail: bool, +} + +impl MockReset { + fn new() -> Self { + Self { + held: std::rc::Rc::new(core::cell::Cell::new(true)), + fail: false, + } + } +} + +impl orchestrator_capabilities::BootControl for MockReset { + type Error = ResetFault; + + fn hold_in_reset(&mut self) -> Result<(), ResetFault> { + if self.fail { + return Err(ResetFault); + } + self.held.set(true); + Ok(()) + } + + fn release(&mut self) -> Result<(), ResetFault> { + if self.fail { + return Err(ResetFault); + } + self.held.set(false); + Ok(()) + } +} + /// The test board's type choices. struct MockBoard; impl BoardCapabilities for MockBoard { type Image = MemImage; type Verifier = XorVerifier; + type BootControl = MockReset; } fn driver(images: [MemImage; 1]) -> PlatformDriver<MockBoard, 1> { PlatformDriver::new(Board { images, verifier: XorVerifier { fault: false }, + boot_controls: [MockReset::new()], }) } @@ -222,6 +271,7 @@ let mut driver = PlatformDriver::<MockBoard, 1>::new(Board { images: [MemImage::holding(valid_image())], verifier: XorVerifier { fault: true }, + boot_controls: [MockReset::new()], }); orch.dispatch(&mut driver, Event::PowerGood(PowerOnResult::Provisioned)); @@ -240,6 +290,7 @@ MemImage::holding(valid_image()), ], verifier: XorVerifier { fault: false }, + boot_controls: [MockReset::new(), MockReset::new()], }); driver.stage_firmware(C0).unwrap(); @@ -268,6 +319,51 @@ ); } +#[test] +fn reset_release_and_assert_reach_the_boot_control() { + let control = MockReset::new(); + let held = control.held.clone(); + let mut driver = PlatformDriver::<MockBoard, 1>::new(Board { + images: [MemImage::holding(valid_image())], + verifier: XorVerifier { fault: false }, + boot_controls: [control], + }); + + driver.release_reset(C0).unwrap(); + assert!(!held.get()); + + driver.assert_reset(C0).unwrap(); + assert!(held.get()); +} + +#[test] +fn reset_of_unknown_component_is_refused() { + let mut driver = driver([MemImage::holding(valid_image())]); + + assert_eq!( + driver.release_reset(ComponentId::new(9)), + Err(DriverError::UnknownComponent) + ); + assert_eq!( + driver.assert_reset(ComponentId::new(9)), + Err(DriverError::UnknownComponent) + ); +} + +#[test] +fn reset_line_fault_is_reported() { + let mut control = MockReset::new(); + control.fail = true; + let mut driver = PlatformDriver::<MockBoard, 1>::new(Board { + images: [MemImage::holding(valid_image())], + verifier: XorVerifier { fault: false }, + boot_controls: [control], + }); + + assert_eq!(driver.release_reset(C0), Err(DriverError::BootControlFault)); + assert_eq!(driver.assert_reset(C0), Err(DriverError::BootControlFault)); +} + // Undrained verdicts eventually fill the queue; the overflow is reported, // not silently dropped. #[test]