btrfs_cli/replace/
start.rs1use 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#[derive(Parser, Debug)]
22#[allow(clippy::doc_markdown, clippy::struct_excessive_bools)]
23pub struct ReplaceStartCommand {
24 pub source: String,
26
27 pub target: PathBuf,
29
30 pub mount_point: PathBuf,
32
33 #[clap(short = 'r', long)]
35 pub redundancy_only: bool,
36
37 #[clap(short = 'f', long)]
39 pub force: bool,
40
41 #[clap(short = 'B', long)]
43 pub no_background: bool,
44
45 #[clap(long)]
47 pub enqueue: bool,
48
49 #[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 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 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 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 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 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 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}