#!/usr/bin/env python3 """Collecte les métriques PVE (RAM, swap, load, disque, état VMs/CTs) et les ajoute à un fichier CSV mensuel. Conçu pour être appelé toutes les 15 min via cron (no_agent=True).""" import csv, os, subprocess, json, sys from datetime import datetime OUTPUT_DIR = os.path.expanduser("~/.hermes/cron/output/pve-monitor") os.makedirs(OUTPUT_DIR, exist_ok=True) # Fichier CSV mensuel (ex: metrics-2026-07.csv) now = datetime.now() MONTH_CSV = os.path.join(OUTPUT_DIR, f"metrics-{now:%Y-%m}.csv") # Champs du CSV (même format que l'ancien système) FIELDS = [ "timestamp", "epoch", "mem_total_mb", "mem_used_mb", "mem_avail_mb", "mem_pct", "swap_total_mb", "swap_used_mb", "swap_pct", "load_1", "load_5", "load_15", "disk_total_gb", "disk_used_gb", "disk_avail_gb", "disk_pct", "arc_size_mb", ] def ssh_cmd(host, cmd): """Exécute une commande SSH et retourne stdout.""" r = subprocess.run( ["ssh", "-o", "ConnectTimeout=5", "-o", "StrictHostKeyChecking=no", f"root@{host}", cmd], capture_output=True, text=True, timeout=15 ) if r.returncode != 0: raise RuntimeError(f"SSH error on {host}: {r.stderr.strip()}") return r.stdout.strip() def collect(): row = {} row["timestamp"] = now.strftime("%Y-%m-%d %H:%M:%S") row["epoch"] = str(int(now.timestamp())) try: # RAM / Swap out = ssh_cmd("192.168.0.20", r"free -m | awk '/^Mem:/{print $2,$3,$7} /^Swap:/{print $2,$3}'") lines = out.split("\n") mem_total, mem_used, mem_avail = lines[0].split() swap_total, swap_used = lines[1].split() if len(lines) > 1 else ("0", "0") row["mem_total_mb"] = mem_total row["mem_used_mb"] = mem_used row["mem_avail_mb"] = mem_avail row["mem_pct"] = f"{float(mem_used) / float(mem_total) * 100:.1f}" if float(mem_total) > 0 else "0" row["swap_total_mb"] = swap_total row["swap_used_mb"] = swap_used row["swap_pct"] = f"{float(swap_used) / float(swap_total) * 100:.1f}" if float(swap_total) > 0 else "0" # Load average load = ssh_cmd("192.168.0.20", "cat /proc/loadavg | awk '{print $1,$2,$3}'") l1, l5, l15 = load.split() row["load_1"] = l1 row["load_5"] = l5 row["load_15"] = l15 # Disque / disk = ssh_cmd("192.168.0.20", r"df -BG / | awk 'NR==2{print $2,$3,$4,$5}'") parts = disk.replace("G", "").replace("%", "").split() row["disk_total_gb"] = parts[0] row["disk_used_gb"] = parts[1] row["disk_avail_gb"] = parts[2] row["disk_pct"] = parts[3] # ARC ZFS arc = ssh_cmd("192.168.0.20", r"cat /proc/spl/kstat/zfs/arcstats 2>/dev/null | grep -E '^size ' | awk '{print $3}' || echo 0") row["arc_size_mb"] = f"{int(arc) // (1024*1024)}" if arc.isdigit() else "0" # VMs vm_out = ssh_cmd("192.168.0.20", "qm list 2>/dev/null | awk 'NR>1{print $1,$3}'") for line in vm_out.split("\n"): if not line.strip(): continue parts = line.split() vid = parts[0] vstatus = parts[1] if len(parts) > 1 else "unknown" # Récupérer la RAM allouée mem = ssh_cmd("192.168.0.20", f"qm config {vid} 2>/dev/null | grep -i '^memory:' | awk '{{print $2}}' || echo 0") row[f"vm_{vid}_mem_gb"] = f"{int(mem) // 1024}" if mem.isdigit() else "0" row[f"vm_{vid}_status"] = vstatus if f"vm_{vid}_mem_gb" not in FIELDS: FIELDS.append(f"vm_{vid}_mem_gb") if f"vm_{vid}_status" not in FIELDS: FIELDS.append(f"vm_{vid}_status") # CTs ct_out = ssh_cmd("192.168.0.20", "pct list 2>/dev/null | awk 'NR>1{print $1,$2}'") for line in ct_out.split("\n"): if not line.strip(): continue parts = line.split() cid = parts[0] cstatus = parts[1] if len(parts) > 1 else "unknown" mem = ssh_cmd("192.168.0.20", f"pct config {cid} 2>/dev/null | grep -i '^memory:' | awk '{{print $2}}' || echo 0") row[f"ct_{cid}_mem_gb"] = f"{int(mem) // 1024}" if mem.isdigit() else "0" row[f"ct_{cid}_status"] = cstatus if f"ct_{cid}_mem_gb" not in FIELDS: FIELDS.append(f"ct_{cid}_mem_gb") if f"ct_{cid}_status" not in FIELDS: FIELDS.append(f"ct_{cid}_status") except Exception as e: print(f"ERREUR collecte: {e}", file=sys.stderr) # Écrire quand même une ligne avec timestamp pour marquer l'échec for f in FIELDS: row.setdefault(f, "ERROR") # Écrire dans le CSV mensuel write_header = not os.path.isfile(MONTH_CSV) with open(MONTH_CSV, "a", newline="") as f: writer = csv.DictWriter(f, fieldnames=FIELDS) if write_header: writer.writeheader() writer.writerow(row) print(f"OK {now:%H:%M} — {os.path.basename(MONTH_CSV)} ({len(row)} champs)") if __name__ == "__main__": collect()