Skip to main content

btrfs_uapi/
device.rs

1//! # Device management: adding, removing, and querying block devices in a filesystem
2//!
3//! Covers adding and removing devices from a mounted filesystem, scanning a
4//! device to register it with the kernel, querying per-device I/O error
5//! statistics, and checking whether all devices of a multi-device filesystem
6//! are present and ready.
7//!
8//! Most operations require `CAP_SYS_ADMIN`.
9
10use crate::{
11    filesystem::FsInfo,
12    raw::{
13        BTRFS_DEV_STATS_RESET, BTRFS_DEVICE_SPEC_BY_ID,
14        btrfs_dev_stat_values_BTRFS_DEV_STAT_CORRUPTION_ERRS,
15        btrfs_dev_stat_values_BTRFS_DEV_STAT_FLUSH_ERRS,
16        btrfs_dev_stat_values_BTRFS_DEV_STAT_GENERATION_ERRS,
17        btrfs_dev_stat_values_BTRFS_DEV_STAT_READ_ERRS,
18        btrfs_dev_stat_values_BTRFS_DEV_STAT_VALUES_MAX,
19        btrfs_dev_stat_values_BTRFS_DEV_STAT_WRITE_ERRS, btrfs_ioc_add_dev, btrfs_ioc_dev_info,
20        btrfs_ioc_devices_ready, btrfs_ioc_forget_dev, btrfs_ioc_get_dev_stats, btrfs_ioc_rm_dev,
21        btrfs_ioc_rm_dev_v2, btrfs_ioc_scan_dev, btrfs_ioctl_dev_info_args,
22        btrfs_ioctl_get_dev_stats, btrfs_ioctl_vol_args, btrfs_ioctl_vol_args_v2,
23    },
24};
25use nix::{errno::Errno, libc::c_char};
26use std::{
27    ffi::CStr,
28    fs::OpenOptions,
29    mem,
30    os::{fd::AsRawFd, unix::io::BorrowedFd},
31};
32use uuid::Uuid;
33
34/// Information about a single device within a btrfs filesystem, as returned
35/// by `BTRFS_IOC_DEV_INFO`.
36#[derive(Debug, Clone)]
37pub struct DevInfo {
38    /// Device ID.
39    pub devid: u64,
40    /// Device UUID.
41    pub uuid: Uuid,
42    /// Number of bytes used on this device.
43    pub bytes_used: u64,
44    /// Total size of this device in bytes.
45    pub total_bytes: u64,
46    /// Path to the block device, e.g. `/dev/sda`.
47    pub path: String,
48}
49
50/// Specifies a device for operations that can address by either path or ID.
51#[derive(Debug, Clone)]
52pub enum DeviceSpec<'a> {
53    /// A block device path (e.g. `/dev/sdb`), or the special strings
54    /// `"missing"` or `"cancel"` accepted by the remove ioctl.
55    Path(&'a CStr),
56    /// A btrfs device ID as reported by `BTRFS_IOC_DEV_INFO`.
57    Id(u64),
58}
59
60/// Per-device I/O error statistics, as returned by `BTRFS_IOC_GET_DEV_STATS`.
61#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
62pub struct DevStats {
63    /// Device ID these stats belong to.
64    pub devid: u64,
65    /// Number of write I/O errors (EIO/EREMOTEIO from lower layers).
66    pub write_errs: u64,
67    /// Number of read I/O errors (EIO/EREMOTEIO from lower layers).
68    pub read_errs: u64,
69    /// Number of flush I/O errors (EIO/EREMOTEIO from lower layers).
70    pub flush_errs: u64,
71    /// Number of checksum or bytenr corruption errors detected on read.
72    pub corruption_errs: u64,
73    /// Number of generation errors (blocks not written where expected).
74    pub generation_errs: u64,
75}
76
77impl DevStats {
78    /// Sum of all error counters.
79    pub fn total_errs(&self) -> u64 {
80        self.write_errs
81            + self.read_errs
82            + self.flush_errs
83            + self.corruption_errs
84            + self.generation_errs
85    }
86
87    /// Returns `true` if every counter is zero.
88    pub fn is_clean(&self) -> bool {
89        self.total_errs() == 0
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96
97    #[test]
98    fn dev_stats_default_is_clean() {
99        let stats = DevStats::default();
100        assert!(stats.is_clean());
101        assert_eq!(stats.total_errs(), 0);
102    }
103
104    #[test]
105    fn dev_stats_total_errs() {
106        let stats = DevStats {
107            devid: 1,
108            write_errs: 1,
109            read_errs: 2,
110            flush_errs: 3,
111            corruption_errs: 4,
112            generation_errs: 5,
113        };
114        assert_eq!(stats.total_errs(), 15);
115        assert!(!stats.is_clean());
116    }
117
118    #[test]
119    fn dev_stats_single_error_not_clean() {
120        let stats = DevStats {
121            corruption_errs: 1,
122            ..DevStats::default()
123        };
124        assert!(!stats.is_clean());
125        assert_eq!(stats.total_errs(), 1);
126    }
127}
128
129/// Copy the bytes of `path` (without the nul terminator) into `name`,
130/// returning `ENAMETOOLONG` if the path (including the terminator that the
131/// kernel expects to already be present via zeroing) does not fit.
132fn copy_path_to_name(name: &mut [c_char], path: &CStr) -> nix::Result<()> {
133    let bytes = path.to_bytes(); // excludes nul terminator
134    if bytes.len() >= name.len() {
135        return Err(Errno::ENAMETOOLONG);
136    }
137    for (i, &b) in bytes.iter().enumerate() {
138        name[i] = b as c_char;
139    }
140    // The remainder of `name` is already zeroed by the caller (mem::zeroed).
141    Ok(())
142}
143
144/// Open `/dev/btrfs-control` for read+write, mapping any `std::io::Error` to
145/// the appropriate `nix::errno::Errno`.
146fn open_control() -> nix::Result<std::fs::File> {
147    OpenOptions::new()
148        .read(true)
149        .write(true)
150        .open("/dev/btrfs-control")
151        .map_err(|e| Errno::from_raw(e.raw_os_error().unwrap_or(nix::libc::ENODEV)))
152}
153
154/// Query information about the device with the given `devid` on the filesystem
155/// referred to by `fd`.
156///
157/// Returns `None` if no device with that ID exists (`ENODEV`).
158pub fn device_info(fd: BorrowedFd, devid: u64) -> nix::Result<Option<DevInfo>> {
159    let mut raw: btrfs_ioctl_dev_info_args = unsafe { mem::zeroed() };
160    raw.devid = devid;
161
162    match unsafe { btrfs_ioc_dev_info(fd.as_raw_fd(), &mut raw) } {
163        Err(Errno::ENODEV) => return Ok(None),
164        Err(e) => return Err(e),
165        Ok(_) => {}
166    }
167
168    let path = unsafe { CStr::from_ptr(raw.path.as_ptr() as *const _) }
169        .to_string_lossy()
170        .into_owned();
171
172    Ok(Some(DevInfo {
173        devid: raw.devid,
174        uuid: Uuid::from_bytes(raw.uuid),
175        bytes_used: raw.bytes_used,
176        total_bytes: raw.total_bytes,
177        path,
178    }))
179}
180
181/// Query information about all devices in the filesystem referred to by `fd`,
182/// using the device count from a previously obtained [`FsInfo`].
183///
184/// Iterates devids `1..=max_id`, skipping any that return `ENODEV` (holes in
185/// the devid space are normal when devices have been removed).
186pub fn device_info_all(fd: BorrowedFd, fs_info: &FsInfo) -> nix::Result<Vec<DevInfo>> {
187    let mut devices = Vec::with_capacity(fs_info.num_devices as usize);
188    for devid in 1..=fs_info.max_id {
189        if let Some(info) = device_info(fd, devid)? {
190            devices.push(info);
191        }
192    }
193    Ok(devices)
194}
195
196/// Add a device to the btrfs filesystem referred to by `fd`.
197///
198/// `path` must be the path to an unmounted block device. The kernel requires
199/// `CAP_SYS_ADMIN`.
200pub fn device_add(fd: BorrowedFd, path: &CStr) -> nix::Result<()> {
201    let mut raw: btrfs_ioctl_vol_args = unsafe { mem::zeroed() };
202    copy_path_to_name(&mut raw.name, path)?;
203    unsafe { btrfs_ioc_add_dev(fd.as_raw_fd(), &raw) }?;
204    Ok(())
205}
206
207/// Remove a device from the btrfs filesystem referred to by `fd`.
208///
209/// The device can be specified either by path or by its btrfs device ID via
210/// [`DeviceSpec`]. Uses `BTRFS_IOC_RM_DEV_V2` and falls back to the older
211/// `BTRFS_IOC_RM_DEV` ioctl on kernels that do not support the v2 variant
212/// (only possible when removing by path). The kernel requires `CAP_SYS_ADMIN`.
213pub fn device_remove(fd: BorrowedFd, spec: DeviceSpec) -> nix::Result<()> {
214    let mut args: btrfs_ioctl_vol_args_v2 = unsafe { mem::zeroed() };
215
216    match spec {
217        DeviceSpec::Id(devid) => {
218            args.flags = BTRFS_DEVICE_SPEC_BY_ID as u64;
219            // SAFETY: devid is the active union member when BTRFS_DEVICE_SPEC_BY_ID is set.
220            args.__bindgen_anon_2.devid = devid;
221            unsafe { btrfs_ioc_rm_dev_v2(fd.as_raw_fd(), &args) }?;
222        }
223        DeviceSpec::Path(path) => {
224            // SAFETY: name is the active union member when flags == 0.
225            unsafe { copy_path_to_name(&mut args.__bindgen_anon_2.name, path) }?;
226            match unsafe { btrfs_ioc_rm_dev_v2(fd.as_raw_fd(), &args) } {
227                Ok(_) => {}
228                // Fall back to the old single-arg ioctl on kernels that either
229                // don't know about v2 (ENOTTY) or don't recognise our flags (EOPNOTSUPP).
230                Err(Errno::ENOTTY) | Err(Errno::EOPNOTSUPP) => {
231                    let mut old: btrfs_ioctl_vol_args = unsafe { mem::zeroed() };
232                    copy_path_to_name(&mut old.name, path)?;
233                    unsafe { btrfs_ioc_rm_dev(fd.as_raw_fd(), &old) }?;
234                }
235                Err(e) => return Err(e),
236            }
237        }
238    }
239
240    Ok(())
241}
242
243/// Register a block device with the kernel's btrfs device scanner so that
244/// multi-device filesystems containing it can be mounted.
245///
246/// Opens `/dev/btrfs-control` and issues `BTRFS_IOC_SCAN_DEV`. `path` must
247/// be the path to a block device that contains a btrfs filesystem member.
248pub fn device_scan(path: &CStr) -> nix::Result<()> {
249    let ctl = open_control()?;
250    let mut raw: btrfs_ioctl_vol_args = unsafe { mem::zeroed() };
251    copy_path_to_name(&mut raw.name, path)?;
252    unsafe { btrfs_ioc_scan_dev(ctl.as_raw_fd(), &raw) }?;
253    Ok(())
254}
255
256/// Unregister a device (or all stale devices) from the kernel's btrfs device
257/// scanner.
258///
259/// Opens `/dev/btrfs-control` and issues `BTRFS_IOC_FORGET_DEV`. If `path`
260/// is `None`, all devices that are not part of a currently mounted filesystem
261/// are unregistered. If `path` is `Some`, only that specific device path is
262/// unregistered.
263pub fn device_forget(path: Option<&CStr>) -> nix::Result<()> {
264    let ctl = open_control()?;
265    let mut raw: btrfs_ioctl_vol_args = unsafe { mem::zeroed() };
266    if let Some(p) = path {
267        copy_path_to_name(&mut raw.name, p)?;
268    }
269    unsafe { btrfs_ioc_forget_dev(ctl.as_raw_fd(), &raw) }?;
270    Ok(())
271}
272
273/// Check whether all member devices of the filesystem that contains `path`
274/// are available and the filesystem is ready to mount.
275///
276/// Opens `/dev/btrfs-control` and issues `BTRFS_IOC_DEVICES_READY`. `path`
277/// must be the path to one of the block devices belonging to the filesystem.
278/// Returns `Ok(())` when all devices are present; returns an error (typically
279/// `ENOENT` or `ENXIO`) if the set is incomplete.
280pub fn device_ready(path: &CStr) -> nix::Result<()> {
281    let ctl = open_control()?;
282    // BTRFS_IOC_DEVICES_READY is declared _IOR but the kernel reads the device
283    // path from args.name, so we pass a mut pointer as ioctl_read! requires.
284    let mut raw: btrfs_ioctl_vol_args = unsafe { mem::zeroed() };
285    copy_path_to_name(&mut raw.name, path)?;
286    unsafe { btrfs_ioc_devices_ready(ctl.as_raw_fd(), &mut raw) }?;
287    Ok(())
288}
289
290/// Query I/O error statistics for the device identified by `devid` within the
291/// filesystem referred to by `fd`.
292///
293/// If `reset` is `true`, the kernel atomically returns the current values and
294/// then resets all counters to zero. The kernel requires `CAP_SYS_ADMIN`.
295pub fn device_stats(fd: BorrowedFd, devid: u64, reset: bool) -> nix::Result<DevStats> {
296    let mut raw: btrfs_ioctl_get_dev_stats = unsafe { mem::zeroed() };
297    raw.devid = devid;
298    raw.nr_items = btrfs_dev_stat_values_BTRFS_DEV_STAT_VALUES_MAX as u64;
299    if reset {
300        raw.flags = BTRFS_DEV_STATS_RESET as u64;
301    }
302
303    unsafe { btrfs_ioc_get_dev_stats(fd.as_raw_fd(), &mut raw) }?;
304
305    Ok(DevStats {
306        devid,
307        write_errs: raw.values[btrfs_dev_stat_values_BTRFS_DEV_STAT_WRITE_ERRS as usize],
308        read_errs: raw.values[btrfs_dev_stat_values_BTRFS_DEV_STAT_READ_ERRS as usize],
309        flush_errs: raw.values[btrfs_dev_stat_values_BTRFS_DEV_STAT_FLUSH_ERRS as usize],
310        corruption_errs: raw.values[btrfs_dev_stat_values_BTRFS_DEV_STAT_CORRUPTION_ERRS as usize],
311        generation_errs: raw.values[btrfs_dev_stat_values_BTRFS_DEV_STAT_GENERATION_ERRS as usize],
312    })
313}