]>
Commit | Line | Data |
---|---|---|
eddd1a1b DM |
1 | use std::mem::replace; |
2 | ||
f28cfb32 | 3 | use anyhow::{bail, Error}; |
4e37d9ce | 4 | use serde::{Deserialize, Serialize}; |
eddd1a1b | 5 | use serde_json::{Map, Value}; |
0727e56a | 6 | |
177a2de9 | 7 | use crate::tools::nom::{ |
3352ee56 DM |
8 | parse_complete, parse_error, parse_failure, |
9 | multispace0, multispace1, notspace1, parse_u64, IResult, | |
177a2de9 DM |
10 | }; |
11 | ||
0727e56a | 12 | use nom::{ |
0727e56a | 13 | bytes::complete::{tag, take_while, take_while1}, |
177a2de9 | 14 | combinator::{opt}, |
0727e56a | 15 | sequence::{preceded}, |
177a2de9 DM |
16 | character::complete::{line_ending}, |
17 | multi::{many0,many1}, | |
0727e56a DM |
18 | }; |
19 | ||
0727e56a DM |
20 | |
21 | #[derive(Debug, Serialize, Deserialize)] | |
22 | pub struct ZFSPoolVDevState { | |
23 | pub name: String, | |
24 | pub lvl: u64, | |
9438aca6 DM |
25 | #[serde(skip_serializing_if="Option::is_none")] |
26 | pub state: Option<String>, | |
27 | #[serde(skip_serializing_if="Option::is_none")] | |
28 | pub read: Option<u64>, | |
29 | #[serde(skip_serializing_if="Option::is_none")] | |
30 | pub write: Option<u64>, | |
31 | #[serde(skip_serializing_if="Option::is_none")] | |
32 | pub cksum: Option<u64>, | |
0727e56a DM |
33 | #[serde(skip_serializing_if="Option::is_none")] |
34 | pub msg: Option<String>, | |
35 | } | |
36 | ||
b29cbc41 DM |
37 | fn expand_tab_length(input: &str) -> usize { |
38 | input.chars().map(|c| if c == '\t' { 8 } else { 1 }).sum() | |
39 | } | |
40 | ||
0727e56a DM |
41 | fn parse_zpool_status_vdev(i: &str) -> IResult<&str, ZFSPoolVDevState> { |
42 | ||
177a2de9 | 43 | let (n, indent) = multispace0(i)?; |
b29cbc41 DM |
44 | |
45 | let indent_len = expand_tab_length(indent); | |
46 | ||
47 | if (indent_len & 1) != 0 { | |
177a2de9 DM |
48 | return Err(parse_failure(n, "wrong indent length")); |
49 | } | |
50 | let i = n; | |
51 | ||
b29cbc41 DM |
52 | let indent_level = (indent_len as u64)/2; |
53 | ||
0727e56a | 54 | let (i, vdev_name) = notspace1(i)?; |
9438aca6 | 55 | |
1ffe0301 | 56 | if let Ok((n, _)) = preceded(multispace0, line_ending)(i) { // special device |
9438aca6 DM |
57 | let vdev = ZFSPoolVDevState { |
58 | name: vdev_name.to_string(), | |
b29cbc41 | 59 | lvl: indent_level, |
9438aca6 DM |
60 | state: None, |
61 | read: None, | |
62 | write: None, | |
63 | cksum: None, | |
64 | msg: None, | |
65 | }; | |
66 | return Ok((n, vdev)); | |
67 | } | |
68 | ||
0727e56a | 69 | let (i, state) = preceded(multispace1, notspace1)(i)?; |
934deeff DC |
70 | if let Ok((n, _)) = preceded(multispace0, line_ending)(i) { // spares |
71 | let vdev = ZFSPoolVDevState { | |
72 | name: vdev_name.to_string(), | |
73 | lvl: indent_level, | |
74 | state: Some(state.to_string()), | |
75 | read: None, | |
76 | write: None, | |
77 | cksum: None, | |
78 | msg: None, | |
79 | }; | |
80 | return Ok((n, vdev)); | |
81 | } | |
82 | ||
0727e56a DM |
83 | let (i, read) = preceded(multispace1, parse_u64)(i)?; |
84 | let (i, write) = preceded(multispace1, parse_u64)(i)?; | |
85 | let (i, cksum) = preceded(multispace1, parse_u64)(i)?; | |
86 | let (i, msg) = opt(preceded(multispace1, take_while(|c| c != '\n')))(i)?; | |
87 | let (i, _) = line_ending(i)?; | |
88 | ||
89 | let vdev = ZFSPoolVDevState { | |
90 | name: vdev_name.to_string(), | |
b29cbc41 | 91 | lvl: indent_level, |
9438aca6 DM |
92 | state: Some(state.to_string()), |
93 | read: Some(read), | |
94 | write: Some(write), | |
95 | cksum: Some(cksum), | |
0727e56a DM |
96 | msg: msg.map(String::from), |
97 | }; | |
98 | ||
99 | Ok((i, vdev)) | |
100 | } | |
101 | ||
102 | fn parse_zpool_status_tree(i: &str) -> IResult<&str, Vec<ZFSPoolVDevState>> { | |
103 | ||
104 | // skip header | |
105 | let (i, _) = tag("NAME")(i)?; | |
106 | let (i, _) = multispace1(i)?; | |
107 | let (i, _) = tag("STATE")(i)?; | |
108 | let (i, _) = multispace1(i)?; | |
109 | let (i, _) = tag("READ")(i)?; | |
110 | let (i, _) = multispace1(i)?; | |
111 | let (i, _) = tag("WRITE")(i)?; | |
112 | let (i, _) = multispace1(i)?; | |
113 | let (i, _) = tag("CKSUM")(i)?; | |
114 | let (i, _) = line_ending(i)?; | |
115 | ||
116 | // parse vdev list | |
177a2de9 | 117 | many1(parse_zpool_status_vdev)(i) |
0727e56a DM |
118 | } |
119 | ||
3352ee56 DM |
120 | fn space_indented_line(indent: usize) -> impl Fn(&str) -> IResult<&str, &str> { |
121 | move |i| { | |
122 | let mut len = 0; | |
123 | let mut n = i; | |
124 | loop { | |
125 | if n.starts_with('\t') { | |
126 | len += 8; | |
127 | n = &n[1..]; | |
128 | } else if n.starts_with(' ') { | |
129 | len += 1; | |
130 | n = &n[1..]; | |
131 | } else { | |
132 | break; | |
133 | } | |
134 | if len >= indent { break; } | |
135 | }; | |
136 | if len != indent { | |
137 | return Err(parse_error(i, "not correctly indented")); | |
138 | } | |
139 | ||
140 | take_while1(|c| c != '\n')(n) | |
141 | } | |
142 | } | |
143 | ||
0727e56a DM |
144 | fn parse_zpool_status_field(i: &str) -> IResult<&str, (String, String)> { |
145 | let (i, prefix) = take_while1(|c| c != ':')(i)?; | |
146 | let (i, _) = tag(":")(i)?; | |
147 | let (i, mut value) = take_while(|c| c != '\n')(i)?; | |
148 | if value.starts_with(' ') { value = &value[1..]; } | |
149 | ||
150 | let (mut i, _) = line_ending(i)?; | |
151 | ||
152 | let field = prefix.trim().to_string(); | |
153 | ||
3352ee56 DM |
154 | let prefix_len = expand_tab_length(prefix); |
155 | ||
156 | let indent: usize = prefix_len + 2; | |
0727e56a | 157 | |
3352ee56 | 158 | let parse_continuation = opt(space_indented_line(indent)); |
0727e56a DM |
159 | |
160 | let mut value = value.to_string(); | |
161 | ||
162 | if field == "config" { | |
163 | let (n, _) = line_ending(i)?; | |
164 | i = n; | |
165 | } | |
166 | ||
167 | loop { | |
168 | let (n, cont) = parse_continuation(i)?; | |
169 | ||
170 | if let Some(cont) = cont { | |
171 | let (n, _) = line_ending(n)?; | |
172 | i = n; | |
173 | if !value.is_empty() { value.push('\n'); } | |
174 | value.push_str(cont); | |
175 | } else { | |
176 | if field == "config" { | |
177 | let (n, _) = line_ending(i)?; | |
178 | value.push('\n'); | |
179 | i = n; | |
180 | } | |
181 | break; | |
182 | } | |
183 | } | |
184 | ||
185 | Ok((i, (field, value))) | |
186 | } | |
187 | ||
188 | pub fn parse_zpool_status_config_tree(i: &str) -> Result<Vec<ZFSPoolVDevState>, Error> { | |
177a2de9 | 189 | parse_complete("zfs status config tree", i, parse_zpool_status_tree) |
0727e56a DM |
190 | } |
191 | ||
9438aca6 | 192 | fn parse_zpool_status(input: &str) -> Result<Vec<(String, String)>, Error> { |
9438aca6 | 193 | parse_complete("zfs status output", &input, many0(parse_zpool_status_field)) |
0727e56a DM |
194 | } |
195 | ||
4e37d9ce | 196 | pub fn vdev_list_to_tree(vdev_list: &[ZFSPoolVDevState]) -> Result<Value, Error> { |
07fb5049 DM |
197 | indented_list_to_tree(vdev_list, |vdev| { |
198 | let node = serde_json::to_value(vdev).unwrap(); | |
199 | (node, vdev.lvl) | |
4e37d9ce WB |
200 | }) |
201 | } | |
0727e56a | 202 | |
4e37d9ce WB |
203 | fn indented_list_to_tree<'a, T, F, I>(items: I, to_node: F) -> Result<Value, Error> |
204 | where | |
205 | T: 'a, | |
206 | I: IntoIterator<Item = &'a T>, | |
07fb5049 | 207 | F: Fn(&T) -> (Value, u64), |
4e37d9ce | 208 | { |
1cb46c6f DM |
209 | struct StackItem { |
210 | node: Map<String, Value>, | |
211 | level: u64, | |
212 | children_of_parent: Vec<Value>, | |
213 | } | |
214 | ||
215 | let mut stack = Vec::<StackItem>::new(); | |
4e37d9ce | 216 | // hold current node and the children of the current parent (as that's where we insert) |
1cb46c6f DM |
217 | let mut cur = StackItem { |
218 | node: Map::<String, Value>::new(), | |
219 | level: 0, | |
220 | children_of_parent: Vec::new(), | |
221 | }; | |
4e37d9ce | 222 | |
4e37d9ce | 223 | for item in items { |
07fb5049 DM |
224 | let (node, node_level) = to_node(&item); |
225 | let vdev_level = 1 + node_level; | |
226 | let mut node = match node { | |
227 | Value::Object(map) => map, | |
228 | _ => bail!("to_node returned wrong type"), | |
229 | }; | |
230 | ||
4e37d9ce WB |
231 | node.insert("leaf".to_string(), Value::Bool(true)); |
232 | ||
233 | // if required, go back up (possibly multiple levels): | |
1cb46c6f DM |
234 | while vdev_level < cur.level { |
235 | cur.children_of_parent.push(Value::Object(cur.node)); | |
215968e0 DM |
236 | let mut parent = stack.pop().unwrap(); |
237 | parent.node.insert("children".to_string(), Value::Array(cur.children_of_parent)); | |
238 | parent.node.insert("leaf".to_string(), Value::Bool(false)); | |
239 | cur = parent; | |
4e37d9ce | 240 | |
1cb46c6f | 241 | if vdev_level > cur.level { |
4e37d9ce WB |
242 | // when we encounter misimatching levels like "0, 2, 1" instead of "0, 1, 2, 1" |
243 | bail!("broken indentation between levels"); | |
0727e56a DM |
244 | } |
245 | } | |
0727e56a | 246 | |
1cb46c6f | 247 | if vdev_level > cur.level { |
4e37d9ce | 248 | // indented further, push our current state and start a new "map" |
1cb46c6f DM |
249 | stack.push(StackItem { |
250 | node: replace(&mut cur.node, node), | |
251 | level: replace(&mut cur.level, vdev_level), | |
252 | children_of_parent: replace(&mut cur.children_of_parent, Vec::new()), | |
d2ce2118 | 253 | }); |
0727e56a | 254 | } else { |
4e37d9ce | 255 | // same indentation level, add to children of the previous level: |
1cb46c6f DM |
256 | cur.children_of_parent.push(Value::Object( |
257 | replace(&mut cur.node, node), | |
4e37d9ce | 258 | )); |
0727e56a | 259 | } |
4e37d9ce | 260 | } |
0727e56a | 261 | |
4e37d9ce | 262 | while !stack.is_empty() { |
1cb46c6f | 263 | cur.children_of_parent.push(Value::Object(cur.node)); |
215968e0 DM |
264 | let mut parent = stack.pop().unwrap(); |
265 | parent.node.insert("children".to_string(), Value::Array(cur.children_of_parent)); | |
266 | parent.node.insert("leaf".to_string(), Value::Bool(false)); | |
267 | cur = parent; | |
0727e56a DM |
268 | } |
269 | ||
1cb46c6f | 270 | Ok(Value::Object(cur.node)) |
4e37d9ce | 271 | } |
0727e56a | 272 | |
4e37d9ce WB |
273 | #[test] |
274 | fn test_vdev_list_to_tree() { | |
275 | const DEFAULT: ZFSPoolVDevState = ZFSPoolVDevState { | |
276 | name: String::new(), | |
277 | lvl: 0, | |
278 | state: None, | |
279 | read: None, | |
280 | write: None, | |
281 | cksum: None, | |
282 | msg: None, | |
283 | }; | |
0727e56a | 284 | |
4e37d9ce WB |
285 | let input = vec![ |
286 | //ZFSPoolVDevState { name: "root".to_string(), lvl: 0, ..DEFAULT }, | |
287 | ZFSPoolVDevState { name: "vdev1".to_string(), lvl: 1, ..DEFAULT }, | |
288 | ZFSPoolVDevState { name: "vdev1-disk1".to_string(), lvl: 2, ..DEFAULT }, | |
289 | ZFSPoolVDevState { name: "vdev1-disk2".to_string(), lvl: 2, ..DEFAULT }, | |
290 | ZFSPoolVDevState { name: "vdev2".to_string(), lvl: 1, ..DEFAULT }, | |
291 | ZFSPoolVDevState { name: "vdev2-g1".to_string(), lvl: 2, ..DEFAULT }, | |
292 | ZFSPoolVDevState { name: "vdev2-g1-d1".to_string(), lvl: 3, ..DEFAULT }, | |
293 | ZFSPoolVDevState { name: "vdev2-g1-d2".to_string(), lvl: 3, ..DEFAULT }, | |
294 | ZFSPoolVDevState { name: "vdev2-g2".to_string(), lvl: 2, ..DEFAULT }, | |
295 | ZFSPoolVDevState { name: "vdev3".to_string(), lvl: 1, ..DEFAULT }, | |
296 | ZFSPoolVDevState { name: "vdev4".to_string(), lvl: 1, ..DEFAULT }, | |
297 | ZFSPoolVDevState { name: "vdev4-g1".to_string(), lvl: 2, ..DEFAULT }, | |
298 | ZFSPoolVDevState { name: "vdev4-g1-d1".to_string(), lvl: 3, ..DEFAULT }, | |
299 | ZFSPoolVDevState { name: "vdev4-g1-d1-x1".to_string(), lvl: 4, ..DEFAULT }, | |
300 | ZFSPoolVDevState { name: "vdev4-g2".to_string(), lvl: 2, ..DEFAULT }, // up by 2 | |
301 | ]; | |
302 | ||
303 | const EXPECTED: &str = "{\ | |
304 | \"children\":[{\ | |
305 | \"children\":[{\ | |
306 | \"leaf\":true,\ | |
307 | \"lvl\":2,\"name\":\"vdev1-disk1\"\ | |
308 | },{\ | |
309 | \"leaf\":true,\ | |
310 | \"lvl\":2,\"name\":\"vdev1-disk2\"\ | |
311 | }],\ | |
312 | \"leaf\":false,\ | |
313 | \"lvl\":1,\"name\":\"vdev1\"\ | |
314 | },{\ | |
315 | \"children\":[{\ | |
316 | \"children\":[{\ | |
317 | \"leaf\":true,\ | |
318 | \"lvl\":3,\"name\":\"vdev2-g1-d1\"\ | |
319 | },{\ | |
320 | \"leaf\":true,\ | |
321 | \"lvl\":3,\"name\":\"vdev2-g1-d2\"\ | |
322 | }],\ | |
323 | \"leaf\":false,\ | |
324 | \"lvl\":2,\"name\":\"vdev2-g1\"\ | |
325 | },{\ | |
326 | \"leaf\":true,\ | |
327 | \"lvl\":2,\"name\":\"vdev2-g2\"\ | |
328 | }],\ | |
329 | \"leaf\":false,\ | |
330 | \"lvl\":1,\"name\":\"vdev2\"\ | |
331 | },{\ | |
332 | \"leaf\":true,\ | |
333 | \"lvl\":1,\"name\":\"vdev3\"\ | |
334 | },{\ | |
335 | \"children\":[{\ | |
336 | \"children\":[{\ | |
337 | \"children\":[{\ | |
338 | \"leaf\":true,\ | |
339 | \"lvl\":4,\"name\":\"vdev4-g1-d1-x1\"\ | |
340 | }],\ | |
341 | \"leaf\":false,\ | |
342 | \"lvl\":3,\"name\":\"vdev4-g1-d1\"\ | |
343 | }],\ | |
344 | \"leaf\":false,\ | |
345 | \"lvl\":2,\"name\":\"vdev4-g1\"\ | |
346 | },{\ | |
347 | \"leaf\":true,\ | |
348 | \"lvl\":2,\"name\":\"vdev4-g2\"\ | |
349 | }],\ | |
350 | \"leaf\":false,\ | |
351 | \"lvl\":1,\"name\":\"vdev4\"\ | |
352 | }],\ | |
215968e0 DM |
353 | \"leaf\":false\ |
354 | }"; | |
4e37d9ce WB |
355 | let expected: Value = serde_json::from_str(EXPECTED) |
356 | .expect("failed to parse expected json value"); | |
357 | ||
358 | let tree = vdev_list_to_tree(&input) | |
359 | .expect("failed to turn valid vdev list into a tree"); | |
360 | assert_eq!(tree, expected); | |
0727e56a DM |
361 | } |
362 | ||
363 | pub fn zpool_status(pool: &str) -> Result<Vec<(String, String)>, Error> { | |
364 | ||
365 | let mut command = std::process::Command::new("zpool"); | |
366 | command.args(&["status", "-p", "-P", pool]); | |
367 | ||
368 | let output = crate::tools::run_command(command, None)?; | |
369 | ||
370 | parse_zpool_status(&output) | |
371 | } | |
372 | ||
373 | #[test] | |
374 | fn test_zpool_status_parser() -> Result<(), Error> { | |
375 | ||
376 | let output = r###" pool: tank | |
377 | state: DEGRADED | |
378 | status: One or more devices could not be opened. Sufficient replicas exist for | |
379 | the pool to continue functioning in a degraded state. | |
380 | action: Attach the missing device and online it using 'zpool online'. | |
381 | see: http://www.sun.com/msg/ZFS-8000-2Q | |
382 | scrub: none requested | |
383 | config: | |
384 | ||
385 | NAME STATE READ WRITE CKSUM | |
386 | tank DEGRADED 0 0 0 | |
9438aca6 | 387 | mirror-0 DEGRADED 0 0 0 |
0727e56a DM |
388 | c1t0d0 ONLINE 0 0 0 |
389 | c1t2d0 ONLINE 0 0 0 | |
390 | c1t1d0 UNAVAIL 0 0 0 cannot open | |
391 | mirror-1 DEGRADED 0 0 0 | |
392 | tank1 DEGRADED 0 0 0 | |
393 | tank2 DEGRADED 0 0 0 | |
394 | ||
395 | errors: No known data errors | |
396 | "###; | |
397 | ||
398 | let key_value_list = parse_zpool_status(&output)?; | |
399 | for (k, v) in key_value_list { | |
400 | println!("{} => {}", k,v); | |
401 | if k == "config" { | |
402 | let vdev_list = parse_zpool_status_config_tree(&v)?; | |
9438aca6 DM |
403 | let _tree = vdev_list_to_tree(&vdev_list); |
404 | //println!("TREE1 {}", serde_json::to_string_pretty(&tree)?); | |
405 | } | |
406 | } | |
407 | ||
408 | Ok(()) | |
409 | } | |
410 | ||
411 | #[test] | |
412 | fn test_zpool_status_parser2() -> Result<(), Error> { | |
413 | ||
414 | // Note: this input create TABS | |
415 | let output = r###" pool: btest | |
416 | state: ONLINE | |
417 | scan: none requested | |
418 | config: | |
419 | ||
420 | NAME STATE READ WRITE CKSUM | |
421 | btest ONLINE 0 0 0 | |
422 | mirror-0 ONLINE 0 0 0 | |
423 | /dev/sda1 ONLINE 0 0 0 | |
424 | /dev/sda2 ONLINE 0 0 0 | |
425 | mirror-1 ONLINE 0 0 0 | |
426 | /dev/sda3 ONLINE 0 0 0 | |
427 | /dev/sda4 ONLINE 0 0 0 | |
428 | logs | |
429 | /dev/sda5 ONLINE 0 0 0 | |
430 | ||
431 | errors: No known data errors | |
432 | "###; | |
433 | ||
434 | let key_value_list = parse_zpool_status(&output)?; | |
435 | for (k, v) in key_value_list { | |
436 | println!("{} => {}", k,v); | |
437 | if k == "config" { | |
438 | let vdev_list = parse_zpool_status_config_tree(&v)?; | |
439 | let _tree = vdev_list_to_tree(&vdev_list); | |
440 | //println!("TREE1 {}", serde_json::to_string_pretty(&tree)?); | |
0727e56a DM |
441 | } |
442 | } | |
443 | ||
444 | Ok(()) | |
445 | } | |
a9649ddc DC |
446 | |
447 | #[test] | |
448 | fn test_zpool_status_parser3() -> Result<(), Error> { | |
449 | ||
450 | let output = r###" pool: bt-est | |
451 | state: ONLINE | |
452 | scan: none requested | |
453 | config: | |
454 | ||
455 | NAME STATE READ WRITE CKSUM | |
456 | bt-est ONLINE 0 0 0 | |
457 | mirror-0 ONLINE 0 0 0 | |
458 | /dev/sda1 ONLINE 0 0 0 | |
459 | /dev/sda2 ONLINE 0 0 0 | |
460 | mirror-1 ONLINE 0 0 0 | |
461 | /dev/sda3 ONLINE 0 0 0 | |
462 | /dev/sda4 ONLINE 0 0 0 | |
463 | logs | |
464 | /dev/sda5 ONLINE 0 0 0 | |
465 | ||
466 | errors: No known data errors | |
467 | "###; | |
468 | ||
469 | let key_value_list = parse_zpool_status(&output)?; | |
470 | for (k, v) in key_value_list { | |
471 | println!("{} => {}", k,v); | |
472 | if k == "config" { | |
473 | let vdev_list = parse_zpool_status_config_tree(&v)?; | |
474 | let _tree = vdev_list_to_tree(&vdev_list); | |
475 | //println!("TREE1 {}", serde_json::to_string_pretty(&tree)?); | |
476 | } | |
477 | } | |
478 | ||
479 | Ok(()) | |
480 | } | |
c162df60 DC |
481 | |
482 | #[test] | |
483 | fn test_zpool_status_parser_spares() -> Result<(), Error> { | |
484 | ||
485 | let output = r###" pool: tank | |
486 | state: ONLINE | |
487 | scan: none requested | |
488 | config: | |
489 | ||
490 | NAME STATE READ WRITE CKSUM | |
491 | tank ONLINE 0 0 0 | |
492 | mirror-0 ONLINE 0 0 0 | |
493 | /dev/sda1 ONLINE 0 0 0 | |
494 | /dev/sda2 ONLINE 0 0 0 | |
495 | mirror-1 ONLINE 0 0 0 | |
496 | /dev/sda3 ONLINE 0 0 0 | |
497 | /dev/sda4 ONLINE 0 0 0 | |
498 | logs | |
499 | /dev/sda5 ONLINE 0 0 0 | |
500 | spares | |
501 | /dev/sdb AVAIL | |
502 | /dev/sdc AVAIL | |
503 | ||
504 | errors: No known data errors | |
505 | "###; | |
506 | ||
507 | let key_value_list = parse_zpool_status(&output)?; | |
508 | for (k, v) in key_value_list { | |
509 | println!("{} => {}", k,v); | |
510 | if k == "config" { | |
511 | let vdev_list = parse_zpool_status_config_tree(&v)?; | |
512 | let _tree = vdev_list_to_tree(&vdev_list); | |
513 | } | |
514 | } | |
515 | ||
516 | Ok(()) | |
517 | } |