tools: disk: rustfmt
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
This commit is contained in:
parent
a1c906cb02
commit
af6fdb9d0d
|
@ -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)?;
|
||||||
|
|
||||||
|
|
|
@ -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"),
|
||||||
|
@ -444,19 +447,19 @@ impl Disk {
|
||||||
/// another kernel driver like the device mapper.
|
/// another kernel driver like the device mapper.
|
||||||
pub fn has_holders(&self) -> io::Result<bool> {
|
pub fn has_holders(&self) -> io::Result<bool> {
|
||||||
Ok(*self
|
Ok(*self
|
||||||
.info
|
.info
|
||||||
.has_holders
|
.has_holders
|
||||||
.get_or_try_init(|| -> io::Result<bool> {
|
.get_or_try_init(|| -> io::Result<bool> {
|
||||||
let mut subdir = self.syspath().to_owned();
|
let mut subdir = self.syspath().to_owned();
|
||||||
subdir.push("holders");
|
subdir.push("holders");
|
||||||
for entry in std::fs::read_dir(subdir)? {
|
for entry in std::fs::read_dir(subdir)? {
|
||||||
match entry?.file_name().as_bytes() {
|
match entry?.file_name().as_bytes() {
|
||||||
b"." | b".." => (),
|
b"." | b".." => (),
|
||||||
_ => return Ok(true),
|
_ => return Ok(true),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(false)
|
Ok(false)
|
||||||
})?)
|
})?)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if this disk is mounted.
|
/// Check if this disk is mounted.
|
||||||
|
@ -473,26 +476,28 @@ 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],
|
||||||
read_sectors: stat[2],
|
read_sectors: stat[2],
|
||||||
write_ios: stat[4] + stat[11], // write + discard
|
write_ios: stat[4] + stat[11], // write + discard
|
||||||
write_sectors: stat[6] + stat[13], // write + discard
|
write_sectors: stat[6] + stat[13], // write + discard
|
||||||
io_ticks: stat[10],
|
io_ticks: stat[10],
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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;
|
||||||
|
|
||||||
|
@ -709,9 +712,9 @@ fn scan_partitions(
|
||||||
found_dm = true;
|
found_dm = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if zfs_devices.contains(&devnum) {
|
if zfs_devices.contains(&devnum) {
|
||||||
found_zfs = true;
|
found_zfs = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if found_mountpoints {
|
if found_mountpoints {
|
||||||
|
@ -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,15 +751,15 @@ 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 =
|
||||||
eprintln!("error getting zfs devices: {}", err);
|
zfs_devices(&lsblk_info, None).or_else(|err| -> Result<HashSet<u64>, Error> {
|
||||||
Ok(HashSet::new())
|
eprintln!("error getting zfs devices: {}", err);
|
||||||
})?;
|
Ok(HashSet::new())
|
||||||
|
})?;
|
||||||
|
|
||||||
let lvm_devices = get_lvm_devices(&lsblk_info)?;
|
let lvm_devices = get_lvm_devices(&lsblk_info)?;
|
||||||
|
|
||||||
|
@ -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
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -849,7 +856,7 @@ pub fn get_disks(
|
||||||
usage = DiskUsageType::DeviceMapper;
|
usage = DiskUsageType::DeviceMapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut status = SmartStatus::Unknown;
|
let mut status = SmartStatus::Unknown;
|
||||||
let mut wearout = None;
|
let mut wearout = None;
|
||||||
|
|
||||||
if !no_smart {
|
if !no_smart {
|
||||||
|
@ -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 =
|
||||||
Ok(dir) => dir,
|
match proxmox_sys::fs::scan_subdir(libc::AT_FDCWD, "/sys/block", &BLOCKDEVICE_NAME_REGEX) {
|
||||||
Err(_) => return vec![],
|
Ok(dir) => dir,
|
||||||
};
|
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()),
|
||||||
|
|
|
@ -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();
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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,44 +58,49 @@ 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
|
||||||
preceded(multispace1, parse_optional_u64), // free
|
preceded(multispace1, parse_optional_u64), // free
|
||||||
preceded(multispace1, notspace1), // checkpoint
|
preceded(multispace1, notspace1), // checkpoint
|
||||||
preceded(multispace1, notspace1), // expandsize
|
preceded(multispace1, notspace1), // expandsize
|
||||||
preceded(multispace1, parse_optional_u64), // fragmentation
|
preceded(multispace1, parse_optional_u64), // fragmentation
|
||||||
preceded(multispace1, notspace1), // capacity
|
preceded(multispace1, notspace1), // capacity
|
||||||
preceded(multispace1, parse_optional_f64), // dedup
|
preceded(multispace1, parse_optional_f64), // dedup
|
||||||
preceded(multispace1, notspace1), // health
|
preceded(multispace1, notspace1), // health
|
||||||
opt(preceded(multispace1, notspace1)), // optional altroot
|
opt(preceded(multispace1, notspace1)), // optional altroot
|
||||||
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 {
|
||||||
ZFSPoolInfo {
|
ZFSPoolInfo {
|
||||||
name: text.into(),
|
name: text.into(),
|
||||||
health: health.into(),
|
health: health.into(),
|
||||||
usage: None,
|
usage: None,
|
||||||
devices: Vec::new(),
|
devices: Vec::new(),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok((i, status))
|
Ok((i, status))
|
||||||
}
|
}
|
||||||
|
|
||||||
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,19 +170,18 @@ 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(),
|
usage: Some(ZFSPoolUsage {
|
||||||
usage: Some(ZFSPoolUsage {
|
size: 427349245952,
|
||||||
size: 427349245952,
|
alloc: 405504,
|
||||||
alloc: 405504,
|
free: 427348840448,
|
||||||
free: 427348840448,
|
dedup: 1.0,
|
||||||
dedup: 1.0,
|
frag: 0,
|
||||||
frag: 0,
|
}),
|
||||||
}),
|
}];
|
||||||
}];
|
|
||||||
|
|
||||||
assert_eq!(data, expect);
|
assert_eq!(data, expect);
|
||||||
|
|
||||||
|
@ -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,22 +275,18 @@ 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 {
|
size: 427349245952,
|
||||||
size: 427349245952,
|
alloc: 761856,
|
||||||
alloc: 761856,
|
free: 427348484096,
|
||||||
free: 427348484096,
|
dedup: 1.0,
|
||||||
dedup: 1.0,
|
frag: 0,
|
||||||
frag: 0,
|
}),
|
||||||
}),
|
devices: vec![String::from("/dev/sda1")],
|
||||||
devices: vec![
|
}];
|
||||||
String::from("/dev/sda1"),
|
|
||||||
]
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
assert_eq!(data, expect);
|
assert_eq!(data, expect);
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue