Skip to main content

squib_api/schemas/
hotplug_memory.rs

1//! `/hotplug/memory` PUT and PATCH bodies — virtio-mem hotplug.
2//!
3//! Per [21-api-compat-matrix.md
4//! `/hotplug/memory`](../../../specs/21-api-compat-matrix.md#hotplugmemory) the endpoint accepts
5//! `PUT` (full config), `GET` (read-only mirror), and `PATCH` (size update). Field names mirror
6//! upstream Firecracker's `MemoryHotplugConfig` and `MemoryHotplugSizeUpdate`.
7
8use serde::{Deserialize, Serialize};
9
10/// Default block size in MiB. Block is the smallest unit the guest can hot(un)plug.
11pub const DEFAULT_BLOCK_SIZE_MIB: u64 = 2;
12
13/// Default slot size in MiB. Slot is the smallest unit the host can (de)attach.
14pub const DEFAULT_SLOT_SIZE_MIB: u64 = 128;
15
16/// Upper bound on the configured `total_size_mib`. The host-RAM cap is enforced at
17/// the controller (Phase 3 territory once the VMM event loop knows the running set).
18pub const MAX_TOTAL_SIZE_MIB: u64 = 1_048_576; // 1 TiB
19
20/// Raw `/hotplug/memory` PUT body.
21#[derive(Debug, Clone, Deserialize)]
22#[serde(deny_unknown_fields)]
23pub struct RawHotplugMemoryConfig {
24    /// Total memory size in MiB that can be hotplugged.
25    pub total_size_mib: u64,
26    /// Block size in MiB.
27    #[serde(default = "default_block_size")]
28    pub block_size_mib: u64,
29    /// Slot size in MiB.
30    #[serde(default = "default_slot_size")]
31    pub slot_size_mib: u64,
32}
33
34fn default_block_size() -> u64 {
35    DEFAULT_BLOCK_SIZE_MIB
36}
37
38fn default_slot_size() -> u64 {
39    DEFAULT_SLOT_SIZE_MIB
40}
41
42/// Validated `/hotplug/memory` PUT body.
43#[derive(Debug, Clone, Serialize)]
44#[non_exhaustive]
45pub struct HotplugMemoryConfig {
46    /// Total memory size in MiB.
47    pub total_size_mib: u64,
48    /// Block size in MiB.
49    pub block_size_mib: u64,
50    /// Slot size in MiB.
51    pub slot_size_mib: u64,
52}
53
54impl TryFrom<RawHotplugMemoryConfig> for HotplugMemoryConfig {
55    type Error = String;
56
57    fn try_from(raw: RawHotplugMemoryConfig) -> Result<Self, Self::Error> {
58        if raw.total_size_mib == 0 || raw.total_size_mib > MAX_TOTAL_SIZE_MIB {
59            return Err(format!(
60                "Invalid total_size_mib: must be 1..={MAX_TOTAL_SIZE_MIB}"
61            ));
62        }
63        if raw.block_size_mib == 0 {
64            return Err("Invalid block_size_mib: must be >= 1".into());
65        }
66        if raw.slot_size_mib == 0 {
67            return Err("Invalid slot_size_mib: must be >= 1".into());
68        }
69        if raw.slot_size_mib < raw.block_size_mib {
70            return Err("Invalid hotplug-memory: slot_size_mib must be >= block_size_mib".into());
71        }
72        if !raw.total_size_mib.is_multiple_of(raw.slot_size_mib) {
73            return Err("Invalid total_size_mib: must be a multiple of slot_size_mib".into());
74        }
75        Ok(Self {
76            total_size_mib: raw.total_size_mib,
77            block_size_mib: raw.block_size_mib,
78            slot_size_mib: raw.slot_size_mib,
79        })
80    }
81}
82
83/// Raw `/hotplug/memory` PATCH body.
84#[derive(Debug, Clone, Deserialize)]
85#[serde(deny_unknown_fields)]
86pub struct RawHotplugMemoryUpdate {
87    /// New requested size in MiB.
88    pub requested_size_mib: u64,
89}
90
91/// Validated `/hotplug/memory` PATCH body.
92#[derive(Debug, Clone, Serialize)]
93#[non_exhaustive]
94pub struct HotplugMemoryUpdate {
95    /// New requested size in MiB.
96    pub requested_size_mib: u64,
97}
98
99impl TryFrom<RawHotplugMemoryUpdate> for HotplugMemoryUpdate {
100    type Error = String;
101
102    fn try_from(raw: RawHotplugMemoryUpdate) -> Result<Self, Self::Error> {
103        if raw.requested_size_mib > MAX_TOTAL_SIZE_MIB {
104            return Err(format!(
105                "Invalid requested_size_mib: must be 0..={MAX_TOTAL_SIZE_MIB}"
106            ));
107        }
108        Ok(Self {
109            requested_size_mib: raw.requested_size_mib,
110        })
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117
118    #[test]
119    fn test_should_accept_minimal_hotplug_memory_config() {
120        let cfg = HotplugMemoryConfig::try_from(RawHotplugMemoryConfig {
121            total_size_mib: 256,
122            block_size_mib: DEFAULT_BLOCK_SIZE_MIB,
123            slot_size_mib: DEFAULT_SLOT_SIZE_MIB,
124        })
125        .unwrap();
126        assert_eq!(cfg.total_size_mib, 256);
127    }
128
129    #[test]
130    fn test_should_reject_unaligned_total_size() {
131        assert!(
132            HotplugMemoryConfig::try_from(RawHotplugMemoryConfig {
133                total_size_mib: 100,
134                block_size_mib: 2,
135                slot_size_mib: 128,
136            })
137            .is_err()
138        );
139    }
140
141    #[test]
142    fn test_should_reject_slot_smaller_than_block() {
143        assert!(
144            HotplugMemoryConfig::try_from(RawHotplugMemoryConfig {
145                total_size_mib: 256,
146                block_size_mib: 64,
147                slot_size_mib: 32,
148            })
149            .is_err()
150        );
151    }
152
153    #[test]
154    fn test_should_accept_zero_requested_size_for_unplug_to_zero() {
155        let upd = HotplugMemoryUpdate::try_from(RawHotplugMemoryUpdate {
156            requested_size_mib: 0,
157        })
158        .unwrap();
159        assert_eq!(upd.requested_size_mib, 0);
160    }
161
162    #[test]
163    fn test_should_round_trip_patch_through_serde() {
164        let raw: RawHotplugMemoryUpdate =
165            serde_json::from_str(r#"{"requested_size_mib":512}"#).unwrap();
166        assert_eq!(raw.requested_size_mib, 512);
167    }
168}