use std::{ffi::OsString, path::PathBuf};
use super::{ZpoolError, ZpoolResult};
use crate::utils::parse_float;
pub trait PropPair {
fn to_pair(&self, key: &str) -> String;
}
impl PropPair for FailMode {
fn to_pair(&self, key: &str) -> String { format!("{}={}", key, self.as_str()) }
}
impl PropPair for bool {
fn to_pair(&self, key: &str) -> String {
let val = if *self { "on" } else { "off" };
format!("{}={}", key, val)
}
}
impl PropPair for CacheType {
fn to_pair(&self, key: &str) -> String { format!("{}={}", key, self.as_str()) }
}
impl PropPair for String {
fn to_pair(&self, key: &str) -> String { format!("{}={}", key, &self) }
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum Health {
Online,
Degraded,
Faulted,
Offline,
Available,
Unavailable,
Removed,
}
impl Health {
#[doc(hidden)]
pub fn try_from_str(val: Option<&str>) -> ZpoolResult<Health> {
let val_str = val.ok_or(ZpoolError::ParseError)?;
match val_str {
"ONLINE" => Ok(Health::Online),
"DEGRADED" => Ok(Health::Degraded),
"FAULTED" => Ok(Health::Faulted),
"OFFLINE" => Ok(Health::Offline),
"AVAIL" => Ok(Health::Available),
"UNAVAIL" => Ok(Health::Unavailable),
"REMOVED" => Ok(Health::Removed),
_ => Err(ZpoolError::ParseError),
}
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum FailMode {
Wait,
Continue,
Panic,
}
impl FailMode {
#[doc(hidden)]
pub fn try_from_str(val: Option<&str>) -> ZpoolResult<FailMode> {
let val_str = val.ok_or(ZpoolError::ParseError)?;
match val_str {
"wait" => Ok(FailMode::Wait),
"continue" => Ok(FailMode::Continue),
"panic" => Ok(FailMode::Panic),
_ => Err(ZpoolError::ParseError),
}
}
#[doc(hidden)]
pub fn as_str(&self) -> &str {
match *self {
FailMode::Wait => "wait",
FailMode::Continue => "continue",
FailMode::Panic => "panic",
}
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum CacheType {
Default,
None,
Custom(String),
}
impl CacheType {
pub fn try_from_str(val: Option<&str>) -> ZpoolResult<CacheType> {
let val_str = val.ok_or(ZpoolError::ParseError)?;
match val_str {
"-" | "" => Ok(CacheType::Default),
"none" => Ok(CacheType::None),
n => Ok(CacheType::Custom(String::from(n))),
}
}
#[doc(hidden)]
pub fn as_str(&self) -> &str {
match *self {
CacheType::Default => "",
CacheType::None => "none",
CacheType::Custom(ref e) => e,
}
}
}
#[derive(Getters, Builder, Debug, Clone, PartialEq, Eq)]
#[get = "pub"]
pub struct ZpoolPropertiesWrite {
#[builder(default = "false")]
read_only: bool,
#[builder(default = "false")]
auto_expand: bool,
#[builder(default = "false")]
auto_replace: bool,
#[builder(default)]
boot_fs: Option<String>,
#[builder(default = "CacheType::Default")]
cache_file: CacheType,
#[builder(default)]
#[builder(setter(into))]
comment: String,
#[builder(default = "false")]
delegation: bool,
#[builder(default = "FailMode::Wait")]
fail_mode: FailMode,
}
impl ZpoolPropertiesWrite {
pub fn builder() -> ZpoolPropertiesWriteBuilder { ZpoolPropertiesWriteBuilder::default() }
#[doc(hidden)]
pub fn into_args(self) -> Vec<OsString> {
let mut ret = Vec::with_capacity(7);
ret.push(PropPair::to_pair(&self.auto_expand, "autoexpand"));
ret.push(PropPair::to_pair(&self.auto_replace, "autoreplace"));
ret.push(PropPair::to_pair(&self.cache_file, "cachefile"));
ret.push(PropPair::to_pair(&self.comment, "comment"));
ret.push(PropPair::to_pair(&self.delegation, "delegation"));
ret.push(PropPair::to_pair(&self.fail_mode, "failmode"));
if let Some(ref btfs) = self.boot_fs {
ret.push(PropPair::to_pair(btfs, "bootfs"));
}
ret.iter().map(OsString::from).collect()
}
}
impl ZpoolPropertiesWriteBuilder {
pub fn from_props(props: &ZpoolProperties) -> ZpoolPropertiesWriteBuilder {
let mut b = ZpoolPropertiesWriteBuilder::default();
b.read_only(props.read_only);
b.auto_expand(props.auto_expand);
b.auto_replace(props.auto_replace);
b.boot_fs(props.boot_fs.clone());
b.cache_file(props.cache_file.clone());
b.delegation(props.delegation);
b.fail_mode(props.fail_mode.clone());
if let Some(ref comment) = props.comment {
b.comment(comment.clone());
}
b
}
}
#[derive(Debug, Clone, PartialEq, Getters)]
#[get = "pub"]
pub struct ZpoolProperties {
alloc: usize,
capacity: u8,
comment: Option<String>,
dedup_ratio: f64,
expand_size: Option<usize>,
fragmentation: i8,
free: i64,
freeing: i64,
guid: u64,
health: Health,
size: usize,
leaked: usize,
alt_root: Option<PathBuf>,
read_only: bool,
auto_expand: bool,
auto_replace: bool,
boot_fs: Option<String>,
cache_file: CacheType,
dedup_ditto: usize,
delegation: bool,
fail_mode: FailMode,
}
fn parse_bool(val: Option<&str>) -> ZpoolResult<bool> {
let val_str = val.ok_or(ZpoolError::ParseError)?;
match val_str {
"off" => Ok(false),
"on" => Ok(true),
_ => Err(ZpoolError::ParseError),
}
}
fn parse_usize(val: Option<&str>) -> ZpoolResult<usize> {
let val_str = val.ok_or(ZpoolError::ParseError)?;
Ok(val_str.parse()?)
}
fn parse_i64(val: Option<&str>) -> ZpoolResult<i64> {
let val_str = val.ok_or(ZpoolError::ParseError)?;
Ok(val_str.parse()?)
}
fn parse_u64(val: Option<&str>) -> ZpoolResult<u64> {
let val_str = val.ok_or(ZpoolError::ParseError)?;
Ok(val_str.parse()?)
}
impl ZpoolProperties {
pub(crate) fn try_from_stdout(out: &[u8]) -> ZpoolResult<ZpoolProperties> {
let mut stdout: String = String::from_utf8_lossy(out).into();
stdout.pop();
let mut cols = stdout.split('\t');
let alloc = parse_usize(cols.next())?;
let cap_str = cols.next().ok_or(ZpoolError::ParseError)?;
let cap: u8 = cap_str.parse()?;
let comment_str = cols.next().ok_or(ZpoolError::ParseError)?;
let comment = match comment_str {
"-" | "" => None,
c => Some(String::from(c)),
};
let mut dedup_ratio_string = cols.next().ok_or(ZpoolError::ParseError).map(String::from)?;
let dedup_ratio: f64 = parse_float(&mut dedup_ratio_string)?;
let expand_size_str = cols.next().ok_or(ZpoolError::ParseError)?;
let expand_size: Option<usize> = match expand_size_str {
"-" => None,
c => Some(c.parse()?),
};
let mut frag_string = cols.next().ok_or(ZpoolError::ParseError).map(String::from)?;
let last_char = {
let chars = frag_string.chars();
chars.last()
};
if last_char == Some('%') {
frag_string.pop();
}
let fragmentation: i8 = frag_string.parse()?;
let free = parse_i64(cols.next())?;
let freeing = parse_i64(cols.next())?;
let guid = parse_u64(cols.next())?;
let health = Health::try_from_str(cols.next())?;
let size = parse_usize(cols.next())?;
let leaked = parse_usize(cols.next())?;
let alt_root_str = cols.next().ok_or(ZpoolError::ParseError)?;
let alt_root = match alt_root_str {
"-" => None,
r => Some(PathBuf::from(r)),
};
let read_only = parse_bool(cols.next())?;
let auto_expand = parse_bool(cols.next())?;
let auto_replace = parse_bool(cols.next())?;
let boot_fs_str = cols.next().ok_or(ZpoolError::ParseError)?;
let boot_fs = match boot_fs_str {
"-" => None,
r => Some(String::from(r)),
};
let cache_file = CacheType::try_from_str(cols.next())?;
let dedup_ditto = parse_usize(cols.next())?;
let delegation = parse_bool(cols.next())?;
let fail_mode = FailMode::try_from_str(cols.next())?;
Ok(ZpoolProperties {
alloc,
capacity: cap,
comment,
dedup_ratio,
expand_size,
fragmentation,
free,
freeing,
guid,
health,
size,
leaked,
alt_root,
read_only,
auto_expand,
auto_replace,
boot_fs,
cache_file,
dedup_ditto,
delegation,
fail_mode,
})
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_defaults() {
let built = ZpoolPropertiesWriteBuilder::default().build().unwrap();
let handmade = ZpoolPropertiesWrite {
read_only: false,
auto_expand: false,
auto_replace: false,
boot_fs: None,
cache_file: CacheType::Default,
comment: String::new(),
delegation: false,
fail_mode: FailMode::Wait,
};
assert_eq!(handmade, built);
}
#[test]
fn test_create_props() {
let built = ZpoolPropertiesWriteBuilder::default()
.boot_fs(Some("bootpool".into()))
.build()
.unwrap();
let args = built.into_args();
assert_eq!(7, args.len());
}
#[test]
fn parsing_health() {
let online = Some("ONLINE");
let degraded = Some("DEGRADED");
let faulted = Some("FAULTED");
let offline = Some("OFFLINE");
let unavailable = Some("UNAVAIL");
let removed = Some("REMOVED");
let bad = Some("wat");
assert_eq!(Health::Online, Health::try_from_str(online).unwrap());
assert_eq!(Health::Degraded, Health::try_from_str(degraded).unwrap());
assert_eq!(Health::Faulted, Health::try_from_str(faulted).unwrap());
assert_eq!(Health::Offline, Health::try_from_str(offline).unwrap());
assert_eq!(Health::Unavailable, Health::try_from_str(unavailable).unwrap());
assert_eq!(Health::Removed, Health::try_from_str(removed).unwrap());
let err = Health::try_from_str(bad);
assert!(err.is_err());
let err = Health::try_from_str(None);
assert!(err.is_err());
}
#[test]
fn parsing_fail_mode() {
let wait = Some("wait");
let cont = Some("continue");
let panic = Some("panic");
let bad = Some("wat");
assert_eq!(FailMode::Wait, FailMode::try_from_str(wait).unwrap());
assert_eq!(FailMode::Continue, FailMode::try_from_str(cont).unwrap());
assert_eq!(FailMode::Panic, FailMode::try_from_str(panic).unwrap());
let err = FailMode::try_from_str(bad);
assert!(err.is_err());
let err = FailMode::try_from_str(None);
assert!(err.is_err());
}
#[test]
fn parsing_cache_file() {
assert_eq!(CacheType::Default, CacheType::try_from_str(Some("-")).unwrap());
assert_eq!(CacheType::Default, CacheType::try_from_str(Some("")).unwrap());
assert_eq!(CacheType::None, CacheType::try_from_str(Some("none")).unwrap());
assert_eq!(
CacheType::Custom("/wat".into()),
CacheType::try_from_str(Some("/wat")).unwrap()
);
let err = CacheType::try_from_str(None);
assert!(err.is_err());
}
#[test]
fn parsing_props_u64_guid() {
let line = b"69120\t0\t-\t1.00x\t-\t1%\t67039744\t0\t15867762423891129245\tONLINE\t67108864\t0\t-\toff\toff\toff\t-\t-\t0\ton\twait\n";
let props = ZpoolProperties::try_from_stdout(line);
assert!(props.is_ok());
}
#[test]
fn parsing_on_zol() {
let line = b"99840\t0\t-\t1.00\t-\t1\t67009024\t0\t5667188105885376774\tONLINE\t67108864\t0\t-\toff\toff\toff\t-\t-\t0\ton\twait\n";
let props = ZpoolProperties::try_from_stdout(line);
assert!(props.is_ok());
}
#[test]
fn parsing_props() {
let line = b"69120\t0\t-\t1.50x\t-\t22%\t67039744\t0\t4957928072935098740\tONLINE\t67108864\t0\t-\toff\toff\toff\t-\t-\t0\ton\twait\n";
let props = ZpoolProperties::try_from_stdout(line);
assert!(props.is_ok());
let line = b"69120\t0\ttouch it\t1.50x\t-\t22%\t67039744\t0\t4957928072935098740\tONLINE\t67108864\t0\t-\toff\toff\toff\t-\t-\t0\ton\tpanic\n";
let props = ZpoolProperties::try_from_stdout(line).unwrap();
assert_eq!(Some(String::from("touch it")), props.comment);
assert_eq!(FailMode::Panic, props.fail_mode);
let line = b"69120\t0\ttouch it\t1.50x\t-\t22%\t67039744\t0\t4957928072935098740\tOFFLINE\t67108864\t0\t/mnt/\toff\toff\toff\t-\t-\t0\ton\twait\n";
let props = ZpoolProperties::try_from_stdout(line).unwrap();
assert_eq!(Health::Offline, props.health);
assert_eq!(Some(PathBuf::from("/mnt")), props.alt_root);
let line = b"waf\tasd";
let props = ZpoolProperties::try_from_stdout(line);
assert!(props.is_err());
let line = b"69120\t0\ttouch it\t1.50x\t1\t22%\t67039744\t0\t4957928072935098740\tOFFLINE\t67108864\t0\t/mnt/\toff\toff\toff\tz/ROOT/default\t-\t0\ton\twait\n";
let props = ZpoolProperties::try_from_stdout(line).unwrap();
assert_eq!(Some(String::from("z/ROOT/default")), props.boot_fs);
assert_eq!(Some(1), props.expand_size);
let line = b"69120\t0\t-\t1.50x\t-\t22%\t67039744\t0\t4957928072935098740\tONLINE\t67108864\t0\t-\toff\toff\toff\t-\t-\t0\tomn\twait\n";
let props = ZpoolProperties::try_from_stdout(line);
assert!(props.is_err());
}
#[test]
fn to_arg() {
let props = ZpoolPropertiesWriteBuilder::default().build().unwrap();
let expected: Vec<OsString> = vec![
"autoexpand=off",
"autoreplace=off",
"cachefile=",
"comment=",
"delegation=off",
"failmode=wait",
]
.into_iter()
.map(OsString::from)
.collect();
let result = props.into_args();
assert_eq!(expected, result);
let props = ZpoolPropertiesWriteBuilder::default()
.auto_expand(true)
.cache_file(CacheType::None)
.fail_mode(FailMode::Panic)
.build()
.unwrap();
let expected: Vec<OsString> = vec![
"autoexpand=on",
"autoreplace=off",
"cachefile=none",
"comment=",
"delegation=off",
"failmode=panic",
]
.into_iter()
.map(OsString::from)
.collect();
let result = props.into_args();
assert_eq!(expected, result);
let props = ZpoolPropertiesWriteBuilder::default()
.fail_mode(FailMode::Continue)
.cache_file(CacheType::Custom("wat".into()))
.build()
.unwrap();
let expected: Vec<OsString> = vec![
"autoexpand=off",
"autoreplace=off",
"cachefile=wat",
"comment=",
"delegation=off",
"failmode=continue",
]
.into_iter()
.map(OsString::from)
.collect();
let result = props.into_args();
assert_eq!(expected, result);
let props = ZpoolPropertiesWriteBuilder::default()
.auto_replace(true)
.comment("a test")
.build()
.unwrap();
let expected: Vec<OsString> = vec![
"autoexpand=off",
"autoreplace=on",
"cachefile=",
"comment=a test",
"delegation=off",
"failmode=wait",
]
.into_iter()
.map(OsString::from)
.collect();
let result = props.into_args();
assert_eq!(expected, result);
}
#[test]
fn write_builder() {
let _right: ZpoolPropertiesWriteBuilder = ZpoolPropertiesWrite::builder();
}
}