diff --git a/src/tools/disks.rs b/src/tools/disks.rs index 07ad1341..134ac85d 100644 --- a/src/tools/disks.rs +++ b/src/tools/disks.rs @@ -23,6 +23,8 @@ use crate::api2::types::{BLOCKDEVICE_NAME_REGEX, StorageStatus}; mod zfs; pub use zfs::*; +mod zpool_status; +pub use zpool_status::*; mod lvm; pub use lvm::*; mod smart; diff --git a/src/tools/disks/zpool_status.rs b/src/tools/disks/zpool_status.rs new file mode 100644 index 00000000..72995228 --- /dev/null +++ b/src/tools/disks/zpool_status.rs @@ -0,0 +1,290 @@ +use anyhow::{bail, Error}; +use serde_json::{json, Value}; +use ::serde::{Deserialize, Serialize}; + +use nom::{ + error::VerboseError, + bytes::complete::{tag, take_while, take_while1}, + combinator::{map_res, all_consuming, recognize, opt}, + sequence::{preceded}, + character::complete::{digit1, line_ending}, + multi::{many0}, +}; + +type IResult> = Result<(I, O), nom::Err>; + +/// Recognizes zero or more spaces and tabs (but not carage returns or line feeds) +fn multispace0(i: &str) -> IResult<&str, &str> { + take_while(|c| c == ' ' || c == '\t')(i) +} + +// Recognizes one or more spaces and tabs (but not carage returns or line feeds) +fn multispace1(i: &str) -> IResult<&str, &str> { + take_while1(|c| c == ' ' || c == '\t')(i) +} + +/// Recognizes one or more non-whitespace-characters +fn notspace1(i: &str) -> IResult<&str, &str> { + take_while1(|c| !(c == ' ' || c == '\t' || c == '\n'))(i) +} + +fn parse_u64(i: &str) -> IResult<&str, u64> { + map_res(recognize(digit1), str::parse)(i) +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct ZFSPoolVDevState { + pub name: String, + pub lvl: u64, + pub state: String, + pub read: u64, + pub write: u64, + pub cksum: u64, + #[serde(skip_serializing_if="Option::is_none")] + pub msg: Option, +} + +fn parse_zpool_status_vdev(i: &str) -> IResult<&str, ZFSPoolVDevState> { + + let (i, indent) = multispace0(i)?; + let (i, vdev_name) = notspace1(i)?; + let (i, state) = preceded(multispace1, notspace1)(i)?; + let (i, read) = preceded(multispace1, parse_u64)(i)?; + let (i, write) = preceded(multispace1, parse_u64)(i)?; + let (i, cksum) = preceded(multispace1, parse_u64)(i)?; + let (i, msg) = opt(preceded(multispace1, take_while(|c| c != '\n')))(i)?; + let (i, _) = line_ending(i)?; + + let vdev = ZFSPoolVDevState { + name: vdev_name.to_string(), + lvl: (indent.len() as u64)/2, + state: state.to_string(), + read, write, cksum, + msg: msg.map(String::from), + }; + + Ok((i, vdev)) +} + +fn parse_zpool_status_tree(i: &str) -> IResult<&str, Vec> { + + // skip header + let (i, _) = tag("NAME")(i)?; + let (i, _) = multispace1(i)?; + let (i, _) = tag("STATE")(i)?; + let (i, _) = multispace1(i)?; + let (i, _) = tag("READ")(i)?; + let (i, _) = multispace1(i)?; + let (i, _) = tag("WRITE")(i)?; + let (i, _) = multispace1(i)?; + let (i, _) = tag("CKSUM")(i)?; + let (i, _) = line_ending(i)?; + + // parse vdev list + many0(parse_zpool_status_vdev)(i) +} + +fn parse_zpool_status_field(i: &str) -> IResult<&str, (String, String)> { + let (i, prefix) = take_while1(|c| c != ':')(i)?; + let (i, _) = tag(":")(i)?; + let (i, mut value) = take_while(|c| c != '\n')(i)?; + if value.starts_with(' ') { value = &value[1..]; } + + let (mut i, _) = line_ending(i)?; + + let field = prefix.trim().to_string(); + + let indent = (0..prefix.len()+2).fold(String::new(), |mut acc, _| { acc.push(' '); acc }); + + let parse_continuation = opt(preceded(tag(indent.as_str()), take_while1(|c| c != '\n'))); + + let mut value = value.to_string(); + + if field == "config" { + let (n, _) = line_ending(i)?; + i = n; + } + + loop { + let (n, cont) = parse_continuation(i)?; + + if let Some(cont) = cont { + let (n, _) = line_ending(n)?; + i = n; + if !value.is_empty() { value.push('\n'); } + value.push_str(cont); + } else { + if field == "config" { + let (n, _) = line_ending(i)?; + value.push('\n'); + i = n; + } + break; + } + } + + Ok((i, (field, value))) +} + +pub fn parse_zpool_status_config_tree(i: &str) -> Result, Error> { + match all_consuming(parse_zpool_status_tree)(&i) { + Err(nom::Err::Error(err)) | + Err(nom::Err::Failure(err)) => { + bail!("unable to parse zfs status config tree - {}", nom::error::convert_error(&i, err)); + } + Err(err) => { + bail!("unable to parse zfs status config tree: {}", err); + } + Ok((_, data)) => Ok(data), + } +} + +fn parse_zpool_status(i: &str) -> Result, Error> { + match all_consuming(many0(parse_zpool_status_field))(i) { + Err(nom::Err::Error(err)) | + Err(nom::Err::Failure(err)) => { + bail!("unable to parse zfs status output - {}", nom::error::convert_error(i, err)); + } + Err(err) => { + bail!("unable to parse zfs status output - {}", err); + } + Ok((_, data)) => Ok(data), + } +} + +pub fn vdev_list_to_tree(vdev_list: &[ZFSPoolVDevState]) -> Value { + + #[derive(Debug)] + struct TreeNode<'a> { + vdev: &'a ZFSPoolVDevState, + children: Vec + } + + fn node_to_json(node_idx: usize, nodes: &[TreeNode]) -> Value { + let node = &nodes[node_idx]; + let mut v = serde_json::to_value(node.vdev).unwrap(); + if node.children.is_empty() { + v["leaf"] = true.into(); + } else { + v["leaf"] = false.into(); + v["children"] = json!([]); + for child in node.children .iter(){ + let c = node_to_json(*child, nodes); + v["children"].as_array_mut().unwrap().push(c); + } + } + v + } + + let mut nodes: Vec = vdev_list.into_iter().map(|vdev| { + TreeNode { + vdev: vdev, + children: Vec::new(), + } + }).collect(); + + let mut stack: Vec = Vec::new(); + + let mut root_children: Vec = Vec::new(); + + for idx in 0..nodes.len() { + + if stack.is_empty() { + root_children.push(idx); + stack.push(idx); + continue; + } + + let node_lvl = nodes[idx].vdev.lvl; + + let stacked_node = &mut nodes[*(stack.last().unwrap())]; + let last_lvl = stacked_node.vdev.lvl; + + if node_lvl > last_lvl { + stacked_node.children.push(idx); + } else if node_lvl == last_lvl { + stack.pop(); + match stack.last() { + Some(parent) => nodes[*parent].children.push(idx), + None => root_children.push(idx), + } + } else { + loop { + if stack.is_empty() { + root_children.push(idx); + break; + } + + let stacked_node = &mut nodes[*(stack.last().unwrap())]; + if node_lvl <= stacked_node.vdev.lvl { + stack.pop(); + } else { + stacked_node.children.push(idx); + break; + } + } + } + + stack.push(idx); + } + + let mut result = json!({ + "name": "root", + "children": json!([]), + }); + + for child in root_children { + let c = node_to_json(child, &nodes); + result["children"].as_array_mut().unwrap().push(c); + } + + result +} + +pub fn zpool_status(pool: &str) -> Result, Error> { + + let mut command = std::process::Command::new("zpool"); + command.args(&["status", "-p", "-P", pool]); + + let output = crate::tools::run_command(command, None)?; + + parse_zpool_status(&output) +} + +#[test] +fn test_zpool_status_parser() -> Result<(), Error> { + + let output = r###" pool: tank + state: DEGRADED +status: One or more devices could not be opened. Sufficient replicas exist for + the pool to continue functioning in a degraded state. +action: Attach the missing device and online it using 'zpool online'. + see: http://www.sun.com/msg/ZFS-8000-2Q + scrub: none requested +config: + + NAME STATE READ WRITE CKSUM + tank DEGRADED 0 0 0 + mirror-0 DEGRADED 0 0 0 + c1t0d0 ONLINE 0 0 0 + c1t2d0 ONLINE 0 0 0 + c1t1d0 UNAVAIL 0 0 0 cannot open + mirror-1 DEGRADED 0 0 0 + tank1 DEGRADED 0 0 0 + tank2 DEGRADED 0 0 0 + +errors: No known data errors +"###; + + let key_value_list = parse_zpool_status(&output)?; + for (k, v) in key_value_list { + println!("{} => {}", k,v); + if k == "config" { + let vdev_list = parse_zpool_status_config_tree(&v)?; + let tree = vdev_list_to_tree(&vdev_list); + println!("TREE1 {}", serde_json::to_string_pretty(&tree)?); + } + } + + Ok(()) +}