Skip to main content

btrfs_cli/device/
remove.rs

1use crate::{Format, Runnable};
2use anyhow::{Context, Result};
3use btrfs_uapi::{
4    device::{DeviceSpec, device_remove},
5    filesystem::filesystem_info,
6    sysfs::SysfsBtrfs,
7};
8use clap::Parser;
9use std::{ffi::CString, fs::File, os::unix::io::AsFd, path::PathBuf};
10
11/// Remove one or more devices from a mounted filesystem
12///
13/// Each device can be specified as a block device path, a numeric device ID,
14/// the special token "missing" (to remove a device that is no longer present),
15/// or "cancel" (to cancel an in-progress removal).
16///
17/// The operation requires CAP_SYS_ADMIN.
18#[derive(Parser, Debug)]
19pub struct DeviceRemoveCommand {
20    /// One or more devices to remove (path, devid, "missing", or "cancel"),
21    /// followed by the filesystem mount point
22    ///
23    /// Example: btrfs device remove /dev/sdb 3 missing /mnt/data
24    /// Wait if another exclusive operation is running, rather than failing
25    #[clap(long)]
26    pub enqueue: bool,
27
28    #[clap(required = true, num_args = 2..)]
29    pub args: Vec<String>,
30}
31
32impl Runnable for DeviceRemoveCommand {
33    fn run(&self, _format: Format, _dry_run: bool) -> Result<()> {
34        // The last argument is the mount point; everything before it is a device spec.
35        // split_last() returns (&last, &[..rest]), so mount_str is first.
36        let (mount_str, specs) = self
37            .args
38            .split_last()
39            .expect("clap ensures at least 2 args");
40
41        let mount = PathBuf::from(mount_str);
42        let file = File::open(&mount)
43            .with_context(|| format!("failed to open '{}'", mount.display()))?;
44        let fd = file.as_fd();
45
46        if self.enqueue {
47            let info = filesystem_info(fd).with_context(|| {
48                format!(
49                    "failed to get filesystem info for '{}'",
50                    mount.display()
51                )
52            })?;
53            let sysfs = SysfsBtrfs::new(&info.uuid);
54            let op =
55                sysfs.wait_for_exclusive_operation().with_context(|| {
56                    format!(
57                        "failed to check exclusive operation on '{}'",
58                        mount.display()
59                    )
60                })?;
61            if op != "none" {
62                eprintln!("waited for exclusive operation '{op}' to finish");
63            }
64        }
65
66        let mut had_error = false;
67
68        for spec_str in specs {
69            match remove_one(fd, spec_str) {
70                Ok(()) => println!("removed device '{spec_str}'"),
71                Err(e) => {
72                    eprintln!("error removing device '{spec_str}': {e}");
73                    had_error = true;
74                }
75            }
76        }
77
78        if had_error {
79            anyhow::bail!("one or more devices could not be removed");
80        }
81
82        Ok(())
83    }
84}
85
86/// Attempt to remove a single device identified by `spec_str` from the
87/// filesystem open on `fd`.
88///
89/// If `spec_str` parses as a `u64` it is treated as a device ID; otherwise it
90/// is treated as a path (or the special strings `"missing"` / `"cancel"`).
91fn remove_one(fd: std::os::unix::io::BorrowedFd, spec_str: &str) -> Result<()> {
92    if let Ok(devid) = spec_str.parse::<u64>() {
93        device_remove(fd, DeviceSpec::Id(devid))
94            .with_context(|| format!("failed to remove devid {devid}"))?;
95    } else {
96        let cpath = CString::new(spec_str).with_context(|| {
97            format!("device spec contains a null byte: '{spec_str}'")
98        })?;
99        device_remove(fd, DeviceSpec::Path(&cpath))
100            .with_context(|| format!("failed to remove device '{spec_str}'"))?;
101    }
102    Ok(())
103}