1use crate::raw::{
10 BTRFS_IOCTL_DEV_REPLACE_CMD_CANCEL, BTRFS_IOCTL_DEV_REPLACE_CMD_START,
11 BTRFS_IOCTL_DEV_REPLACE_CMD_STATUS,
12 BTRFS_IOCTL_DEV_REPLACE_CONT_READING_FROM_SRCDEV_MODE_ALWAYS,
13 BTRFS_IOCTL_DEV_REPLACE_CONT_READING_FROM_SRCDEV_MODE_AVOID,
14 BTRFS_IOCTL_DEV_REPLACE_RESULT_ALREADY_STARTED,
15 BTRFS_IOCTL_DEV_REPLACE_RESULT_NO_ERROR,
16 BTRFS_IOCTL_DEV_REPLACE_RESULT_NOT_STARTED,
17 BTRFS_IOCTL_DEV_REPLACE_RESULT_SCRUB_INPROGRESS,
18 BTRFS_IOCTL_DEV_REPLACE_STATE_CANCELED,
19 BTRFS_IOCTL_DEV_REPLACE_STATE_FINISHED,
20 BTRFS_IOCTL_DEV_REPLACE_STATE_NEVER_STARTED,
21 BTRFS_IOCTL_DEV_REPLACE_STATE_STARTED,
22 BTRFS_IOCTL_DEV_REPLACE_STATE_SUSPENDED, btrfs_ioc_dev_replace,
23 btrfs_ioctl_dev_replace_args,
24};
25use nix::errno::Errno;
26use std::{
27 ffi::CStr,
28 mem,
29 os::{fd::AsRawFd, unix::io::BorrowedFd},
30 time::{Duration, SystemTime, UNIX_EPOCH},
31};
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum ReplaceState {
36 NeverStarted,
37 Started,
38 Finished,
39 Canceled,
40 Suspended,
41}
42
43impl ReplaceState {
44 fn from_raw(val: u64) -> Option<ReplaceState> {
45 match val {
46 x if x
47 == u64::from(BTRFS_IOCTL_DEV_REPLACE_STATE_NEVER_STARTED) =>
48 {
49 Some(ReplaceState::NeverStarted)
50 }
51 x if x == u64::from(BTRFS_IOCTL_DEV_REPLACE_STATE_STARTED) => {
52 Some(ReplaceState::Started)
53 }
54 x if x == u64::from(BTRFS_IOCTL_DEV_REPLACE_STATE_FINISHED) => {
55 Some(ReplaceState::Finished)
56 }
57 x if x == u64::from(BTRFS_IOCTL_DEV_REPLACE_STATE_CANCELED) => {
58 Some(ReplaceState::Canceled)
59 }
60 x if x == u64::from(BTRFS_IOCTL_DEV_REPLACE_STATE_SUSPENDED) => {
61 Some(ReplaceState::Suspended)
62 }
63 _ => None,
64 }
65 }
66}
67
68#[derive(Debug, Clone)]
70pub struct ReplaceStatus {
71 pub state: ReplaceState,
73 pub progress_1000: u64,
75 pub time_started: Option<SystemTime>,
77 pub time_stopped: Option<SystemTime>,
79 pub num_write_errors: u64,
81 pub num_uncorrectable_read_errors: u64,
83}
84
85fn epoch_to_systemtime(secs: u64) -> Option<SystemTime> {
86 if secs == 0 {
87 None
88 } else {
89 Some(UNIX_EPOCH + Duration::from_secs(secs))
90 }
91}
92
93pub enum ReplaceSource<'a> {
95 DevId(u64),
97 Path(&'a CStr),
99}
100
101pub fn replace_status(fd: BorrowedFd) -> nix::Result<ReplaceStatus> {
104 let mut args: btrfs_ioctl_dev_replace_args = unsafe { mem::zeroed() };
105 args.cmd = u64::from(BTRFS_IOCTL_DEV_REPLACE_CMD_STATUS);
106
107 unsafe { btrfs_ioc_dev_replace(fd.as_raw_fd(), &raw mut args) }?;
108
109 let status = unsafe { &args.__bindgen_anon_1.status };
111 let state =
112 ReplaceState::from_raw(status.replace_state).ok_or(Errno::EINVAL)?;
113
114 Ok(ReplaceStatus {
115 state,
116 progress_1000: status.progress_1000,
117 time_started: epoch_to_systemtime(status.time_started),
118 time_stopped: epoch_to_systemtime(status.time_stopped),
119 num_write_errors: status.num_write_errors,
120 num_uncorrectable_read_errors: status.num_uncorrectable_read_errors,
121 })
122}
123
124#[derive(Debug, Clone, Copy, PartialEq, Eq)]
128pub enum ReplaceStartError {
129 AlreadyStarted,
131 ScrubInProgress,
133}
134
135impl std::fmt::Display for ReplaceStartError {
136 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
137 match self {
138 ReplaceStartError::AlreadyStarted => {
139 write!(f, "a device replace operation is already in progress")
140 }
141 ReplaceStartError::ScrubInProgress => {
142 write!(f, "a scrub is in progress; cancel it first")
143 }
144 }
145 }
146}
147
148impl std::error::Error for ReplaceStartError {}
149
150pub fn replace_start(
167 fd: BorrowedFd,
168 source: ReplaceSource,
169 tgtdev_path: &CStr,
170 avoid_srcdev: bool,
171) -> nix::Result<Result<(), ReplaceStartError>> {
172 let mut args: btrfs_ioctl_dev_replace_args = unsafe { mem::zeroed() };
173 args.cmd = u64::from(BTRFS_IOCTL_DEV_REPLACE_CMD_START);
174
175 let start = unsafe { &mut args.__bindgen_anon_1.start };
177
178 match source {
179 ReplaceSource::DevId(devid) => {
180 start.srcdevid = devid;
181 }
182 ReplaceSource::Path(path) => {
183 start.srcdevid = 0;
184 let bytes = path.to_bytes();
185 if bytes.len() >= start.srcdev_name.len() {
186 return Err(Errno::ENAMETOOLONG);
187 }
188 start.srcdev_name[..bytes.len()].copy_from_slice(bytes);
189 }
190 }
191
192 let tgt_bytes = tgtdev_path.to_bytes();
193 if tgt_bytes.len() >= start.tgtdev_name.len() {
194 return Err(Errno::ENAMETOOLONG);
195 }
196 start.tgtdev_name[..tgt_bytes.len()].copy_from_slice(tgt_bytes);
197
198 start.cont_reading_from_srcdev_mode = if avoid_srcdev {
199 u64::from(BTRFS_IOCTL_DEV_REPLACE_CONT_READING_FROM_SRCDEV_MODE_AVOID)
200 } else {
201 u64::from(BTRFS_IOCTL_DEV_REPLACE_CONT_READING_FROM_SRCDEV_MODE_ALWAYS)
202 };
203
204 unsafe { btrfs_ioc_dev_replace(fd.as_raw_fd(), &raw mut args) }?;
205
206 match args.result {
207 x if x == u64::from(BTRFS_IOCTL_DEV_REPLACE_RESULT_NO_ERROR) => {
208 Ok(Ok(()))
209 }
210 x if x == u64::from(BTRFS_IOCTL_DEV_REPLACE_RESULT_ALREADY_STARTED) => {
211 Ok(Err(ReplaceStartError::AlreadyStarted))
212 }
213 x if x
214 == u64::from(BTRFS_IOCTL_DEV_REPLACE_RESULT_SCRUB_INPROGRESS) =>
215 {
216 Ok(Err(ReplaceStartError::ScrubInProgress))
217 }
218 _ => Err(Errno::EINVAL),
219 }
220}
221
222pub fn replace_cancel(fd: BorrowedFd) -> nix::Result<bool> {
228 let mut args: btrfs_ioctl_dev_replace_args = unsafe { mem::zeroed() };
229 args.cmd = u64::from(BTRFS_IOCTL_DEV_REPLACE_CMD_CANCEL);
230
231 unsafe { btrfs_ioc_dev_replace(fd.as_raw_fd(), &raw mut args) }?;
232
233 match args.result {
234 x if x == u64::from(BTRFS_IOCTL_DEV_REPLACE_RESULT_NO_ERROR) => {
235 Ok(true)
236 }
237 x if x == u64::from(BTRFS_IOCTL_DEV_REPLACE_RESULT_NOT_STARTED) => {
238 Ok(false)
239 }
240 _ => Err(Errno::EINVAL),
241 }
242}
243
244#[cfg(test)]
245mod tests {
246 use super::*;
247
248 #[test]
251 fn epoch_zero_is_none() {
252 assert!(epoch_to_systemtime(0).is_none());
253 }
254
255 #[test]
256 fn epoch_nonzero_is_some() {
257 let t = epoch_to_systemtime(1700000000).unwrap();
258 assert_eq!(t, UNIX_EPOCH + Duration::from_secs(1700000000));
259 }
260
261 #[test]
264 fn replace_state_from_raw_all_variants() {
265 assert!(matches!(
266 ReplaceState::from_raw(
267 BTRFS_IOCTL_DEV_REPLACE_STATE_NEVER_STARTED as u64
268 ),
269 Some(ReplaceState::NeverStarted)
270 ));
271 assert!(matches!(
272 ReplaceState::from_raw(
273 BTRFS_IOCTL_DEV_REPLACE_STATE_STARTED as u64
274 ),
275 Some(ReplaceState::Started)
276 ));
277 assert!(matches!(
278 ReplaceState::from_raw(
279 BTRFS_IOCTL_DEV_REPLACE_STATE_FINISHED as u64
280 ),
281 Some(ReplaceState::Finished)
282 ));
283 assert!(matches!(
284 ReplaceState::from_raw(
285 BTRFS_IOCTL_DEV_REPLACE_STATE_CANCELED as u64
286 ),
287 Some(ReplaceState::Canceled)
288 ));
289 assert!(matches!(
290 ReplaceState::from_raw(
291 BTRFS_IOCTL_DEV_REPLACE_STATE_SUSPENDED as u64
292 ),
293 Some(ReplaceState::Suspended)
294 ));
295 }
296
297 #[test]
298 fn replace_state_from_raw_unknown() {
299 assert!(ReplaceState::from_raw(9999).is_none());
300 }
301
302 #[test]
305 fn replace_start_error_display() {
306 assert_eq!(
307 format!("{}", ReplaceStartError::AlreadyStarted),
308 "a device replace operation is already in progress"
309 );
310 assert_eq!(
311 format!("{}", ReplaceStartError::ScrubInProgress),
312 "a scrub is in progress; cancel it first"
313 );
314 }
315}