Initial import: monitoring, power-management, maintenance, web scripts

This commit is contained in:
2026-08-05 12:26:50 +02:00
commit 1936ded08f
10 changed files with 1306 additions and 0 deletions
+125
View File
@@ -0,0 +1,125 @@
#!/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()