tools: disk: rustfmt

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
This commit is contained in:
Thomas Lamprecht 2022-02-10 17:46:13 +01:00
parent a1c906cb02
commit af6fdb9d0d
6 changed files with 248 additions and 229 deletions

View File

@ -1,13 +1,13 @@
use std::collections::HashSet; use std::collections::HashSet;
use std::os::unix::fs::MetadataExt; use std::os::unix::fs::MetadataExt;
use anyhow::{Error}; use anyhow::Error;
use serde_json::Value;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use serde_json::Value;
use super::LsblkInfo; use super::LsblkInfo;
lazy_static!{ lazy_static! {
static ref LVM_UUIDS: HashSet<&'static str> = { static ref LVM_UUIDS: HashSet<&'static str> = {
let mut set = HashSet::new(); let mut set = HashSet::new();
set.insert("e6d6d379-f507-44c2-a23c-238f2a3df928"); set.insert("e6d6d379-f507-44c2-a23c-238f2a3df928");
@ -18,14 +18,18 @@ lazy_static!{
/// Get set of devices used by LVM (pvs). /// Get set of devices used by LVM (pvs).
/// ///
/// The set is indexed by using the unix raw device number (dev_t is u64) /// The set is indexed by using the unix raw device number (dev_t is u64)
pub fn get_lvm_devices( pub fn get_lvm_devices(lsblk_info: &[LsblkInfo]) -> Result<HashSet<u64>, Error> {
lsblk_info: &[LsblkInfo],
) -> Result<HashSet<u64>, Error> {
const PVS_BIN_PATH: &str = "pvs"; const PVS_BIN_PATH: &str = "pvs";
let mut command = std::process::Command::new(PVS_BIN_PATH); let mut command = std::process::Command::new(PVS_BIN_PATH);
command.args(&["--reportformat", "json", "--noheadings", "--readonly", "-o", "pv_name"]); command.args(&[
"--reportformat",
"json",
"--noheadings",
"--readonly",
"-o",
"pv_name",
]);
let output = proxmox_sys::command::run_command(command, None)?; let output = proxmox_sys::command::run_command(command, None)?;

View File

@ -14,12 +14,12 @@ use once_cell::sync::OnceCell;
use ::serde::{Deserialize, Serialize}; use ::serde::{Deserialize, Serialize};
use proxmox_sys::error::io_err_other;
use proxmox_sys::linux::procfs::{MountInfo, mountinfo::Device};
use proxmox_sys::{io_bail, io_format_err};
use proxmox_schema::api; use proxmox_schema::api;
use proxmox_sys::error::io_err_other;
use proxmox_sys::linux::procfs::{mountinfo::Device, MountInfo};
use proxmox_sys::{io_bail, io_format_err};
use pbs_api_types::{BLOCKDEVICE_NAME_REGEX, StorageStatus}; use pbs_api_types::{StorageStatus, BLOCKDEVICE_NAME_REGEX};
mod zfs; mod zfs;
pub use zfs::*; pub use zfs::*;
@ -32,7 +32,7 @@ pub use lvm::*;
mod smart; mod smart;
pub use smart::*; pub use smart::*;
lazy_static::lazy_static!{ lazy_static::lazy_static! {
static ref ISCSI_PATH_REGEX: regex::Regex = static ref ISCSI_PATH_REGEX: regex::Regex =
regex::Regex::new(r"host[^/]*/session[^/]*").unwrap(); regex::Regex::new(r"host[^/]*/session[^/]*").unwrap();
} }
@ -153,7 +153,6 @@ impl DiskManage {
&self, &self,
path: &std::path::Path, path: &std::path::Path,
) -> Result<Option<(String, Device, Option<OsString>)>, Error> { ) -> Result<Option<(String, Device, Option<OsString>)>, Error> {
let stat = nix::sys::stat::stat(path)?; let stat = nix::sys::stat::stat(path)?;
let device = Device::from_dev_t(stat.st_dev); let device = Device::from_dev_t(stat.st_dev);
@ -161,7 +160,11 @@ impl DiskManage {
for (_id, entry) in self.mount_info()? { for (_id, entry) in self.mount_info()? {
if entry.root == root_path && entry.device == device { if entry.root == root_path && entry.device == device {
return Ok(Some((entry.fs_type.clone(), entry.device, entry.mount_source.clone()))); return Ok(Some((
entry.fs_type.clone(),
entry.device,
entry.mount_source.clone(),
)));
} }
} }
@ -300,7 +303,7 @@ impl Disk {
/// Get the disk's size in bytes. /// Get the disk's size in bytes.
pub fn size(&self) -> io::Result<u64> { pub fn size(&self) -> io::Result<u64> {
Ok(*self.info.size.get_or_try_init(|| { Ok(*self.info.size.get_or_try_init(|| {
self.read_sys_u64("size")?.map(|s| s*512).ok_or_else(|| { self.read_sys_u64("size")?.map(|s| s * 512).ok_or_else(|| {
io_format_err!( io_format_err!(
"failed to get disk size from {:?}", "failed to get disk size from {:?}",
self.syspath().join("size"), self.syspath().join("size"),
@ -473,11 +476,14 @@ impl Disk {
pub fn read_stat(&self) -> std::io::Result<Option<BlockDevStat>> { pub fn read_stat(&self) -> std::io::Result<Option<BlockDevStat>> {
if let Some(stat) = self.read_sys(Path::new("stat"))? { if let Some(stat) = self.read_sys(Path::new("stat"))? {
let stat = unsafe { std::str::from_utf8_unchecked(&stat) }; let stat = unsafe { std::str::from_utf8_unchecked(&stat) };
let stat: Vec<u64> = stat.split_ascii_whitespace().map(|s| { let stat: Vec<u64> = stat
u64::from_str_radix(s, 10).unwrap_or(0) .split_ascii_whitespace()
}).collect(); .map(|s| u64::from_str_radix(s, 10).unwrap_or(0))
.collect();
if stat.len() < 15 { return Ok(None); } if stat.len() < 15 {
return Ok(None);
}
return Ok(Some(BlockDevStat { return Ok(Some(BlockDevStat {
read_ios: stat[0], read_ios: stat[0],
@ -492,7 +498,6 @@ impl Disk {
/// List device partitions /// List device partitions
pub fn partitions(&self) -> Result<HashMap<u64, Disk>, Error> { pub fn partitions(&self) -> Result<HashMap<u64, Disk>, Error> {
let sys_path = self.syspath(); let sys_path = self.syspath();
let device = self.sysname().to_string_lossy().to_string(); let device = self.sysname().to_string_lossy().to_string();
@ -505,7 +510,9 @@ impl Disk {
Err(_) => continue, // skip non utf8 entries Err(_) => continue, // skip non utf8 entries
}; };
if !name.starts_with(&device) { continue; } if !name.starts_with(&device) {
continue;
}
let mut part_path = sys_path.to_owned(); let mut part_path = sys_path.to_owned();
part_path.push(name); part_path.push(name);
@ -523,7 +530,6 @@ impl Disk {
/// Returns disk usage information (total, used, avail) /// Returns disk usage information (total, used, avail)
pub fn disk_usage(path: &std::path::Path) -> Result<StorageStatus, Error> { pub fn disk_usage(path: &std::path::Path) -> Result<StorageStatus, Error> {
let mut stat: libc::statfs64 = unsafe { std::mem::zeroed() }; let mut stat: libc::statfs64 = unsafe { std::mem::zeroed() };
use nix::NixPath; use nix::NixPath;
@ -533,16 +539,16 @@ pub fn disk_usage(path: &std::path::Path) -> Result<StorageStatus, Error> {
let bsize = stat.f_bsize as u64; let bsize = stat.f_bsize as u64;
Ok(StorageStatus{ Ok(StorageStatus {
total: stat.f_blocks*bsize, total: stat.f_blocks * bsize,
used: (stat.f_blocks-stat.f_bfree)*bsize, used: (stat.f_blocks - stat.f_bfree) * bsize,
avail: stat.f_bavail*bsize, avail: stat.f_bavail * bsize,
}) })
} }
#[api()] #[api()]
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all="lowercase")] #[serde(rename_all = "lowercase")]
/// This is just a rough estimate for a "type" of disk. /// This is just a rough estimate for a "type" of disk.
pub enum DiskType { pub enum DiskType {
/// We know nothing. /// We know nothing.
@ -570,7 +576,6 @@ pub struct BlockDevStat {
/// Use lsblk to read partition type uuids and file system types. /// Use lsblk to read partition type uuids and file system types.
pub fn get_lsblk_info() -> Result<Vec<LsblkInfo>, Error> { pub fn get_lsblk_info() -> Result<Vec<LsblkInfo>, Error> {
let mut command = std::process::Command::new("lsblk"); let mut command = std::process::Command::new("lsblk");
command.args(&["--json", "-o", "path,parttype,fstype"]); command.args(&["--json", "-o", "path,parttype,fstype"]);
@ -584,10 +589,7 @@ pub fn get_lsblk_info() -> Result<Vec<LsblkInfo>, Error> {
/// Get set of devices with a file system label. /// Get set of devices with a file system label.
/// ///
/// The set is indexed by using the unix raw device number (dev_t is u64) /// The set is indexed by using the unix raw device number (dev_t is u64)
fn get_file_system_devices( fn get_file_system_devices(lsblk_info: &[LsblkInfo]) -> Result<HashSet<u64>, Error> {
lsblk_info: &[LsblkInfo],
) -> Result<HashSet<u64>, Error> {
let mut device_set: HashSet<u64> = HashSet::new(); let mut device_set: HashSet<u64> = HashSet::new();
for info in lsblk_info.iter() { for info in lsblk_info.iter() {
@ -602,7 +604,7 @@ fn get_file_system_devices(
#[api()] #[api()]
#[derive(Debug, Serialize, Deserialize, PartialEq)] #[derive(Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all="lowercase")] #[serde(rename_all = "lowercase")]
pub enum DiskUsageType { pub enum DiskUsageType {
/// Disk is not used (as far we can tell) /// Disk is not used (as far we can tell)
Unused, Unused,
@ -634,7 +636,7 @@ pub enum DiskUsageType {
} }
)] )]
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all="kebab-case")] #[serde(rename_all = "kebab-case")]
/// Information about how a Disk is used /// Information about how a Disk is used
pub struct DiskUsageInfo { pub struct DiskUsageInfo {
/// Disk name (/sys/block/<name>) /// Disk name (/sys/block/<name>)
@ -668,7 +670,6 @@ fn scan_partitions(
zfs_devices: &HashSet<u64>, zfs_devices: &HashSet<u64>,
device: &str, device: &str,
) -> Result<DiskUsageType, Error> { ) -> Result<DiskUsageType, Error> {
let mut sys_path = std::path::PathBuf::from("/sys/block"); let mut sys_path = std::path::PathBuf::from("/sys/block");
sys_path.push(device); sys_path.push(device);
@ -686,7 +687,9 @@ fn scan_partitions(
Ok(name) => name, Ok(name) => name,
Err(_) => continue, // skip non utf8 entries Err(_) => continue, // skip non utf8 entries
}; };
if !name.starts_with(device) { continue; } if !name.starts_with(device) {
continue;
}
found_partitions = true; found_partitions = true;
@ -729,12 +732,8 @@ fn scan_partitions(
Ok(used) Ok(used)
} }
/// Get disk usage information for a single disk /// Get disk usage information for a single disk
pub fn get_disk_usage_info( pub fn get_disk_usage_info(disk: &str, no_smart: bool) -> Result<DiskUsageInfo, Error> {
disk: &str,
no_smart: bool,
) -> Result<DiskUsageInfo, Error> {
let mut filter = Vec::new(); let mut filter = Vec::new();
filter.push(disk.to_string()); filter.push(disk.to_string());
let mut map = get_disks(Some(filter), no_smart)?; let mut map = get_disks(Some(filter), no_smart)?;
@ -752,12 +751,12 @@ pub fn get_disks(
// do no include data from smartctl // do no include data from smartctl
no_smart: bool, no_smart: bool,
) -> Result<HashMap<String, DiskUsageInfo>, Error> { ) -> Result<HashMap<String, DiskUsageInfo>, Error> {
let disk_manager = DiskManage::new(); let disk_manager = DiskManage::new();
let lsblk_info = get_lsblk_info()?; let lsblk_info = get_lsblk_info()?;
let zfs_devices = zfs_devices(&lsblk_info, None).or_else(|err| -> Result<HashSet<u64>, Error> { let zfs_devices =
zfs_devices(&lsblk_info, None).or_else(|err| -> Result<HashSet<u64>, Error> {
eprintln!("error getting zfs devices: {}", err); eprintln!("error getting zfs devices: {}", err);
Ok(HashSet::new()) Ok(HashSet::new())
})?; })?;
@ -770,20 +769,25 @@ pub fn get_disks(
let mut result = HashMap::new(); let mut result = HashMap::new();
for item in proxmox_sys::fs::scan_subdir(libc::AT_FDCWD, "/sys/block", &BLOCKDEVICE_NAME_REGEX)? { for item in proxmox_sys::fs::scan_subdir(libc::AT_FDCWD, "/sys/block", &BLOCKDEVICE_NAME_REGEX)?
{
let item = item?; let item = item?;
let name = item.file_name().to_str().unwrap().to_string(); let name = item.file_name().to_str().unwrap().to_string();
if let Some(ref disks) = disks { if let Some(ref disks) = disks {
if !disks.contains(&name) { continue; } if !disks.contains(&name) {
continue;
}
} }
let sys_path = format!("/sys/block/{}", name); let sys_path = format!("/sys/block/{}", name);
if let Ok(target) = std::fs::read_link(&sys_path) { if let Ok(target) = std::fs::read_link(&sys_path) {
if let Some(target) = target.to_str() { if let Some(target) = target.to_str() {
if ISCSI_PATH_REGEX.is_match(target) { continue; } // skip iSCSI devices if ISCSI_PATH_REGEX.is_match(target) {
continue;
} // skip iSCSI devices
} }
} }
@ -809,7 +813,7 @@ pub fn get_disks(
match disk.is_mounted() { match disk.is_mounted() {
Ok(true) => usage = DiskUsageType::Mounted, Ok(true) => usage = DiskUsageType::Mounted,
Ok(false) => {}, Ok(false) => {}
Err(_) => continue, // skip devices with undetectable mount status Err(_) => continue, // skip devices with undetectable mount status
} }
@ -817,17 +821,20 @@ pub fn get_disks(
usage = DiskUsageType::ZFS; usage = DiskUsageType::ZFS;
} }
let vendor = disk.vendor().unwrap_or(None). let vendor = disk
map(|s| s.to_string_lossy().trim().to_string()); .vendor()
.unwrap_or(None)
.map(|s| s.to_string_lossy().trim().to_string());
let model = disk.model().map(|s| s.to_string_lossy().into_owned()); let model = disk.model().map(|s| s.to_string_lossy().into_owned());
let serial = disk.serial().map(|s| s.to_string_lossy().into_owned()); let serial = disk.serial().map(|s| s.to_string_lossy().into_owned());
let devpath = disk.device_path().map(|p| p.to_owned()) let devpath = disk
.device_path()
.map(|p| p.to_owned())
.map(|p| p.to_string_lossy().to_string()); .map(|p| p.to_string_lossy().to_string());
let wwn = disk.wwn().map(|s| s.to_string_lossy().into_owned()); let wwn = disk.wwn().map(|s| s.to_string_lossy().into_owned());
if usage != DiskUsageType::Mounted { if usage != DiskUsageType::Mounted {
@ -836,7 +843,7 @@ pub fn get_disks(
if part_usage != DiskUsageType::Unused { if part_usage != DiskUsageType::Unused {
usage = part_usage; usage = part_usage;
} }
}, }
Err(_) => continue, // skip devices if scan_partitions fail Err(_) => continue, // skip devices if scan_partitions fail
}; };
} }
@ -861,8 +868,15 @@ pub fn get_disks(
let info = DiskUsageInfo { let info = DiskUsageInfo {
name: name.clone(), name: name.clone(),
vendor, model, serial, devpath, size, wwn, disk_type, vendor,
status, wearout, model,
serial,
devpath,
size,
wwn,
disk_type,
status,
wearout,
used: usage, used: usage,
gpt: disk.has_gpt(), gpt: disk.has_gpt(),
rpm: disk.ata_rotation_rate_rpm(), rpm: disk.ata_rotation_rate_rpm(),
@ -876,7 +890,6 @@ pub fn get_disks(
/// Try to reload the partition table /// Try to reload the partition table
pub fn reread_partition_table(disk: &Disk) -> Result<(), Error> { pub fn reread_partition_table(disk: &Disk) -> Result<(), Error> {
let disk_path = match disk.device_path() { let disk_path = match disk.device_path() {
Some(path) => path, Some(path) => path,
None => bail!("disk {:?} has no node in /dev", disk.syspath()), None => bail!("disk {:?} has no node in /dev", disk.syspath()),
@ -893,7 +906,6 @@ pub fn reread_partition_table(disk: &Disk) -> Result<(), Error> {
/// Initialize disk by writing a GPT partition table /// Initialize disk by writing a GPT partition table
pub fn inititialize_gpt_disk(disk: &Disk, uuid: Option<&str>) -> Result<(), Error> { pub fn inititialize_gpt_disk(disk: &Disk, uuid: Option<&str>) -> Result<(), Error> {
let disk_path = match disk.device_path() { let disk_path = match disk.device_path() {
Some(path) => path, Some(path) => path,
None => bail!("disk {:?} has no node in /dev", disk.syspath()), None => bail!("disk {:?} has no node in /dev", disk.syspath()),
@ -912,7 +924,6 @@ pub fn inititialize_gpt_disk(disk: &Disk, uuid: Option<&str>) -> Result<(), Erro
/// Create a single linux partition using the whole available space /// Create a single linux partition using the whole available space
pub fn create_single_linux_partition(disk: &Disk) -> Result<Disk, Error> { pub fn create_single_linux_partition(disk: &Disk) -> Result<Disk, Error> {
let disk_path = match disk.device_path() { let disk_path = match disk.device_path() {
Some(path) => path, Some(path) => path,
None => bail!("disk {:?} has no node in /dev", disk.syspath()), None => bail!("disk {:?} has no node in /dev", disk.syspath()),
@ -934,7 +945,7 @@ pub fn create_single_linux_partition(disk: &Disk) -> Result<Disk, Error> {
#[api()] #[api()]
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)] #[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all="lowercase")] #[serde(rename_all = "lowercase")]
pub enum FileSystemType { pub enum FileSystemType {
/// Linux Ext4 /// Linux Ext4
Ext4, Ext4,
@ -963,7 +974,6 @@ impl std::str::FromStr for FileSystemType {
/// Create a file system on a disk or disk partition /// Create a file system on a disk or disk partition
pub fn create_file_system(disk: &Disk, fs_type: FileSystemType) -> Result<(), Error> { pub fn create_file_system(disk: &Disk, fs_type: FileSystemType) -> Result<(), Error> {
let disk_path = match disk.device_path() { let disk_path = match disk.device_path() {
Some(path) => path, Some(path) => path,
None => bail!("disk {:?} has no node in /dev", disk.syspath()), None => bail!("disk {:?} has no node in /dev", disk.syspath()),
@ -982,21 +992,21 @@ pub fn create_file_system(disk: &Disk, fs_type: FileSystemType) -> Result<(), Er
/// Block device name completion helper /// Block device name completion helper
pub fn complete_disk_name(_arg: &str, _param: &HashMap<String, String>) -> Vec<String> { pub fn complete_disk_name(_arg: &str, _param: &HashMap<String, String>) -> Vec<String> {
let dir = match proxmox_sys::fs::scan_subdir(libc::AT_FDCWD, "/sys/block", &BLOCKDEVICE_NAME_REGEX) { let dir =
match proxmox_sys::fs::scan_subdir(libc::AT_FDCWD, "/sys/block", &BLOCKDEVICE_NAME_REGEX) {
Ok(dir) => dir, Ok(dir) => dir,
Err(_) => return vec![], Err(_) => return vec![],
}; };
dir.flatten().map(|item| { dir.flatten()
item.file_name().to_str().unwrap().to_string() .map(|item| item.file_name().to_str().unwrap().to_string())
}).collect() .collect()
} }
/// Read the FS UUID (parse blkid output) /// Read the FS UUID (parse blkid output)
/// ///
/// Note: Calling blkid is more reliable than using the udev ID_FS_UUID property. /// Note: Calling blkid is more reliable than using the udev ID_FS_UUID property.
pub fn get_fs_uuid(disk: &Disk) -> Result<String, Error> { pub fn get_fs_uuid(disk: &Disk) -> Result<String, Error> {
let disk_path = match disk.device_path() { let disk_path = match disk.device_path() {
Some(path) => path, Some(path) => path,
None => bail!("disk {:?} has no node in /dev", disk.syspath()), None => bail!("disk {:?} has no node in /dev", disk.syspath()),

View File

@ -1,14 +1,14 @@
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use lazy_static::lazy_static;
use anyhow::{bail, Error};
use ::serde::{Deserialize, Serialize}; use ::serde::{Deserialize, Serialize};
use anyhow::{bail, Error};
use lazy_static::lazy_static;
use proxmox_schema::api; use proxmox_schema::api;
#[api()] #[api()]
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all="lowercase")] #[serde(rename_all = "lowercase")]
/// SMART status /// SMART status
pub enum SmartStatus { pub enum SmartStatus {
/// Smart tests passed - everything is OK /// Smart tests passed - everything is OK
@ -29,23 +29,22 @@ pub struct SmartAttribute {
value: String, value: String,
// the rest of the values is available for ATA type // the rest of the values is available for ATA type
/// ATA Attribute ID /// ATA Attribute ID
#[serde(skip_serializing_if="Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
id: Option<u64>, id: Option<u64>,
/// ATA Flags /// ATA Flags
#[serde(skip_serializing_if="Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
flags: Option<String>, flags: Option<String>,
/// ATA normalized value (0..100) /// ATA normalized value (0..100)
#[serde(skip_serializing_if="Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
normalized: Option<f64>, normalized: Option<f64>,
/// ATA worst /// ATA worst
#[serde(skip_serializing_if="Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
worst: Option<f64>, worst: Option<f64>,
/// ATA threshold /// ATA threshold
#[serde(skip_serializing_if="Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
threshold: Option<f64>, threshold: Option<f64>,
} }
#[api( #[api(
properties: { properties: {
status: { status: {
@ -74,16 +73,14 @@ pub struct SmartData {
} }
/// Read smartctl data for a disk (/dev/XXX). /// Read smartctl data for a disk (/dev/XXX).
pub fn get_smart_data( pub fn get_smart_data(disk: &super::Disk, health_only: bool) -> Result<SmartData, Error> {
disk: &super::Disk,
health_only: bool,
) -> Result<SmartData, Error> {
const SMARTCTL_BIN_PATH: &str = "smartctl"; const SMARTCTL_BIN_PATH: &str = "smartctl";
let mut command = std::process::Command::new(SMARTCTL_BIN_PATH); let mut command = std::process::Command::new(SMARTCTL_BIN_PATH);
command.arg("-H"); command.arg("-H");
if !health_only { command.args(&["-A", "-j"]); } if !health_only {
command.args(&["-A", "-j"]);
}
let disk_path = match disk.device_path() { let disk_path = match disk.device_path() {
Some(path) => path, Some(path) => path,
@ -91,9 +88,12 @@ pub fn get_smart_data(
}; };
command.arg(disk_path); command.arg(disk_path);
let output = proxmox_sys::command::run_command(command, Some(|exitcode| let output = proxmox_sys::command::run_command(
(exitcode & 0b0111) == 0 // only bits 0-2 are fatal errors command,
))?; Some(
|exitcode| (exitcode & 0b0111) == 0, // only bits 0-2 are fatal errors
),
)?;
let output: serde_json::Value = output.parse()?; let output: serde_json::Value = output.parse()?;
@ -196,8 +196,11 @@ pub fn get_smart_data(
Some(false) => SmartStatus::Failed, Some(false) => SmartStatus::Failed,
}; };
Ok(SmartData {
Ok(SmartData { status, wearout, attributes }) status,
wearout,
attributes,
})
} }
static WEAROUT_FIELD_ORDER: &[&'static str] = &[ static WEAROUT_FIELD_ORDER: &[&'static str] = &[
@ -213,11 +216,10 @@ static WEAROUT_FIELD_ORDER: &[&'static str] = &[
"Lifetime_Remaining", "Lifetime_Remaining",
"Percent_Life_Remaining", "Percent_Life_Remaining",
"Percent_Lifetime_Used", "Percent_Lifetime_Used",
"Perc_Rated_Life_Used" "Perc_Rated_Life_Used",
]; ];
lazy_static! { lazy_static! {
static ref WEAROUT_FIELD_NAMES: HashSet<&'static str> = { static ref WEAROUT_FIELD_NAMES: HashSet<&'static str> =
WEAROUT_FIELD_ORDER.iter().cloned().collect() WEAROUT_FIELD_ORDER.iter().cloned().collect();
};
} }

View File

@ -1,6 +1,6 @@
use std::path::PathBuf;
use std::collections::HashSet; use std::collections::HashSet;
use std::os::unix::fs::MetadataExt; use std::os::unix::fs::MetadataExt;
use std::path::PathBuf;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use anyhow::{bail, Error}; use anyhow::{bail, Error};
@ -10,7 +10,7 @@ use proxmox_schema::const_regex;
use super::*; use super::*;
lazy_static!{ lazy_static! {
static ref ZFS_UUIDS: HashSet<&'static str> = { static ref ZFS_UUIDS: HashSet<&'static str> = {
let mut set = HashSet::new(); let mut set = HashSet::new();
set.insert("6a898cc3-1dd2-11b2-99a6-080020736631"); // apple set.insert("6a898cc3-1dd2-11b2-99a6-080020736631"); // apple
@ -29,14 +29,15 @@ fn get_pool_from_dataset(dataset: &str) -> &str {
/// Returns kernel IO-stats for zfs pools /// Returns kernel IO-stats for zfs pools
pub fn zfs_pool_stats(pool: &OsStr) -> Result<Option<BlockDevStat>, Error> { pub fn zfs_pool_stats(pool: &OsStr) -> Result<Option<BlockDevStat>, Error> {
let mut path = PathBuf::from("/proc/spl/kstat/zfs"); let mut path = PathBuf::from("/proc/spl/kstat/zfs");
path.push(pool); path.push(pool);
path.push("io"); path.push("io");
let text = match proxmox_sys::fs::file_read_optional_string(&path)? { let text = match proxmox_sys::fs::file_read_optional_string(&path)? {
Some(text) => text, Some(text) => text,
None => { return Ok(None); } None => {
return Ok(None);
}
}; };
let lines: Vec<&str> = text.lines().collect(); let lines: Vec<&str> = text.lines().collect();
@ -50,15 +51,16 @@ pub fn zfs_pool_stats(pool: &OsStr) -> Result<Option<BlockDevStat>, Error> {
// Note: w -> wait (wtime -> wait time) // Note: w -> wait (wtime -> wait time)
// Note: r -> run (rtime -> run time) // Note: r -> run (rtime -> run time)
// All times are nanoseconds // All times are nanoseconds
let stat: Vec<u64> = lines[2].split_ascii_whitespace().map(|s| { let stat: Vec<u64> = lines[2]
u64::from_str_radix(s, 10).unwrap_or(0) .split_ascii_whitespace()
}).collect(); .map(|s| u64::from_str_radix(s, 10).unwrap_or(0))
.collect();
let ticks = (stat[4] + stat[7])/1_000_000; // convert to milisec let ticks = (stat[4] + stat[7]) / 1_000_000; // convert to milisec
let stat = BlockDevStat { let stat = BlockDevStat {
read_sectors: stat[0]>>9, read_sectors: stat[0] >> 9,
write_sectors: stat[1]>>9, write_sectors: stat[1] >> 9,
read_ios: stat[2], read_ios: stat[2],
write_ios: stat[3], write_ios: stat[3],
io_ticks: ticks, io_ticks: ticks,
@ -70,11 +72,7 @@ pub fn zfs_pool_stats(pool: &OsStr) -> Result<Option<BlockDevStat>, Error> {
/// Get set of devices used by zfs (or a specific zfs pool) /// Get set of devices used by zfs (or a specific zfs pool)
/// ///
/// The set is indexed by using the unix raw device number (dev_t is u64) /// The set is indexed by using the unix raw device number (dev_t is u64)
pub fn zfs_devices( pub fn zfs_devices(lsblk_info: &[LsblkInfo], pool: Option<String>) -> Result<HashSet<u64>, Error> {
lsblk_info: &[LsblkInfo],
pool: Option<String>,
) -> Result<HashSet<u64>, Error> {
let list = zpool_list(pool, true)?; let list = zpool_list(pool, true)?;
let mut device_set = HashSet::new(); let mut device_set = HashSet::new();
@ -162,7 +160,6 @@ fn parse_objset_stat(pool: &str, objset_id: &str) -> Result<(String, BlockDevSta
Ok((dataset_name, stat)) Ok((dataset_name, stat))
} }
fn get_mapping(dataset: &str) -> Option<(String, String)> { fn get_mapping(dataset: &str) -> Option<(String, String)> {
ZFS_DATASET_OBJSET_MAP ZFS_DATASET_OBJSET_MAP
.lock() .lock()

View File

@ -1,15 +1,13 @@
use anyhow::{bail, Error}; use anyhow::{bail, Error};
use pbs_tools::nom::{ use pbs_tools::nom::{multispace0, multispace1, notspace1, IResult};
multispace0, multispace1, notspace1, IResult,
};
use nom::{ use nom::{
bytes::complete::{take_while1, take_till, take_till1}, bytes::complete::{take_till, take_till1, take_while1},
combinator::{map_res, all_consuming, recognize, opt}, character::complete::{char, digit1, line_ending},
combinator::{all_consuming, map_res, opt, recognize},
multi::many0,
sequence::{preceded, tuple}, sequence::{preceded, tuple},
character::complete::{digit1, char, line_ending},
multi::{many0},
}; };
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
@ -29,7 +27,6 @@ pub struct ZFSPoolInfo {
pub devices: Vec<String>, pub devices: Vec<String>,
} }
fn parse_optional_u64(i: &str) -> IResult<&str, Option<u64>> { fn parse_optional_u64(i: &str) -> IResult<&str, Option<u64>> {
if let Some(rest) = i.strip_prefix('-') { if let Some(rest) = i.strip_prefix('-') {
Ok((rest, None)) Ok((rest, None))
@ -61,9 +58,7 @@ fn parse_pool_device(i: &str) -> IResult<&str, String> {
fn parse_zpool_list_header(i: &str) -> IResult<&str, ZFSPoolInfo> { fn parse_zpool_list_header(i: &str) -> IResult<&str, ZFSPoolInfo> {
// name, size, allocated, free, checkpoint, expandsize, fragmentation, capacity, dedupratio, health, altroot. // name, size, allocated, free, checkpoint, expandsize, fragmentation, capacity, dedupratio, health, altroot.
let (i, (text, size, alloc, free, _, _, let (i, (text, size, alloc, free, _, _, frag, _, dedup, health, _altroot, _eol)) = tuple((
frag, _, dedup, health,
_altroot, _eol)) = tuple((
take_while1(|c| char::is_alphanumeric(c) || c == '-' || c == ':' || c == '_' || c == '.'), // name take_while1(|c| char::is_alphanumeric(c) || c == '-' || c == ':' || c == '_' || c == '.'), // name
preceded(multispace1, parse_optional_u64), // size preceded(multispace1, parse_optional_u64), // size
preceded(multispace1, parse_optional_u64), // allocated preceded(multispace1, parse_optional_u64), // allocated
@ -78,11 +73,19 @@ fn parse_zpool_list_header(i: &str) -> IResult<&str, ZFSPoolInfo> {
line_ending, line_ending,
))(i)?; ))(i)?;
let status = if let (Some(size), Some(alloc), Some(free), Some(frag), Some(dedup)) = (size, alloc, free, frag, dedup) { let status = if let (Some(size), Some(alloc), Some(free), Some(frag), Some(dedup)) =
(size, alloc, free, frag, dedup)
{
ZFSPoolInfo { ZFSPoolInfo {
name: text.into(), name: text.into(),
health: health.into(), health: health.into(),
usage: Some(ZFSPoolUsage { size, alloc, free, frag, dedup }), usage: Some(ZFSPoolUsage {
size,
alloc,
free,
frag,
dedup,
}),
devices: Vec::new(), devices: Vec::new(),
} }
} else { } else {
@ -98,7 +101,6 @@ fn parse_zpool_list_header(i: &str) -> IResult<&str, ZFSPoolInfo> {
} }
fn parse_zpool_list_item(i: &str) -> IResult<&str, ZFSPoolInfo> { fn parse_zpool_list_item(i: &str) -> IResult<&str, ZFSPoolInfo> {
let (i, mut stat) = parse_zpool_list_header(i)?; let (i, mut stat) = parse_zpool_list_header(i)?;
let (i, devices) = many0(parse_pool_device)(i)?; let (i, devices) = many0(parse_pool_device)(i)?;
@ -117,9 +119,11 @@ fn parse_zpool_list_item(i: &str) -> IResult<&str, ZFSPoolInfo> {
/// the zpool list output format is not really defined... /// the zpool list output format is not really defined...
fn parse_zpool_list(i: &str) -> Result<Vec<ZFSPoolInfo>, Error> { fn parse_zpool_list(i: &str) -> Result<Vec<ZFSPoolInfo>, Error> {
match all_consuming(many0(parse_zpool_list_item))(i) { match all_consuming(many0(parse_zpool_list_item))(i) {
Err(nom::Err::Error(err)) | Err(nom::Err::Error(err)) | Err(nom::Err::Failure(err)) => {
Err(nom::Err::Failure(err)) => { bail!(
bail!("unable to parse zfs list output - {}", nom::error::convert_error(i, err)); "unable to parse zfs list output - {}",
nom::error::convert_error(i, err)
);
} }
Err(err) => { Err(err) => {
bail!("unable to parse zfs list output - {}", err); bail!("unable to parse zfs list output - {}", err);
@ -133,7 +137,6 @@ fn parse_zpool_list(i: &str) -> Result<Vec<ZFSPoolInfo>, Error> {
/// Devices are only included when run with verbose flags /// Devices are only included when run with verbose flags
/// set. Without, device lists are empty. /// set. Without, device lists are empty.
pub fn zpool_list(pool: Option<String>, verbose: bool) -> Result<Vec<ZFSPoolInfo>, Error> { pub fn zpool_list(pool: Option<String>, verbose: bool) -> Result<Vec<ZFSPoolInfo>, Error> {
// Note: zpools list verbose output can include entries for 'special', 'cache' and 'logs' // Note: zpools list verbose output can include entries for 'special', 'cache' and 'logs'
// and maybe other things. // and maybe other things.
@ -143,9 +146,13 @@ pub fn zpool_list(pool: Option<String>, verbose: bool) -> Result<Vec<ZFSPoolInfo
// Note: We do not use -o to define output properties, because zpool command ignores // Note: We do not use -o to define output properties, because zpool command ignores
// that completely for special vdevs and devices // that completely for special vdevs and devices
if verbose { command.arg("-v"); } if verbose {
command.arg("-v");
}
if let Some(pool) = pool { command.arg(pool); } if let Some(pool) = pool {
command.arg(pool);
}
let output = proxmox_sys::command::run_command(command, None)?; let output = proxmox_sys::command::run_command(command, None)?;
@ -154,7 +161,6 @@ pub fn zpool_list(pool: Option<String>, verbose: bool) -> Result<Vec<ZFSPoolInfo
#[test] #[test]
fn test_zfs_parse_list() -> Result<(), Error> { fn test_zfs_parse_list() -> Result<(), Error> {
let output = ""; let output = "";
let data = parse_zpool_list(output)?; let data = parse_zpool_list(output)?;
@ -164,8 +170,7 @@ fn test_zfs_parse_list() -> Result<(), Error> {
let output = "btest 427349245952 405504 427348840448 - - 0 0 1.00 ONLINE -\n"; let output = "btest 427349245952 405504 427348840448 - - 0 0 1.00 ONLINE -\n";
let data = parse_zpool_list(output)?; let data = parse_zpool_list(output)?;
let expect = vec![ let expect = vec![ZFSPoolInfo {
ZFSPoolInfo {
name: "btest".to_string(), name: "btest".to_string(),
health: "ONLINE".to_string(), health: "ONLINE".to_string(),
devices: Vec::new(), devices: Vec::new(),
@ -195,10 +200,12 @@ logs
ZFSPoolInfo { ZFSPoolInfo {
name: String::from("rpool"), name: String::from("rpool"),
health: String::from("ONLINE"), health: String::from("ONLINE"),
devices: vec![String::from("/dev/disk/by-id/ata-Crucial_CT500MX200SSD1_154210EB4078-part3")], devices: vec![String::from(
"/dev/disk/by-id/ata-Crucial_CT500MX200SSD1_154210EB4078-part3",
)],
usage: Some(ZFSPoolUsage { usage: Some(ZFSPoolUsage {
size: 535260299264, size: 535260299264,
alloc:402852388864 , alloc: 402852388864,
free: 132407910400, free: 132407910400,
dedup: 1.0, dedup: 1.0,
frag: 22, frag: 22,
@ -249,7 +256,7 @@ logs - - - - - - - - -
String::from("/dev/sda2"), String::from("/dev/sda2"),
String::from("/dev/sda3"), String::from("/dev/sda3"),
String::from("/dev/sda4"), String::from("/dev/sda4"),
] ],
}, },
ZFSPoolInfo { ZFSPoolInfo {
name: String::from("logs"), name: String::from("logs"),
@ -268,8 +275,7 @@ b.test 427349245952 761856 427348484096 - - 0 0 1.00 ONLINE -
"; ";
let data = parse_zpool_list(output)?; let data = parse_zpool_list(output)?;
let expect = vec![ let expect = vec![ZFSPoolInfo {
ZFSPoolInfo {
name: String::from("b.test"), name: String::from("b.test"),
health: String::from("ONLINE"), health: String::from("ONLINE"),
usage: Some(ZFSPoolUsage { usage: Some(ZFSPoolUsage {
@ -279,11 +285,8 @@ b.test 427349245952 761856 427348484096 - - 0 0 1.00 ONLINE -
dedup: 1.0, dedup: 1.0,
frag: 0, frag: 0,
}), }),
devices: vec![ devices: vec![String::from("/dev/sda1")],
String::from("/dev/sda1"), }];
]
},
];
assert_eq!(data, expect); assert_eq!(data, expect);

View File

@ -5,32 +5,31 @@ use serde::{Deserialize, Serialize};
use serde_json::{Map, Value}; use serde_json::{Map, Value};
use pbs_tools::nom::{ use pbs_tools::nom::{
parse_complete, parse_error, parse_failure, multispace0, multispace1, notspace1, parse_complete, parse_error, parse_failure, parse_u64,
multispace0, multispace1, notspace1, parse_u64, IResult, IResult,
}; };
use nom::{ use nom::{
bytes::complete::{tag, take_while, take_while1}, bytes::complete::{tag, take_while, take_while1},
combinator::{opt}, character::complete::line_ending,
sequence::{preceded}, combinator::opt,
character::complete::{line_ending}, multi::{many0, many1},
multi::{many0,many1}, sequence::preceded,
}; };
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct ZFSPoolVDevState { pub struct ZFSPoolVDevState {
pub name: String, pub name: String,
pub lvl: u64, pub lvl: u64,
#[serde(skip_serializing_if="Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub state: Option<String>, pub state: Option<String>,
#[serde(skip_serializing_if="Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub read: Option<u64>, pub read: Option<u64>,
#[serde(skip_serializing_if="Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub write: Option<u64>, pub write: Option<u64>,
#[serde(skip_serializing_if="Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub cksum: Option<u64>, pub cksum: Option<u64>,
#[serde(skip_serializing_if="Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub msg: Option<String>, pub msg: Option<String>,
} }
@ -39,7 +38,6 @@ fn expand_tab_length(input: &str) -> usize {
} }
fn parse_zpool_status_vdev(i: &str) -> IResult<&str, ZFSPoolVDevState> { fn parse_zpool_status_vdev(i: &str) -> IResult<&str, ZFSPoolVDevState> {
let (n, indent) = multispace0(i)?; let (n, indent) = multispace0(i)?;
let indent_len = expand_tab_length(indent); let indent_len = expand_tab_length(indent);
@ -49,11 +47,12 @@ fn parse_zpool_status_vdev(i: &str) -> IResult<&str, ZFSPoolVDevState> {
} }
let i = n; let i = n;
let indent_level = (indent_len as u64)/2; let indent_level = (indent_len as u64) / 2;
let (i, vdev_name) = notspace1(i)?; let (i, vdev_name) = notspace1(i)?;
if let Ok((n, _)) = preceded(multispace0, line_ending)(i) { // special device if let Ok((n, _)) = preceded(multispace0, line_ending)(i) {
// special device
let vdev = ZFSPoolVDevState { let vdev = ZFSPoolVDevState {
name: vdev_name.to_string(), name: vdev_name.to_string(),
lvl: indent_level, lvl: indent_level,
@ -67,7 +66,8 @@ fn parse_zpool_status_vdev(i: &str) -> IResult<&str, ZFSPoolVDevState> {
} }
let (i, state) = preceded(multispace1, notspace1)(i)?; let (i, state) = preceded(multispace1, notspace1)(i)?;
if let Ok((n, _)) = preceded(multispace0, line_ending)(i) { // spares if let Ok((n, _)) = preceded(multispace0, line_ending)(i) {
// spares
let vdev = ZFSPoolVDevState { let vdev = ZFSPoolVDevState {
name: vdev_name.to_string(), name: vdev_name.to_string(),
lvl: indent_level, lvl: indent_level,
@ -100,7 +100,6 @@ fn parse_zpool_status_vdev(i: &str) -> IResult<&str, ZFSPoolVDevState> {
} }
fn parse_zpool_status_tree(i: &str) -> IResult<&str, Vec<ZFSPoolVDevState>> { fn parse_zpool_status_tree(i: &str) -> IResult<&str, Vec<ZFSPoolVDevState>> {
// skip header // skip header
let (i, _) = tag("NAME")(i)?; let (i, _) = tag("NAME")(i)?;
let (i, _) = multispace1(i)?; let (i, _) = multispace1(i)?;
@ -130,8 +129,10 @@ fn space_indented_line(indent: usize) -> impl Fn(&str) -> IResult<&str, &str> {
break; break;
} }
n = &n[1..]; n = &n[1..];
if len >= indent { break; } if len >= indent {
}; break;
}
}
if len != indent { if len != indent {
return Err(parse_error(i, "not correctly indented")); return Err(parse_error(i, "not correctly indented"));
} }
@ -144,7 +145,9 @@ fn parse_zpool_status_field(i: &str) -> IResult<&str, (String, String)> {
let (i, prefix) = take_while1(|c| c != ':')(i)?; let (i, prefix) = take_while1(|c| c != ':')(i)?;
let (i, _) = tag(":")(i)?; let (i, _) = tag(":")(i)?;
let (i, mut value) = take_while(|c| c != '\n')(i)?; let (i, mut value) = take_while(|c| c != '\n')(i)?;
if value.starts_with(' ') { value = &value[1..]; } if value.starts_with(' ') {
value = &value[1..];
}
let (mut i, _) = line_ending(i)?; let (mut i, _) = line_ending(i)?;
@ -169,7 +172,9 @@ fn parse_zpool_status_field(i: &str) -> IResult<&str, (String, String)> {
if let Some(cont) = cont { if let Some(cont) = cont {
let (n, _) = line_ending(n)?; let (n, _) = line_ending(n)?;
i = n; i = n;
if !value.is_empty() { value.push('\n'); } if !value.is_empty() {
value.push('\n');
}
value.push_str(cont); value.push_str(cont);
} else { } else {
if field == "config" { if field == "config" {
@ -233,7 +238,9 @@ where
while vdev_level < cur.level { while vdev_level < cur.level {
cur.children_of_parent.push(Value::Object(cur.node)); cur.children_of_parent.push(Value::Object(cur.node));
let mut parent = stack.pop().unwrap(); let mut parent = stack.pop().unwrap();
parent.node.insert("children".to_string(), Value::Array(cur.children_of_parent)); parent
.node
.insert("children".to_string(), Value::Array(cur.children_of_parent));
parent.node.insert("leaf".to_string(), Value::Bool(false)); parent.node.insert("leaf".to_string(), Value::Bool(false));
cur = parent; cur = parent;
@ -252,16 +259,17 @@ where
}); });
} else { } else {
// same indentation level, add to children of the previous level: // same indentation level, add to children of the previous level:
cur.children_of_parent.push(Value::Object( cur.children_of_parent
replace(&mut cur.node, node), .push(Value::Object(replace(&mut cur.node, node)));
));
} }
} }
while !stack.is_empty() { while !stack.is_empty() {
cur.children_of_parent.push(Value::Object(cur.node)); cur.children_of_parent.push(Value::Object(cur.node));
let mut parent = stack.pop().unwrap(); let mut parent = stack.pop().unwrap();
parent.node.insert("children".to_string(), Value::Array(cur.children_of_parent)); parent
.node
.insert("children".to_string(), Value::Array(cur.children_of_parent));
parent.node.insert("leaf".to_string(), Value::Bool(false)); parent.node.insert("leaf".to_string(), Value::Bool(false));
cur = parent; cur = parent;
} }
@ -281,6 +289,7 @@ fn test_vdev_list_to_tree() {
msg: None, msg: None,
}; };
#[rustfmt::skip]
let input = vec![ let input = vec![
//ZFSPoolVDevState { name: "root".to_string(), lvl: 0, ..DEFAULT }, //ZFSPoolVDevState { name: "root".to_string(), lvl: 0, ..DEFAULT },
ZFSPoolVDevState { name: "vdev1".to_string(), lvl: 1, ..DEFAULT }, ZFSPoolVDevState { name: "vdev1".to_string(), lvl: 1, ..DEFAULT },
@ -351,16 +360,14 @@ fn test_vdev_list_to_tree() {
}],\ }],\
\"leaf\":false\ \"leaf\":false\
}"; }";
let expected: Value = serde_json::from_str(EXPECTED) let expected: Value =
.expect("failed to parse expected json value"); serde_json::from_str(EXPECTED).expect("failed to parse expected json value");
let tree = vdev_list_to_tree(&input) let tree = vdev_list_to_tree(&input).expect("failed to turn valid vdev list into a tree");
.expect("failed to turn valid vdev list into a tree");
assert_eq!(tree, expected); assert_eq!(tree, expected);
} }
pub fn zpool_status(pool: &str) -> Result<Vec<(String, String)>, Error> { pub fn zpool_status(pool: &str) -> Result<Vec<(String, String)>, Error> {
let mut command = std::process::Command::new("zpool"); let mut command = std::process::Command::new("zpool");
command.args(&["status", "-p", "-P", pool]); command.args(&["status", "-p", "-P", pool]);
@ -390,7 +397,6 @@ fn test_parse(output: &str) -> Result<(), Error> {
#[test] #[test]
fn test_zpool_status_parser() -> Result<(), Error> { fn test_zpool_status_parser() -> Result<(), Error> {
let output = r###" pool: tank let output = r###" pool: tank
state: DEGRADED state: DEGRADED
status: One or more devices could not be opened. Sufficient replicas exist for status: One or more devices could not be opened. Sufficient replicas exist for
@ -418,7 +424,6 @@ errors: No known data errors
#[test] #[test]
fn test_zpool_status_parser2() -> Result<(), Error> { fn test_zpool_status_parser2() -> Result<(), Error> {
// Note: this input create TABS // Note: this input create TABS
let output = r###" pool: btest let output = r###" pool: btest
state: ONLINE state: ONLINE
@ -443,7 +448,6 @@ errors: No known data errors
#[test] #[test]
fn test_zpool_status_parser3() -> Result<(), Error> { fn test_zpool_status_parser3() -> Result<(), Error> {
let output = r###" pool: bt-est let output = r###" pool: bt-est
state: ONLINE state: ONLINE
scan: none requested scan: none requested
@ -468,7 +472,6 @@ errors: No known data errors
#[test] #[test]
fn test_zpool_status_parser_spares() -> Result<(), Error> { fn test_zpool_status_parser_spares() -> Result<(), Error> {
let output = r###" pool: tank let output = r###" pool: tank
state: ONLINE state: ONLINE
scan: none requested scan: none requested