Skip to main content

squib_api/
action.rs

1//! Cross-thread `ApiAction` / `ApiResponse` enums.
2//!
3//! Per [10-data-model.md § 4.1](../../../specs/10-data-model.md#41-api--vmm), the API
4//! layer never touches device or hypervisor state directly. Mutating handlers serialize
5//! through this enum onto a bounded mpsc channel; the VMM event loop consumes the
6//! channel one action at a time.
7//!
8//! `ApiAction` carries fully-validated payloads. Construction of any variant from raw
9//! JSON goes through the corresponding `Raw*` → newtype `TryFrom`, so the VMM event
10//! loop never has to re-validate.
11
12use serde::Serialize;
13
14use crate::schemas::{
15    BalloonConfig, BalloonHintingOp, BalloonStatsUpdate, BalloonUpdate, BootSourceConfig,
16    CpuConfig, DriveConfig, DriveId, DrivePatch, EntropyConfig, HotplugMemoryConfig,
17    HotplugMemoryUpdate, IfaceId, InstanceAction, LoggerConfig, MachineConfig, MachineConfigPatch,
18    MetricsConfig, MmdsConfig, MmdsContents, NetworkInterfaceConfig, NetworkPatch, PmemConfig,
19    PmemPatch, SerialConfig, SnapshotCreateConfig, SnapshotLoadConfig, VmStateChange, VsockConfig,
20};
21
22/// Per-action class used to look up the `tokio::time::timeout` budget for an action
23/// ([70-security.md § 6](../../../specs/70-security.md#6-resource-limits)).
24#[derive(Debug, Clone, Copy, Eq, PartialEq)]
25pub enum ActionClass {
26    /// Pre-boot configuration mutation (PUT/PATCH/DELETE on `/drives`,
27    /// `/network-interfaces`, `/machine-config`, etc.). Default 5 s.
28    PreBootConfig,
29    /// `Action(InstanceStart)`. Default 30 s — boot orchestration including FDT build,
30    /// kernel load, GIC create.
31    InstanceStart,
32    /// `PUT /snapshot/create`. Default 5 min — bounded by memory-file write throughput.
33    SnapshotCreate,
34    /// `PUT /snapshot/load`. Default 5 min — symmetric to `SnapshotCreate`.
35    SnapshotLoad,
36    /// `PATCH /vm` (Pause/Resume). Default 5 s — quiesce wait.
37    VmStateChange,
38    /// `PATCH /balloon` resize. Default 30 s — large balloons take time as the guest
39    /// releases pages.
40    BalloonResize,
41    /// Other in-flight actions (e.g. `FlushMetrics`). Default 5 s.
42    Other,
43}
44
45impl ActionClass {
46    /// Human-readable name for log lines and `fault_message` payloads.
47    #[must_use]
48    pub const fn label(self) -> &'static str {
49        match self {
50            Self::PreBootConfig => "pre-boot config",
51            Self::InstanceStart => "InstanceStart",
52            Self::SnapshotCreate => "PUT /snapshot/create",
53            Self::SnapshotLoad => "PUT /snapshot/load",
54            Self::VmStateChange => "PATCH /vm",
55            Self::BalloonResize => "PATCH /balloon",
56            Self::Other => "VMM action",
57        }
58    }
59}
60
61/// Mutating action posted onto the API → VMM channel.
62#[derive(Debug)]
63pub enum ApiAction {
64    /// `PUT /boot-source`.
65    PutBootSource(BootSourceConfig),
66    /// `PUT /drives/{id}`.
67    PutDrive(DriveConfig),
68    /// `PATCH /drives/{id}`.
69    PatchDrive(DrivePatch),
70    /// `DELETE /drives/{id}`.
71    DeleteDrive {
72        /// Validated drive ID.
73        drive_id: DriveId,
74    },
75    /// `PUT /network-interfaces/{id}`.
76    PutNetwork(NetworkInterfaceConfig),
77    /// `PATCH /network-interfaces/{id}`.
78    PatchNetwork(NetworkPatch),
79    /// `DELETE /network-interfaces/{id}`.
80    DeleteNetwork {
81        /// Validated interface ID.
82        iface_id: IfaceId,
83    },
84    /// `PUT /vsock`.
85    PutVsock(VsockConfig),
86    /// `PUT /mmds` — replace MMDS data store.
87    PutMmds(MmdsContents),
88    /// `PATCH /mmds` — JSON-merge-patch on the data store.
89    PatchMmds(MmdsContents),
90    /// `PUT /mmds/config`.
91    PutMmdsConfig(MmdsConfig),
92    /// `PUT /balloon`.
93    PutBalloon(BalloonConfig),
94    /// `PATCH /balloon`.
95    PatchBalloon(BalloonUpdate),
96    /// `PATCH /balloon/statistics`.
97    PatchBalloonStats(BalloonStatsUpdate),
98    /// `PATCH /balloon/hinting/{op}`.
99    PatchBalloonHinting {
100        /// `start | status | stop`.
101        op: BalloonHintingOp,
102    },
103    /// `PUT /entropy`.
104    PutEntropy(EntropyConfig),
105    /// `PUT /serial`.
106    PutSerial(SerialConfig),
107    /// `PUT /pmem/{id}`.
108    PutPmem(PmemConfig),
109    /// `PATCH /pmem/{id}`.
110    PatchPmem(PmemPatch),
111    /// `DELETE /pmem/{id}`.
112    DeletePmem {
113        /// Validated pmem ID.
114        pmem_id: DriveId,
115    },
116    /// `PUT /hotplug/memory`.
117    PutHotplugMemory(HotplugMemoryConfig),
118    /// `PATCH /hotplug/memory`.
119    PatchHotplugMemory(HotplugMemoryUpdate),
120    /// `PUT /cpu-config`.
121    PutCpuConfig(CpuConfig),
122    /// `PUT /machine-config`.
123    PutMachineConfig(MachineConfig),
124    /// `PATCH /machine-config`.
125    PatchMachineConfig(MachineConfigPatch),
126    /// `PUT /logger`.
127    PutLogger(LoggerConfig),
128    /// `PUT /metrics`.
129    PutMetrics(MetricsConfig),
130    /// `PUT /actions` — every action variant including `InstanceStart`.
131    Action(InstanceAction),
132    /// `PATCH /vm` — pause/resume.
133    PatchVm(VmStateChange),
134    /// `PUT /snapshot/create`.
135    SnapshotCreate(SnapshotCreateConfig),
136    /// `PUT /snapshot/load`.
137    SnapshotLoad(SnapshotLoadConfig),
138    /// SIGINT / process shutdown.
139    Shutdown,
140}
141
142impl ApiAction {
143    /// The timeout class for this action.
144    #[must_use]
145    pub const fn class(&self) -> ActionClass {
146        match self {
147            Self::Action(InstanceAction::InstanceStart) => ActionClass::InstanceStart,
148            Self::Action(_) | Self::Shutdown => ActionClass::Other,
149            Self::PatchVm(_) => ActionClass::VmStateChange,
150            Self::PatchBalloon(_) => ActionClass::BalloonResize,
151            Self::SnapshotCreate(_) => ActionClass::SnapshotCreate,
152            Self::SnapshotLoad(_) => ActionClass::SnapshotLoad,
153            _ => ActionClass::PreBootConfig,
154        }
155    }
156
157    /// `true` if this action should be admissible **before** the microVM has booted.
158    #[must_use]
159    pub const fn is_pre_boot(&self) -> bool {
160        matches!(
161            self,
162            Self::PutBootSource(_)
163                | Self::PutDrive(_)
164                | Self::PutNetwork(_)
165                | Self::PutVsock(_)
166                | Self::PutMmds(_)
167                | Self::PatchMmds(_)
168                | Self::PutMmdsConfig(_)
169                | Self::PutBalloon(_)
170                | Self::PutEntropy(_)
171                | Self::PutSerial(_)
172                | Self::PutPmem(_)
173                | Self::PutHotplugMemory(_)
174                | Self::PutCpuConfig(_)
175                | Self::PutMachineConfig(_)
176                | Self::PatchMachineConfig(_)
177                | Self::PutLogger(_)
178                | Self::PutMetrics(_)
179                | Self::PatchDrive(_)
180                | Self::PatchNetwork(_)
181                | Self::Action(InstanceAction::InstanceStart)
182                | Self::SnapshotLoad(_)
183                | Self::Shutdown
184        )
185    }
186
187    /// `true` if this action is admissible **after** the microVM has booted.
188    #[must_use]
189    pub const fn is_post_boot(&self) -> bool {
190        matches!(
191            self,
192            Self::PatchVm(_)
193                | Self::PatchBalloon(_)
194                | Self::PatchBalloonStats(_)
195                | Self::PatchBalloonHinting { .. }
196                | Self::PatchHotplugMemory(_)
197                | Self::PatchMmds(_)
198                | Self::PutMmds(_)
199                | Self::PatchDrive(_)
200                | Self::PatchNetwork(_)
201                | Self::PatchPmem(_)
202                | Self::DeleteDrive { .. }
203                | Self::DeleteNetwork { .. }
204                | Self::DeletePmem { .. }
205                | Self::SnapshotCreate(_)
206                | Self::Action(InstanceAction::FlushMetrics)
207                | Self::Shutdown
208        )
209    }
210
211    /// Human-readable label for log lines.
212    #[must_use]
213    pub const fn label(&self) -> &'static str {
214        match self {
215            Self::PutBootSource(_) => "PUT /boot-source",
216            Self::PutDrive(_) => "PUT /drives/{id}",
217            Self::PatchDrive(_) => "PATCH /drives/{id}",
218            Self::DeleteDrive { .. } => "DELETE /drives/{id}",
219            Self::PutNetwork(_) => "PUT /network-interfaces/{id}",
220            Self::PatchNetwork(_) => "PATCH /network-interfaces/{id}",
221            Self::DeleteNetwork { .. } => "DELETE /network-interfaces/{id}",
222            Self::PutVsock(_) => "PUT /vsock",
223            Self::PutMmds(_) => "PUT /mmds",
224            Self::PatchMmds(_) => "PATCH /mmds",
225            Self::PutMmdsConfig(_) => "PUT /mmds/config",
226            Self::PutBalloon(_) => "PUT /balloon",
227            Self::PatchBalloon(_) => "PATCH /balloon",
228            Self::PatchBalloonStats(_) => "PATCH /balloon/statistics",
229            Self::PatchBalloonHinting { .. } => "PATCH /balloon/hinting/{op}",
230            Self::PutEntropy(_) => "PUT /entropy",
231            Self::PutSerial(_) => "PUT /serial",
232            Self::PutPmem(_) => "PUT /pmem/{id}",
233            Self::PatchPmem(_) => "PATCH /pmem/{id}",
234            Self::DeletePmem { .. } => "DELETE /pmem/{id}",
235            Self::PutHotplugMemory(_) => "PUT /hotplug/memory",
236            Self::PatchHotplugMemory(_) => "PATCH /hotplug/memory",
237            Self::PutCpuConfig(_) => "PUT /cpu-config",
238            Self::PutMachineConfig(_) => "PUT /machine-config",
239            Self::PatchMachineConfig(_) => "PATCH /machine-config",
240            Self::PutLogger(_) => "PUT /logger",
241            Self::PutMetrics(_) => "PUT /metrics",
242            Self::Action(_) => "PUT /actions",
243            Self::PatchVm(_) => "PATCH /vm",
244            Self::SnapshotCreate(_) => "PUT /snapshot/create",
245            Self::SnapshotLoad(_) => "PUT /snapshot/load",
246            Self::Shutdown => "Shutdown",
247        }
248    }
249}
250
251/// Response posted back through the `oneshot` paired with each `ApiAction`.
252#[derive(Debug, Clone, Serialize)]
253#[serde(untagged)]
254pub enum ApiResponse {
255    /// `204 No Content`.
256    NoContent,
257    /// `200 OK` with a JSON body.
258    Json(serde_json::Value),
259    /// `4xx` with a `fault_message`.
260    Fault {
261        /// HTTP status code (400, 413, ...).
262        status: u16,
263        /// Body `fault_message` value.
264        fault_message: String,
265    },
266}
267
268#[cfg(test)]
269mod tests {
270    use super::*;
271
272    #[test]
273    fn test_should_classify_instance_start_for_30s_timeout() {
274        let action = ApiAction::Action(InstanceAction::InstanceStart);
275        assert_eq!(action.class(), ActionClass::InstanceStart);
276    }
277
278    #[test]
279    fn test_should_classify_snapshot_actions_for_5min_timeout() {
280        let create_label = ActionClass::SnapshotCreate.label();
281        assert_eq!(create_label, "PUT /snapshot/create");
282    }
283
284    #[test]
285    fn test_should_classify_pre_boot_config_default() {
286        let action = ApiAction::PutEntropy(EntropyConfig::default());
287        assert_eq!(action.class(), ActionClass::PreBootConfig);
288    }
289
290    #[test]
291    fn test_should_label_actions_human_readable() {
292        let action = ApiAction::PatchVm(VmStateChange::Paused);
293        assert_eq!(action.label(), "PATCH /vm");
294    }
295}