4 Commits

Author SHA1 Message Date
Gilles Soulier 7fb47ffde8 fix(smart v0.1.17): smart_status optionnel + AmbientCapabilities CAP_SYS_ADMIN
- SmartJson.smart_status devient Option<SmartStatus> avec #[serde(default)]
  → parsing non-bloquant si le champ est absent (ex: NVME_IOCTL_ADMIN_CMD échoue)
- Service: suppression NoNewPrivileges, ajout AmbientCapabilities=CAP_SYS_ADMIN
  → smartctl hérite la capability via execve (kernel ≥ 5.2)
- Nettoyage logs debug (suppression dump JSON brut)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 14:10:53 +02:00
Gilles Soulier 3c15943e2e debug(smart v0.1.16): log JSON brut complet en cas d'échec parse
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 13:51:30 +02:00
Gilles Soulier a9506a5505 fix(smart v0.1.15): contrôleur NVMe + règle udev disk group
Cause racine : smartctl -a -j /dev/nvme0n1 (namespace) retourne exit 4
et omet smart_status car les commandes admin échouent via le namespace.
Solution : utiliser /dev/nvme0 (contrôleur) accessible grâce à la règle
udev SUBSYSTEM==nvme GROUP=disk.

- smart.rs : scan /sys/class/nvme/ pour les contrôleurs (nvme0, nvme1)
  au lieu de /sys/block/ pour les namespaces (nvme0n1)
- deploy/99-nanometrics-smart.rules : udev rule KERNEL==nvme* GROUP=disk
- deploy/install.sh : déploie la règle udev + udevadm trigger

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 13:39:13 +02:00
Gilles Soulier ee5e8710a3 fix(smart v0.1.14): filtre NVMe correct + SmartStatus défensif
- Filtre nvme : n[4..].contains('n') au lieu de n.contains('n')
  pour distinguer nvme0n1 (namespace) de nvme0 (contrôleur)
