refactor(firmware): centralize configuration and diagnostics
This commit is contained in:
1 parent
7048ed89d0
commit
387fdaad23
140 files changed
+6891
-4541
No files matched your search
@@ -1,8 +1,8 @@
|
||||
//! Transactional committed-versus-active configuration lifecycle.
|
||||
|
||||
use crate::{
|
||||
ConfigModel, ConfigValue, ConfigViolation, FieldDescriptor, FieldId, FieldWriteError,
|
||||
Generation, ProfileStore,
|
||||
ConfigModel, ConfigStore, ConfigValue, ConfigViolation, FieldDescriptor, FieldId,
|
||||
FieldWriteError,
|
||||
};
|
||||
use kaptan_log::KaptanError;
|
||||
|
||||
@@ -13,7 +13,7 @@ pub enum ConfigState {
|
||||
Active,
|
||||
/// One isolated RAM transaction is open.
|
||||
Editing,
|
||||
/// A newer committed snapshot will take effect after reboot.
|
||||
/// A newer committed snapshot will take effect after restart.
|
||||
RestartRequired,
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ pub enum CommitError<E> {
|
||||
Store(E),
|
||||
}
|
||||
|
||||
/// Owns the immutable boot-active profile, the latest durable candidate, and
|
||||
/// Owns the immutable boot-active configuration, the latest durable candidate, and
|
||||
/// exactly one RAM edit transaction.
|
||||
///
|
||||
/// P1 deliberately has no activation operation: [`Self::active`] never
|
||||
@@ -61,11 +61,10 @@ pub enum CommitError<E> {
|
||||
/// candidate, then reports [`ConfigState::RestartRequired`].
|
||||
pub struct ConfigManager<C: ConfigModel, S> {
|
||||
active: C::Active,
|
||||
factory: C,
|
||||
committed: C,
|
||||
transaction: Option<C>,
|
||||
context: C::Context,
|
||||
active_generation: Option<Generation>,
|
||||
committed_generation: Option<Generation>,
|
||||
store: S,
|
||||
restart_required: bool,
|
||||
}
|
||||
@@ -73,7 +72,7 @@ pub struct ConfigManager<C: ConfigModel, S> {
|
||||
impl<C, S> ConfigManager<C, S>
|
||||
where
|
||||
C: ConfigModel,
|
||||
S: ProfileStore<C>,
|
||||
S: ConfigStore<C>,
|
||||
{
|
||||
/// Starts from a validated factory candidate when no persistent record is
|
||||
/// available. Flight callers separately decide whether setup/calibration
|
||||
@@ -82,11 +81,10 @@ where
|
||||
let active = factory.validate(&context)?;
|
||||
Ok(Self {
|
||||
active,
|
||||
factory: factory.clone(),
|
||||
committed: factory,
|
||||
transaction: None,
|
||||
context,
|
||||
active_generation: None,
|
||||
committed_generation: None,
|
||||
store,
|
||||
restart_required: false,
|
||||
})
|
||||
@@ -99,7 +97,7 @@ where
|
||||
context: C::Context,
|
||||
mut store: S,
|
||||
) -> Result<Self, StartupError<S::Error>> {
|
||||
let Some((generation, committed)) = store.load().map_err(StartupError::Store)? else {
|
||||
let Some(committed) = store.load().map_err(StartupError::Store)? else {
|
||||
return Self::new(factory, context, store).map_err(StartupError::InvalidPersisted);
|
||||
};
|
||||
let active = committed
|
||||
@@ -107,11 +105,41 @@ where
|
||||
.map_err(StartupError::InvalidPersisted)?;
|
||||
Ok(Self {
|
||||
active,
|
||||
factory,
|
||||
committed,
|
||||
transaction: None,
|
||||
context,
|
||||
store,
|
||||
restart_required: false,
|
||||
})
|
||||
}
|
||||
|
||||
/// Recovers the newest candidate compatible with the current context,
|
||||
/// falling back to the supplied factory when no compatible record exists.
|
||||
///
|
||||
/// This is an explicit recovery policy for output-inhibited setup images.
|
||||
/// Safety-critical compositions should use [`Self::recover`] if an
|
||||
/// incompatible durable record must prevent initialization.
|
||||
pub fn recover_compatible_or_factory(
|
||||
factory: C,
|
||||
context: C::Context,
|
||||
mut store: S,
|
||||
) -> Result<Self, StartupError<S::Error>> {
|
||||
let Some(committed) = store
|
||||
.load_matching(|candidate| candidate.validate(&context).is_ok())
|
||||
.map_err(StartupError::Store)?
|
||||
else {
|
||||
return Self::new(factory, context, store).map_err(StartupError::InvalidPersisted);
|
||||
};
|
||||
let active = committed
|
||||
.validate(&context)
|
||||
.map_err(StartupError::InvalidPersisted)?;
|
||||
Ok(Self {
|
||||
active,
|
||||
factory,
|
||||
committed,
|
||||
transaction: None,
|
||||
context,
|
||||
active_generation: Some(generation),
|
||||
committed_generation: Some(generation),
|
||||
store,
|
||||
restart_required: false,
|
||||
})
|
||||
@@ -127,6 +155,11 @@ where
|
||||
self.committed.field_value(id)
|
||||
}
|
||||
|
||||
/// Returns the selected board/image factory value for descriptor metadata.
|
||||
pub fn factory_value(&self, id: FieldId) -> Option<ConfigValue> {
|
||||
self.factory.field_value(id)
|
||||
}
|
||||
|
||||
/// Returns a value from the immutable boot-active snapshot.
|
||||
pub fn active_value(&self, id: FieldId) -> Option<ConfigValue> {
|
||||
C::active_field_value(&self.active, id)
|
||||
@@ -158,23 +191,6 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the boot-active snapshot generation.
|
||||
pub const fn active_generation(&self) -> Option<Generation> {
|
||||
self.active_generation
|
||||
}
|
||||
|
||||
/// Returns the most recently recovered or committed generation.
|
||||
pub const fn committed_generation(&self) -> Option<Generation> {
|
||||
self.committed_generation
|
||||
}
|
||||
|
||||
/// Returns the most recently recovered or committed generation.
|
||||
///
|
||||
/// Retained as a concise alias for optimistic commit callers.
|
||||
pub const fn generation(&self) -> Option<Generation> {
|
||||
self.committed_generation
|
||||
}
|
||||
|
||||
/// Opens the only transaction slot from the last durably committed state.
|
||||
pub fn begin(&mut self) -> Result<(), BeginError> {
|
||||
if self.transaction.is_some() {
|
||||
@@ -209,25 +225,8 @@ where
|
||||
.map_err(CommitError::InvalidCandidate)
|
||||
}
|
||||
|
||||
/// Replaces the open RAM transaction with one complete imported candidate.
|
||||
///
|
||||
/// Unlike scalar edits, imports are accepted only after complete
|
||||
/// relational and capability validation. Neither the boot-active nor the
|
||||
/// committed snapshot is changed here.
|
||||
pub fn replace_transaction(&mut self, candidate: C) -> Result<(), CommitError<S::Error>> {
|
||||
candidate
|
||||
.validate(&self.context)
|
||||
.map_err(CommitError::InvalidCandidate)?;
|
||||
let transaction = self
|
||||
.transaction
|
||||
.as_mut()
|
||||
.ok_or(CommitError::NoTransaction)?;
|
||||
*transaction = candidate;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Validates and durably commits the complete open transaction.
|
||||
pub fn commit(&mut self) -> Result<Generation, CommitError<S::Error>> {
|
||||
pub fn commit(&mut self) -> Result<(), CommitError<S::Error>> {
|
||||
let candidate = self
|
||||
.transaction
|
||||
.as_ref()
|
||||
@@ -235,29 +234,23 @@ where
|
||||
candidate
|
||||
.validate(&self.context)
|
||||
.map_err(CommitError::InvalidCandidate)?;
|
||||
let generation = self.store.persist(candidate).map_err(CommitError::Store)?;
|
||||
self.store.persist(candidate).map_err(CommitError::Store)?;
|
||||
self.committed = candidate.clone();
|
||||
self.transaction = None;
|
||||
self.committed_generation = Some(generation);
|
||||
self.restart_required = true;
|
||||
Ok(generation)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Drops the RAM transaction without changing active or committed state.
|
||||
pub fn abort(&mut self) -> bool {
|
||||
self.transaction.take().is_some()
|
||||
}
|
||||
|
||||
/// Releases the owned store when the configuration owner shuts down.
|
||||
pub fn into_store(self) -> S {
|
||||
self.store
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{ApplyPolicy, ConfigValue, ValidationRule, ValueKind};
|
||||
use crate::{ConfigValue, ValidationRule, ValueKind};
|
||||
|
||||
const VALUE: FieldId = FieldId::new(1);
|
||||
const OTHER: FieldId = FieldId::new(2);
|
||||
@@ -268,9 +261,8 @@ mod tests {
|
||||
kind: ValueKind::U32,
|
||||
minimum: Some(ConfigValue::U32(1)),
|
||||
maximum: Some(ConfigValue::U32(10)),
|
||||
default: ConfigValue::U32(2),
|
||||
enum_options: &[],
|
||||
unit: "count",
|
||||
apply_policy: ApplyPolicy::RestartRequired,
|
||||
},
|
||||
FieldDescriptor {
|
||||
id: OTHER,
|
||||
@@ -278,9 +270,8 @@ mod tests {
|
||||
kind: ValueKind::U32,
|
||||
minimum: Some(ConfigValue::U32(1)),
|
||||
maximum: Some(ConfigValue::U32(10)),
|
||||
default: ConfigValue::U32(4),
|
||||
enum_options: &[],
|
||||
unit: "count",
|
||||
apply_policy: ApplyPolicy::RestartRequired,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -364,15 +355,29 @@ mod tests {
|
||||
|
||||
struct MemoryStore;
|
||||
|
||||
impl ProfileStore<Candidate> for MemoryStore {
|
||||
impl ConfigStore<Candidate> for MemoryStore {
|
||||
type Error = ();
|
||||
|
||||
fn load(&mut self) -> Result<Option<(Generation, Candidate)>, Self::Error> {
|
||||
fn load(&mut self) -> Result<Option<Candidate>, Self::Error> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn persist(&mut self, _: &Candidate) -> Result<Generation, Self::Error> {
|
||||
Ok(Generation::new(7))
|
||||
fn persist(&mut self, _: &Candidate) -> Result<(), Self::Error> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
struct IncompatibleStore;
|
||||
|
||||
impl ConfigStore<Candidate> for IncompatibleStore {
|
||||
type Error = ();
|
||||
|
||||
fn load(&mut self) -> Result<Option<Candidate>, Self::Error> {
|
||||
Ok(Some(Candidate { value: 9, other: 1 }))
|
||||
}
|
||||
|
||||
fn persist(&mut self, _: &Candidate) -> Result<(), Self::Error> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -405,11 +410,9 @@ mod tests {
|
||||
ConfigManager::new(Candidate { value: 2, other: 4 }, (), MemoryStore).unwrap();
|
||||
manager.begin().unwrap();
|
||||
manager.set(VALUE, ConfigValue::U32(3)).unwrap();
|
||||
assert_eq!(manager.commit(), Ok(Generation::new(7)));
|
||||
assert_eq!(manager.commit(), Ok(()));
|
||||
assert_eq!(manager.active(), &Active { value: 2, other: 4 });
|
||||
assert_eq!(manager.committed_value(VALUE), Some(ConfigValue::U32(3)));
|
||||
assert_eq!(manager.active_generation(), None);
|
||||
assert_eq!(manager.committed_generation(), Some(Generation::new(7)));
|
||||
assert_eq!(manager.state(), ConfigState::RestartRequired);
|
||||
manager.begin().unwrap();
|
||||
manager.set(VALUE, ConfigValue::U32(4)).unwrap();
|
||||
@@ -417,4 +420,17 @@ mod tests {
|
||||
assert_eq!(manager.committed_value(VALUE), Some(ConfigValue::U32(3)));
|
||||
assert_eq!(manager.state(), ConfigState::RestartRequired);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_recovery_policy_uses_factory_when_no_record_matches_context() {
|
||||
let factory = Candidate { value: 2, other: 4 };
|
||||
assert!(matches!(
|
||||
ConfigManager::recover(factory.clone(), (), IncompatibleStore),
|
||||
Err(StartupError::InvalidPersisted(_))
|
||||
));
|
||||
|
||||
let manager =
|
||||
ConfigManager::recover_compatible_or_factory(factory, (), IncompatibleStore).unwrap();
|
||||
assert_eq!(manager.active(), &Active { value: 2, other: 4 });
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user