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    /// Maximum number of concurrent extended-attribute FIDs (zero is unlimited).
69    max_xattr: Option<u64>,
70
71    /// Throttling limits in bytes per second.
72    throttling_bps_total: Option<u64>,
73    throttling_bps_read: Option<u64>,
74    throttling_bps_write: Option<u64>,
75
76    /// Bursts in bytes per second.
77    throttling_bps_total_max: Option<u64>,
78    bps_read_max: Option<u64>,
79    bps_write_max: Option<u64>,
80
81    /// Request rate limits in requests per second.
82    throttling_iops_total: Option<u64>,
83    throttling_iops_read: Option<u64>,
84    throttling_iops_write: Option<u64>,
85
86    /// Bursts in requests per second.
87    throttling_iops_total_max: Option<u64>,
88    throttling_iops_read_max: Option<u64>,
89    throttling_iops_write_max: Option<u64>,
90
91    /// Request size for IOPS throttling accounting.
92    throttling_iops_size: Option<u64>,
93}
94
95/// A synthetic `-fsdev synth,...` backend used by QTests.
96#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Builder, Arbitrary)]
97pub struct FsDevSynth {
98    /// Specifies identifier for this device.
99    id: String,
100    /// Emit `readonly=on` when enabled.
101    readonly: Option<()>,
102    /// Maximum number of concurrent extended-attribute FIDs (zero is unlimited).
103    max_xattr: Option<u64>,
104}
105
106/// Define a new QEMU file system device.
107#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Arbitrary)]
108pub enum FsDev {
109    Local(Box<FsDevLocal>),
110    Synth(FsDevSynth),
111}
112
113impl ToCommand for FsDev {
114    fn command(&self) -> String {
115        ARG_FSDEV.to_string()
116    }
117
118    fn to_args(&self) -> Vec<String> {
119        let mut args = vec![];
120        match self {
121            FsDev::Local(local) => {
122                args.push("local".to_string());
123                args.push(format!("id={}", local.id));
124                args.push(format!("path={}", local.path.display()));
125                args.push(format!("security_model={}", local.security_model.to_arg()));
126
127                if local.writeout.is_some() {
128                    args.push("writeout=immediate".to_string());
129                }
130                if local.readonly.is_some() {
131                    args.push("readonly=on".to_string());
132                }
133                if let Some(fmode) = &local.fmode {
134                    args.push(format!("fmode={}", fmode));
135                }
136                if let Some(dmode) = &local.dmode {
137                    args.push(format!("dmode={}", dmode));
138                }
139                if let Some(max_xattr) = local.max_xattr {
140                    args.push(format!("max_xattr={}", max_xattr));
141                }
142                if let Some(v) = local.throttling_bps_total {
143                    args.push(format!("throttling.bps-total={}", v));
144                }
145                if let Some(v) = local.throttling_bps_read {
146                    args.push(format!("throttling.bps-read={}", v));
147                }
148                if let Some(v) = local.throttling_bps_write {
149                    args.push(format!("throttling.bps-write={}", v));
150                }
151                if let Some(v) = local.throttling_bps_total_max {
152                    args.push(format!("throttling.bps-total-max={}", v));
153                }
154                if let Some(v) = local.bps_read_max {
155                    args.push(format!("throttling.bps-read-max={}", v));
156                }
157                if let Some(v) = local.bps_write_max {
158                    args.push(format!("throttling.bps-write-max={}", v));
159                }
160                if let Some(v) = local.throttling_iops_total {
161                    args.push(format!("throttling.iops-total={}", v));
162                }
163                if let Some(v) = local.throttling_iops_read {
164                    args.push(format!("throttling.iops-read={}", v));
165                }
166                if let Some(v) = local.throttling_iops_write {
167                    args.push(format!("throttling.iops-write={}", v));
168                }
169                if let Some(v) = local.throttling_iops_total_max {
170                    args.push(format!("throttling.iops-total-max={}", v));
171                }
172                if let Some(v) = local.throttling_iops_read_max {
173                    args.push(format!("throttling.iops-read-max={}", v));
174                }
175                if let Some(v) = local.throttling_iops_write_max {
176                    args.push(format!("throttling.iops-write-max={}", v));
177                }
178                if let Some(v) = local.throttling_iops_size {
179                    args.push(format!("throttling.iops-size={}", v));
180                }
181            }
182            FsDev::Synth(synth) => {
183                args.push("synth".to_string());
184                args.push(format!("id={}", synth.id));
185                if synth.readonly.is_some() {
186                    args.push("readonly=on".to_string());
187                }
188                if let Some(max_xattr) = synth.max_xattr {
189                    args.push(format!("max_xattr={}", max_xattr));
190                }
191            }
192        }
193
194        vec![args.join(DELIM_COMMA)]
195    }
196}
197
198impl FromStr for FsDev {
199    type Err = String;
200
201    fn from_str(s: &str) -> Result<Self, Self::Err> {
202        let mut parts = s.split(DELIM_COMMA);
203        let backend = parts.next().ok_or_else(|| "empty fsdev argument".to_string())?;
204        match backend {
205            "local" => parse_local_fsdev(parts.collect()),
206            "synth" => parse_synth_fsdev(parts.collect()),
207            other => Err(format!("unsupported fsdev backend: {other}")),
208        }
209    }
210}
211
212fn parse_local_fsdev(parts: Vec<&str>) -> Result<FsDev, String> {
213    let mut id = None;
214    let mut path = None;
215    let mut security_model = None;
216    let mut writeout = None;
217    let mut readonly = None;
218    let mut fmode = None;
219    let mut dmode = None;
220    let mut max_xattr = None;
221    let mut throttling_bps_total = None;
222    let mut throttling_bps_read = None;
223    let mut throttling_bps_write = None;
224    let mut throttling_bps_total_max = None;
225    let mut bps_read_max = None;
226    let mut bps_write_max = None;
227    let mut throttling_iops_total = None;
228    let mut throttling_iops_read = None;
229    let mut throttling_iops_write = None;
230    let mut throttling_iops_total_max = None;
231    let mut throttling_iops_read_max = None;
232    let mut throttling_iops_write_max = None;
233    let mut throttling_iops_size = None;
234
235    for part in parts {
236        let (key, value) = part.split_once('=').ok_or_else(|| format!("invalid fsdev local option: {part}"))?;
237        match key {
238            "id" => id = Some(value.to_string()),
239            "path" => path = Some(PathBuf::from(value)),
240            "security_model" => security_model = Some(value.parse::<SecurityModel>()?),
241            "writeout" => {
242                if value != "immediate" {
243                    return Err(format!("invalid writeout value: {value}"));
244                }
245                writeout = Some(());
246            }
247            "readonly" => {
248                if value != "on" {
249                    return Err(format!("invalid readonly value: {value}"));
250                }
251                readonly = Some(());
252            }
253            "fmode" => fmode = Some(value.to_string()),
254            "dmode" => dmode = Some(value.to_string()),
255            "max_xattr" => max_xattr = Some(value.parse::<u64>().map_err(|e| e.to_string())?),
256            "throttling.bps-total" => throttling_bps_total = Some(value.parse::<u64>().map_err(|e| e.to_string())?),
257            "throttling.bps-read" => throttling_bps_read = Some(value.parse::<u64>().map_err(|e| e.to_string())?),
258            "throttling.bps-write" => throttling_bps_write = Some(value.parse::<u64>().map_err(|e| e.to_string())?),
259            "throttling.bps-total-max" => throttling_bps_total_max = Some(value.parse::<u64>().map_err(|e| e.to_string())?),
260            "throttling.bps-read-max" => bps_read_max = Some(value.parse::<u64>().map_err(|e| e.to_string())?),
261            "throttling.bps-write-max" => bps_write_max = Some(value.parse::<u64>().map_err(|e| e.to_string())?),
262            "throttling.iops-total" => throttling_iops_total = Some(value.parse::<u64>().map_err(|e| e.to_string())?),
263            "throttling.iops-read" => throttling_iops_read = Some(value.parse::<u64>().map_err(|e| e.to_string())?),
264            "throttling.iops-write" => throttling_iops_write = Some(value.parse::<u64>().map_err(|e| e.to_string())?),
265            "throttling.iops-total-max" => throttling_iops_total_max = Some(value.parse::<u64>().map_err(|e| e.to_string())?),
266            "throttling.iops-read-max" => throttling_iops_read_max = Some(value.parse::<u64>().map_err(|e| e.to_string())?),
267            "throttling.iops-write-max" => throttling_iops_write_max = Some(value.parse::<u64>().map_err(|e| e.to_string())?),
268            "throttling.iops-size" => throttling_iops_size = Some(value.parse::<u64>().map_err(|e| e.to_string())?),
269            other => return Err(format!("unsupported fsdev local option: {other}")),
270        }
271    }
272
273    Ok(FsDev::Local(Box::new(FsDevLocal {
274        id: id.ok_or_else(|| "fsdev local requires id=".to_string())?,
275        path: path.ok_or_else(|| "fsdev local requires path=".to_string())?,
276        security_model: security_model.ok_or_else(|| "fsdev local requires security_model=".to_string())?,
277        writeout,
278        readonly,
279        fmode,
280        dmode,
281        max_xattr,
282        throttling_bps_total,
283        throttling_bps_read,
284        throttling_bps_write,
285        throttling_bps_total_max,
286        bps_read_max,
287        bps_write_max,
288        throttling_iops_total,
289        throttling_iops_read,
290        throttling_iops_write,
291        throttling_iops_total_max,
292        throttling_iops_read_max,
293        throttling_iops_write_max,
294        throttling_iops_size,
295    })))
296}
297
298fn parse_synth_fsdev(parts: Vec<&str>) -> Result<FsDev, String> {
299    let mut id = None;
300    let mut readonly = None;
301    let mut max_xattr = None;
302
303    for part in parts {
304        let (key, value) = part.split_once('=').ok_or_else(|| format!("invalid fsdev synth option: {part}"))?;
305        match key {
306            "id" => id = Some(value.to_string()),
307            "readonly" => {
308                if value != "on" {
309                    return Err(format!("invalid readonly value: {value}"));
310                }
311                readonly = Some(());
312            }
313            "max_xattr" => max_xattr = Some(value.parse::<u64>().map_err(|e| e.to_string())?),
314            other => return Err(format!("unsupported fsdev synth option: {other}")),
315        }
316    }
317
318    Ok(FsDev::Synth(FsDevSynth {
319        id: id.ok_or_else(|| "fsdev synth requires id=".to_string())?,
320        readonly,
321        max_xattr,
322    }))
323}