126 lines
5.0 KiB
Python
126 lines
5.0 KiB
Python
#!/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()
|