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