diff --git a/agent/Cargo.lock b/agent/Cargo.lock index 8f666ea..e5cd6c3 100644 --- a/agent/Cargo.lock +++ b/agent/Cargo.lock @@ -248,7 +248,7 @@ dependencies = [ [[package]] name = "nanometrics-agent" -version = "0.1.4" +version = "0.1.5" dependencies = [ "libc", "rumqttc", diff --git a/agent/Cargo.toml b/agent/Cargo.toml index 03f8da7..a965f48 100644 --- a/agent/Cargo.toml +++ b/agent/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nanometrics-agent" -version = "0.1.4" +version = "0.1.5" edition = "2021" [lib] diff --git a/agent/src/metrics/smart.rs b/agent/src/metrics/smart.rs index a29420e..d83f9ba 100644 --- a/agent/src/metrics/smart.rs +++ b/agent/src/metrics/smart.rs @@ -1,5 +1,4 @@ use serde::Deserialize; -use crate::payload::SmartMetrics; #[derive(Deserialize)] struct SmartJson { @@ -42,7 +41,7 @@ pub fn is_available() -> bool { .unwrap_or(false) } -pub fn parse_json(json: &str) -> Result { +pub fn parse_json(json: &str) -> Result { let s: SmartJson = serde_json::from_str(json)?; let temperature = s.temperature.as_ref().map(|t| t.current) @@ -71,7 +70,8 @@ pub fn parse_json(json: &str) -> Result { } } - Ok(SmartMetrics { + Ok(crate::payload::SmartMetrics { + device: String::new(), passed: s.smart_status.passed, temperature, reallocated_sectors: reallocated, @@ -80,11 +80,10 @@ pub fn parse_json(json: &str) -> Result { }) } -pub fn collect() -> Option { +pub fn collect() -> Option> { if !is_available() { return None; } - // Détecter les disques réels depuis /sys/block (sd*, nvme*) let mut devs: Vec = std::fs::read_dir("/sys/block") .into_iter() .flatten() @@ -95,14 +94,18 @@ pub fn collect() -> Option { .collect(); devs.sort(); + let mut results = Vec::new(); for dev in &devs { let Ok(output) = std::process::Command::new("smartctl") .args(["-j", dev]) .output() else { continue }; let json = String::from_utf8_lossy(&output.stdout); if let Ok(metrics) = parse_json(&json) { - return Some(metrics); + results.push(crate::payload::SmartMetrics { + device: dev.trim_start_matches("/dev/").to_string(), + ..metrics + }); } } - None + if results.is_empty() { None } else { Some(results) } } diff --git a/agent/src/payload.rs b/agent/src/payload.rs index 4f955e0..d33fa57 100644 --- a/agent/src/payload.rs +++ b/agent/src/payload.rs @@ -18,11 +18,13 @@ pub struct AgentMetrics { pub network_rx: Option, pub network_tx: Option, pub temperature: Option, - pub smart: Option, + pub smart: Option>, } #[derive(Serialize, Deserialize, Debug, Clone)] pub struct SmartMetrics { + #[serde(default)] + pub device: String, pub passed: bool, pub temperature: Option, pub reallocated_sectors: Option, diff --git a/agent/tests/payload_test.rs b/agent/tests/payload_test.rs index 98adde6..85cf06b 100644 --- a/agent/tests/payload_test.rs +++ b/agent/tests/payload_test.rs @@ -32,13 +32,14 @@ fn test_serialize_avec_smart() { let m = AgentMetrics { hostname: "srv-01".to_string(), ip: "10.0.0.11".to_string(), - smart: Some(SmartMetrics { + smart: Some(vec![SmartMetrics { + device: "sda".to_string(), passed: true, temperature: Some(34), reallocated_sectors: Some(0), power_on_hours: Some(4213), wear_level: Some(98), - }), + }]), status: "online".to_string(), ..Default::default() }; diff --git a/dashboard/js/grid.js b/dashboard/js/grid.js index e0cf66b..6d3fafb 100644 --- a/dashboard/js/grid.js +++ b/dashboard/js/grid.js @@ -77,10 +77,11 @@ const Grid = (() => { uptimeStr = d > 0 ? `${d}j ${h}h` : `${h}h`; } - const smartIco = !offline && metrics?.smart != null - ? (metrics.smart.passed - ? `` - : ``) + const smartIco = !offline && metrics?.smart?.length > 0 + ? metrics.smart.map(s => s.passed + ? `` + : `` + ).join('') : ''; const iconContent = ` { ? `
min ${Grid.fmt(ramMin)}max ${Grid.fmt(ramMax)}
` : ''; - const smartBtn = metrics?.smart - ? `
-
- SMART + const smartBtn = metrics?.smart?.length > 0 + ? metrics.smart.map((s, i) => ` +
+
+ ${esc(s.device) || 'disque'} · - ${metrics.smart.passed ? 'PASSED' : 'FAILED'} - ${metrics.smart.temperature ? ` ${metrics.smart.temperature}°C` : ''} + ${s.passed ? 'PASSED' : 'FAILED'} + ${s.temperature != null ? ` ${s.temperature}°C` : ''} -
` +
`).join('') : ''; const protos = [ @@ -389,10 +390,11 @@ const Popups = (() => { } // ══ POPUP SMART ══ - function showSmart(agentId) { - const m = Grid.getAgent(agentId)?.metrics?.smart; - if (!m) return; - document.getElementById('smart-sub').textContent = agentId; + function showSmart(agentId, diskIdx = 0) { + const smartList = Grid.getAgent(agentId)?.metrics?.smart; + if (!smartList?.length) return; + const m = smartList[diskIdx] ?? smartList[0]; + document.getElementById('smart-sub').textContent = m.device ? `${agentId} — ${m.device}` : agentId; const passColor = m.passed ? 'var(--ok)' : 'var(--err)'; const passText = m.passed ? 'Disque en bonne santé' : 'Disque en mauvais état'; const passSub = m.passed diff --git a/server/db/db.go b/server/db/db.go index 226dcba..3f1ff11 100644 --- a/server/db/db.go +++ b/server/db/db.go @@ -67,6 +67,7 @@ func (d *DB) migrate() error { _, _ = d.conn.Exec(`ALTER TABLE metrics ADD COLUMN smart_realloc INTEGER`) _, _ = d.conn.Exec(`ALTER TABLE metrics ADD COLUMN smart_hours INTEGER`) _, _ = d.conn.Exec(`ALTER TABLE metrics ADD COLUMN smart_wear INTEGER`) + _, _ = d.conn.Exec(`ALTER TABLE metrics ADD COLUMN smart_json TEXT`) return nil } @@ -86,30 +87,24 @@ func (d *DB) UpsertAgent(m *models.AgentMetrics) error { func (d *DB) InsertMetrics(m *models.AgentMetrics) error { ts := time.Now().Unix() - var smartPassed, smartTemp, smartRealloc, smartHours, smartWear interface{} - if m.Smart != nil { - b := 0 - if m.Smart.Passed { - b = 1 + var smartJSON interface{} + if len(m.Smart) > 0 { + if b, err := json.Marshal(m.Smart); err == nil { + smartJSON = string(b) } - smartPassed = b - smartTemp = m.Smart.Temperature - smartRealloc = m.Smart.ReallocatedSectors - smartHours = m.Smart.PowerOnHours - smartWear = m.Smart.WearLevel } _, err := d.conn.Exec(` INSERT INTO metrics (agent_id, ts, cpu_percent, memory_used, memory_free, memory_total, hdd_used, hdd_free, hdd_total, uptime, network_rx, network_tx, temperature, - smart_passed, smart_temp, smart_realloc, smart_hours, smart_wear) - VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`, + smart_json) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)`, m.Hostname, ts, m.CPUPercent, m.MemoryUsed, m.MemoryFree, m.MemoryTotal, m.HDDUsed, m.HDDFree, m.HDDTotal, m.Uptime, m.NetworkRX, m.NetworkTX, m.Temperature, - smartPassed, smartTemp, smartRealloc, smartHours, smartWear) + smartJSON) return err } @@ -137,11 +132,8 @@ func (d *DB) GetLastMetrics(agentID string) (*models.AgentMetrics, error) { var cpu, temperature *float64 var memUsed, memFree, memTotal, hddUsed, hddFree, hddTotal *int64 var uptime, netRX, netTX *int64 - var smartPassed, smartTemp, smartRealloc, smartHours, smartWear *int64 + var smartJSON *string - // Chaque sous-requête prend la dernière valeur NON NULL de sa colonne. - // Nécessaire car les paquets rapides (2s) ne contiennent pas les métriques - // lentes (disque, smart) qui sont envoyées toutes les 60s. err := d.conn.QueryRow(` SELECT (SELECT cpu_percent FROM metrics WHERE agent_id=? AND cpu_percent IS NOT NULL ORDER BY ts DESC LIMIT 1), @@ -155,19 +147,15 @@ func (d *DB) GetLastMetrics(agentID string) (*models.AgentMetrics, error) { (SELECT network_rx FROM metrics WHERE agent_id=? AND network_rx IS NOT NULL ORDER BY ts DESC LIMIT 1), (SELECT network_tx FROM metrics WHERE agent_id=? AND network_tx IS NOT NULL ORDER BY ts DESC LIMIT 1), (SELECT temperature FROM metrics WHERE agent_id=? AND temperature IS NOT NULL ORDER BY ts DESC LIMIT 1), - (SELECT smart_passed FROM metrics WHERE agent_id=? AND smart_passed IS NOT NULL ORDER BY ts DESC LIMIT 1), - (SELECT smart_temp FROM metrics WHERE agent_id=? AND smart_passed IS NOT NULL ORDER BY ts DESC LIMIT 1), - (SELECT smart_realloc FROM metrics WHERE agent_id=? AND smart_passed IS NOT NULL ORDER BY ts DESC LIMIT 1), - (SELECT smart_hours FROM metrics WHERE agent_id=? AND smart_passed IS NOT NULL ORDER BY ts DESC LIMIT 1), - (SELECT smart_wear FROM metrics WHERE agent_id=? AND smart_passed IS NOT NULL ORDER BY ts DESC LIMIT 1)`, + (SELECT smart_json FROM metrics WHERE agent_id=? AND smart_json IS NOT NULL ORDER BY ts DESC LIMIT 1)`, agentID, agentID, agentID, agentID, agentID, agentID, agentID, agentID, agentID, agentID, agentID, - agentID, agentID, agentID, agentID, agentID). + agentID). Scan(&cpu, &memUsed, &memFree, &memTotal, &hddUsed, &hddFree, &hddTotal, &uptime, &netRX, &netTX, &temperature, - &smartPassed, &smartTemp, &smartRealloc, &smartHours, &smartWear) + &smartJSON) if err == sql.ErrNoRows { return nil, nil } @@ -187,14 +175,8 @@ func (d *DB) GetLastMetrics(agentID string) (*models.AgentMetrics, error) { NetworkTX: netTX, Temperature: temperature, } - if smartPassed != nil { - m.Smart = &models.SmartMetrics{ - Passed: *smartPassed == 1, - Temperature: smartTemp, - ReallocatedSectors: smartRealloc, - PowerOnHours: smartHours, - WearLevel: smartWear, - } + if smartJSON != nil { + _ = json.Unmarshal([]byte(*smartJSON), &m.Smart) } return m, nil } diff --git a/server/models/models.go b/server/models/models.go index 857e55e..f8d4c39 100644 --- a/server/models/models.go +++ b/server/models/models.go @@ -16,10 +16,11 @@ type AgentMetrics struct { NetworkRX *int64 `json:"network_rx"` NetworkTX *int64 `json:"network_tx"` Temperature *float64 `json:"temperature"` - Smart *SmartMetrics `json:"smart"` + Smart []SmartMetrics `json:"smart"` } type SmartMetrics struct { + Device string `json:"device"` Passed bool `json:"passed"` Temperature *int64 `json:"temperature"` ReallocatedSectors *int64 `json:"reallocated_sectors"`