Skip to main content

squib_api/schemas/
config_file.rs

1//! `--config-file` static-config envelope ([20-firecracker-api.md §
2//! 6](../../../specs/20-firecracker-api.md#6-static-config-file---config-file)).
3//!
4//! Kebab-case top-level keys per upstream. The `"squib": {...}` extension is
5//! `#[serde(default)]` so files remain portable in the squib→firecracker direction;
6//! upstream Firecracker silently ignores unknown top-level keys. The `"squib"`
7//! sub-object itself is `deny_unknown_fields` so typos inside it fail loudly.
8//!
9//! Top-level `deny_unknown_fields` is **not** applied to this envelope: per the spec
10//! we tolerate unknown keys for forward-compat with future squib extensions.
11
12use serde::Deserialize;
13
14use super::{
15    balloon::RawBalloonConfig, boot_source::RawBootSourceConfig, cpu_config::RawCpuConfig,
16    drive::RawDriveConfig, entropy::RawEntropyConfig, logger::RawLoggerConfig,
17    machine_config::RawMachineConfig, metrics::RawMetricsConfig, mmds::RawMmdsConfig,
18    network::RawNetworkInterfaceConfig, pmem::RawPmemConfig, serial::RawSerialConfig,
19    vsock::RawVsockConfig,
20};
21
22/// Networking mode squib understands. Mirrors `apps/squib-cli::cli::NetworkMode`.
23#[derive(Debug, Clone, Copy, Eq, PartialEq, Default, Deserialize)]
24#[serde(rename_all = "lowercase")]
25pub enum SquibNetworkMode {
26    /// vmnet-shared (NAT). Default.
27    #[default]
28    Shared,
29    /// vmnet-bridged.
30    Bridged,
31    /// vmnet-host (host-only network).
32    Host,
33    /// Embedded gvproxy userspace stack.
34    Userspace,
35}
36
37/// Squib-only extension keys. Always `#[serde(default)]` and never required.
38#[derive(Debug, Clone, Default, Deserialize)]
39#[serde(deny_unknown_fields)]
40pub struct SquibExtension {
41    /// Networking mode override.
42    #[serde(default)]
43    pub network: Option<SquibNetworkMode>,
44    /// Opt-in TSI mode for vsock.
45    #[serde(default)]
46    pub vsock_tsi: bool,
47    /// Path to a bundled `gvproxy` binary.
48    #[serde(default)]
49    pub gvproxy_path: Option<String>,
50    /// Optional macOS sandbox profile name.
51    #[serde(default)]
52    pub macos_sandbox_profile: Option<String>,
53}
54
55/// Static-config-file envelope.
56///
57/// Replayed in the deterministic order documented in [20 §
58/// 6](../../../specs/20-firecracker-api.md#6-static-config-file---config-file): `machine-config` →
59/// `cpu-config` → `boot-source` → drives → NICs → vsock → mmds-config → mmds → balloon → entropy →
60/// serial → pmem → hotplug-memory → logger → metrics → `Action(InstanceStart)` (only if `--no-api`
61/// is also set).
62#[derive(Debug, Clone, Default, Deserialize)]
63#[serde(rename_all = "kebab-case")]
64pub struct ConfigFile {
65    /// `boot-source` is the only mandatory member at the controller level. Marked
66    /// optional here so the parser can produce a precise "missing boot-source" error
67    /// rather than serde's terser one.
68    #[serde(default)]
69    pub boot_source: Option<RawBootSourceConfig>,
70    /// `drives` array (max 8 — enforced at the controller).
71    #[serde(default)]
72    pub drives: Vec<RawDriveConfig>,
73    /// `machine-config`.
74    #[serde(default)]
75    pub machine_config: Option<RawMachineConfig>,
76    /// `cpu-config`.
77    #[serde(default)]
78    pub cpu_config: Option<RawCpuConfig>,
79    /// `network-interfaces` array (max 8).
80    #[serde(default)]
81    pub network_interfaces: Vec<RawNetworkInterfaceConfig>,
82    /// `vsock`.
83    #[serde(default)]
84    pub vsock: Option<RawVsockConfig>,
85    /// `mmds-config`.
86    #[serde(default)]
87    pub mmds_config: Option<RawMmdsConfig>,
88    /// `mmds` data store seed (any JSON tree).
89    #[serde(default)]
90    pub mmds: Option<serde_json::Value>,
91    /// `balloon`.
92    #[serde(default)]
93    pub balloon: Option<RawBalloonConfig>,
94    /// `entropy`.
95    #[serde(default)]
96    pub entropy: Option<RawEntropyConfig>,
97    /// `serial`.
98    #[serde(default)]
99    pub serial: Option<RawSerialConfig>,
100    /// `pmem` array (max 4).
101    #[serde(default)]
102    pub pmem: Vec<RawPmemConfig>,
103    /// `hotplug-memory`.
104    #[serde(default)]
105    pub hotplug_memory: Option<super::hotplug_memory::RawHotplugMemoryConfig>,
106    /// `logger`.
107    #[serde(default)]
108    pub logger: Option<RawLoggerConfig>,
109    /// `metrics`.
110    #[serde(default)]
111    pub metrics: Option<RawMetricsConfig>,
112    /// Squib-only extension sub-object (forward-compat).
113    #[serde(default)]
114    pub squib: SquibExtension,
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120
121    #[test]
122    fn test_should_parse_minimal_config_file() {
123        let json = r#"{"boot-source":{"kernel_image_path":"/tmp/k"}}"#;
124        let cfg: ConfigFile = serde_json::from_str(json).unwrap();
125        assert!(cfg.boot_source.is_some());
126        assert!(cfg.drives.is_empty());
127    }
128
129    #[test]
130    fn test_should_tolerate_unknown_top_level_keys() {
131        let json = r#"{"boot-source":{"kernel_image_path":"/tmp/k"},"future-key":42}"#;
132        let cfg: ConfigFile = serde_json::from_str(json).unwrap();
133        assert!(cfg.boot_source.is_some());
134    }
135
136    #[test]
137    fn test_should_reject_unknown_keys_inside_squib_extension() {
138        let json = r#"{"squib":{"unknown":1}}"#;
139        let res: Result<ConfigFile, _> = serde_json::from_str(json);
140        assert!(res.is_err());
141    }
142
143    #[test]
144    fn test_should_parse_squib_network_mode() {
145        let json = r#"{"squib":{"network":"shared"}}"#;
146        let cfg: ConfigFile = serde_json::from_str(json).unwrap();
147        assert_eq!(cfg.squib.network, Some(SquibNetworkMode::Shared));
148    }
149
150    #[test]
151    fn test_should_parse_full_envelope() {
152        let json = r#"{
153            "boot-source": {"kernel_image_path":"/tmp/k","boot_args":"console=ttyAMA0"},
154            "machine-config": {"vcpu_count":2,"mem_size_mib":256},
155            "drives": [{"drive_id":"rootfs","path_on_host":"/tmp/r.img","is_root_device":true}],
156            "network-interfaces": [{"iface_id":"eth0","host_dev_name":"tap0"}],
157            "logger": {"log_path":"/tmp/squib.log"},
158            "squib": {"network":"userspace","vsock_tsi":false}
159        }"#;
160        let cfg: ConfigFile = serde_json::from_str(json).unwrap();
161        assert_eq!(cfg.machine_config.as_ref().unwrap().vcpu_count, 2);
162        assert_eq!(cfg.drives.len(), 1);
163        assert_eq!(cfg.network_interfaces.len(), 1);
164        assert_eq!(cfg.squib.network, Some(SquibNetworkMode::Userspace));
165    }
166}