017d7bb1bb
- /sys/block expose nvme0n1 (namespace), mais smartctl a besoin du contrôleur nvme0 → déduplication via HashSet pour éviter les doublons nvme0n1/nvme0 - smartctl -j → smartctl -a -j pour inclure nvme_smart_health_information_log (sans -a, le log de santé NVMe n'est pas dans la sortie JSON) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
126 lines
3.5 KiB
Rust
126 lines
3.5 KiB
Rust
use serde::Deserialize;
|
|
|
|
#[derive(Deserialize)]
|
|
struct SmartJson {
|
|
smart_status: SmartStatus,
|
|
temperature: Option<SmartTemp>,
|
|
ata_smart_attributes: Option<SmartAttrs>,
|
|
nvme_smart_health_information_log: Option<NvmeHealth>,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct SmartStatus { passed: bool }
|
|
|
|
#[derive(Deserialize)]
|
|
struct SmartTemp { current: i64 }
|
|
|
|
#[derive(Deserialize)]
|
|
struct SmartAttrs { table: Vec<SmartAttr> }
|
|
|
|
#[derive(Deserialize)]
|
|
struct SmartAttr {
|
|
id: u8,
|
|
raw: SmartAttrRaw,
|
|
value: Option<i64>,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct SmartAttrRaw { value: i64 }
|
|
|
|
#[derive(Deserialize)]
|
|
struct NvmeHealth {
|
|
percentage_used: Option<i64>,
|
|
temperature: Option<i64>,
|
|
}
|
|
|
|
pub fn is_available() -> bool {
|
|
std::process::Command::new("which")
|
|
.arg("smartctl")
|
|
.output()
|
|
.map(|o| o.status.success())
|
|
.unwrap_or(false)
|
|
}
|
|
|
|
pub fn parse_json(json: &str) -> Result<crate::payload::SmartMetrics, serde_json::Error> {
|
|
let s: SmartJson = serde_json::from_str(json)?;
|
|
|
|
let temperature = s.temperature.as_ref().map(|t| t.current)
|
|
.or_else(|| s.nvme_smart_health_information_log.as_ref()?.temperature);
|
|
|
|
let mut reallocated = None;
|
|
let mut power_hours = None;
|
|
let mut wear = None;
|
|
|
|
if let Some(attrs) = &s.ata_smart_attributes {
|
|
for attr in &attrs.table {
|
|
match attr.id {
|
|
5 => reallocated = Some(attr.raw.value),
|
|
9 => power_hours = Some(attr.raw.value),
|
|
177 => wear = attr.value,
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
if wear.is_none() {
|
|
if let Some(nvme) = &s.nvme_smart_health_information_log {
|
|
if let Some(pct) = nvme.percentage_used {
|
|
wear = Some(100 - pct);
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(crate::payload::SmartMetrics {
|
|
device: String::new(),
|
|
passed: s.smart_status.passed,
|
|
temperature,
|
|
reallocated_sectors: reallocated,
|
|
power_on_hours: power_hours,
|
|
wear_level: wear,
|
|
})
|
|
}
|
|
|
|
pub fn collect() -> Option<Vec<crate::payload::SmartMetrics>> {
|
|
if !is_available() {
|
|
return None;
|
|
}
|
|
let mut devs: Vec<String> = std::fs::read_dir("/sys/block")
|
|
.into_iter()
|
|
.flatten()
|
|
.flatten()
|
|
.map(|e| e.file_name().into_string().unwrap_or_default())
|
|
.filter_map(|n| {
|
|
if n.starts_with("sd") {
|
|
Some(format!("/dev/{}", n))
|
|
} else if n.starts_with("nvme") && !n.contains('n') {
|
|
// /sys/block contient nvme0n1 (namespace) — on utilise le contrôleur nvme0
|
|
Some(format!("/dev/{}", n))
|
|
} else if n.starts_with("nvme") && n.contains('n') {
|
|
// Déduit le contrôleur depuis nvme0n1 → nvme0
|
|
let ctrl = n.split('n').next()?;
|
|
Some(format!("/dev/{}", ctrl))
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.collect::<std::collections::HashSet<_>>()
|
|
.into_iter()
|
|
.collect();
|
|
devs.sort();
|
|
|
|
let mut results = Vec::new();
|
|
for dev in &devs {
|
|
let Ok(output) = std::process::Command::new("smartctl")
|
|
.args(["-a", "-j", dev])
|
|
.output() else { continue };
|
|
let json = String::from_utf8_lossy(&output.stdout);
|
|
if let Ok(metrics) = parse_json(&json) {
|
|
results.push(crate::payload::SmartMetrics {
|
|
device: dev.trim_start_matches("/dev/").to_string(),
|
|
..metrics
|
|
});
|
|
}
|
|
}
|
|
if results.is_empty() { None } else { Some(results) }
|
|
}
|