btrfs_cli/replace/
start.rs1use 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#[derive(Parser, Debug)]
24pub struct ReplaceStartCommand {
25 pub source: String,
27
28 pub target: PathBuf,
30
31 pub mount_point: PathBuf,
33
34 #[clap(short = 'r', long)]
36 pub redundancy_only: bool,
37
38 #[clap(short = 'f', long)]
40 pub force: bool,
41
42 #[clap(short = 'B', long)]
44 pub no_background: bool,
45
46 #[clap(long)]
48 pub enqueue: bool,
49
50 #[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 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 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 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 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 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 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}