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 == BTRFS_IOCTL_DEV_REPLACE_STATE_NEVER_STARTED as u64 => {
47 Some(ReplaceState::NeverStarted)
48 }
49 x if x == BTRFS_IOCTL_DEV_REPLACE_STATE_STARTED as u64 => {
50 Some(ReplaceState::Started)
51 }
52 x if x == BTRFS_IOCTL_DEV_REPLACE_STATE_FINISHED as u64 => {
53 Some(ReplaceState::Finished)
54 }
55 x if x == BTRFS_IOCTL_DEV_REPLACE_STATE_CANCELED as u64 => {
56 Some(ReplaceState::Canceled)
57 }
58 x if x == BTRFS_IOCTL_DEV_REPLACE_STATE_SUSPENDED as u64 => {
59 Some(ReplaceState::Suspended)
60 }
61 _ => None,
62 }
63 }
64}
65
66#[derive(Debug, Clone)]
68pub struct ReplaceStatus {
69 pub state: ReplaceState,
71 pub progress_1000: u64,
73 pub time_started: Option<SystemTime>,
75 pub time_stopped: Option<SystemTime>,
77 pub num_write_errors: u64,
79 pub num_uncorrectable_read_errors: u64,
81}
82
83fn epoch_to_systemtime(secs: u64) -> Option<SystemTime> {
84 if secs == 0 {
85 None
86 } else {
87 Some(UNIX_EPOCH + Duration::from_secs(secs))
88 }
89}
90
91pub enum ReplaceSource<'a> {
93 DevId(u64),
95 Path(&'a CStr),
97}
98
99pub fn replace_status(fd: BorrowedFd) -> nix::Result<ReplaceStatus> {
102 let mut args: btrfs_ioctl_dev_replace_args = unsafe { mem::zeroed() };
103 args.cmd = BTRFS_IOCTL_DEV_REPLACE_CMD_STATUS as u64;
104
105 unsafe { btrfs_ioc_dev_replace(fd.as_raw_fd(), &mut args) }?;
106
107 let status = unsafe { &args.__bindgen_anon_1.status };
109 let state =
110 ReplaceState::from_raw(status.replace_state).ok_or(Errno::EINVAL)?;
111
112 Ok(ReplaceStatus {
113 state,
114 progress_1000: status.progress_1000,
115 time_started: epoch_to_systemtime(status.time_started),
116 time_stopped: epoch_to_systemtime(status.time_stopped),
117 num_write_errors: status.num_write_errors,
118 num_uncorrectable_read_errors: status.num_uncorrectable_read_errors,
119 })
120}
121
122#[derive(Debug, Clone, Copy, PartialEq, Eq)]
126pub enum ReplaceStartError {
127 AlreadyStarted,
129 ScrubInProgress,
131}
132
133impl std::fmt::Display for ReplaceStartError {
134 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
135 match self {
136 ReplaceStartError::AlreadyStarted => {
137 write!(f, "a device replace operation is already in progress")
138 }
139 ReplaceStartError::ScrubInProgress => {
140 write!(f, "a scrub is in progress; cancel it first")
141 }
142 }
143 }
144}
145
146impl std::error::Error for ReplaceStartError {}
147
148pub fn replace_start(
158 fd: BorrowedFd,
159 source: ReplaceSource,
160 tgtdev_path: &CStr,
161 avoid_srcdev: bool,
162) -> nix::Result<Result<(), ReplaceStartError>> {
163 let mut args: btrfs_ioctl_dev_replace_args = unsafe { mem::zeroed() };
164 args.cmd = BTRFS_IOCTL_DEV_REPLACE_CMD_START as u64;
165
166 let start = unsafe { &mut args.__bindgen_anon_1.start };
168
169 match source {
170 ReplaceSource::DevId(devid) => {
171 start.srcdevid = devid;
172 }
173 ReplaceSource::Path(path) => {
174 start.srcdevid = 0;
175 let bytes = path.to_bytes();
176 if bytes.len() >= start.srcdev_name.len() {
177 return Err(Errno::ENAMETOOLONG);
178 }
179 start.srcdev_name[..bytes.len()].copy_from_slice(bytes);
180 }
181 }
182
183 let tgt_bytes = tgtdev_path.to_bytes();
184 if tgt_bytes.len() >= start.tgtdev_name.len() {
185 return Err(Errno::ENAMETOOLONG);
186 }
187 start.tgtdev_name[..tgt_bytes.len()].copy_from_slice(tgt_bytes);
188
189 start.cont_reading_from_srcdev_mode = if avoid_srcdev {
190 BTRFS_IOCTL_DEV_REPLACE_CONT_READING_FROM_SRCDEV_MODE_AVOID as u64
191 } else {
192 BTRFS_IOCTL_DEV_REPLACE_CONT_READING_FROM_SRCDEV_MODE_ALWAYS as u64
193 };
194
195 unsafe { btrfs_ioc_dev_replace(fd.as_raw_fd(), &mut args) }?;
196
197 match args.result {
198 x if x == BTRFS_IOCTL_DEV_REPLACE_RESULT_NO_ERROR as u64 => Ok(Ok(())),
199 x if x == BTRFS_IOCTL_DEV_REPLACE_RESULT_ALREADY_STARTED as u64 => {
200 Ok(Err(ReplaceStartError::AlreadyStarted))
201 }
202 x if x == BTRFS_IOCTL_DEV_REPLACE_RESULT_SCRUB_INPROGRESS as u64 => {
203 Ok(Err(ReplaceStartError::ScrubInProgress))
204 }
205 _ => Err(Errno::EINVAL),
206 }
207}
208
209pub fn replace_cancel(fd: BorrowedFd) -> nix::Result<bool> {
215 let mut args: btrfs_ioctl_dev_replace_args = unsafe { mem::zeroed() };
216 args.cmd = BTRFS_IOCTL_DEV_REPLACE_CMD_CANCEL as u64;
217
218 unsafe { btrfs_ioc_dev_replace(fd.as_raw_fd(), &mut args) }?;
219
220 match args.result {
221 x if x == BTRFS_IOCTL_DEV_REPLACE_RESULT_NO_ERROR as u64 => Ok(true),
222 x if x == BTRFS_IOCTL_DEV_REPLACE_RESULT_NOT_STARTED as u64 => {
223 Ok(false)
224 }
225 _ => Err(Errno::EINVAL),
226 }
227}
228
229#[cfg(test)]
230mod tests {
231 use super::*;
232
233 #[test]
236 fn epoch_zero_is_none() {
237 assert!(epoch_to_systemtime(0).is_none());
238 }
239
240 #[test]
241 fn epoch_nonzero_is_some() {
242 let t = epoch_to_systemtime(1700000000).unwrap();
243 assert_eq!(t, UNIX_EPOCH + Duration::from_secs(1700000000));
244 }
245
246 #[test]
249 fn replace_state_from_raw_all_variants() {
250 assert!(matches!(
251 ReplaceState::from_raw(
252 BTRFS_IOCTL_DEV_REPLACE_STATE_NEVER_STARTED as u64
253 ),
254 Some(ReplaceState::NeverStarted)
255 ));
256 assert!(matches!(
257 ReplaceState::from_raw(
258 BTRFS_IOCTL_DEV_REPLACE_STATE_STARTED as u64
259 ),
260 Some(ReplaceState::Started)
261 ));
262 assert!(matches!(
263 ReplaceState::from_raw(
264 BTRFS_IOCTL_DEV_REPLACE_STATE_FINISHED as u64
265 ),
266 Some(ReplaceState::Finished)
267 ));
268 assert!(matches!(
269 ReplaceState::from_raw(
270 BTRFS_IOCTL_DEV_REPLACE_STATE_CANCELED as u64
271 ),
272 Some(ReplaceState::Canceled)
273 ));
274 assert!(matches!(
275 ReplaceState::from_raw(
276 BTRFS_IOCTL_DEV_REPLACE_STATE_SUSPENDED as u64
277 ),
278 Some(ReplaceState::Suspended)
279 ));
280 }
281
282 #[test]
283 fn replace_state_from_raw_unknown() {
284 assert!(ReplaceState::from_raw(9999).is_none());
285 }
286
287 #[test]
290 fn replace_start_error_display() {
291 assert_eq!(
292 format!("{}", ReplaceStartError::AlreadyStarted),
293 "a device replace operation is already in progress"
294 );
295 assert_eq!(
296 format!("{}", ReplaceStartError::ScrubInProgress),
297 "a scrub is in progress; cancel it first"
298 );
299 }
300}