- SmartStatus.passed : #[serde(default)] pour éviter crash si absent

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 13:20:04 +02:00
6 changed files with 52 additions and 27 deletions
+1 -1
View File
@@ -248,7 +248,7 @@ dependencies = [
[[package]]
name = "nanometrics-agent"
version = "0.1.13"
version = "0.1.17"
dependencies = [
"libc",
"rumqttc",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nanometrics-agent"
version = "0.1.13"
version = "0.1.17"
edition = "2021"
[lib]
+29 -24
View File
@@ -2,14 +2,18 @@ use serde::Deserialize;
#[derive(Deserialize)]
struct SmartJson {
smart_status: SmartStatus,
#[serde(default)]
smart_status: Option<SmartStatus>,
temperature: Option<SmartTemp>,
ata_smart_attributes: Option<SmartAttrs>,
nvme_smart_health_information_log: Option<NvmeHealth>,
}
#[derive(Deserialize)]
struct SmartStatus { passed: bool }
struct SmartStatus {
#[serde(default)]
passed: bool,
}
#[derive(Deserialize)]
struct SmartTemp {
@@ -75,7 +79,7 @@ pub fn parse_json(json: &str) -> Result<crate::payload::SmartMetrics, serde_json
Ok(crate::payload::SmartMetrics {
device: String::new(),
passed: s.smart_status.passed,
passed: s.smart_status.as_ref().map(|s| s.passed).unwrap_or(false),
temperature,
reallocated_sectors: reallocated,
power_on_hours: power_hours,
@@ -88,23 +92,28 @@ pub fn collect() -> Option<Vec<crate::payload::SmartMetrics>> {
eprintln!("[smart] smartctl introuvable dans PATH");
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') {
Some(format!("/dev/{}", n))
} else {
None
}
})
.collect::<std::collections::HashSet<_>>()
.into_iter()
.collect();
let mut set = std::collections::HashSet::new();
// SATA/SAS : /sys/block/sd* → /dev/sda, /dev/sdb…
for e in std::fs::read_dir("/sys/block").into_iter().flatten().flatten() {
let n = e.file_name().into_string().unwrap_or_default();
if n.starts_with("sd") {
set.insert(format!("/dev/{}", n));
}
}
// NVMe : /sys/class/nvme/nvme* → /dev/nvme0, /dev/nvme1…
// On utilise le contrôleur (char device), pas le namespace (block device),
// car smartctl ne peut exécuter les commandes admin SMART que via le contrôleur.
// La règle udev 99-nanometrics-smart.rules lui donne l'accès groupe disk.
for e in std::fs::read_dir("/sys/class/nvme").into_iter().flatten().flatten() {
let n = e.file_name().into_string().unwrap_or_default();
if n.starts_with("nvme") {
set.insert(format!("/dev/{}", n));
}
}
let mut devs: Vec<String> = set.into_iter().collect();
devs.sort();
eprintln!("[smart] disques détectés: {:?}", devs);
@@ -117,12 +126,9 @@ pub fn collect() -> Option<Vec<crate::payload::SmartMetrics>> {
Ok(o) => o,
Err(e) => { eprintln!("[smart] erreur exec smartctl {}: {}", dev, e); continue }
};
eprintln!("[smart] smartctl {} → exit={}, stdout={} octets, stderr={} octets",
dev, output.status, output.stdout.len(), output.stderr.len());
let json = String::from_utf8_lossy(&output.stdout);
match parse_json(&json) {
Ok(metrics) => {
eprintln!("[smart] {} parsé OK (passed={})", dev, metrics.passed);
results.push(crate::payload::SmartMetrics {
device: dev.trim_start_matches("/dev/").to_string(),
..metrics
@@ -130,7 +136,6 @@ pub fn collect() -> Option<Vec<crate::payload::SmartMetrics>> {
}
Err(e) => {
eprintln!("[smart] {} parse JSON échoué: {}", dev, e);
eprintln!("[smart] premiers 200 octets stdout: {:?}", &json.chars().take(200).collect::<String>());
}
}
}
+4
View File
@@ -0,0 +1,4 @@
# Nanometrics: accès groupe disk aux contrôleurs NVMe pour SMART
# Sans cette règle, /dev/nvme0 est crw------- root root (root only),
# ce qui empêche smartctl d'exécuter les commandes admin et omet smart_status du JSON.
KERNEL=="nvme[0-9]*", SUBSYSTEM=="nvme", GROUP="disk", MODE="0660"
+11
View File
@@ -45,6 +45,17 @@ else
fi
echo ""
# ── 2. Règle udev NVMe (accès SMART pour le groupe disk) ──────────────────────
UDEV_RULE="/etc/udev/rules.d/99-nanometrics-smart.rules"
cat > "$UDEV_RULE" << 'UDEVRULE'
# Nanometrics: accès groupe disk aux contrôleurs NVMe pour SMART
KERNEL=="nvme[0-9]*", SUBSYSTEM=="nvme", GROUP="disk", MODE="0660"
UDEVRULE
udevadm control --reload-rules
udevadm trigger --subsystem-match=nvme 2>/dev/null || true
ok "Règle udev NVMe installée ($UDEV_RULE)"
echo ""
# ── 3. Détection de l'architecture ────────────────────────────────────────────
ARCH="$(uname -m)"
case "$ARCH" in
+6 -1
View File
@@ -17,7 +17,12 @@ ConfigurationDirectoryMode=0755
ProtectSystem=strict
ProtectHome=read-only
PrivateTmp=yes
NoNewPrivileges=yes
# CAP_SYS_ADMIN est requis par le noyau pour NVME_IOCTL_ADMIN_CMD (lecture SMART NVMe).
# NoNewPrivileges est retiré car il efface les ambient capabilities sur exec (noyau ≥ 5.2),
# ce qui empêcherait smartctl enfant d'hériter la capability.
# CapabilityBoundingSet borne à la seule cap nécessaire.
CapabilityBoundingSet=CAP_SYS_ADMIN
AmbientCapabilities=CAP_SYS_ADMIN
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX