btrfs_uapi/sysfs.rs
1//! # Sysfs interface: reading filesystem and device state from `/sys/fs/btrfs/`
2//!
3//! The kernel exposes per-filesystem information under
4//! `/sys/fs/btrfs/<uuid>/`, where `<uuid>` is the filesystem UUID as returned
5//! by [`fs_info`][`crate::filesystem::fs_info`]. This includes commit statistics,
6//! feature flags, quota state, and per-device scrub limits.
7//!
8//! The primary entry point is [`SysfsBtrfs`], which is constructed from a
9//! filesystem UUID and provides typed accessors for each sysfs file:
10//!
11//! ```no_run
12//! # use btrfs_uapi::{filesystem::fs_info, sysfs::SysfsBtrfs};
13//! # use std::{fs::File, os::unix::io::AsFd};
14//! # let file = File::open("/mnt/btrfs").unwrap();
15//! # let fd = file.as_fd();
16//! let info = fs_info(fd).unwrap();
17//! let sysfs = SysfsBtrfs::new(&info.uuid);
18//! println!("label: {}", sysfs.label().unwrap());
19//! println!("quota status: {:?}", sysfs.quota_status().unwrap());
20//! ```
21//!
22//! All accessors return [`std::io::Result`] and will return an error with kind
23//! [`std::io::ErrorKind::NotFound`] if the filesystem is not currently mounted.
24
25use std::{ffi::OsStr, fs, io, path::PathBuf};
26use uuid::Uuid;
27
28/// Returns the sysfs directory path for the btrfs filesystem with the given
29/// UUID: `/sys/fs/btrfs/<uuid>`.
30pub fn sysfs_btrfs_path(uuid: &Uuid) -> PathBuf {
31 PathBuf::from(format!("/sys/fs/btrfs/{}", uuid.as_hyphenated()))
32}
33
34/// Returns the path to a named file within the sysfs directory for the
35/// filesystem with the given UUID: `/sys/fs/btrfs/<uuid>/<name>`.
36pub fn sysfs_btrfs_path_file(uuid: &Uuid, name: &str) -> PathBuf {
37 sysfs_btrfs_path(uuid).join(name)
38}
39
40/// Commit statistics for a mounted btrfs filesystem, read from
41/// `/sys/fs/btrfs/<uuid>/commit_stats`.
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct CommitStats {
44 /// Total number of commits since mount.
45 pub commits: u64,
46 /// Duration of the current (in-progress) commit in milliseconds.
47 pub cur_commit_ms: u64,
48 /// Duration of the last completed commit in milliseconds.
49 pub last_commit_ms: u64,
50 /// Maximum commit duration since mount (or last reset) in milliseconds.
51 pub max_commit_ms: u64,
52 /// Total time spent in commits since mount in milliseconds.
53 pub total_commit_ms: u64,
54}
55
56/// Provides typed access to the sysfs files exposed for a single mounted btrfs
57/// filesystem under `/sys/fs/btrfs/<uuid>/`.
58pub struct SysfsBtrfs {
59 base: PathBuf,
60}
61
62impl SysfsBtrfs {
63 /// Create a new `SysfsBtrfs` for the filesystem with the given UUID.
64 pub fn new(uuid: &Uuid) -> Self {
65 Self {
66 base: sysfs_btrfs_path(uuid),
67 }
68 }
69
70 fn read_file(&self, name: &str) -> io::Result<String> {
71 let s = fs::read_to_string(self.base.join(name))?;
72 Ok(s.trim_end().to_owned())
73 }
74
75 fn read_u64(&self, name: &str) -> io::Result<u64> {
76 let s = self.read_file(name)?;
77 s.parse()
78 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
79 }
80
81 fn read_bool(&self, name: &str) -> io::Result<bool> {
82 Ok(self.read_u64(name)? != 0)
83 }
84
85 /// Background reclaim threshold as a percentage (0–100).
86 /// `/sys/fs/btrfs/<uuid>/bg_reclaim_threshold`
87 pub fn bg_reclaim_threshold(&self) -> io::Result<u64> {
88 self.read_u64("bg_reclaim_threshold")
89 }
90
91 /// Checksum algorithm in use, e.g. `"crc32c (crc32c-lib)"`.
92 /// `/sys/fs/btrfs/<uuid>/checksum`
93 pub fn checksum(&self) -> io::Result<String> {
94 self.read_file("checksum")
95 }
96
97 /// Minimum clone/reflink alignment in bytes.
98 /// `/sys/fs/btrfs/<uuid>/clone_alignment`
99 pub fn clone_alignment(&self) -> io::Result<u64> {
100 self.read_u64("clone_alignment")
101 }
102
103 /// Commit statistics since mount (or last reset).
104 /// `/sys/fs/btrfs/<uuid>/commit_stats`
105 pub fn commit_stats(&self) -> io::Result<CommitStats> {
106 let contents = self.read_file("commit_stats")?;
107 let mut commits = None;
108 let mut cur_commit_ms = None;
109 let mut last_commit_ms = None;
110 let mut max_commit_ms = None;
111 let mut total_commit_ms = None;
112
113 for line in contents.lines() {
114 let mut parts = line.splitn(2, ' ');
115 let key = parts.next().unwrap_or("").trim();
116 let val: u64 = parts
117 .next()
118 .unwrap_or("")
119 .trim()
120 .parse()
121 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
122 match key {
123 "commits" => commits = Some(val),
124 "cur_commit_ms" => cur_commit_ms = Some(val),
125 "last_commit_ms" => last_commit_ms = Some(val),
126 "max_commit_ms" => max_commit_ms = Some(val),
127 "total_commit_ms" => total_commit_ms = Some(val),
128 _ => {}
129 }
130 }
131
132 let missing = |name| {
133 io::Error::new(
134 io::ErrorKind::InvalidData,
135 format!("commit_stats: missing field '{name}'"),
136 )
137 };
138
139 Ok(CommitStats {
140 commits: commits.ok_or_else(|| missing("commits"))?,
141 cur_commit_ms: cur_commit_ms.ok_or_else(|| missing("cur_commit_ms"))?,
142 last_commit_ms: last_commit_ms.ok_or_else(|| missing("last_commit_ms"))?,
143 max_commit_ms: max_commit_ms.ok_or_else(|| missing("max_commit_ms"))?,
144 total_commit_ms: total_commit_ms.ok_or_else(|| missing("total_commit_ms"))?,
145 })
146 }
147
148 /// Reset the `max_commit_ms` counter by writing `0` to the commit_stats
149 /// file. Requires root.
150 /// `/sys/fs/btrfs/<uuid>/commit_stats`
151 pub fn reset_commit_stats(&self) -> io::Result<()> {
152 fs::write(self.base.join("commit_stats"), b"0")
153 }
154
155 /// Name of the exclusive operation currently running, e.g. `"none"`,
156 /// `"balance"`, `"device add"`.
157 /// `/sys/fs/btrfs/<uuid>/exclusive_operation`
158 pub fn exclusive_operation(&self) -> io::Result<String> {
159 self.read_file("exclusive_operation")
160 }
161
162 /// Names of the filesystem features that are enabled. Each feature
163 /// corresponds to a file in the `features/` subdirectory.
164 /// `/sys/fs/btrfs/<uuid>/features/`
165 pub fn features(&self) -> io::Result<Vec<String>> {
166 let mut features = Vec::new();
167 for entry in fs::read_dir(self.base.join("features"))? {
168 let entry = entry?;
169 if let Some(name) = entry.file_name().to_str() {
170 features.push(name.to_owned());
171 }
172 }
173 features.sort();
174 Ok(features)
175 }
176
177 /// Current filesystem generation number.
178 /// `/sys/fs/btrfs/<uuid>/generation`
179 pub fn generation(&self) -> io::Result<u64> {
180 self.read_u64("generation")
181 }
182
183 /// Filesystem label. Empty string if no label is set.
184 /// `/sys/fs/btrfs/<uuid>/label`
185 pub fn label(&self) -> io::Result<String> {
186 self.read_file("label")
187 }
188
189 /// Metadata UUID. May differ from the filesystem UUID if the metadata UUID
190 /// feature is in use.
191 /// `/sys/fs/btrfs/<uuid>/metadata_uuid`
192 pub fn metadata_uuid(&self) -> io::Result<Uuid> {
193 let s = self.read_file("metadata_uuid")?;
194 Uuid::parse_str(&s).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
195 }
196
197 /// B-tree node size in bytes.
198 /// `/sys/fs/btrfs/<uuid>/nodesize`
199 pub fn nodesize(&self) -> io::Result<u64> {
200 self.read_u64("nodesize")
201 }
202
203 /// Whether the quota override is active.
204 /// `/sys/fs/btrfs/<uuid>/quota_override`
205 pub fn quota_override(&self) -> io::Result<bool> {
206 self.read_bool("quota_override")
207 }
208
209 /// Read policy for RAID profiles, e.g. `"[pid]"` or `"[roundrobin]"`.
210 /// `/sys/fs/btrfs/<uuid>/read_policy`
211 pub fn read_policy(&self) -> io::Result<String> {
212 self.read_file("read_policy")
213 }
214
215 /// Sector size in bytes.
216 /// `/sys/fs/btrfs/<uuid>/sectorsize`
217 pub fn sectorsize(&self) -> io::Result<u64> {
218 self.read_u64("sectorsize")
219 }
220
221 /// Whether a temporary fsid is in use (seeding device feature).
222 /// `/sys/fs/btrfs/<uuid>/temp_fsid`
223 pub fn temp_fsid(&self) -> io::Result<bool> {
224 self.read_bool("temp_fsid")
225 }
226
227 /// Read the per-device scrub throughput limit for the given device, in
228 /// bytes per second. A value of `0` means no limit is set (unlimited).
229 /// `/sys/fs/btrfs/<uuid>/devinfo/<devid>/scrub_speed_max`
230 pub fn scrub_speed_max_get(&self, devid: u64) -> io::Result<u64> {
231 let path = format!("devinfo/{devid}/scrub_speed_max");
232 match self.read_u64(&path) {
233 Ok(v) => Ok(v),
234 Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(0),
235 Err(e) => Err(e),
236 }
237 }
238
239 /// Set the per-device scrub throughput limit for the given device, in
240 /// bytes per second. Pass `0` to remove the limit (unlimited).
241 /// Requires root.
242 /// `/sys/fs/btrfs/<uuid>/devinfo/<devid>/scrub_speed_max`
243 pub fn scrub_speed_max_set(&self, devid: u64, limit: u64) -> io::Result<()> {
244 let path = self.base.join(format!("devinfo/{devid}/scrub_speed_max"));
245 fs::write(path, format!("{limit}\n"))
246 }
247
248 /// Quota status for this filesystem, read from
249 /// `/sys/fs/btrfs/<uuid>/qgroups/`.
250 ///
251 /// Returns `Ok(QuotaStatus { enabled: false, .. })` when quota is not
252 /// enabled (the `qgroups/` directory does not exist). Returns an
253 /// [`io::Error`] with kind `NotFound` if the sysfs entry for this UUID
254 /// does not exist (i.e. the filesystem is not currently mounted).
255 pub fn quota_status(&self) -> io::Result<QuotaStatus> {
256 let qgroups = self.base.join("qgroups");
257
258 if !qgroups.exists() {
259 return Ok(QuotaStatus {
260 enabled: false,
261 mode: None,
262 inconsistent: None,
263 override_limits: None,
264 drop_subtree_threshold: None,
265 total_count: None,
266 level0_count: None,
267 });
268 }
269
270 let mode = {
271 let s = fs::read_to_string(qgroups.join("mode"))?;
272 s.trim_end().to_owned()
273 };
274 let inconsistent = fs::read_to_string(qgroups.join("inconsistent"))?
275 .trim()
276 .parse::<u64>()
277 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?
278 != 0;
279 let override_limits = self.read_bool("quota_override")?;
280 let drop_subtree_threshold = fs::read_to_string(qgroups.join("drop_subtree_threshold"))?
281 .trim()
282 .parse::<u64>()
283 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
284
285 let mut total_count: u64 = 0;
286 let mut level0_count: u64 = 0;
287 for entry in fs::read_dir(&qgroups)? {
288 let entry = entry?;
289 let raw_name = entry.file_name();
290 let name = raw_name.to_string_lossy();
291 if let Some((level, _id)) = parse_qgroup_entry_name(OsStr::new(name.as_ref())) {
292 total_count += 1;
293 if level == 0 {
294 level0_count += 1;
295 }
296 }
297 }
298
299 Ok(QuotaStatus {
300 enabled: true,
301 mode: Some(mode),
302 inconsistent: Some(inconsistent),
303 override_limits: Some(override_limits),
304 drop_subtree_threshold: Some(drop_subtree_threshold),
305 total_count: Some(total_count),
306 level0_count: Some(level0_count),
307 })
308 }
309}
310
311/// Parse a qgroups sysfs directory entry name of the form `<level>_<id>`.
312///
313/// Returns `Some((level, id))` for valid entries, `None` for anything else
314/// (e.g. `mode`, `inconsistent`, and other non-qgroup files in the directory).
315fn parse_qgroup_entry_name(name: &OsStr) -> Option<(u64, u64)> {
316 let s = name.to_str()?;
317 let (level_str, id_str) = s.split_once('_')?;
318 let level: u64 = level_str.parse().ok()?;
319 let id: u64 = id_str.parse().ok()?;
320 Some((level, id))
321}
322
323/// Quota status for a mounted btrfs filesystem, read from sysfs under
324/// `/sys/fs/btrfs/<uuid>/qgroups/`.
325#[derive(Debug, Clone, PartialEq, Eq)]
326pub struct QuotaStatus {
327 /// Whether quota accounting is currently enabled.
328 pub enabled: bool,
329 /// Accounting mode: `"qgroup"` (full backref accounting) or `"squota"`
330 /// (simplified lifetime accounting). `None` when quotas are disabled.
331 pub mode: Option<String>,
332 /// Whether the quota tree is inconsistent; a rescan is needed to restore
333 /// accurate numbers. `None` when quotas are disabled.
334 pub inconsistent: Option<bool>,
335 /// Whether the quota override flag is active (limits are bypassed for
336 /// the current mount). `None` when quotas are disabled.
337 pub override_limits: Option<bool>,
338 /// Drop-subtree threshold: qgroup hierarchy levels below this value skip
339 /// detailed tracking during heavy write workloads. `None` when disabled.
340 pub drop_subtree_threshold: Option<u64>,
341 /// Total number of qgroups tracked by the kernel. `None` when disabled.
342 pub total_count: Option<u64>,
343 /// Number of level-0 qgroups (one per subvolume). `None` when disabled.
344 pub level0_count: Option<u64>,
345}