Skip to main content

btrfs_cli/replace/
start.rs

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