Skip to main content

tuwunel_core/utils/sys/
storage.rs

1//! Block-device, filesystem, and queue-discovery utilities.
2//!
3//! The helpers inspect filesystem metadata and system block-device information.
4//! Discovery covers backing device names, software RAID, multi-queue
5//! properties, and the filesystem hosting a path.
6//!
7//! Discovery reads sysfs under `/sys/dev/block/`, stats the device through
8//! `MetadataExt`, and calls `statfs(2)`, which not every target provides.
9//! Where an interface is missing these functions report no raid or return an
10//! unsupported error, so callers need no condition of their own.
11
12#[cfg(unix)]
13use std::{
14	ffi::OsStr,
15	fs,
16	fs::{FileType, read_to_string},
17	path::PathBuf,
18};
19use std::{fmt, path::Path};
20
21#[cfg(unix)]
22use itertools::Itertools;
23#[cfg(unix)]
24use libc::dev_t;
25
26use crate::Result;
27#[cfg(unix)]
28use crate::{
29	result::FlatOk,
30	utils::{result::LogDebugErr, string::SplitInfallible},
31};
32
33/// Multi-Device (md) i.e. software raid properties.
34#[derive(Clone, Debug, Default)]
35pub struct MultiDevice {
36	/// Type of raid (i.e. `raid1`); None if no raid present or detected.
37	pub level: Option<String>,
38
39	/// Number of participating devices.
40	pub raid_disks: usize,
41
42	/// The MQ's discovered on the devices; or empty.
43	pub md: Vec<MultiQueue>,
44}
45
46/// Multi-Queue (mq) characteristics.
47#[derive(Clone, Debug, Default)]
48pub struct MultiQueue {
49	/// Number of requests for the device.
50	pub nr_requests: Option<usize>,
51
52	/// Individual queue characteristics.
53	pub mq: Vec<Queue>,
54}
55
56/// Single-queue characteristics
57#[derive(Clone, Debug, Default)]
58pub struct Queue {
59	/// Queue's indice.
60	pub id: usize,
61
62	/// Number of requests for the queue.
63	pub nr_tags: Option<usize>,
64
65	/// CPU affinities for the queue.
66	pub cpu_list: Vec<usize>,
67}
68
69/// Filesystem hosting a path, among those needing special handling.
70///
71/// Copy-on-Write filesystems interact badly with RocksDB's `fallocate(2)`
72/// preallocation. Anything else is reported as absent rather than named.
73#[derive(Clone, Copy, Debug, Eq, PartialEq)]
74pub enum Filesystem {
75	/// Preallocation survives truncation, so a short log pins a whole extent.
76	Btrfs,
77
78	/// Preallocation is unimplemented and `fallocate(2)` gives `EOPNOTSUPP`.
79	Zfs,
80}
81
82impl fmt::Display for Filesystem {
83	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84		f.write_str(match self {
85			| Self::Btrfs => "btrfs",
86			| Self::Zfs => "ZFS",
87		})
88	}
89}
90
91/// Get properties of a MultiDevice (md) storage system
92#[cfg(unix)]
93#[must_use]
94pub fn md_discover(path: &Path) -> MultiDevice {
95	let dev_id = dev_from_path(path)
96		.log_debug_err()
97		.unwrap_or_default();
98
99	let md_path = block_path(dev_id).join("md/");
100
101	let raid_disks_path = md_path.join("raid_disks");
102
103	let raid_disks: usize = read_to_string(&raid_disks_path)
104		.ok()
105		.as_deref()
106		.map(str::trim)
107		.map(str::parse)
108		.flat_ok()
109		.unwrap_or(0);
110
111	let single_fallback = raid_disks.eq(&0).then(|| block_path(dev_id));
112
113	MultiDevice {
114		raid_disks,
115
116		level: read_to_string(md_path.join("level"))
117			.ok()
118			.as_deref()
119			.map(str::trim)
120			.map(ToOwned::to_owned),
121
122		md: (0..raid_disks)
123			.map(|i| format!("rd{i}/block"))
124			.map(|path| md_path.join(&path))
125			.filter_map(|ref path| path.canonicalize().ok())
126			.map(|mut path| {
127				path.pop();
128				path
129			})
130			.chain(single_fallback)
131			.map(|path| mq_discover(&path))
132			.filter(|mq| !mq.mq.is_empty())
133			.collect(),
134	}
135}
136
137/// Get properties of a MultiDevice (md) storage system.
138///
139/// Reports no raid, since discovery needs sysfs, which this platform does
140/// not have.
141#[cfg(not(unix))]
142#[must_use]
143pub fn md_discover(_path: &Path) -> MultiDevice { MultiDevice::default() }
144
145/// Get properties of a MultiQueue within a MultiDevice.
146#[cfg(unix)]
147#[must_use]
148fn mq_discover(path: &Path) -> MultiQueue {
149	let mq_path = path.join("mq/");
150
151	let nr_requests_path = path.join("queue/nr_requests");
152
153	MultiQueue {
154		nr_requests: read_to_string(&nr_requests_path)
155			.ok()
156			.as_deref()
157			.map(str::trim)
158			.map(str::parse)
159			.flat_ok(),
160
161		mq: fs::read_dir(&mq_path)
162			.into_iter()
163			.flat_map(IntoIterator::into_iter)
164			.filter_map(Result::ok)
165			.filter(|entry| {
166				entry
167					.file_type()
168					.as_ref()
169					.is_ok_and(FileType::is_dir)
170			})
171			.map(|dir| queue_discover(&dir.path()))
172			.sorted_by_key(|mq| mq.id)
173			.collect::<Vec<_>>(),
174	}
175}
176
177/// Get properties of a Queue within a MultiQueue.
178#[cfg(unix)]
179fn queue_discover(dir: &Path) -> Queue {
180	let queue_id = dir.file_name();
181
182	let nr_tags_path = dir.join("nr_tags");
183
184	let cpu_list_path = dir.join("cpu_list");
185
186	Queue {
187		id: queue_id
188			.and_then(OsStr::to_str)
189			.map(str::parse)
190			.flat_ok()
191			.expect("queue has some numerical identifier"),
192
193		nr_tags: read_to_string(&nr_tags_path)
194			.ok()
195			.as_deref()
196			.map(str::trim)
197			.map(str::parse)
198			.flat_ok(),
199
200		cpu_list: read_to_string(&cpu_list_path)
201			.iter()
202			.flat_map(|list| list.trim().split(','))
203			.map(str::trim)
204			.map(str::parse)
205			.filter_map(Result::ok)
206			.collect(),
207	}
208}
209
210/// Get the name of the block device on which Path is mounted.
211#[cfg(unix)]
212pub fn name_from_path(path: &Path) -> Result<String> {
213	use std::io::{Error, ErrorKind::NotFound};
214
215	let (major, minor) = dev_from_path(path)?;
216	let path = block_path((major, minor)).join("uevent");
217	read_to_string(path)
218		.iter()
219		.map(String::as_str)
220		.flat_map(str::lines)
221		.map(|line| line.split_once_infallible("="))
222		.find_map(|(key, val)| (key == "DEVNAME").then_some(val))
223		.ok_or_else(|| Error::new(NotFound, "DEVNAME not found."))
224		.map_err(Into::into)
225		.map(Into::into)
226}
227
228/// Get the name of the block device on which Path is mounted.
229///
230/// Naming the device requires sysfs, so this always returns an unsupported
231/// error.
232#[cfg(not(unix))]
233pub fn name_from_path(_path: &Path) -> Result<String> {
234	use std::io::{Error, ErrorKind::Unsupported};
235
236	Err(Error::new(Unsupported, "Block device discovery requires sysfs.").into())
237}
238
239/// Get the filesystem on which Path is mounted, when it needs special
240/// handling.
241///
242/// Linux reports a superblock magic rather than a name, and neither libc nor
243/// nix carries one for out-of-tree OpenZFS. Any other filesystem reports
244/// `None`.
245#[cfg(any(target_os = "linux", target_os = "android"))]
246pub fn filesystem_from_path(path: &Path) -> Result<Option<Filesystem>> {
247	use nix::sys::statfs::{BTRFS_SUPER_MAGIC, FsType, statfs};
248
249	// <https://github.com/openzfs/zfs/blob/zfs-2.4.3/include/sys/fs/zfs.h>
250	const ZFS_SUPER_MAGIC: FsType = FsType(0x2FC1_2FC1);
251
252	Ok(match statfs(path)?.filesystem_type() {
253		| BTRFS_SUPER_MAGIC => Some(Filesystem::Btrfs),
254		| ZFS_SUPER_MAGIC => Some(Filesystem::Zfs),
255		| _ => None,
256	})
257}
258
259/// Get the filesystem on which Path is mounted, when it needs special
260/// handling.
261///
262/// The BSDs and macOS report a type name rather than a superblock magic. Any
263/// other filesystem reports `None`.
264#[cfg(any(
265	target_os = "freebsd",
266	target_os = "dragonfly",
267	target_os = "openbsd",
268	target_vendor = "apple"
269))]
270pub fn filesystem_from_path(path: &Path) -> Result<Option<Filesystem>> {
271	use nix::sys::statfs::statfs;
272
273	Ok(match statfs(path)?.filesystem_type_name() {
274		| "btrfs" => Some(Filesystem::Btrfs),
275		| "zfs" => Some(Filesystem::Zfs),
276		| _ => None,
277	})
278}
279
280/// Get the filesystem on which Path is mounted, when it needs special
281/// handling.
282///
283/// Naming the filesystem requires `statfs(2)`, so this always returns an
284/// unsupported error.
285#[cfg(not(any(
286	target_os = "linux",
287	target_os = "android",
288	target_os = "freebsd",
289	target_os = "dragonfly",
290	target_os = "openbsd",
291	target_vendor = "apple"
292)))]
293pub fn filesystem_from_path(_path: &Path) -> Result<Option<Filesystem>> {
294	use std::io::{Error, ErrorKind::Unsupported};
295
296	Err(Error::new(Unsupported, "Filesystem discovery requires statfs.").into())
297}
298
299/// Get the (major, minor) of the block device on which Path is mounted.
300#[cfg(unix)]
301fn dev_from_path(path: &Path) -> Result<(dev_t, dev_t)> {
302	use std::os::unix::fs::MetadataExt;
303
304	let stat = fs::metadata(path)?;
305
306	// Metadata::dev() is u64 on every unix; dev_t itself is not, so the
307	// conversions below differ per platform.
308	#[cfg(target_os = "linux")]
309	let dev_id = stat.dev();
310
311	#[cfg(not(target_os = "linux"))]
312	let dev_id = stat.dev().try_into()?;
313
314	let (major, minor) = (libc::major(dev_id), libc::minor(dev_id));
315
316	#[cfg(target_os = "linux")]
317	let (major, minor) = (major.into(), minor.into());
318
319	#[cfg(target_os = "android")]
320	let (major, minor) = (major.try_into()?, minor.try_into()?);
321
322	#[cfg(not(any(
323		target_os = "linux",
324		target_os = "android",
325		target_vendor = "apple"
326	)))]
327	let (major, minor) = (major.try_into()?, minor.try_into()?);
328
329	Ok((major, minor))
330}
331
332#[cfg(unix)]
333fn block_path((major, minor): (dev_t, dev_t)) -> PathBuf {
334	format!("/sys/dev/block/{major}:{minor}/").into()
335}