Skip to main content

btrfs_cli/replace/
start.rs

1use crate::{
2    Format, Runnable,
3    util::{check_device_for_overwrite, open_path},
4};
5use anyhow::{Context, Result, bail};
6use btrfs_uapi::{
7    filesystem::filesystem_info,
8    replace::{ReplaceSource, ReplaceState, replace_start, replace_status},
9    sysfs::SysfsBtrfs,
10};
11use clap::Parser;
12use std::{
13    ffi::CString, fs, os::unix::io::AsFd, path::PathBuf, thread, time::Duration,
14};
15
16/// Replace a device in the filesystem.
17///
18/// The source device can be specified either as a path (e.g. /dev/sdb) or as a
19/// numeric device ID. The target device will be used to replace the source. The
20/// filesystem must be mounted at mount_point.
21#[derive(Parser, Debug)]
22#[allow(clippy::doc_markdown, clippy::struct_excessive_bools)]
23pub struct ReplaceStartCommand {
24    /// Source device path or devid to replace
25    pub source: String,
26
27    /// Target device that will replace the source
28    pub target: PathBuf,
29
30    /// Mount point of the filesystem
31    pub mount_point: PathBuf,
32
33    /// Only read from srcdev if no other zero-defect mirror exists
34    #[clap(short = 'r', long)]
35    pub redundancy_only: bool,
36
37    /// Force using and overwriting targetdev even if it contains a valid btrfs filesystem
38    #[clap(short = 'f', long)]
39    pub force: bool,
40
41    /// Do not background the replace operation; wait for it to finish
42    #[clap(short = 'B', long)]
43    pub no_background: bool,
44
45    /// Wait if there's another exclusive operation running, instead of returning an error
46    #[clap(long)]
47    pub enqueue: bool,
48
49    /// Do not perform whole device TRIM on the target device
50    #[clap(short = 'K', long)]
51    pub nodiscard: bool,
52}
53
54impl Runnable for ReplaceStartCommand {
55    #[allow(clippy::too_many_lines)]
56    fn run(&self, _format: Format, _dry_run: bool) -> Result<()> {
57        // Validate the target device before opening the filesystem.
58        check_device_for_overwrite(&self.target, self.force)?;
59
60        let file = open_path(&self.mount_point)?;
61        let fd = file.as_fd();
62
63        // If --enqueue is set, wait for any running exclusive operation to finish.
64        if self.enqueue {
65            let info = filesystem_info(fd).with_context(|| {
66                format!(
67                    "failed to get filesystem info for '{}'",
68                    self.mount_point.display()
69                )
70            })?;
71            let sysfs = SysfsBtrfs::new(&info.uuid);
72            let op =
73                sysfs.wait_for_exclusive_operation().with_context(|| {
74                    format!(
75                        "failed to check exclusive operation on '{}'",
76                        self.mount_point.display()
77                    )
78                })?;
79            if op != "none" {
80                eprintln!("waited for exclusive operation '{op}' to finish");
81            }
82        }
83
84        // Check if a replace is already running.
85        let current = replace_status(fd).with_context(|| {
86            format!(
87                "failed to get replace status on '{}'",
88                self.mount_point.display()
89            )
90        })?;
91        if current.state == ReplaceState::Started {
92            bail!(
93                "a device replace operation is already in progress on '{}'",
94                self.mount_point.display()
95            );
96        }
97
98        // Resolve source: if it parses as a number, treat it as a devid;
99        // otherwise treat it as a device path.
100        let source = if let Ok(devid) = self.source.parse::<u64>() {
101            ReplaceSource::DevId(devid)
102        } else {
103            ReplaceSource::Path(
104                &CString::new(self.source.as_bytes()).with_context(|| {
105                    format!("invalid source device path '{}'", self.source)
106                })?,
107            )
108        };
109
110        let tgtdev = CString::new(self.target.as_os_str().as_encoded_bytes())
111            .with_context(|| {
112            format!("invalid target device path '{}'", self.target.display())
113        })?;
114
115        // Discard (TRIM) the target device unless --nodiscard is set.
116        if !self.nodiscard {
117            let tgtfile = fs::OpenOptions::new()
118                .write(true)
119                .open(&self.target)
120                .with_context(|| {
121                    format!(
122                        "failed to open target device '{}' for discard",
123                        self.target.display()
124                    )
125                })?;
126            match btrfs_uapi::blkdev::discard_whole_device(tgtfile.as_fd()) {
127                Ok(0) => {}
128                Ok(_) => eprintln!(
129                    "discarded target device '{}'",
130                    self.target.display()
131                ),
132                Err(e) => {
133                    eprintln!(
134                        "warning: discard failed on '{}': {e}; continuing anyway",
135                        self.target.display()
136                    );
137                }
138            }
139        }
140
141        match replace_start(fd, &source, &tgtdev, self.redundancy_only)
142            .with_context(|| {
143                format!(
144                    "failed to start replace on '{}'",
145                    self.mount_point.display()
146                )
147            })? {
148            Ok(()) => {}
149            Err(e) => bail!("{e}"),
150        }
151
152        println!(
153            "replace started: {} -> {}",
154            self.source,
155            self.target.display(),
156        );
157
158        if self.no_background {
159            // Poll until the replace finishes.
160            loop {
161                thread::sleep(Duration::from_secs(1));
162                let status = replace_status(fd).with_context(|| {
163                    format!(
164                        "failed to get replace status on '{}'",
165                        self.mount_point.display()
166                    )
167                })?;
168
169                #[allow(clippy::cast_precision_loss)]
170                let pct = status.progress_1000 as f64 / 10.0;
171                eprint!(
172                    "\r{pct:.1}% done, {} write errs, {} uncorr. read errs",
173                    status.num_write_errors,
174                    status.num_uncorrectable_read_errors,
175                );
176
177                if status.state != ReplaceState::Started {
178                    eprintln!();
179                    match status.state {
180                        ReplaceState::Finished => {
181                            println!("replace finished successfully");
182                        }
183                        ReplaceState::Canceled => {
184                            bail!("replace was cancelled");
185                        }
186                        _ => {
187                            bail!(
188                                "replace ended in unexpected state: {:?}",
189                                status.state
190                            );
191                        }
192                    }
193                    break;
194                }
195            }
196        }
197
198        Ok(())
199    }
200}