Skip to main content

qemu_command_builder/args/
fsdev.rs

1use crate::parsers::{ARG_FSDEV, DELIM_COMMA};
2use crate::to_command::{ToArg, ToCommand};
3use bon::Builder;
4use proptest_derive::Arbitrary;
5use std::path::PathBuf;
6use std::str::FromStr;
7
8/// QEMU `security_model=` values for `-fsdev local`.
9#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Arbitrary)]
10pub enum SecurityModel {
11    Passthrough,
12    MappedXAttr,
13    MappedFile,
14    None,
15}
16
17impl ToArg for SecurityModel {
18    fn to_arg(&self) -> &str {
19        match self {
20            SecurityModel::Passthrough => "passthrough",
21            SecurityModel::MappedXAttr => "mapped-xattr",
22            SecurityModel::MappedFile => "mapped-file",
23            SecurityModel::None => "none",
24        }
25    }
26}
27
28impl FromStr for SecurityModel {
29    type Err = String;
30
31    fn from_str(s: &str) -> Result<Self, Self::Err> {
32        match s {
33            "passthrough" => Ok(Self::Passthrough),
34            "mapped-xattr" => Ok(Self::MappedXAttr),
35            "mapped-file" => Ok(Self::MappedFile),
36            "none" => Ok(Self::None),
37            _ => Err(format!("invalid security_model value: {s}")),
38        }
39    }
40}
41
42/// A `-fsdev local,...` backend.
43///
44/// This exports a host path for a guest 9p device.
45#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Builder, Arbitrary)]
46pub struct FsDevLocal {
47    /// Specifies identifier for this device.
48    id: String,
49
50    /// Specifies the export path for the file system device.
51    path: PathBuf,
52
53    /// Specifies the security model to be used for this export path.
54    security_model: SecurityModel,
55
56    /// Emit `writeout=immediate` when enabled.
57    writeout: Option<()>,
58
59    /// Emit `readonly=on` when enabled.
60    readonly: Option<()>,
61
62    /// Specifies the default mode for newly created files on the host.
63    fmode: Option<String>,
64
65    /// Specifies the default mode for newly created directories on the host.
66    dmode: Option<String>,
67
68    /// Throttling limits in bytes per second.
69    throttling_bps_total: Option<usize>,
70    throttling_bps_read: Option<usize>,
71    throttling_bps_write: Option<usize>,
72
73    /// Bursts in bytes per second.
74    throttling_bps_total_max: Option<usize>,
75    bps_read_max: Option<usize>,
76    bps_write_max: Option<usize>,
77
78    /// Request rate limits in requests per second.
79    throttling_iops_total: Option<usize>,
80    throttling_iops_read: Option<usize>,
81    throttling_iops_write: Option<usize>,
82
83    /// Bursts in requests per second.
84    throttling_iops_total_max: Option<usize>,
85    throttling_iops_read_max: Option<usize>,
86    throttling_iops_write_max: Option<usize>,
87
88    /// Request size for IOPS throttling accounting.
89    throttling_iops_size: Option<usize>,
90}
91
92/// A synthetic `-fsdev synth,...` backend used by QTests.
93#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Builder, Arbitrary)]
94pub struct FsDevSynth {
95    /// Specifies identifier for this device.
96    id: String,
97    /// Emit `readonly=on` when enabled.
98    readonly: Option<()>,
99}
100
101/// Define a new QEMU file system device.
102#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Arbitrary)]
103pub enum FsDev {
104    Local(Box<FsDevLocal>),
105    Synth(FsDevSynth),
106}
107
108impl ToCommand for FsDev {
109    fn command(&self) -> String {
110        ARG_FSDEV.to_string()
111    }
112
113    fn to_args(&self) -> Vec<String> {
114        let mut args = vec![];
115        match self {
116            FsDev::Local(local) => {
117                args.push("local".to_string());
118                args.push(format!("id={}", local.id));
119                args.push(format!("path={}", local.path.display()));
120                args.push(format!("security_model={}", local.security_model.to_arg()));
121
122                if local.writeout.is_some() {
123                    args.push("writeout=immediate".to_string());
124                }
125                if local.readonly.is_some() {
126                    args.push("readonly=on".to_string());
127                }
128                if let Some(fmode) = &local.fmode {
129                    args.push(format!("fmode={}", fmode));
130                }
131                if let Some(dmode) = &local.dmode {
132                    args.push(format!("dmode={}", dmode));
133                }
134                if let Some(v) = local.throttling_bps_total {
135                    args.push(format!("throttling.bps-total={}", v));
136                }
137                if let Some(v) = local.throttling_bps_read {
138                    args.push(format!("throttling.bps-read={}", v));
139                }
140                if let Some(v) = local.throttling_bps_write {
141                    args.push(format!("throttling.bps-write={}", v));
142                }
143                if let Some(v) = local.throttling_bps_total_max {
144                    args.push(format!("throttling.bps-total-max={}", v));
145                }
146                if let Some(v) = local.bps_read_max {
147                    args.push(format!("throttling.bps-read-max={}", v));
148                }
149                if let Some(v) = local.bps_write_max {
150                    args.push(format!("throttling.bps-write-max={}", v));
151                }
152                if let Some(v) = local.throttling_iops_total {
153                    args.push(format!("throttling.iops-total={}", v));
154                }
155                if let Some(v) = local.throttling_iops_read {
156                    args.push(format!("throttling.iops-read={}", v));
157                }
158                if let Some(v) = local.throttling_iops_write {
159                    args.push(format!("throttling.iops-write={}", v));
160                }
161                if let Some(v) = local.throttling_iops_total_max {
162                    args.push(format!("throttling.iops-total-max={}", v));
163                }
164                if let Some(v) = local.throttling_iops_read_max {
165                    args.push(format!("throttling.iops-read-max={}", v));
166                }
167                if let Some(v) = local.throttling_iops_write_max {
168                    args.push(format!("throttling.iops-write-max={}", v));
169                }
170                if let Some(v) = local.throttling_iops_size {
171                    args.push(format!("throttling.iops-size={}", v));
172                }
173            }
174            FsDev::Synth(synth) => {
175                args.push("synth".to_string());
176                args.push(format!("id={}", synth.id));
177                if synth.readonly.is_some() {
178                    args.push("readonly=on".to_string());
179                }
180            }
181        }
182
183        vec![args.join(DELIM_COMMA)]
184    }
185}
186
187impl FromStr for FsDev {
188    type Err = String;
189
190    fn from_str(s: &str) -> Result<Self, Self::Err> {
191        let mut parts = s.split(DELIM_COMMA);
192        let backend = parts.next().ok_or_else(|| "empty fsdev argument".to_string())?;
193        match backend {
194            "local" => parse_local_fsdev(parts.collect()),
195            "synth" => parse_synth_fsdev(parts.collect()),
196            other => Err(format!("unsupported fsdev backend: {other}")),
197        }
198    }
199}
200
201fn parse_local_fsdev(parts: Vec<&str>) -> Result<FsDev, String> {
202    let mut id = None;
203    let mut path = None;
204    let mut security_model = None;
205    let mut writeout = None;
206    let mut readonly = None;
207    let mut fmode = None;
208    let mut dmode = None;
209    let mut throttling_bps_total = None;
210    let mut throttling_bps_read = None;
211    let mut throttling_bps_write = None;
212    let mut throttling_bps_total_max = None;
213    let mut bps_read_max = None;
214    let mut bps_write_max = None;
215    let mut throttling_iops_total = None;
216    let mut throttling_iops_read = None;
217    let mut throttling_iops_write = None;
218    let mut throttling_iops_total_max = None;
219    let mut throttling_iops_read_max = None;
220    let mut throttling_iops_write_max = None;
221    let mut throttling_iops_size = None;
222
223    for part in parts {
224        let (key, value) = part.split_once('=').ok_or_else(|| format!("invalid fsdev local option: {part}"))?;
225        match key {
226            "id" => id = Some(value.to_string()),
227            "path" => path = Some(PathBuf::from(value)),
228            "security_model" => security_model = Some(value.parse::<SecurityModel>()?),
229            "writeout" => {
230                if value != "immediate" {
231                    return Err(format!("invalid writeout value: {value}"));
232                }
233                writeout = Some(());
234            }
235            "readonly" => {
236                if value != "on" {
237                    return Err(format!("invalid readonly value: {value}"));
238                }
239                readonly = Some(());
240            }
241            "fmode" => fmode = Some(value.to_string()),
242            "dmode" => dmode = Some(value.to_string()),
243            "throttling.bps-total" => throttling_bps_total = Some(value.parse::<usize>().map_err(|e| e.to_string())?),
244            "throttling.bps-read" => throttling_bps_read = Some(value.parse::<usize>().map_err(|e| e.to_string())?),
245            "throttling.bps-write" => throttling_bps_write = Some(value.parse::<usize>().map_err(|e| e.to_string())?),
246            "throttling.bps-total-max" => throttling_bps_total_max = Some(value.parse::<usize>().map_err(|e| e.to_string())?),
247            "throttling.bps-read-max" => bps_read_max = Some(value.parse::<usize>().map_err(|e| e.to_string())?),
248            "throttling.bps-write-max" => bps_write_max = Some(value.parse::<usize>().map_err(|e| e.to_string())?),
249            "throttling.iops-total" => throttling_iops_total = Some(value.parse::<usize>().map_err(|e| e.to_string())?),
250            "throttling.iops-read" => throttling_iops_read = Some(value.parse::<usize>().map_err(|e| e.to_string())?),
251            "throttling.iops-write" => throttling_iops_write = Some(value.parse::<usize>().map_err(|e| e.to_string())?),
252            "throttling.iops-total-max" => throttling_iops_total_max = Some(value.parse::<usize>().map_err(|e| e.to_string())?),
253            "throttling.iops-read-max" => throttling_iops_read_max = Some(value.parse::<usize>().map_err(|e| e.to_string())?),
254            "throttling.iops-write-max" => throttling_iops_write_max = Some(value.parse::<usize>().map_err(|e| e.to_string())?),
255            "throttling.iops-size" => throttling_iops_size = Some(value.parse::<usize>().map_err(|e| e.to_string())?),
256            other => return Err(format!("unsupported fsdev local option: {other}")),
257        }
258    }
259
260    Ok(FsDev::Local(Box::new(FsDevLocal {
261        id: id.ok_or_else(|| "fsdev local requires id=".to_string())?,
262        path: path.ok_or_else(|| "fsdev local requires path=".to_string())?,
263        security_model: security_model.ok_or_else(|| "fsdev local requires security_model=".to_string())?,
264        writeout,
265        readonly,
266        fmode,
267        dmode,
268        throttling_bps_total,
269        throttling_bps_read,
270        throttling_bps_write,
271        throttling_bps_total_max,
272        bps_read_max,
273        bps_write_max,
274        throttling_iops_total,
275        throttling_iops_read,
276        throttling_iops_write,
277        throttling_iops_total_max,
278        throttling_iops_read_max,
279        throttling_iops_write_max,
280        throttling_iops_size,
281    })))
282}
283
284fn parse_synth_fsdev(parts: Vec<&str>) -> Result<FsDev, String> {
285    let mut id = None;
286    let mut readonly = None;
287
288    for part in parts {
289        let (key, value) = part.split_once('=').ok_or_else(|| format!("invalid fsdev synth option: {part}"))?;
290        match key {
291            "id" => id = Some(value.to_string()),
292            "readonly" => {
293                if value != "on" {
294                    return Err(format!("invalid readonly value: {value}"));
295                }
296                readonly = Some(());
297            }
298            other => return Err(format!("unsupported fsdev synth option: {other}")),
299        }
300    }
301
302    Ok(FsDev::Synth(FsDevSynth {
303        id: id.ok_or_else(|| "fsdev synth requires id=".to_string())?,
304        readonly,
305    }))
306}