tuwunel_core/utils/sys/
storage.rs1#[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#[derive(Clone, Debug, Default)]
35pub struct MultiDevice {
36 pub level: Option<String>,
38
39 pub raid_disks: usize,
41
42 pub md: Vec<MultiQueue>,
44}
45
46#[derive(Clone, Debug, Default)]
48pub struct MultiQueue {
49 pub nr_requests: Option<usize>,
51
52 pub mq: Vec<Queue>,
54}
55
56#[derive(Clone, Debug, Default)]
58pub struct Queue {
59 pub id: usize,
61
62 pub nr_tags: Option<usize>,
64
65 pub cpu_list: Vec<usize>,
67}
68
69#[derive(Clone, Copy, Debug, Eq, PartialEq)]
74pub enum Filesystem {
75 Btrfs,
77
78 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#[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#[cfg(not(unix))]
142#[must_use]
143pub fn md_discover(_path: &Path) -> MultiDevice { MultiDevice::default() }
144
145#[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#[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#[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#[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#[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 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#[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#[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#[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 #[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}