Skip to main content

squib_api/schemas/
machine_config.rs

1//! `/machine-config` request and response shapes.
2//!
3//! Per [21-api-compat-matrix.md § 2
4//! `/machine-config`](../../../specs/21-api-compat-matrix.md#machine-config):
5//!
6//! | field | rule |
7//! |-------|------|
8//! | `vcpu_count` | `1..=32` (D19 / `MAX_SUPPORTED_VCPUS`) |
9//! | `mem_size_mib` | `>= 1`; upper bound enforced at controller against host RAM |
10//! | `smt` | accept `false` only; `true` rejected with the upstream-shaped fault |
11//! | `track_dirty_pages` | bool, default false |
12//! | `cpu_template` | `Some("V1N1")` ⇒ aarch64 sysreg subset; x86 templates ⇒ A (warn) |
13//! | `huge_pages` | `Some("2M")` ⇒ A (warn); `None` default |
14
15use serde::{Deserialize, Serialize};
16
17use super::common::{MAX_VCPU_COUNT, MemSizeMib};
18
19/// Raw shape of the `/machine-config` PUT body, exactly as it arrives off the wire.
20#[derive(Debug, Clone, Deserialize)]
21#[serde(deny_unknown_fields)]
22pub struct RawMachineConfig {
23    /// Number of vCPUs to allocate.
24    pub vcpu_count: u32,
25    /// Guest RAM size, in MiB.
26    pub mem_size_mib: u64,
27    /// Symmetric multithreading. Must be `false` on aarch64.
28    #[serde(default)]
29    pub smt: bool,
30    /// Enable dirty-page tracking (required for Diff snapshots).
31    #[serde(default)]
32    pub track_dirty_pages: bool,
33    /// CPU template name. Aarch64-only `V1N1` is honored; x86 names accept-and-warn.
34    #[serde(default)]
35    pub cpu_template: Option<String>,
36    /// Huge-page request. Currently `Some("2M")` ⇒ A (warn); `None` default.
37    #[serde(default)]
38    pub huge_pages: Option<String>,
39}
40
41/// Validated `/machine-config` PUT body.
42#[derive(Debug, Clone, Serialize)]
43#[non_exhaustive]
44pub struct MachineConfig {
45    /// Validated vCPU count (`1..=32`).
46    pub vcpu_count: u32,
47    /// Validated RAM size in MiB (`>= 1`).
48    pub mem_size_mib: MemSizeMib,
49    /// Always `false` on aarch64 — the constructor rejects `true`.
50    pub smt: bool,
51    /// Whether dirty-page tracking is enabled.
52    pub track_dirty_pages: bool,
53    /// Validated CPU template (aarch64 names honored, x86 names recorded for warn).
54    pub cpu_template: Option<String>,
55    /// Huge-page request (`Some("2M")` ⇒ accept-and-warn).
56    pub huge_pages: Option<String>,
57}
58
59impl TryFrom<RawMachineConfig> for MachineConfig {
60    type Error = String;
61
62    fn try_from(raw: RawMachineConfig) -> Result<Self, Self::Error> {
63        if raw.vcpu_count == 0 || raw.vcpu_count > MAX_VCPU_COUNT {
64            return Err(format!(
65                "Invalid vcpu_count: must be in 1..={MAX_VCPU_COUNT}, got {}",
66                raw.vcpu_count
67            ));
68        }
69        if raw.smt {
70            // R row in the compat matrix.
71            return Err("Invalid arch field for SMT: SMT not supported on Apple Silicon".into());
72        }
73        let mem_size_mib = MemSizeMib::new(raw.mem_size_mib)?;
74        if let Some(t) = raw.cpu_template.as_deref() {
75            if t.is_empty() {
76                return Err("Invalid cpu_template: must not be empty".into());
77            }
78            if t.len() > 64 {
79                return Err("Invalid cpu_template: exceeds 64 bytes".into());
80            }
81        }
82        if let Some(h) = raw.huge_pages.as_deref()
83            && (h.is_empty() || h.len() > 16)
84        {
85            return Err("Invalid huge_pages: empty or exceeds 16 bytes".into());
86        }
87        Ok(Self {
88            vcpu_count: raw.vcpu_count,
89            mem_size_mib,
90            smt: raw.smt,
91            track_dirty_pages: raw.track_dirty_pages,
92            cpu_template: raw.cpu_template,
93            huge_pages: raw.huge_pages,
94        })
95    }
96}
97
98/// Raw shape of the `/machine-config` PATCH body — every field optional.
99#[derive(Debug, Clone, Deserialize)]
100#[serde(deny_unknown_fields)]
101pub struct RawMachineConfigPatch {
102    /// New vCPU count.
103    #[serde(default)]
104    pub vcpu_count: Option<u32>,
105    /// New RAM size in MiB.
106    #[serde(default)]
107    pub mem_size_mib: Option<u64>,
108    /// SMT toggle (must remain `false` on aarch64).
109    #[serde(default)]
110    pub smt: Option<bool>,
111    /// Toggle dirty-page tracking.
112    #[serde(default)]
113    pub track_dirty_pages: Option<bool>,
114    /// Replacement CPU template.
115    #[serde(default)]
116    pub cpu_template: Option<String>,
117    /// Replacement huge-pages setting.
118    #[serde(default)]
119    pub huge_pages: Option<String>,
120}
121
122/// Validated `/machine-config` PATCH body.
123#[derive(Debug, Clone, Serialize)]
124#[non_exhaustive]
125pub struct MachineConfigPatch {
126    /// New vCPU count, validated `1..=32` if present.
127    pub vcpu_count: Option<u32>,
128    /// New RAM size, validated `>= 1` if present.
129    pub mem_size_mib: Option<MemSizeMib>,
130    /// SMT toggle (must remain `false`).
131    pub smt: Option<bool>,
132    /// Dirty-page tracking toggle.
133    pub track_dirty_pages: Option<bool>,
134    /// Replacement CPU template.
135    pub cpu_template: Option<String>,
136    /// Replacement huge-pages setting.
137    pub huge_pages: Option<String>,
138}
139
140impl TryFrom<RawMachineConfigPatch> for MachineConfigPatch {
141    type Error = String;
142
143    fn try_from(raw: RawMachineConfigPatch) -> Result<Self, Self::Error> {
144        if let Some(v) = raw.vcpu_count
145            && (v == 0 || v > MAX_VCPU_COUNT)
146        {
147            return Err(format!(
148                "Invalid vcpu_count: must be in 1..={MAX_VCPU_COUNT}, got {v}"
149            ));
150        }
151        if let Some(true) = raw.smt {
152            return Err("Invalid arch field for SMT: SMT not supported on Apple Silicon".into());
153        }
154        let mem_size_mib = match raw.mem_size_mib {
155            Some(v) => Some(MemSizeMib::new(v)?),
156            None => None,
157        };
158        Ok(Self {
159            vcpu_count: raw.vcpu_count,
160            mem_size_mib,
161            smt: raw.smt,
162            track_dirty_pages: raw.track_dirty_pages,
163            cpu_template: raw.cpu_template,
164            huge_pages: raw.huge_pages,
165        })
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172
173    fn raw(vcpu: u32, mem: u64) -> RawMachineConfig {
174        RawMachineConfig {
175            vcpu_count: vcpu,
176            mem_size_mib: mem,
177            smt: false,
178            track_dirty_pages: false,
179            cpu_template: None,
180            huge_pages: None,
181        }
182    }
183
184    #[test]
185    fn test_should_accept_minimum_machine_config() {
186        let mc = MachineConfig::try_from(raw(1, 256)).unwrap();
187        assert_eq!(mc.vcpu_count, 1);
188        assert_eq!(mc.mem_size_mib.get(), 256);
189    }
190
191    #[test]
192    fn test_should_accept_maximum_vcpu_count() {
193        let mc = MachineConfig::try_from(raw(32, 256)).unwrap();
194        assert_eq!(mc.vcpu_count, 32);
195    }
196
197    #[test]
198    fn test_should_reject_zero_vcpu_count() {
199        assert!(MachineConfig::try_from(raw(0, 256)).is_err());
200    }
201
202    #[test]
203    fn test_should_reject_vcpu_count_above_32() {
204        let err = MachineConfig::try_from(raw(33, 256)).unwrap_err();
205        assert!(err.contains("vcpu_count"));
206    }
207
208    #[test]
209    fn test_should_reject_smt_true_with_upstream_message() {
210        let mut r = raw(1, 256);
211        r.smt = true;
212        let err = MachineConfig::try_from(r).unwrap_err();
213        assert!(err.contains("SMT not supported on Apple Silicon"));
214    }
215
216    #[test]
217    fn test_should_reject_zero_mem_size_mib() {
218        assert!(MachineConfig::try_from(raw(1, 0)).is_err());
219    }
220
221    #[test]
222    fn test_should_reject_unknown_fields() {
223        let json = r#"{"vcpu_count":1,"mem_size_mib":256,"unexpected":true}"#;
224        let res: Result<RawMachineConfig, _> = serde_json::from_str(json);
225        assert!(res.is_err());
226    }
227
228    #[test]
229    fn test_should_validate_patch_partial_fields() {
230        let raw = RawMachineConfigPatch {
231            vcpu_count: Some(4),
232            mem_size_mib: None,
233            smt: None,
234            track_dirty_pages: Some(true),
235            cpu_template: None,
236            huge_pages: None,
237        };
238        let p = MachineConfigPatch::try_from(raw).unwrap();
239        assert_eq!(p.vcpu_count, Some(4));
240        assert_eq!(p.track_dirty_pages, Some(true));
241    }
242}