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