Skip to main content

squib_api/schemas/
network.rs

1//! `/network-interfaces/{id}` PUT and PATCH bodies.
2//!
3//! Per [21-api-compat-matrix.md `/network-interfaces/{id}`
4//! PUT](../../../specs/21-api-compat-matrix.md#network-interfacesid-put):
5//!
6//! - `iface_id` — `^[A-Za-z0-9_]{1,64}$`.
7//! - `host_dev_name` — `P` deviation: mapped to a vmnet handle name `squib-tap-<iface_id>`; literal
8//!   Linux TAP names are not honored.
9//! - `guest_mac` — auto-generated if missing.
10//! - `rx_rate_limiter`, `tx_rate_limiter` — passthrough validated structurally.
11
12use serde::{Deserialize, Serialize};
13use squib_core::HostDevName;
14
15use super::common::{IfaceId, MacAddr};
16
17/// Raw `/network-interfaces/{id}` PUT body off the wire.
18#[derive(Debug, Clone, Deserialize)]
19#[serde(deny_unknown_fields)]
20pub struct RawNetworkInterfaceConfig {
21    /// Interface identifier.
22    pub iface_id: String,
23    /// Caller-supplied host device name. Squib maps it to a vmnet handle name.
24    pub host_dev_name: String,
25    /// Optional guest MAC address; auto-generated when absent.
26    #[serde(default)]
27    pub guest_mac: Option<String>,
28    /// RX rate limiter passthrough.
29    #[serde(default)]
30    pub rx_rate_limiter: Option<serde_json::Value>,
31    /// TX rate limiter passthrough.
32    #[serde(default)]
33    pub tx_rate_limiter: Option<serde_json::Value>,
34}
35
36/// Validated `/network-interfaces/{id}` PUT body.
37#[derive(Debug, Clone, Serialize)]
38#[non_exhaustive]
39pub struct NetworkInterfaceConfig {
40    /// Validated interface ID.
41    pub iface_id: IfaceId,
42    /// Caller-requested host device name (informational; the vmnet handle is derived).
43    /// Wrapped in [`HostDevName`] so downstream consumers (`squib-vmm::NetSpec`) cannot
44    /// re-introduce an unvalidated `String` by accident.
45    pub host_dev_name: HostDevName,
46    /// Validated guest MAC; `None` requests auto-generation downstream.
47    pub guest_mac: Option<MacAddr>,
48    /// RX rate limiter passthrough.
49    pub rx_rate_limiter: Option<serde_json::Value>,
50    /// TX rate limiter passthrough.
51    pub tx_rate_limiter: Option<serde_json::Value>,
52}
53
54impl TryFrom<RawNetworkInterfaceConfig> for NetworkInterfaceConfig {
55    type Error = String;
56
57    fn try_from(raw: RawNetworkInterfaceConfig) -> Result<Self, Self::Error> {
58        let iface_id = IfaceId::new(raw.iface_id)?;
59        let host_dev_name = HostDevName::new(raw.host_dev_name).map_err(|e| e.to_string())?;
60        let guest_mac = match raw.guest_mac {
61            Some(s) => Some(MacAddr::parse(&s)?),
62            None => None,
63        };
64        Ok(Self {
65            iface_id,
66            host_dev_name,
67            guest_mac,
68            rx_rate_limiter: raw.rx_rate_limiter,
69            tx_rate_limiter: raw.tx_rate_limiter,
70        })
71    }
72}
73
74/// Raw `/network-interfaces/{id}` PATCH body.
75#[derive(Debug, Clone, Deserialize)]
76#[serde(deny_unknown_fields)]
77pub struct RawNetworkPatch {
78    /// Interface ID being patched (must match the URL `{id}`).
79    pub iface_id: String,
80    /// Replacement RX rate limiter.
81    #[serde(default)]
82    pub rx_rate_limiter: Option<serde_json::Value>,
83    /// Replacement TX rate limiter.
84    #[serde(default)]
85    pub tx_rate_limiter: Option<serde_json::Value>,
86}
87
88/// Validated `/network-interfaces/{id}` PATCH body.
89#[derive(Debug, Clone, Serialize)]
90#[non_exhaustive]
91pub struct NetworkPatch {
92    /// Validated interface ID.
93    pub iface_id: IfaceId,
94    /// Replacement RX rate limiter.
95    pub rx_rate_limiter: Option<serde_json::Value>,
96    /// Replacement TX rate limiter.
97    pub tx_rate_limiter: Option<serde_json::Value>,
98}
99
100impl TryFrom<RawNetworkPatch> for NetworkPatch {
101    type Error = String;
102
103    fn try_from(raw: RawNetworkPatch) -> Result<Self, Self::Error> {
104        Ok(Self {
105            iface_id: IfaceId::new(raw.iface_id)?,
106            rx_rate_limiter: raw.rx_rate_limiter,
107            tx_rate_limiter: raw.tx_rate_limiter,
108        })
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    #[test]
117    fn test_should_accept_minimal_network_interface() {
118        let raw = RawNetworkInterfaceConfig {
119            iface_id: "eth0".into(),
120            host_dev_name: "tap0".into(),
121            guest_mac: None,
122            rx_rate_limiter: None,
123            tx_rate_limiter: None,
124        };
125        let cfg = NetworkInterfaceConfig::try_from(raw).unwrap();
126        assert_eq!(cfg.iface_id.as_str(), "eth0");
127        assert!(cfg.guest_mac.is_none());
128    }
129
130    #[test]
131    fn test_should_validate_guest_mac() {
132        let raw = RawNetworkInterfaceConfig {
133            iface_id: "eth0".into(),
134            host_dev_name: "tap0".into(),
135            guest_mac: Some("aa:bb:cc:dd:ee:ff".into()),
136            rx_rate_limiter: None,
137            tx_rate_limiter: None,
138        };
139        let cfg = NetworkInterfaceConfig::try_from(raw).unwrap();
140        assert!(cfg.guest_mac.is_some());
141    }
142
143    #[test]
144    fn test_should_reject_empty_host_dev_name() {
145        let raw = RawNetworkInterfaceConfig {
146            iface_id: "eth0".into(),
147            host_dev_name: String::new(),
148            guest_mac: None,
149            rx_rate_limiter: None,
150            tx_rate_limiter: None,
151        };
152        assert!(NetworkInterfaceConfig::try_from(raw).is_err());
153    }
154
155    #[test]
156    fn test_should_reject_unknown_fields() {
157        let json = r#"{"iface_id":"eth0","host_dev_name":"tap0","unexpected":1}"#;
158        assert!(serde_json::from_str::<RawNetworkInterfaceConfig>(json).is_err());
159    }
160}