Skip to main content

btrfs_cli/device/
add.rs

1use crate::{Format, Runnable, util::check_device_for_overwrite};
2use anyhow::{Context, Result};
3use btrfs_uapi::{
4    device::device_add, filesystem::filesystem_info, sysfs::SysfsBtrfs,
5};
6use clap::Parser;
7use std::{ffi::CString, fs, os::unix::io::AsFd, path::PathBuf};
8
9/// Add one or more devices to a mounted filesystem
10///
11/// The device must not be mounted and should not contain a filesystem or
12/// other data. The operation requires CAP_SYS_ADMIN.
13#[derive(Parser, Debug)]
14pub struct DeviceAddCommand {
15    /// Force overwrite of an existing filesystem on the device
16    #[clap(short = 'f', long)]
17    pub force: bool,
18
19    /// Do not perform whole device TRIM (discard) before adding
20    #[clap(short = 'K', long)]
21    pub nodiscard: bool,
22
23    /// Wait if there's another exclusive operation running, instead of returning an error
24    #[clap(long)]
25    pub enqueue: bool,
26
27    /// One or more block devices to add
28    #[clap(required = true, num_args = 1..)]
29    pub devices: Vec<PathBuf>,
30
31    /// Mount point of the target filesystem
32    pub target: PathBuf,
33}
34
35impl Runnable for DeviceAddCommand {
36    fn run(&self, _format: Format, _dry_run: bool) -> Result<()> {
37        let file = fs::File::open(&self.target).with_context(|| {
38            format!("failed to open '{}'", self.target.display())
39        })?;
40        let fd = file.as_fd();
41
42        // If --enqueue is set, wait for any running exclusive operation to finish.
43        if self.enqueue {
44            let info = filesystem_info(fd).with_context(|| {
45                format!(
46                    "failed to get filesystem info for '{}'",
47                    self.target.display()
48                )
49            })?;
50            let sysfs = SysfsBtrfs::new(&info.uuid);
51            let op =
52                sysfs.wait_for_exclusive_operation().with_context(|| {
53                    format!(
54                        "failed to check exclusive operation on '{}'",
55                        self.target.display()
56                    )
57                })?;
58            if op != "none" {
59                eprintln!("waited for exclusive operation '{op}' to finish");
60            }
61        }
62
63        let mut had_error = false;
64
65        for device in &self.devices {
66            // Validate the device: must be a block device, not mounted, no
67            // existing btrfs filesystem (unless --force).
68            if let Err(e) = check_device_for_overwrite(device, self.force) {
69                eprintln!("error: {e:#}");
70                had_error = true;
71                continue;
72            }
73
74            // Discard (TRIM) the device unless --nodiscard is set.
75            if !self.nodiscard {
76                match fs::OpenOptions::new().write(true).open(device) {
77                    Ok(tgtfile) => {
78                        match btrfs_uapi::blkdev::discard_whole_device(
79                            tgtfile.as_fd(),
80                        ) {
81                            Ok(0) => {}
82                            Ok(_) => eprintln!(
83                                "discarded device '{}'",
84                                device.display()
85                            ),
86                            Err(e) => {
87                                eprintln!(
88                                    "warning: discard failed on '{}': {e}; continuing anyway",
89                                    device.display()
90                                );
91                            }
92                        }
93                    }
94                    Err(e) => {
95                        eprintln!(
96                            "warning: could not open '{}' for discard: {e}; continuing anyway",
97                            device.display()
98                        );
99                    }
100                }
101            }
102
103            let path_str = device.to_str().ok_or_else(|| {
104                anyhow::anyhow!(
105                    "device path is not valid UTF-8: '{}'",
106                    device.display()
107                )
108            })?;
109
110            let cpath = CString::new(path_str).with_context(|| {
111                format!(
112                    "device path contains a null byte: '{}'",
113                    device.display()
114                )
115            })?;
116
117            match device_add(fd, &cpath) {
118                Ok(()) => println!("added device '{}'", device.display()),
119                Err(e) => {
120                    eprintln!(
121                        "error adding device '{}': {e}",
122                        device.display()
123                    );
124                    had_error = true;
125                }
126            }
127        }
128
129        if had_error {
130            anyhow::bail!("one or more devices could not be added");
131        }
132
133        Ok(())
134    }
135}