squib_core/lifecycle.rs
1//! VM lifecycle phases — internal six-state enum.
2//!
3//! The internal phase enum is richer than the upstream three-value wire vocabulary so
4//! handlers can produce precise `fault_message`s on misordered requests. The wire form
5//! is a separate type owned by `squib-api`; this enum never crosses the HTTP boundary.
6//!
7//! See [11-runtime-core.md §
8//! 3.1](../../../specs/11-runtime-core.md#31-internal-lifecyclephase-vs-wire-vmstate).
9
10use core::fmt;
11
12/// The wire-shape vocabulary served by `GET /` — exactly the upstream three values.
13///
14/// Serializes to the literal upstream strings (`"Not started"`, `"Running"`, `"Paused"`)
15/// — note the space + lowercase `s` in `"Not started"`. SDKs and `firectl` sniff these
16/// strings; squib emits them verbatim.
17///
18/// `WireVmState` lives here (not in `squib-api`) so the `LifecyclePhase::wire_state`
19/// collapse function can be defined alongside its target without `squib-core` taking a
20/// dependency on the API crate.
21#[derive(Debug, Default, Clone, Copy, Eq, PartialEq)]
22pub enum WireVmState {
23 /// VMM has started but no microvm is running. Wire string: `"Not started"`.
24 #[default]
25 NotStarted,
26 /// Microvm has booted and at least one vCPU is active. Wire string: `"Running"`.
27 Running,
28 /// Microvm has booted but vCPUs are paused. Wire string: `"Paused"`.
29 Paused,
30}
31
32impl fmt::Display for WireVmState {
33 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34 match self {
35 Self::NotStarted => f.write_str("Not started"),
36 Self::Running => f.write_str("Running"),
37 Self::Paused => f.write_str("Paused"),
38 }
39 }
40}
41
42/// Internal lifecycle phase — six values. Never serialized to the wire.
43#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
44pub enum LifecyclePhase {
45 /// No configuration posted yet.
46 #[default]
47 Uninitialized,
48 /// Configuration posted, awaiting `PUT /actions {InstanceStart}`.
49 NotStarted,
50 /// Boot orchestration in progress (vCPUs spawning, GIC live, devices wired).
51 Starting,
52 /// At least one vCPU is active.
53 Running,
54 /// Microvm booted but vCPUs are paused.
55 Paused,
56 /// Terminal state after PSCI `SYSTEM_OFF` / `SYSTEM_RESET` or fatal vCPU panic.
57 Shutdown,
58}
59
60impl LifecyclePhase {
61 /// Collapse to the upstream three-value vocabulary served by `GET /`.
62 ///
63 /// `Uninitialized | NotStarted | Starting | Shutdown` all map to
64 /// [`WireVmState::NotStarted`] — clients see only the upstream three values.
65 #[must_use]
66 pub const fn wire_state(self) -> WireVmState {
67 match self {
68 Self::Uninitialized | Self::NotStarted | Self::Starting | Self::Shutdown => {
69 WireVmState::NotStarted
70 }
71 Self::Running => WireVmState::Running,
72 Self::Paused => WireVmState::Paused,
73 }
74 }
75
76 /// `true` once the microvm has booted (Running or Paused).
77 #[must_use]
78 pub const fn is_post_boot(self) -> bool {
79 matches!(self, Self::Running | Self::Paused)
80 }
81
82 /// `true` while the VMM is still accepting pre-boot configuration mutations.
83 #[must_use]
84 pub const fn is_pre_boot(self) -> bool {
85 matches!(self, Self::Uninitialized | Self::NotStarted)
86 }
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92
93 #[test]
94 fn wire_state_collapses_per_spec_11_3_1() {
95 assert_eq!(
96 LifecyclePhase::Uninitialized.wire_state(),
97 WireVmState::NotStarted
98 );
99 assert_eq!(
100 LifecyclePhase::NotStarted.wire_state(),
101 WireVmState::NotStarted
102 );
103 assert_eq!(
104 LifecyclePhase::Starting.wire_state(),
105 WireVmState::NotStarted
106 );
107 assert_eq!(
108 LifecyclePhase::Shutdown.wire_state(),
109 WireVmState::NotStarted
110 );
111 assert_eq!(LifecyclePhase::Running.wire_state(), WireVmState::Running);
112 assert_eq!(LifecyclePhase::Paused.wire_state(), WireVmState::Paused);
113 }
114
115 #[test]
116 fn wire_state_displays_upstream_strings_verbatim() {
117 assert_eq!(WireVmState::NotStarted.to_string(), "Not started");
118 assert_eq!(WireVmState::Running.to_string(), "Running");
119 assert_eq!(WireVmState::Paused.to_string(), "Paused");
120 }
121
122 #[test]
123 fn pre_post_boot_predicates() {
124 assert!(LifecyclePhase::Uninitialized.is_pre_boot());
125 assert!(LifecyclePhase::NotStarted.is_pre_boot());
126 assert!(!LifecyclePhase::Starting.is_pre_boot());
127 assert!(LifecyclePhase::Running.is_post_boot());
128 assert!(LifecyclePhase::Paused.is_post_boot());
129 assert!(!LifecyclePhase::Shutdown.is_post_boot());
130 }
131}