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