qemu_command_builder/args/blockdev.rs
1use crate::parsers::ARG_BLOCKDEV;
2use std::collections::BTreeMap;
3use std::str::FromStr;
4
5use bon::Builder;
6use proptest_derive::Arbitrary;
7
8use crate::common::{IgnoreUnmap, OnOff, OnOffUnmap};
9use crate::to_command::{ToArg, ToCommand};
10
11/// Define a new block driver node. Some of the options apply to all
12/// block drivers, other options are only accepted for a specific block
13/// driver. See below for a list of generic options and options for the
14/// most common block drivers.
15///
16/// Options that expect a reference to another node (e.g. ``file``) can
17/// be given in two ways. Either you specify the node name of an already
18/// existing node (file=node-name), or you define a new node inline,
19/// adding options for the referenced node after a dot
20/// (file.filename=path,file.aio=native).
21///
22/// A block driver node created with ``-blockdev`` can be used for a
23/// guest device by specifying its node name for the ``drive`` property
24/// in a ``-device`` argument that defines a block device.
25#[derive(Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Builder, Arbitrary)]
26pub struct BlockDev {
27 /// Specifies the block driver to use for the given node.
28 pub driver: String,
29
30 /// This defines the name of the block driver node by which it
31 /// will be referenced later. The name must be unique, i.e. it
32 /// must not match the name of a different block driver node, or
33 /// (if you use ``-drive`` as well) the ID of a drive.
34 ///
35 /// If no node name is specified, it is automatically generated.
36 /// The generated node name is not intended to be predictable
37 /// and changes between QEMU invocations. For the top level, an
38 /// explicit node name must be specified.
39 pub node_name: Option<String>,
40
41 /// discard is one of "ignore" (or "off") or "unmap" (or "on")
42 /// and controls whether ``discard`` (also known as ``trim`` or
43 /// ``unmap``) requests are ignored or passed to the filesystem.
44 /// Some machine types may not support discard requests.
45 pub discard: Option<IgnoreUnmap>,
46
47 /// The host page cache can be avoided with ``cache.direct=on``.
48 /// This will attempt to do disk IO directly to the guest's
49 /// memory. QEMU may still perform an internal copy of the data.
50 pub cache_direct: Option<OnOff>,
51
52 /// In case you don't care about data integrity over host
53 /// failures, you can use ``cache.no-flush=on``. This option
54 /// tells QEMU that it never needs to write any data to the disk
55 /// but can instead keep things in cache. If anything goes
56 /// wrong, like your host losing power, the disk storage getting
57 /// disconnected accidentally, etc. your image will most
58 /// probably be rendered unusable.
59 pub cache_no_flush: Option<OnOff>,
60
61 /// Open the node read-only. Guest write attempts will fail.
62 ///
63 /// Note that some block drivers support only read-only access,
64 /// either generally or in certain configurations. In this case,
65 /// the default value ``read-only=off`` does not work and the
66 /// option must be specified explicitly.
67 pub read_only: Option<OnOff>,
68
69 /// If ``auto-read-only=on`` is set, QEMU may fall back to
70 /// read-only usage even when ``read-only=off`` is requested, or
71 /// even switch between modes as needed, e.g. depending on
72 /// whether the image file is writable or whether a writing user
73 /// is attached to the node.
74 pub auto_read_only: Option<OnOff>,
75
76 /// Override the image locking system of QEMU by forcing the
77 /// node to utilize weaker shared access for permissions where
78 /// it would normally request exclusive access. When there is
79 /// the potential for multiple instances to have the same file
80 /// open (whether this invocation of QEMU is the first or the
81 /// second instance), both instances must permit shared access
82 /// for the second instance to succeed at opening the file.
83 ///
84 /// Enabling ``force-share=on`` requires ``read-only=on``.
85 pub force_share: Option<OnOff>,
86
87 /// detect-zeroes is "off", "on" or "unmap" and enables the
88 /// automatic conversion of plain zero writes by the OS to
89 /// driver specific optimized zero write commands. You may even
90 /// choose "unmap" if discard is set to "unmap" to allow a zero
91 /// write to be converted to an ``unmap`` operation.
92 pub detect_zeroes: Option<OnOffUnmap>,
93
94 /// Driver-specific options such as `filename=...` or `file.driver=...`.
95 ///
96 /// These are emitted after the generic blockdev options in sorted key
97 /// order so parsing and formatting remain stable.
98 pub driver_opts: Option<BTreeMap<String, String>>,
99 json: Option<String>,
100}
101
102impl BlockDev {
103 /// Creates a block driver node for the given `driver=...` value.
104 pub fn new(driver: impl Into<String>) -> Self {
105 Self {
106 driver: driver.into(),
107 node_name: None,
108 discard: None,
109 cache_direct: None,
110 cache_no_flush: None,
111 read_only: None,
112 auto_read_only: None,
113 force_share: None,
114 detect_zeroes: None,
115 driver_opts: None,
116 json: None,
117 }
118 }
119 /// Creates a block node from QEMU's JSON command-line form.
120 pub fn from_json(json: impl Into<String>) -> Result<Self, String> {
121 let json = json.into();
122 if !json.trim().starts_with('{') || !json.trim().ends_with('}') {
123 return Err("-blockdev JSON must be a JSON object".to_string());
124 }
125 let mut value = Self::new(String::new());
126 value.json = Some(json);
127 Ok(value)
128 }
129
130 /// Adds a driver-specific `key=value` option.
131 pub fn add_driver_opt<K: AsRef<str>, V: AsRef<str>>(&mut self, key: K, value: V) -> &mut Self {
132 self.driver_opts.get_or_insert_with(BTreeMap::new).insert(key.as_ref().to_string(), value.as_ref().to_string());
133 self
134 }
135}
136
137impl ToCommand for BlockDev {
138 fn command(&self) -> String {
139 ARG_BLOCKDEV.to_string()
140 }
141 fn to_args(&self) -> Vec<String> {
142 if let Some(json) = &self.json {
143 return vec![json.clone()];
144 }
145 let mut args = vec![];
146
147 args.push(format!("driver={}", self.driver));
148 if let Some(node_name) = &self.node_name {
149 args.push(format!("node-name={}", node_name));
150 }
151 if let Some(discard) = &self.discard {
152 args.push(format!("discard={}", discard.to_arg()));
153 }
154 if let Some(cache_direct) = &self.cache_direct {
155 args.push(format!("cache.direct={}", cache_direct.to_arg()));
156 }
157 if let Some(cache_no_flush) = &self.cache_no_flush {
158 args.push(format!("cache.no-flush={}", cache_no_flush.to_arg()));
159 }
160 if let Some(read_only) = &self.read_only {
161 args.push(format!("read-only={}", read_only.to_arg()));
162 }
163 if let Some(auto_read_only) = &self.auto_read_only {
164 args.push(format!("auto-read-only={}", auto_read_only.to_arg()));
165 }
166 if let Some(force_share) = &self.force_share {
167 args.push(format!("force-share={}", force_share.to_arg()));
168 }
169 if let Some(detect_zeroes) = &self.detect_zeroes {
170 args.push(format!("detect-zeroes={}", detect_zeroes.to_arg()));
171 }
172 if let Some(driver_opts) = &self.driver_opts {
173 for (key, val) in driver_opts {
174 args.push(format!("{}={}", key, val));
175 }
176 }
177
178 vec![args.join(",")]
179 }
180}
181
182impl FromStr for BlockDev {
183 type Err = String;
184
185 fn from_str(s: &str) -> Result<Self, Self::Err> {
186 if s.trim().starts_with('{') {
187 return Self::from_json(s);
188 }
189 let mut parts = s.split(',');
190 let first = parts.next().ok_or_else(|| "empty blockdev argument".to_string())?;
191 let driver = first.strip_prefix("driver=").unwrap_or(first).to_string();
192 if driver.is_empty() {
193 return Err("missing blockdev driver".to_string());
194 }
195
196 let mut blockdev = BlockDev::new(driver);
197 let mut driver_opts = BTreeMap::new();
198
199 for part in parts {
200 let (key, value) = part.split_once('=').ok_or_else(|| format!("invalid blockdev option: {part}"))?;
201 match key {
202 "node-name" => blockdev.node_name = Some(value.to_string()),
203 "discard" => blockdev.discard = Some(value.parse::<IgnoreUnmap>().map_err(|_| format!("invalid discard value: {value}"))?),
204 "cache.direct" => blockdev.cache_direct = Some(value.parse::<OnOff>().map_err(|_| format!("invalid cache.direct value: {value}"))?),
205 "cache.no-flush" => blockdev.cache_no_flush = Some(value.parse::<OnOff>().map_err(|_| format!("invalid cache.no-flush value: {value}"))?),
206 "read-only" => blockdev.read_only = Some(value.parse::<OnOff>().map_err(|_| format!("invalid read-only value: {value}"))?),
207 "auto-read-only" => blockdev.auto_read_only = Some(value.parse::<OnOff>().map_err(|_| format!("invalid auto-read-only value: {value}"))?),
208 "force-share" => blockdev.force_share = Some(value.parse::<OnOff>().map_err(|_| format!("invalid force-share value: {value}"))?),
209 "detect-zeroes" => blockdev.detect_zeroes = Some(value.parse::<OnOffUnmap>().map_err(|_| format!("invalid detect-zeroes value: {value}"))?),
210 other => {
211 driver_opts.insert(other.to_string(), value.to_string());
212 }
213 }
214 }
215
216 if !driver_opts.is_empty() {
217 blockdev.driver_opts = Some(driver_opts);
218 }
219
220 Ok(blockdev)
221 }
222}