rigtop_core/linux/
mdstat.rs1use std::fs;
15
16use super::SysRoots;
17use crate::snapshot::{MdArray, MdMember, MdSync};
18
19const SYNC_ACTIONS: &[&str] = &["resync", "recovery", "reshape", "check"];
21
22pub 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
29pub(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 if let Some(array) = arrays.last_mut() {
38 parse_detail(line, array);
39 }
40 }
41 }
42 arrays
43}
44
45fn 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 _ if array.level.is_empty() => array.level = token.to_owned(),
68 _ => {}
69 }
70 }
71 Some(array)
72}
73
74fn 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
86fn 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 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 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}