src/tools/disks/smart.rs: parse output from smartctl
This commit is contained in:
parent
c26aad405f
commit
eb80aac288
|
@ -20,6 +20,8 @@ mod zfs;
|
||||||
pub use zfs::*;
|
pub use zfs::*;
|
||||||
mod lvm;
|
mod lvm;
|
||||||
pub use lvm::*;
|
pub use lvm::*;
|
||||||
|
mod smart;
|
||||||
|
pub use smart::*;
|
||||||
|
|
||||||
bitflags! {
|
bitflags! {
|
||||||
/// Ways a device is being used.
|
/// Ways a device is being used.
|
||||||
|
|
|
@ -0,0 +1,181 @@
|
||||||
|
use anyhow::{format_err, Error};
|
||||||
|
use ::serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use proxmox::api::api;
|
||||||
|
|
||||||
|
#[api()]
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all="lowercase")]
|
||||||
|
/// SMART status
|
||||||
|
pub enum SmartStatus {
|
||||||
|
/// Smart tests passed - everything is OK
|
||||||
|
Passed,
|
||||||
|
/// Smart tests failed - disk has problems
|
||||||
|
Failed,
|
||||||
|
/// Unknown status
|
||||||
|
Unknown,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[api()]
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
/// SMART Attribute
|
||||||
|
pub struct SmartAttribute {
|
||||||
|
/// Attribute name
|
||||||
|
name: String,
|
||||||
|
/// Attribute raw value
|
||||||
|
value: String,
|
||||||
|
// the rest of the values is available for ATA type
|
||||||
|
/// ATA Attribute ID
|
||||||
|
#[serde(skip_serializing_if="Option::is_none")]
|
||||||
|
id: Option<u64>,
|
||||||
|
/// ATA Flags
|
||||||
|
#[serde(skip_serializing_if="Option::is_none")]
|
||||||
|
flags: Option<String>,
|
||||||
|
/// ATA normalized value (0..100)
|
||||||
|
#[serde(skip_serializing_if="Option::is_none")]
|
||||||
|
normalized: Option<f64>,
|
||||||
|
/// ATA worst
|
||||||
|
#[serde(skip_serializing_if="Option::is_none")]
|
||||||
|
worst: Option<f64>,
|
||||||
|
/// ATA threshold
|
||||||
|
#[serde(skip_serializing_if="Option::is_none")]
|
||||||
|
threshold: Option<f64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[api(
|
||||||
|
properties: {
|
||||||
|
status: {
|
||||||
|
type: SmartStatus,
|
||||||
|
},
|
||||||
|
wearout: {
|
||||||
|
description: "Wearout level.",
|
||||||
|
type: f64,
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
attributes: {
|
||||||
|
description: "SMART attributes.",
|
||||||
|
type: Array,
|
||||||
|
items: {
|
||||||
|
type: SmartAttribute,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)]
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
/// Data from smartctl
|
||||||
|
pub struct SmartData {
|
||||||
|
status: SmartStatus,
|
||||||
|
wearout: Option<f64>,
|
||||||
|
attributes: Vec<SmartAttribute>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read smartctl data for a disk (/dev/XXX).
|
||||||
|
pub fn get_smart_data(
|
||||||
|
disk: &str,
|
||||||
|
health_only: bool,
|
||||||
|
) -> Result<SmartData, Error> {
|
||||||
|
|
||||||
|
const SMARTCTL_BIN_PATH: &str = "/usr/sbin/smartctl";
|
||||||
|
|
||||||
|
let mut command = std::process::Command::new(SMARTCTL_BIN_PATH);
|
||||||
|
command.arg("-H");
|
||||||
|
if !health_only { command.args(&["-A", "-j"]); }
|
||||||
|
command.arg(disk);
|
||||||
|
|
||||||
|
let output = command.output()
|
||||||
|
.map_err(|err| format_err!("failed to execute '{}' - {}", SMARTCTL_BIN_PATH, err))?;
|
||||||
|
|
||||||
|
let output = crate::tools::command_output(output, None)
|
||||||
|
.map_err(|err| format_err!("smartctl command failed: {}", err))?;
|
||||||
|
|
||||||
|
let output: serde_json::Value = output.parse()?;
|
||||||
|
|
||||||
|
let mut wearout = None;
|
||||||
|
|
||||||
|
let mut attributes = Vec::new();
|
||||||
|
|
||||||
|
// ATA devices
|
||||||
|
if let Some(list) = output["ata_smart_attributes"]["table"].as_array() {
|
||||||
|
for item in list {
|
||||||
|
let id = match item["id"].as_u64() {
|
||||||
|
Some(id) => id,
|
||||||
|
None => continue, // skip attributes without id
|
||||||
|
};
|
||||||
|
|
||||||
|
let name = match item["name"].as_str() {
|
||||||
|
Some(name) => name.to_string(),
|
||||||
|
None => continue, // skip attributes without name
|
||||||
|
};
|
||||||
|
|
||||||
|
let raw_value = match item["raw"]["string"].as_str() {
|
||||||
|
Some(value) => value.to_string(),
|
||||||
|
None => continue, // skip attributes without raw value
|
||||||
|
};
|
||||||
|
|
||||||
|
let flags = match item["flags"]["string"].as_str() {
|
||||||
|
Some(flags) => flags.to_string(),
|
||||||
|
None => continue, // skip attributes without flags
|
||||||
|
};
|
||||||
|
|
||||||
|
let normalized = match item["value"].as_f64() {
|
||||||
|
Some(v) => v,
|
||||||
|
None => continue, // skip attributes without normalize value
|
||||||
|
};
|
||||||
|
|
||||||
|
let worst = match item["worst"].as_f64() {
|
||||||
|
Some(v) => v,
|
||||||
|
None => continue, // skip attributes without worst entry
|
||||||
|
};
|
||||||
|
|
||||||
|
let threshold = match item["thresh"].as_f64() {
|
||||||
|
Some(v) => v,
|
||||||
|
None => continue, // skip attributes without threshold entry
|
||||||
|
};
|
||||||
|
|
||||||
|
attributes.push(SmartAttribute {
|
||||||
|
name,
|
||||||
|
value: raw_value,
|
||||||
|
id: Some(id),
|
||||||
|
flags: Some(flags),
|
||||||
|
normalized: Some(normalized),
|
||||||
|
worst: Some(worst),
|
||||||
|
threshold: Some(threshold),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NVME devices
|
||||||
|
if let Some(list) = output["nvme_smart_health_information_log"].as_object() {
|
||||||
|
for (name, value) in list {
|
||||||
|
if name == "percentage_used" {
|
||||||
|
// extract wearout from nvme text, allow for decimal values
|
||||||
|
if let Some(v) = value.as_f64() {
|
||||||
|
if v <= 100.0 {
|
||||||
|
wearout = Some(100.0 - v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(value) = value.as_f64() {
|
||||||
|
attributes.push(SmartAttribute {
|
||||||
|
name: name.to_string(),
|
||||||
|
value: value.to_string(),
|
||||||
|
id: None,
|
||||||
|
flags: None,
|
||||||
|
normalized: None,
|
||||||
|
worst: None,
|
||||||
|
threshold: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let status = match output["smart_status"]["passed"].as_bool() {
|
||||||
|
None => SmartStatus::Unknown,
|
||||||
|
Some(true) => SmartStatus::Passed,
|
||||||
|
Some(false) => SmartStatus::Failed,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
Ok(SmartData { status, wearout, attributes })
|
||||||
|
}
|
Loading…
Reference in New Issue