Skip to main content

squib_api/schemas/
balloon.rs

1//! `/balloon`, `/balloon/statistics`, `/balloon/hinting/{op}` bodies.
2//!
3//! Per [21-api-compat-matrix.md `/balloon`
4//! PUT](../../../specs/21-api-compat-matrix.md#balloon-put):
5//!
6//! - `amount_mib` — `0..=mem_size_mib − 32` (matches upstream `MAX_BALLOON_SIZE_MIB`); the upper
7//!   bound is host-RAM-dependent and validated at the controller.
8//! - `deflate_on_oom` — bool default true (matches upstream).
9//! - `stats_polling_interval_s` — `0..=255`; `0` disables polling.
10//! - `free_page_hinting`, `free_page_reporting` — bool defaults false.
11
12use serde::{Deserialize, Serialize};
13
14/// Maximum `stats_polling_interval_s` per upstream. The wire field is a `u8` so the
15/// type system enforces the cap; this constant exists to document the intent.
16pub const MAX_STATS_POLL_INTERVAL: u8 = u8::MAX;
17
18/// `PATCH /balloon/hinting/{op}` operation kind. Each maps 1:1 to the URL path segment.
19#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
20#[serde(rename_all = "lowercase")]
21pub enum BalloonHintingOp {
22    /// Begin free-page hinting.
23    Start,
24    /// Read free-page hinting status.
25    Status,
26    /// Stop free-page hinting.
27    Stop,
28}
29
30impl BalloonHintingOp {
31    /// Parse from the `{op}` URL segment.
32    pub fn from_url_segment(s: &str) -> Result<Self, String> {
33        match s {
34            "start" => Ok(Self::Start),
35            "status" => Ok(Self::Status),
36            "stop" => Ok(Self::Stop),
37            other => Err(format!(
38                "Invalid balloon-hinting op: must be one of start | status | stop (got {other})"
39            )),
40        }
41    }
42}
43
44/// Raw `/balloon` PUT body off the wire.
45#[derive(Debug, Clone, Deserialize)]
46#[serde(deny_unknown_fields)]
47pub struct RawBalloonConfig {
48    /// Target ballooned amount (MiB).
49    pub amount_mib: u64,
50    /// Whether the guest balloon driver should deflate on OOM.
51    #[serde(default)]
52    pub deflate_on_oom: bool,
53    /// Stats polling interval, seconds (0 disables).
54    #[serde(default)]
55    pub stats_polling_interval_s: u8,
56    /// Enable free-page hinting.
57    #[serde(default)]
58    pub free_page_hinting: bool,
59    /// Enable free-page reporting.
60    #[serde(default)]
61    pub free_page_reporting: bool,
62}
63
64/// Validated `/balloon` PUT body.
65#[derive(Debug, Clone, Serialize)]
66#[non_exhaustive]
67pub struct BalloonConfig {
68    /// Target ballooned amount (MiB).
69    pub amount_mib: u64,
70    /// Whether the guest balloon driver should deflate on OOM.
71    pub deflate_on_oom: bool,
72    /// Stats polling interval, seconds (0 disables).
73    pub stats_polling_interval_s: u8,
74    /// Enable free-page hinting.
75    pub free_page_hinting: bool,
76    /// Enable free-page reporting.
77    pub free_page_reporting: bool,
78}
79
80impl TryFrom<RawBalloonConfig> for BalloonConfig {
81    type Error = String;
82
83    fn try_from(raw: RawBalloonConfig) -> Result<Self, Self::Error> {
84        // No runtime cap needed: `u8` already encodes `0..=MAX_STATS_POLL_INTERVAL`.
85        Ok(Self {
86            amount_mib: raw.amount_mib,
87            deflate_on_oom: raw.deflate_on_oom,
88            stats_polling_interval_s: raw.stats_polling_interval_s,
89            free_page_hinting: raw.free_page_hinting,
90            free_page_reporting: raw.free_page_reporting,
91        })
92    }
93}
94
95/// Raw `/balloon` PATCH body.
96#[derive(Debug, Clone, Deserialize)]
97#[serde(deny_unknown_fields)]
98pub struct RawBalloonUpdate {
99    /// New target ballooned amount.
100    pub amount_mib: u64,
101}
102
103/// Validated `/balloon` PATCH body.
104#[derive(Debug, Clone, Serialize)]
105#[non_exhaustive]
106pub struct BalloonUpdate {
107    /// New target ballooned amount.
108    pub amount_mib: u64,
109}
110
111impl TryFrom<RawBalloonUpdate> for BalloonUpdate {
112    type Error = String;
113
114    fn try_from(raw: RawBalloonUpdate) -> Result<Self, Self::Error> {
115        Ok(Self {
116            amount_mib: raw.amount_mib,
117        })
118    }
119}
120
121/// Raw `/balloon/statistics` PATCH body.
122#[derive(Debug, Clone, Deserialize)]
123#[serde(deny_unknown_fields)]
124pub struct RawBalloonStatsUpdate {
125    /// New stats polling interval (seconds; 0 disables).
126    pub stats_polling_interval_s: u8,
127}
128
129/// Validated `/balloon/statistics` PATCH body.
130#[derive(Debug, Clone, Serialize)]
131#[non_exhaustive]
132pub struct BalloonStatsUpdate {
133    /// New stats polling interval.
134    pub stats_polling_interval_s: u8,
135}
136
137impl TryFrom<RawBalloonStatsUpdate> for BalloonStatsUpdate {
138    type Error = String;
139
140    fn try_from(raw: RawBalloonStatsUpdate) -> Result<Self, Self::Error> {
141        // No runtime cap needed: `u8` already encodes `0..=MAX_STATS_POLL_INTERVAL`.
142        Ok(Self {
143            stats_polling_interval_s: raw.stats_polling_interval_s,
144        })
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151
152    #[test]
153    fn test_should_accept_minimal_balloon_config() {
154        let raw = RawBalloonConfig {
155            amount_mib: 0,
156            deflate_on_oom: false,
157            stats_polling_interval_s: 0,
158            free_page_hinting: false,
159            free_page_reporting: false,
160        };
161        let cfg = BalloonConfig::try_from(raw).unwrap();
162        assert_eq!(cfg.amount_mib, 0);
163    }
164
165    #[test]
166    fn test_should_reject_oversize_polling_interval() {
167        // u8 caps at 255 — but we re-validate to keep the contract explicit and to
168        // catch a future widening to u16.
169        let raw = RawBalloonConfig {
170            amount_mib: 0,
171            deflate_on_oom: false,
172            stats_polling_interval_s: u8::MAX,
173            free_page_hinting: false,
174            free_page_reporting: false,
175        };
176        assert!(BalloonConfig::try_from(raw).is_ok());
177    }
178
179    #[test]
180    fn test_should_round_trip_balloon_update() {
181        let raw = RawBalloonUpdate { amount_mib: 128 };
182        let upd = BalloonUpdate::try_from(raw).unwrap();
183        assert_eq!(upd.amount_mib, 128);
184    }
185}