Skip to main content

tuwunel_core/utils/sys/
storage.rs

1//! System utilities related to devices/peripherals
2
3use std::{
4	ffi::OsStr,
5	fs,
6	fs::{FileType, read_to_string},
7	iter::IntoIterator,
8	path::{Path, PathBuf},
9};
10
11use itertools::Itertools;
12use libc::dev_t;
13
14use crate::{
15	Result,
16	result::FlatOk,
17	utils::{result::LogDebugErr, string::SplitInfallible},
18};
19
20/// Multi-Device (md) i.e. software raid properties.
21#[derive(Clone, Debug, Default)]
22pub struct MultiDevice {
23	/// Type of raid (i.e. `raid1`); None if no raid present or detected.
24	pub level: Option<String>,
25
26	/// Number of participating devices.
27	pub raid_disks: usize,
28
29	/// The MQ's discovered on the devices; or empty.
30	pub md: Vec<MultiQueue>,
31}
32
33/// Multi-Queue (mq) characteristics.
34#[derive(Clone, Debug, Default)]
35pub struct MultiQueue {
36	/// Number of requests for the device.
37	pub nr_requests: Option<usize>,
38
39	/// Individual queue characteristics.
40	pub mq: Vec<Queue>,
41}
42
43/// Single-queue characteristics
44#[derive(Clone, Debug, Default)]
45pub struct Queue {
46	/// Queue's indice.
47	pub id: usize,
48
49	/// Number of requests for the queue.
50	pub nr_tags: Option<usize>,
51
52	/// CPU affinities for the queue.
53	pub cpu_list: Vec<usize>,
54}
55
56/// Get properties of a MultiDevice (md) storage system
57#[must_use]
58pub fn md_discover(path: &Path) -> MultiDevice {
59	let dev_id = dev_from_path(path)
60		.log_debug_err()
61		.unwrap_or_default();
62
63	let md_path = block_path(dev_id).join("md/");
64
65	let raid_disks_path = md_path.join("raid_disks");
66
67	let raid_disks: usize = read_to_string(&raid_disks_path)
68		.ok()
69		.as_deref()
70		.map(str::trim)
71		.map(str::parse)
72		.flat_ok()
73		.unwrap_or(0);
74
75	let single_fallback = raid_disks.eq(&0).then(|| block_path(dev_id));
76
77	MultiDevice {
78		raid_disks,
79
80		level: read_to_string(md_path.join("level"))
81			.ok()
82			.as_deref()
83			.map(str::trim)
84			.map(ToOwned::to_owned),
85
86		md: (0..raid_disks)
87			.map(|i| format!("rd{i}/block"))
88			.map(|path| md_path.join(&path))
89			.filter_map(|ref path| path.canonicalize().ok())
90			.map(|mut path| {
91				path.pop();
92				path
93			})
94			.chain(single_fallback)
95			.map(|path| mq_discover(&path))
96			.filter(|mq| !mq.mq.is_empty())
97			.collect(),
98	}
99}
100
101/// Get properties of a MultiQueue within a MultiDevice.
102#[must_use]
103fn mq_discover(path: &Path) -> MultiQueue {
104	let mq_path = path.join("mq/");
105
106	let nr_requests_path = path.join("queue/nr_requests");
107
108	MultiQueue {
109		nr_requests: read_to_string(&nr_requests_path)
110			.ok()
111			.as_deref()
112			.map(str::trim)
113			.map(str::parse)
114			.flat_ok(),
115
116		mq: fs::read_dir(&mq_path)
117			.into_iter()
118			.flat_map(IntoIterator::into_iter)
119			.filter_map(Result::ok)
120			.filter(|entry| {
121				entry
122					.file_type()
123					.as_ref()
124					.is_ok_and(FileType::is_dir)
125			})
126			.map(|dir| queue_discover(&dir.path()))
127			.sorted_by_key(|mq| mq.id)
128			.collect::<Vec<_>>(),
129	}
130}
131
132/// Get properties of a Queue within a MultiQueue.
133fn queue_discover(dir: &Path) -> Queue {
134	let queue_id = dir.file_name();
135
136	let nr_tags_path = dir.join("nr_tags");
137
138	let cpu_list_path = dir.join("cpu_list");
139
140	Queue {
141		id: queue_id
142			.and_then(OsStr::to_str)
143			.map(str::parse)
144			.flat_ok()
145			.expect("queue has some numerical identifier"),
146
147		nr_tags: read_to_string(&nr_tags_path)
148			.ok()
149			.as_deref()
150			.map(str::trim)
151			.map(str::parse)
152			.flat_ok(),
153
154		cpu_list: read_to_string(&cpu_list_path)
155			.iter()
156			.flat_map(|list| list.trim().split(','))
157			.map(str::trim)
158			.map(str::parse)
159			.filter_map(Result::ok)
160			.collect(),
161	}
162}
163
164/// Get the name of the block device on which Path is mounted.
165pub fn name_from_path(path: &Path) -> Result<String> {
166	use std::io::{Error, ErrorKind::NotFound};
167
168	let (major, minor) = dev_from_path(path)?;
169	let path = block_path((major, minor)).join("uevent");
170	read_to_string(path)
171		.iter()
172		.map(String::as_str)
173		.flat_map(str::lines)
174		.map(|line| line.split_once_infallible("="))
175		.find_map(|(key, val)| (key == "DEVNAME").then_some(val))
176		.ok_or_else(|| Error::new(NotFound, "DEVNAME not found."))
177		.map_err(Into::into)
178		.map(Into::into)
179}
180
181/// Get the (major, minor) of the block device on which Path is mounted.
182//TODO: Use conditional cfg for expect() minding cross-platformness.
183#[allow(
184	clippy::useless_conversion,
185	clippy::unnecessary_fallible_conversions
186)]
187fn dev_from_path(path: &Path) -> Result<(dev_t, dev_t)> {
188	#[cfg(target_family = "unix")]
189	use std::os::unix::fs::MetadataExt;
190
191	let stat = fs::metadata(path)?;
192	let dev_id = stat.dev().try_into()?;
193	let (major, minor) = (libc::major(dev_id), libc::minor(dev_id));
194
195	Ok((major.try_into()?, minor.try_into()?))
196}
197
198fn block_path((major, minor): (dev_t, dev_t)) -> PathBuf {
199	format!("/sys/dev/block/{major}:{minor}/").into()
200}