]> git.proxmox.com Git - proxmox-backup.git/commitdiff
src/tools/disks/zpool_status.rs: parse zpool status output
authorDietmar Maurer <dietmar@proxmox.com>
Thu, 18 Jun 2020 08:23:15 +0000 (10:23 +0200)
committerDietmar Maurer <dietmar@proxmox.com>
Thu, 18 Jun 2020 08:23:15 +0000 (10:23 +0200)
src/tools/disks.rs
src/tools/disks/zpool_status.rs [new file with mode: 0644]

index 07ad13413c474033fef20045f96d0202a13e0067..134ac85d0605b8800f2b88a07bde48e23f7b1294 100644 (file)
@@ -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 (file)
index 0000000..7299522
--- /dev/null
@@ -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<I, O, E = VerboseError<I>> = Result<(I, O), nom::Err<E>>;
+
+/// 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<String>,
+}
+
+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<ZFSPoolVDevState>> {
+
+    // 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<Vec<ZFSPoolVDevState>, 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<Vec<(String, String)>, 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<usize>
+    }
+
+    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<TreeNode> = vdev_list.into_iter().map(|vdev| {
+        TreeNode {
+            vdev: vdev,
+            children: Vec::new(),
+        }
+    }).collect();
+
+    let mut stack: Vec<usize> = Vec::new();
+
+    let mut root_children: Vec<usize> = 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<Vec<(String, String)>, 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(())
+}