Skip to main content

rigtop_core/linux/
mdstat.rs

1//! Linux md (mdadm) arrays from `/proc/mdstat`.
2//!
3//! Example of the format:
4//!
5//! ```text
6//! Personalities : [raid1] [raid5]
7//! md1 : active raid5 sdc1[3](S) sdd1[2] sde1[1](F) sdf1[0]
8//!       1953260544 blocks super 1.2 level 5, 512k chunk, algorithm 2 [3/2] [U_U]
9//!       [=>...................]  recovery =  8.5% (83046144/976630272) finish=79.2min speed=187904K/sec
10//!
11//! unused devices: <none>
12//! ```
13
14use std::fs;
15
16use super::SysRoots;
17use crate::snapshot::{MdArray, MdMember, MdSync};
18
19/// Background operations that report progress.
20const SYNC_ACTIONS: &[&str] = &["resync", "recovery", "reshape", "check"];
21
22/// Reads `/proc/mdstat`. Returns an empty list when md is not loaded.
23pub fn read_arrays(roots: &SysRoots) -> Vec<MdArray> {
24    fs::read_to_string(roots.proc.join("mdstat"))
25        .map(|text| parse(&text))
26        .unwrap_or_default()
27}
28
29/// Parses `/proc/mdstat` text.
30pub(crate) fn parse(text: &str) -> Vec<MdArray> {
31    let mut arrays: Vec<MdArray> = Vec::new();
32    for line in text.lines() {
33        if let Some(array) = parse_header(line) {
34            arrays.push(array);
35        } else if line.starts_with(char::is_whitespace) {
36            // Indented lines continue the most recent array.
37            if let Some(array) = arrays.last_mut() {
38                parse_detail(line, array);
39            }
40        }
41    }
42    arrays
43}
44
45/// Parses `md1 : active raid5 sdc1[3](S) ...`. Returns `None` for other lines
46/// (`Personalities`, `unused devices`, details).
47fn parse_header(line: &str) -> Option<MdArray> {
48    let (name, rest) = line.split_once(" : ")?;
49    if !name.starts_with("md") {
50        return None;
51    }
52    let mut array = MdArray {
53        name: name.trim().to_owned(),
54        ..Default::default()
55    };
56    for token in rest.split_whitespace() {
57        match token {
58            "active" => array.active = true,
59            "inactive" => array.active = false,
60            "(read-only)" | "(auto-read-only)" => array.read_only = true,
61            _ if token.contains('[') => {
62                if let Some(member) = parse_member(token) {
63                    array.members.push(member);
64                }
65            }
66            // The only other bare word on an active array is the level.
67            _ if array.level.is_empty() => array.level = token.to_owned(),
68            _ => {}
69        }
70    }
71    Some(array)
72}
73
74/// Parses a member token such as `sde1[1](F)`.
75fn parse_member(token: &str) -> Option<MdMember> {
76    let (name, rest) = token.split_once('[')?;
77    let (role, flags) = rest.split_once(']')?;
78    Some(MdMember {
79        name: name.to_owned(),
80        role: role.parse().ok()?,
81        faulty: flags.contains("(F)"),
82        spare: flags.contains("(S)"),
83    })
84}
85
86/// Picks the device counts and sync progress out of a detail line.
87fn parse_detail(line: &str, array: &mut MdArray) {
88    let tokens: Vec<&str> = line.split_whitespace().collect();
89    for (i, token) in tokens.iter().enumerate() {
90        // "[3/2]": expected/working device counts.
91        if let Some(inner) = token.strip_prefix('[').and_then(|t| t.strip_suffix(']')) {
92            if let Some((expected, working)) = inner.split_once('/')
93                && let (Ok(e), Ok(w)) = (expected.parse(), working.parse())
94            {
95                array.expected_devices = Some(e);
96                array.working_devices = Some(w);
97            }
98            continue;
99        }
100        // "recovery = 8.5%": the action name, a lone "=", then the percentage.
101        if SYNC_ACTIONS.contains(token) && tokens.get(i + 1) == Some(&"=") {
102            let percent = tokens
103                .get(i + 2)
104                .and_then(|p| p.strip_suffix('%'))
105                .and_then(|p| p.parse().ok());
106            if let Some(percent) = percent {
107                array.sync = Some(MdSync {
108                    action: (*token).to_owned(),
109                    percent,
110                    ..Default::default()
111                });
112            }
113            continue;
114        }
115        if let Some(sync) = array.sync.as_mut() {
116            if let Some(minutes) = token
117                .strip_prefix("finish=")
118                .and_then(|v| v.strip_suffix("min"))
119            {
120                sync.finish_minutes = minutes.parse().ok();
121            } else if let Some(speed) = token
122                .strip_prefix("speed=")
123                .and_then(|v| v.strip_suffix("K/sec"))
124            {
125                sync.speed_kib_per_sec = speed.parse().ok();
126            }
127        }
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134
135    const SAMPLE: &str = "\
136Personalities : [raid1] [raid6] [raid5] [raid4]
137md127 : active (auto-read-only) raid1 sdb1[1] sda1[0]
138      976630464 blocks super 1.2 [2/2] [UU]
139      bitmap: 0/8 pages [0KB], 65536KB chunk
140
141md1 : active raid5 sdc1[3](S) sdd1[2] sde1[1](F) sdf1[0]
142      1953260544 blocks super 1.2 level 5, 512k chunk, algorithm 2 [3/2] [U_U]
143      [=>...................]  recovery =  8.5% (83046144/976630272) finish=79.2min speed=187904K/sec
144
145md2 : inactive sdg1[0](S)
146      976630464 blocks super 1.2
147
148unused devices: <none>
149";
150
151    #[test]
152    fn parses_healthy_degraded_and_inactive_arrays() {
153        let arrays = parse(SAMPLE);
154        assert_eq!(arrays.len(), 3);
155
156        let md127 = &arrays[0];
157        assert_eq!(md127.name, "md127");
158        assert!(md127.active && md127.read_only);
159        assert_eq!(md127.level, "raid1");
160        assert_eq!(md127.members.len(), 2);
161        assert_eq!(
162            (md127.expected_devices, md127.working_devices),
163            (Some(2), Some(2))
164        );
165        assert!(!md127.is_degraded());
166        assert!(md127.sync.is_none());
167
168        let md1 = &arrays[1];
169        assert_eq!(md1.level, "raid5");
170        assert!(md1.members.iter().any(|m| m.name == "sde1" && m.faulty));
171        assert!(
172            md1.members
173                .iter()
174                .any(|m| m.name == "sdc1" && m.spare && m.role == 3)
175        );
176        assert!(md1.is_degraded());
177        let sync = md1.sync.as_ref().unwrap();
178        assert_eq!(sync.action, "recovery");
179        assert!((sync.percent - 8.5).abs() < 1e-6);
180        assert_eq!(sync.finish_minutes, Some(79.2));
181        assert_eq!(sync.speed_kib_per_sec, Some(187904));
182
183        let md2 = &arrays[2];
184        assert!(!md2.active);
185        assert!(md2.level.is_empty());
186        assert_eq!(md2.members.len(), 1);
187    }
188
189    #[test]
190    fn empty_mdstat_has_no_arrays() {
191        assert!(parse("Personalities : \nunused devices: <none>\n").is_empty());
192    }
193}