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::*;
|
||||
mod lvm;
|
||||
pub use lvm::*;
|
||||
mod smart;
|
||||
pub use smart::*;
|
||||
|
||||
bitflags! {
|
||||
/// 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