Skip to main content

squib_api/schemas/
mod.rs

1//! Request and response types that mirror Firecracker's `OpenAPI` shapes.
2//!
3//! Two-shape pattern per [10-data-model.md ยง
4//! 2.3](../../../specs/10-data-model.md#23-schema-layer): every external-facing struct
5//! is split into a `Raw*` shape (the literal serde target with no validation rules) and
6//! a validated newtype with private fields. Validation runs **inside** `TryFrom`, not
7//! after `serde`. This makes "unvalidated `DriveConfig`" unrepresentable: the only path
8//! from JSON to the domain type is through the validating constructor.
9//!
10//! Field names match upstream's swagger exactly (`snake_case` in nested JSON, derived
11//! from `firecracker.yaml`); the static-config envelope uses `kebab-case` only at the
12//! top level.
13
14pub mod actions;
15pub mod balloon;
16pub mod boot_source;
17pub mod common;
18pub mod config_file;
19pub mod cpu_config;
20pub mod drive;
21pub mod entropy;
22pub mod hotplug_memory;
23pub mod logger;
24pub mod machine_config;
25pub mod metrics;
26pub mod mmds;
27pub mod network;
28pub mod pmem;
29pub mod serial;
30pub mod snapshot;
31pub mod vm;
32pub mod vsock;
33
34pub use actions::{InstanceAction, InstanceActionInfo};
35pub use balloon::{BalloonConfig, BalloonHintingOp, BalloonStatsUpdate, BalloonUpdate};
36pub use boot_source::BootSourceConfig;
37pub use common::{
38    DriveId, IfaceId, InstanceId, MAX_DRIVES, MAX_NICS, MAX_PMEM, MAX_VIRTIO_MEM, MacAddr,
39    MemSizeMib, SafePath, UdsPath, VsockId,
40};
41pub use config_file::ConfigFile;
42pub use cpu_config::CpuConfig;
43pub use drive::{DriveConfig, DrivePatch};
44pub use entropy::EntropyConfig;
45pub use hotplug_memory::{HotplugMemoryConfig, HotplugMemoryUpdate};
46pub use logger::LoggerConfig;
47pub use machine_config::{MachineConfig, MachineConfigPatch};
48pub use metrics::MetricsConfig;
49pub use mmds::{MmdsConfig, MmdsContents};
50pub use network::{NetworkInterfaceConfig, NetworkPatch};
51pub use pmem::{PmemConfig, PmemPatch};
52use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
53pub use serial::SerialConfig;
54pub use snapshot::{SnapshotCreateConfig, SnapshotLoadConfig};
55use squib_core::WireVmState;
56pub use vm::VmStateChange;
57pub use vsock::VsockConfig;
58
59/// Body of `GET /version`.
60///
61/// Upstream returns the literal Firecracker version string (e.g. `"1.16.0"`) so SDK
62/// version sniffers continue to work. Squib emits the same string for compatibility.
63#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
64pub struct VersionResponse {
65    /// Firecracker-compatible version string; what SDKs see when they probe the server.
66    pub firecracker_version: String,
67}
68
69/// Body of `GET /` (the `InstanceInfo` resource).
70///
71/// Mirrors upstream's `InstanceInfo` model (`id`, `state`, `vmm_version`, `app_name`).
72#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
73pub struct InstanceInfo {
74    /// Microvm instance ID (the `--id` flag value).
75    pub id: String,
76    /// Current state of the microvm โ€” wire-shape three-value enum.
77    pub state: VmState,
78    /// VMM build identifier โ€” squib uses `"<firecracker-compat-version> (squib X.Y.Z)"`.
79    pub vmm_version: String,
80    /// Application name; we identify as `"Firecracker"` for SDK sniffing parity.
81    pub app_name: String,
82}
83
84/// Wire-shape lifecycle state served by `GET /` and inside snapshot metadata.
85///
86/// Mirrors upstream `vmm/src/vmm_config/instance_info.rs::VmState` exactly: three
87/// variants, with `NotStarted` serializing as the literal string `"Not started"` (note
88/// the space + lowercase `s`). SDKs and `firectl` sniff these strings byte-for-byte;
89/// any deviation breaks compatibility.
90#[derive(Debug, Default, Clone, Copy, Eq, PartialEq)]
91pub enum VmState {
92    /// VMM has started but no microvm is running. Wire string: `"Not started"`.
93    #[default]
94    NotStarted,
95    /// Microvm has booted and at least one vCPU is active. Wire string: `"Running"`.
96    Running,
97    /// Microvm has booted but vCPUs are paused. Wire string: `"Paused"`.
98    Paused,
99}
100
101impl VmState {
102    /// The exact upstream wire string for this state.
103    #[must_use]
104    pub const fn as_wire_str(self) -> &'static str {
105        match self {
106            Self::NotStarted => "Not started",
107            Self::Running => "Running",
108            Self::Paused => "Paused",
109        }
110    }
111}
112
113impl From<WireVmState> for VmState {
114    fn from(s: WireVmState) -> Self {
115        match s {
116            WireVmState::NotStarted => Self::NotStarted,
117            WireVmState::Running => Self::Running,
118            WireVmState::Paused => Self::Paused,
119        }
120    }
121}
122
123impl Serialize for VmState {
124    fn serialize<S: Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
125        ser.serialize_str(self.as_wire_str())
126    }
127}
128
129impl<'de> Deserialize<'de> for VmState {
130    fn deserialize<D: Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
131        let s = <&str>::deserialize(de)?;
132        match s {
133            "Not started" => Ok(Self::NotStarted),
134            "Running" => Ok(Self::Running),
135            "Paused" => Ok(Self::Paused),
136            other => Err(de::Error::unknown_variant(
137                other,
138                &["Not started", "Running", "Paused"],
139            )),
140        }
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147
148    #[test]
149    fn test_should_serialize_version_response_in_snake_case() {
150        let v = VersionResponse {
151            firecracker_version: "1.16.0".into(),
152        };
153        let json = serde_json::to_string(&v).unwrap();
154        assert_eq!(json, r#"{"firecracker_version":"1.16.0"}"#);
155    }
156
157    #[test]
158    fn test_should_round_trip_instance_info() {
159        let original = InstanceInfo {
160            id: "anonymous".into(),
161            state: VmState::NotStarted,
162            vmm_version: "1.16.0 (squib 0.1.0)".into(),
163            app_name: "Firecracker".into(),
164        };
165        let json = serde_json::to_string(&original).unwrap();
166        let back: InstanceInfo = serde_json::from_str(&json).unwrap();
167        assert_eq!(original, back);
168    }
169
170    #[test]
171    fn test_should_serialize_vm_state_to_upstream_strings_verbatim() {
172        assert_eq!(
173            serde_json::to_string(&VmState::NotStarted).unwrap(),
174            r#""Not started""#
175        );
176        assert_eq!(
177            serde_json::to_string(&VmState::Running).unwrap(),
178            r#""Running""#
179        );
180        assert_eq!(
181            serde_json::to_string(&VmState::Paused).unwrap(),
182            r#""Paused""#
183        );
184    }
185
186    #[test]
187    fn test_should_reject_pascalcase_vm_state_on_deserialize() {
188        let s: VmState = serde_json::from_str(r#""Not started""#).unwrap();
189        assert_eq!(s, VmState::NotStarted);
190
191        // PascalCase variant the squib draft used to emit must now be rejected โ€” any
192        // SDK seeing it would already have broken in production.
193        assert!(serde_json::from_str::<VmState>(r#""NotStarted""#).is_err());
194    }
195
196    #[test]
197    fn test_should_collapse_lifecycle_phase_to_vm_state() {
198        use squib_core::LifecyclePhase;
199
200        let cases = [
201            (LifecyclePhase::Uninitialized, VmState::NotStarted),
202            (LifecyclePhase::NotStarted, VmState::NotStarted),
203            (LifecyclePhase::Starting, VmState::NotStarted),
204            (LifecyclePhase::Shutdown, VmState::NotStarted),
205            (LifecyclePhase::Running, VmState::Running),
206            (LifecyclePhase::Paused, VmState::Paused),
207        ];
208        for (phase, expected) in cases {
209            assert_eq!(VmState::from(phase.wire_state()), expected, "{phase:?}");
210        }
211    }
212}