Skip to main content

btrfs_uapi/
replace.rs

1//! # Device replacement: replacing a device with another while the filesystem is online
2//!
3//! A replace operation copies all data from a source device to a target device,
4//! then swaps the target into the filesystem in place of the source. The
5//! filesystem remains mounted and usable throughout.
6//!
7//! Requires `CAP_SYS_ADMIN`.
8
9use 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, BTRFS_IOCTL_DEV_REPLACE_RESULT_NO_ERROR,
15    BTRFS_IOCTL_DEV_REPLACE_RESULT_NOT_STARTED, BTRFS_IOCTL_DEV_REPLACE_RESULT_SCRUB_INPROGRESS,
16    BTRFS_IOCTL_DEV_REPLACE_STATE_CANCELED, BTRFS_IOCTL_DEV_REPLACE_STATE_FINISHED,
17    BTRFS_IOCTL_DEV_REPLACE_STATE_NEVER_STARTED, BTRFS_IOCTL_DEV_REPLACE_STATE_STARTED,
18    BTRFS_IOCTL_DEV_REPLACE_STATE_SUSPENDED, btrfs_ioc_dev_replace, btrfs_ioctl_dev_replace_args,
19};
20use nix::errno::Errno;
21use std::{
22    ffi::CStr,
23    mem,
24    os::{fd::AsRawFd, unix::io::BorrowedFd},
25    time::{Duration, SystemTime, UNIX_EPOCH},
26};
27
28/// Current state of a device replace operation.
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum ReplaceState {
31    NeverStarted,
32    Started,
33    Finished,
34    Canceled,
35    Suspended,
36}
37
38impl ReplaceState {
39    fn from_raw(val: u64) -> Option<ReplaceState> {
40        match val {
41            x if x == BTRFS_IOCTL_DEV_REPLACE_STATE_NEVER_STARTED as u64 => {
42                Some(ReplaceState::NeverStarted)
43            }
44            x if x == BTRFS_IOCTL_DEV_REPLACE_STATE_STARTED as u64 => Some(ReplaceState::Started),
45            x if x == BTRFS_IOCTL_DEV_REPLACE_STATE_FINISHED as u64 => Some(ReplaceState::Finished),
46            x if x == BTRFS_IOCTL_DEV_REPLACE_STATE_CANCELED as u64 => Some(ReplaceState::Canceled),
47            x if x == BTRFS_IOCTL_DEV_REPLACE_STATE_SUSPENDED as u64 => {
48                Some(ReplaceState::Suspended)
49            }
50            _ => None,
51        }
52    }
53}
54
55/// Status of a device replace operation, as returned by the status query.
56#[derive(Debug, Clone)]
57pub struct ReplaceStatus {
58    /// Current state of the replace operation.
59    pub state: ReplaceState,
60    /// Progress in tenths of a percent (0..=1000).
61    pub progress_1000: u64,
62    /// Time the replace operation was started.
63    pub time_started: Option<SystemTime>,
64    /// Time the replace operation stopped (finished, canceled, or suspended).
65    pub time_stopped: Option<SystemTime>,
66    /// Number of write errors encountered during the replace.
67    pub num_write_errors: u64,
68    /// Number of uncorrectable read errors encountered during the replace.
69    pub num_uncorrectable_read_errors: u64,
70}
71
72fn epoch_to_systemtime(secs: u64) -> Option<SystemTime> {
73    if secs == 0 {
74        None
75    } else {
76        Some(UNIX_EPOCH + Duration::from_secs(secs))
77    }
78}
79
80/// How to identify the source device for a replace operation.
81pub enum ReplaceSource<'a> {
82    /// Source device identified by its btrfs device ID.
83    DevId(u64),
84    /// Source device identified by its block device path.
85    Path(&'a CStr),
86}
87
88/// Query the status of a device replace operation on the filesystem referred
89/// to by `fd`.
90pub fn replace_status(fd: BorrowedFd) -> nix::Result<ReplaceStatus> {
91    let mut args: btrfs_ioctl_dev_replace_args = unsafe { mem::zeroed() };
92    args.cmd = BTRFS_IOCTL_DEV_REPLACE_CMD_STATUS as u64;
93
94    unsafe { btrfs_ioc_dev_replace(fd.as_raw_fd(), &mut args) }?;
95
96    // SAFETY: we issued CMD_STATUS so the status union member is active.
97    let status = unsafe { &args.__bindgen_anon_1.status };
98    let state = ReplaceState::from_raw(status.replace_state).ok_or(Errno::EINVAL)?;
99
100    Ok(ReplaceStatus {
101        state,
102        progress_1000: status.progress_1000,
103        time_started: epoch_to_systemtime(status.time_started),
104        time_stopped: epoch_to_systemtime(status.time_stopped),
105        num_write_errors: status.num_write_errors,
106        num_uncorrectable_read_errors: status.num_uncorrectable_read_errors,
107    })
108}
109
110/// Result of a replace start attempt that the kernel rejected at the
111/// application level (ioctl succeeded but the `result` field indicates a
112/// problem).
113#[derive(Debug, Clone, Copy, PartialEq, Eq)]
114pub enum ReplaceStartError {
115    /// A replace operation is already in progress.
116    AlreadyStarted,
117    /// A scrub is in progress and must finish before replace can start.
118    ScrubInProgress,
119}
120
121impl std::fmt::Display for ReplaceStartError {
122    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
123        match self {
124            ReplaceStartError::AlreadyStarted => {
125                write!(f, "a device replace operation is already in progress")
126            }
127            ReplaceStartError::ScrubInProgress => {
128                write!(f, "a scrub is in progress; cancel it first")
129            }
130        }
131    }
132}
133
134impl std::error::Error for ReplaceStartError {}
135
136/// Start a device replace operation, copying all data from `source` to the
137/// target device at `tgtdev_path`.
138///
139/// When `avoid_srcdev` is true, the kernel will only read from the source
140/// device when no other zero-defect mirror is available (useful for replacing
141/// a device with known read errors).
142///
143/// On success returns `Ok(Ok(()))`. If the kernel returns an application-level
144/// error in the result field, returns `Ok(Err(ReplaceStartError))`.
145pub fn replace_start(
146    fd: BorrowedFd,
147    source: ReplaceSource,
148    tgtdev_path: &CStr,
149    avoid_srcdev: bool,
150) -> nix::Result<Result<(), ReplaceStartError>> {
151    let mut args: btrfs_ioctl_dev_replace_args = unsafe { mem::zeroed() };
152    args.cmd = BTRFS_IOCTL_DEV_REPLACE_CMD_START as u64;
153
154    // SAFETY: we are filling in the start union member before issuing CMD_START.
155    let start = unsafe { &mut args.__bindgen_anon_1.start };
156
157    match source {
158        ReplaceSource::DevId(devid) => {
159            start.srcdevid = devid;
160        }
161        ReplaceSource::Path(path) => {
162            start.srcdevid = 0;
163            let bytes = path.to_bytes();
164            if bytes.len() >= start.srcdev_name.len() {
165                return Err(Errno::ENAMETOOLONG);
166            }
167            start.srcdev_name[..bytes.len()].copy_from_slice(bytes);
168        }
169    }
170
171    let tgt_bytes = tgtdev_path.to_bytes();
172    if tgt_bytes.len() >= start.tgtdev_name.len() {
173        return Err(Errno::ENAMETOOLONG);
174    }
175    start.tgtdev_name[..tgt_bytes.len()].copy_from_slice(tgt_bytes);
176
177    start.cont_reading_from_srcdev_mode = if avoid_srcdev {
178        BTRFS_IOCTL_DEV_REPLACE_CONT_READING_FROM_SRCDEV_MODE_AVOID as u64
179    } else {
180        BTRFS_IOCTL_DEV_REPLACE_CONT_READING_FROM_SRCDEV_MODE_ALWAYS as u64
181    };
182
183    unsafe { btrfs_ioc_dev_replace(fd.as_raw_fd(), &mut args) }?;
184
185    match args.result {
186        x if x == BTRFS_IOCTL_DEV_REPLACE_RESULT_NO_ERROR as u64 => Ok(Ok(())),
187        x if x == BTRFS_IOCTL_DEV_REPLACE_RESULT_ALREADY_STARTED as u64 => {
188            Ok(Err(ReplaceStartError::AlreadyStarted))
189        }
190        x if x == BTRFS_IOCTL_DEV_REPLACE_RESULT_SCRUB_INPROGRESS as u64 => {
191            Ok(Err(ReplaceStartError::ScrubInProgress))
192        }
193        _ => Err(Errno::EINVAL),
194    }
195}
196
197/// Cancel a running device replace operation on the filesystem referred to
198/// by `fd`.
199///
200/// Returns `Ok(true)` if the replace was successfully cancelled, or
201/// `Ok(false)` if no replace operation was in progress.
202pub fn replace_cancel(fd: BorrowedFd) -> nix::Result<bool> {
203    let mut args: btrfs_ioctl_dev_replace_args = unsafe { mem::zeroed() };
204    args.cmd = BTRFS_IOCTL_DEV_REPLACE_CMD_CANCEL as u64;
205
206    unsafe { btrfs_ioc_dev_replace(fd.as_raw_fd(), &mut args) }?;
207
208    match args.result {
209        x if x == BTRFS_IOCTL_DEV_REPLACE_RESULT_NO_ERROR as u64 => Ok(true),
210        x if x == BTRFS_IOCTL_DEV_REPLACE_RESULT_NOT_STARTED as u64 => Ok(false),
211        _ => Err(Errno::EINVAL),
212    }
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218
219    // --- epoch_to_systemtime ---
220
221    #[test]
222    fn epoch_zero_is_none() {
223        assert!(epoch_to_systemtime(0).is_none());
224    }
225
226    #[test]
227    fn epoch_nonzero_is_some() {
228        let t = epoch_to_systemtime(1700000000).unwrap();
229        assert_eq!(t, UNIX_EPOCH + Duration::from_secs(1700000000));
230    }
231
232    // --- ReplaceState::from_raw ---
233
234    #[test]
235    fn replace_state_from_raw_all_variants() {
236        assert!(matches!(
237            ReplaceState::from_raw(BTRFS_IOCTL_DEV_REPLACE_STATE_NEVER_STARTED as u64),
238            Some(ReplaceState::NeverStarted)
239        ));
240        assert!(matches!(
241            ReplaceState::from_raw(BTRFS_IOCTL_DEV_REPLACE_STATE_STARTED as u64),
242            Some(ReplaceState::Started)
243        ));
244        assert!(matches!(
245            ReplaceState::from_raw(BTRFS_IOCTL_DEV_REPLACE_STATE_FINISHED as u64),
246            Some(ReplaceState::Finished)
247        ));
248        assert!(matches!(
249            ReplaceState::from_raw(BTRFS_IOCTL_DEV_REPLACE_STATE_CANCELED as u64),
250            Some(ReplaceState::Canceled)
251        ));
252        assert!(matches!(
253            ReplaceState::from_raw(BTRFS_IOCTL_DEV_REPLACE_STATE_SUSPENDED as u64),
254            Some(ReplaceState::Suspended)
255        ));
256    }
257
258    #[test]
259    fn replace_state_from_raw_unknown() {
260        assert!(ReplaceState::from_raw(9999).is_none());
261    }
262
263    // --- ReplaceStartError Display ---
264
265    #[test]
266    fn replace_start_error_display() {
267        assert_eq!(
268            format!("{}", ReplaceStartError::AlreadyStarted),
269            "a device replace operation is already in progress"
270        );
271        assert_eq!(
272            format!("{}", ReplaceStartError::ScrubInProgress),
273            "a scrub is in progress; cancel it first"
274        );
275    }
276}