use crate::{
filesystem::FilesystemInfo,
raw::{
BTRFS_DEV_EXTENT_KEY, BTRFS_DEV_STATS_RESET, BTRFS_DEV_TREE_OBJECTID,
BTRFS_DEVICE_SPEC_BY_ID,
btrfs_dev_stat_values_BTRFS_DEV_STAT_CORRUPTION_ERRS,
btrfs_dev_stat_values_BTRFS_DEV_STAT_FLUSH_ERRS,
btrfs_dev_stat_values_BTRFS_DEV_STAT_GENERATION_ERRS,
btrfs_dev_stat_values_BTRFS_DEV_STAT_READ_ERRS,
btrfs_dev_stat_values_BTRFS_DEV_STAT_VALUES_MAX,
btrfs_dev_stat_values_BTRFS_DEV_STAT_WRITE_ERRS, btrfs_ioc_add_dev,
btrfs_ioc_dev_info, btrfs_ioc_devices_ready, btrfs_ioc_forget_dev,
btrfs_ioc_get_dev_stats, btrfs_ioc_rm_dev, btrfs_ioc_rm_dev_v2,
btrfs_ioc_scan_dev, btrfs_ioctl_dev_info_args,
btrfs_ioctl_get_dev_stats, btrfs_ioctl_vol_args,
btrfs_ioctl_vol_args_v2,
},
tree_search::{SearchFilter, tree_search},
};
use nix::{errno::Errno, libc::c_char};
use std::{
ffi::CStr,
fs::OpenOptions,
mem,
os::{fd::AsRawFd, unix::io::BorrowedFd},
};
use uuid::Uuid;
#[derive(Debug, Clone)]
pub struct DeviceInfo {
pub devid: u64,
pub uuid: Uuid,
pub bytes_used: u64,
pub total_bytes: u64,
pub path: String,
}
#[derive(Debug, Clone)]
pub enum DeviceSpec<'a> {
Path(&'a CStr),
Id(u64),
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct DeviceStats {
pub devid: u64,
pub write_errs: u64,
pub read_errs: u64,
pub flush_errs: u64,
pub corruption_errs: u64,
pub generation_errs: u64,
}
impl DeviceStats {
#[must_use]
pub fn total_errs(&self) -> u64 {
self.write_errs
+ self.read_errs
+ self.flush_errs
+ self.corruption_errs
+ self.generation_errs
}
#[must_use]
pub fn is_clean(&self) -> bool {
self.total_errs() == 0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dev_stats_default_is_clean() {
let stats = DeviceStats::default();
assert!(stats.is_clean());
assert_eq!(stats.total_errs(), 0);
}
#[test]
fn dev_stats_total_errs() {
let stats = DeviceStats {
devid: 1,
write_errs: 1,
read_errs: 2,
flush_errs: 3,
corruption_errs: 4,
generation_errs: 5,
};
assert_eq!(stats.total_errs(), 15);
assert!(!stats.is_clean());
}
#[test]
fn dev_stats_single_error_not_clean() {
let stats = DeviceStats {
corruption_errs: 1,
..DeviceStats::default()
};
assert!(!stats.is_clean());
assert_eq!(stats.total_errs(), 1);
}
}
#[allow(clippy::cast_possible_wrap)] fn copy_path_to_name(name: &mut [c_char], path: &CStr) -> nix::Result<()> {
let bytes = path.to_bytes(); if bytes.len() >= name.len() {
return Err(Errno::ENAMETOOLONG);
}
for (i, &b) in bytes.iter().enumerate() {
name[i] = b as c_char;
}
Ok(())
}
fn open_control() -> nix::Result<std::fs::File> {
OpenOptions::new()
.read(true)
.write(true)
.open("/dev/btrfs-control")
.map_err(|e| {
Errno::from_raw(e.raw_os_error().unwrap_or(nix::libc::ENODEV))
})
}
pub fn device_info(
fd: BorrowedFd,
devid: u64,
) -> nix::Result<Option<DeviceInfo>> {
let mut raw: btrfs_ioctl_dev_info_args = unsafe { mem::zeroed() };
raw.devid = devid;
match unsafe { btrfs_ioc_dev_info(fd.as_raw_fd(), &raw mut raw) } {
Err(Errno::ENODEV) => return Ok(None),
Err(e) => return Err(e),
Ok(_) => {}
}
let path = unsafe { CStr::from_ptr(raw.path.as_ptr().cast()) }
.to_string_lossy()
.into_owned();
Ok(Some(DeviceInfo {
devid: raw.devid,
uuid: Uuid::from_bytes(raw.uuid),
bytes_used: raw.bytes_used,
total_bytes: raw.total_bytes,
path,
}))
}
pub fn device_info_all(
fd: BorrowedFd,
fs_info: &FilesystemInfo,
) -> nix::Result<Vec<DeviceInfo>> {
#[allow(clippy::cast_possible_truncation)]
let mut devices = Vec::with_capacity(fs_info.num_devices as usize);
for devid in 1..=fs_info.max_id {
if let Some(info) = device_info(fd, devid)? {
devices.push(info);
}
}
Ok(devices)
}
pub fn device_add(fd: BorrowedFd, path: &CStr) -> nix::Result<()> {
let mut raw: btrfs_ioctl_vol_args = unsafe { mem::zeroed() };
copy_path_to_name(&mut raw.name, path)?;
unsafe { btrfs_ioc_add_dev(fd.as_raw_fd(), &raw const raw) }?;
Ok(())
}
pub fn device_remove(fd: BorrowedFd, spec: &DeviceSpec<'_>) -> nix::Result<()> {
let mut args: btrfs_ioctl_vol_args_v2 = unsafe { mem::zeroed() };
match *spec {
DeviceSpec::Id(devid) => {
args.flags = u64::from(BTRFS_DEVICE_SPEC_BY_ID);
args.__bindgen_anon_2.devid = devid;
unsafe { btrfs_ioc_rm_dev_v2(fd.as_raw_fd(), &raw const args) }?;
}
DeviceSpec::Path(path) => {
unsafe {
copy_path_to_name(&mut args.__bindgen_anon_2.name, path)
}?;
match unsafe {
btrfs_ioc_rm_dev_v2(fd.as_raw_fd(), &raw const args)
} {
Ok(_) => {}
Err(Errno::ENOTTY | Errno::EOPNOTSUPP) => {
let mut old: btrfs_ioctl_vol_args =
unsafe { mem::zeroed() };
copy_path_to_name(&mut old.name, path)?;
unsafe {
btrfs_ioc_rm_dev(fd.as_raw_fd(), &raw const old)
}?;
}
Err(e) => return Err(e),
}
}
}
Ok(())
}
pub fn device_scan(path: &CStr) -> nix::Result<()> {
let ctl = open_control()?;
let mut raw: btrfs_ioctl_vol_args = unsafe { mem::zeroed() };
copy_path_to_name(&mut raw.name, path)?;
unsafe { btrfs_ioc_scan_dev(ctl.as_raw_fd(), &raw const raw) }?;
Ok(())
}
pub fn device_forget(path: Option<&CStr>) -> nix::Result<()> {
let ctl = open_control()?;
let mut raw: btrfs_ioctl_vol_args = unsafe { mem::zeroed() };
if let Some(p) = path {
copy_path_to_name(&mut raw.name, p)?;
}
unsafe { btrfs_ioc_forget_dev(ctl.as_raw_fd(), &raw const raw) }?;
Ok(())
}
pub fn device_ready(path: &CStr) -> nix::Result<()> {
let ctl = open_control()?;
let mut raw: btrfs_ioctl_vol_args = unsafe { mem::zeroed() };
copy_path_to_name(&mut raw.name, path)?;
unsafe { btrfs_ioc_devices_ready(ctl.as_raw_fd(), &raw mut raw) }?;
Ok(())
}
pub fn device_stats(
fd: BorrowedFd,
devid: u64,
reset: bool,
) -> nix::Result<DeviceStats> {
let mut raw: btrfs_ioctl_get_dev_stats = unsafe { mem::zeroed() };
raw.devid = devid;
raw.nr_items = u64::from(btrfs_dev_stat_values_BTRFS_DEV_STAT_VALUES_MAX);
if reset {
raw.flags = u64::from(BTRFS_DEV_STATS_RESET);
}
unsafe { btrfs_ioc_get_dev_stats(fd.as_raw_fd(), &raw mut raw) }?;
Ok(DeviceStats {
devid,
write_errs: raw.values
[btrfs_dev_stat_values_BTRFS_DEV_STAT_WRITE_ERRS as usize],
read_errs: raw.values
[btrfs_dev_stat_values_BTRFS_DEV_STAT_READ_ERRS as usize],
flush_errs: raw.values
[btrfs_dev_stat_values_BTRFS_DEV_STAT_FLUSH_ERRS as usize],
corruption_errs: raw.values
[btrfs_dev_stat_values_BTRFS_DEV_STAT_CORRUPTION_ERRS as usize],
generation_errs: raw.values
[btrfs_dev_stat_values_BTRFS_DEV_STAT_GENERATION_ERRS as usize],
})
}
const SZ_1M: u64 = 1024 * 1024;
const SZ_32M: u64 = 32 * 1024 * 1024;
const BTRFS_SUPER_MIRROR_MAX: usize = 3;
fn sb_offset(i: usize) -> u64 {
match i {
0 => 64 * 1024,
_ => 1u64 << (20 + 10 * (i as u64)),
}
}
#[derive(Debug, Clone, Copy)]
struct Extent {
start: u64,
end: u64,
}
pub fn device_min_size(fd: BorrowedFd, devid: u64) -> nix::Result<u64> {
let mut dev_extents: Vec<(u64, u64)> = Vec::new();
tree_search(
fd,
SearchFilter::for_objectid_range(
u64::from(BTRFS_DEV_TREE_OBJECTID),
BTRFS_DEV_EXTENT_KEY,
devid,
devid,
),
|hdr, data| {
let Some(de) = btrfs_disk::items::DeviceExtent::parse(data) else {
return Ok(());
};
dev_extents.push((hdr.offset, de.length));
Ok(())
},
)?;
Ok(compute_min_size(&dev_extents))
}
#[must_use]
pub fn compute_min_size(dev_extents: &[(u64, u64)]) -> u64 {
let mut min_size: u64 = SZ_1M;
let mut extents: Vec<Extent> = Vec::new();
let mut holes: Vec<Extent> = Vec::new();
let mut last_pos: Option<u64> = None;
for &(phys_start, len) in dev_extents {
min_size += len;
extents.push(Extent {
start: phys_start,
end: phys_start + len - 1,
});
if let Some(prev_end) = last_pos
&& prev_end != phys_start
{
holes.push(Extent {
start: prev_end,
end: phys_start - 1,
});
}
last_pos = Some(phys_start + len);
}
extents.sort_by(|a, b| b.end.cmp(&a.end));
adjust_min_size(&mut extents, &mut holes, &mut min_size);
min_size
}
fn hole_includes_sb_mirror(start: u64, end: u64) -> bool {
(0..BTRFS_SUPER_MIRROR_MAX).any(|i| {
let bytenr = sb_offset(i);
bytenr >= start && bytenr <= end
})
}
fn adjust_min_size(
extents: &mut Vec<Extent>,
holes: &mut Vec<Extent>,
min_size: &mut u64,
) {
let mut scratch_space: u64 = 0;
while let Some(&ext) = extents.first() {
if ext.end < *min_size {
break;
}
let extent_len = ext.end - ext.start + 1;
let hole_idx = holes.iter().position(|h| {
let hole_len = h.end - h.start + 1;
hole_len >= extent_len
});
let Some(idx) = hole_idx else {
*min_size = ext.end + 1;
break;
};
if hole_includes_sb_mirror(
holes[idx].start,
holes[idx].start + extent_len - 1,
) {
*min_size += extent_len;
}
let hole_len = holes[idx].end - holes[idx].start + 1;
if hole_len > extent_len {
holes[idx].start += extent_len;
} else {
holes.remove(idx);
}
extents.remove(0);
if extent_len > scratch_space {
scratch_space = extent_len;
}
}
if scratch_space > 0 {
*min_size += scratch_space;
*min_size += SZ_32M;
}
}