diff --git a/src/tools/disks.rs b/src/tools/disks.rs index 68ef6305..c79f7190 100644 --- a/src/tools/disks.rs +++ b/src/tools/disks.rs @@ -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. diff --git a/src/tools/disks/smart.rs b/src/tools/disks/smart.rs new file mode 100644 index 00000000..7b11f64d --- /dev/null +++ b/src/tools/disks/smart.rs @@ -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, + /// ATA Flags + #[serde(skip_serializing_if="Option::is_none")] + flags: Option, + /// ATA normalized value (0..100) + #[serde(skip_serializing_if="Option::is_none")] + normalized: Option, + /// ATA worst + #[serde(skip_serializing_if="Option::is_none")] + worst: Option, + /// ATA threshold + #[serde(skip_serializing_if="Option::is_none")] + threshold: Option, +} + + +#[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, + attributes: Vec, +} + +/// Read smartctl data for a disk (/dev/XXX). +pub fn get_smart_data( + disk: &str, + health_only: bool, +) -> Result { + + 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 }) +}