Skip to main content

tuwunel_core/utils/sys/
storage.rs

1//! Block-device and queue-discovery utilities.
2//!
3//! The helpers inspect filesystem metadata and system block-device information.
4//! Discovery covers backing device names, software RAID, and multi-queue
5//! properties.
6//!
7//! Discovery reads sysfs under `/sys/dev/block/` and stats the device through
8//! `MetadataExt`, neither of which exists outside the unix family. On
9//! non-unix targets these functions report no raid or return an unsupported
10//! error, so callers need no condition of their own.
11
12use std::path::Path;
13#[cfg(unix)]
14use std::{
15	ffi::OsStr,
16	fs,
17	fs::{FileType, read_to_string},
18	path::PathBuf,
19};
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/// Get properties of a MultiDevice (md) storage system
70#[cfg(unix)]
71#[must_use]
72pub fn md_discover(path: &Path) -> MultiDevice {
73	let dev_id = dev_from_path(path)
74		.log_debug_err()
75		.unwrap_or_default();
76
77	let md_path = block_path(dev_id).join("md/");
78
79	let raid_disks_path = md_path.join("raid_disks");
80
81	let raid_disks: usize = read_to_string(&raid_disks_path)
82		.ok()
83		.as_deref()
84		.map(str::trim)
85		.map(str::parse)
86		.flat_ok()
87		.unwrap_or(0);
88
89	let single_fallback = raid_disks.eq(&0).then(|| block_path(dev_id));
90
91	MultiDevice {
92		raid_disks,
93
94		level: read_to_string(md_path.join("level"))
95			.ok()
96			.as_deref()
97			.map(str::trim)
98			.map(ToOwned::to_owned),
99
100		md: (0..raid_disks)
101			.map(|i| format!("rd{i}/block"))
102			.map(|path| md_path.join(&path))
103			.filter_map(|ref path| path.canonicalize().ok())
104			.map(|mut path| {
105				path.pop();
106				path
107			})
108			.chain(single_fallback)
109			.map(|path| mq_discover(&path))
110			.filter(|mq| !mq.mq.is_empty())
111			.collect(),
112	}
113}
114
115/// Get properties of a MultiDevice (md) storage system.
116///
117/// Reports no raid, since discovery needs sysfs, which this platform does
118/// not have.
119#[cfg(not(unix))]
120#[must_use]
121pub fn md_discover(_path: &Path) -> MultiDevice { MultiDevice::default() }
122
123/// Get properties of a MultiQueue within a MultiDevice.
124#[cfg(unix)]
125#[must_use]
126fn mq_discover(path: &Path) -> MultiQueue {
127	let mq_path = path.join("mq/");
128
129	let nr_requests_path = path.join("queue/nr_requests");
130
131	MultiQueue {
132		nr_requests: read_to_string(&nr_requests_path)
133			.ok()
134			.as_deref()
135			.map(str::trim)
136			.map(str::parse)
137			.flat_ok(),
138
139		mq: fs::read_dir(&mq_path)
140			.into_iter()
141			.flat_map(IntoIterator::into_iter)
142			.filter_map(Result::ok)
143			.filter(|entry| {
144				entry
145					.file_type()
146					.as_ref()
147					.is_ok_and(FileType::is_dir)
148			})
149			.map(|dir| queue_discover(&dir.path()))
150			.sorted_by_key(|mq| mq.id)
151			.collect::<Vec<_>>(),
152	}
153}
154
155/// Get properties of a Queue within a MultiQueue.
156#[cfg(unix)]
157fn queue_discover(dir: &Path) -> Queue {
158	let queue_id = dir.file_name();
159
160	let nr_tags_path = dir.join("nr_tags");
161
162	let cpu_list_path = dir.join("cpu_list");
163
164	Queue {
165		id: queue_id
166			.and_then(OsStr::to_str)
167			.map(str::parse)
168			.flat_ok()
169			.expect("queue has some numerical identifier"),
170
171		nr_tags: read_to_string(&nr_tags_path)
172			.ok()
173			.as_deref()
174			.map(str::trim)
175			.map(str::parse)
176			.flat_ok(),
177
178		cpu_list: read_to_string(&cpu_list_path)
179			.iter()
180			.flat_map(|list| list.trim().split(','))
181			.map(str::trim)
182			.map(str::parse)
183			.filter_map(Result::ok)
184			.collect(),
185	}
186}
187
188/// Get the name of the block device on which Path is mounted.
189#[cfg(unix)]
190pub fn name_from_path(path: &Path) -> Result<String> {
191	use std::io::{Error, ErrorKind::NotFound};
192
193	let (major, minor) = dev_from_path(path)?;
194	let path = block_path((major, minor)).join("uevent");
195	read_to_string(path)
196		.iter()
197		.map(String::as_str)
198		.flat_map(str::lines)
199		.map(|line| line.split_once_infallible("="))
200		.find_map(|(key, val)| (key == "DEVNAME").then_some(val))
201		.ok_or_else(|| Error::new(NotFound, "DEVNAME not found."))
202		.map_err(Into::into)
203		.map(Into::into)
204}
205
206/// Get the name of the block device on which Path is mounted.
207///
208/// Naming the device requires sysfs, so this always returns an unsupported
209/// error.
210#[cfg(not(unix))]
211pub fn name_from_path(_path: &Path) -> Result<String> {
212	use std::io::{Error, ErrorKind::Unsupported};
213
214	Err(Error::new(Unsupported, "Block device discovery requires sysfs.").into())
215}
216
217/// Get the (major, minor) of the block device on which Path is mounted.
218#[cfg(unix)]
219fn dev_from_path(path: &Path) -> Result<(dev_t, dev_t)> {
220	use std::os::unix::fs::MetadataExt;
221
222	let stat = fs::metadata(path)?;
223
224	// Metadata::dev() is u64 on every unix; dev_t itself is not, so the
225	// conversions below differ per platform.
226	#[cfg(target_os = "linux")]
227	let dev_id = stat.dev();
228
229	#[cfg(not(target_os = "linux"))]
230	let dev_id = stat.dev().try_into()?;
231
232	let (major, minor) = (libc::major(dev_id), libc::minor(dev_id));
233
234	#[cfg(target_os = "linux")]
235	let (major, minor) = (major.into(), minor.into());
236
237	#[cfg(target_os = "android")]
238	let (major, minor) = (major.try_into()?, minor.try_into()?);
239
240	#[cfg(not(any(
241		target_os = "linux",
242		target_os = "android",
243		target_vendor = "apple"
244	)))]
245	let (major, minor) = (major.try_into()?, minor.try_into()?);
246
247	Ok((major, minor))
248}
249
250#[cfg(unix)]
251fn block_path((major, minor): (dev_t, dev_t)) -> PathBuf {
252	format!("/sys/dev/block/{major}:{minor}/").into()
253}