Les hyperviseurs exposaient temperatures, ventilateurs, SMART et usure NVMe sans que rien les regarde. scripts/materiel.py porte capteurs et seuils ; Grafana en tire le tableau Materiel, Icinga un service materiel par hyperviseur. Premier verdict : le disque sda de gandalf (OSD Ceph) a 56 secteurs en attente, stables. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
150 lines
5.9 KiB
Python
150 lines
5.9 KiB
Python
#!/usr/bin/env python3
|
|
"""Verdict materiel d'un equipement — Icinga le lance, Prometheus lui repond.
|
|
|
|
GERE par Set-OPS (role serveur_icinga). Ne pas editer a la main.
|
|
|
|
POURQUOI UN CONTROLE ACTIF QUI LIT PROMETHEUS (2026-09-17). Les hyperviseurs ne peuvent rien
|
|
pousser (route par defaut gelee, D-57) et la frontiere n'accueille aucun agent. Prometheus,
|
|
lui, les TIRE deja. On juge donc ce qu'il a recolte, avec le catalogue que le tableau
|
|
Grafana affiche (`scripts/materiel.py`) : memes capteurs, memes seuils.
|
|
|
|
UNE MESURE QUI ECHOUE N'EST PAS UN MATERIEL SAIN. Prometheus injoignable, ou aucune serie
|
|
pour l'equipement : UNKNOWN, jamais OK.
|
|
|
|
Usage :
|
|
check_materiel.py --hote asgard --catalogue /etc/icinga2/setops-materiel.json
|
|
check_materiel.py --hote asgard --catalogue ... --joignabilite # controle d'hote
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
import urllib.error
|
|
import urllib.parse
|
|
import urllib.request
|
|
|
|
OK, AVERT, CRIT, INCONNU = 0, 1, 2, 3
|
|
ETATS = {OK: "OK", AVERT: "AVERTISSEMENT", CRIT: "CRITIQUE", INCONNU: "INCONNU"}
|
|
JOBS = 'job=~"fabric|frontiere"'
|
|
|
|
|
|
class Muette(Exception):
|
|
pass
|
|
|
|
|
|
def requete(prom: str, expr: str) -> list:
|
|
url = prom.rstrip("/") + "/api/v1/query?" + urllib.parse.urlencode({"query": expr})
|
|
try:
|
|
with urllib.request.urlopen(url, timeout=15) as r:
|
|
rep = json.load(r)
|
|
except urllib.error.HTTPError as exc:
|
|
# UNE REQUETE REFUSEE N'EST PAS UN PROMETHEUS INJOIGNABLE (eprouve le 2026-09-17 : un
|
|
# 422 a d'abord ete rapporte comme une panne de Prometheus). On dit ce qu'il repond.
|
|
try:
|
|
corps = json.load(exc)
|
|
except ValueError:
|
|
corps = {}
|
|
raise Muette(f"Prometheus refuse une requete ({exc.code}) : "
|
|
f"{corps.get('error', exc.reason)}") from exc
|
|
except (OSError, ValueError) as exc:
|
|
raise Muette(f"Prometheus injoignable ({prom}) : {exc}") from exc
|
|
if rep.get("status") != "success":
|
|
raise Muette(f"requete refusee par Prometheus : {rep.get('error', '?')}")
|
|
return rep["data"]["result"]
|
|
|
|
|
|
def joignabilite(prom: str, hote: str) -> int:
|
|
series = requete(prom, f'up{{{JOBS},hote="{hote}"}}')
|
|
if not series:
|
|
print(f"INCONNU — Prometheus ne connait aucune cible nommee « {hote} ».")
|
|
return INCONNU
|
|
if all(float(s["value"][1]) == 1 for s in series):
|
|
print(f"OK — {hote} repond a Prometheus.")
|
|
return OK
|
|
print(f"CRITIQUE — l'exportateur de {hote} ne repond plus a Prometheus : equipement "
|
|
f"eteint, injoignable, ou exportateur arrete.")
|
|
return CRIT
|
|
|
|
|
|
def verdict(prom: str, hote: str, cat: dict) -> int:
|
|
code = joignabilite_silencieuse(prom, hote)
|
|
if code is not None:
|
|
return code
|
|
|
|
etat = OK
|
|
lignes, perf, resume = [], [], []
|
|
for f in cat["temperatures"]:
|
|
expr = (f'max by (hote, capteur) (avg_over_time(({f["expr_juge"]})'
|
|
f'[{f["jugee_sur"]}:1m]))')
|
|
mesures = sorted(
|
|
(s["metric"].get("capteur", "?"), float(s["value"][1]))
|
|
for s in requete(prom, expr) if s["metric"].get("hote") == hote)
|
|
if not mesures:
|
|
continue # famille sans capteur sur cet equipement : ni panne ni oubli
|
|
pire = max(v for _, v in mesures)
|
|
niveau = CRIT if pire >= f["crit"] else AVERT if pire >= f["avert"] else OK
|
|
etat = max(etat, niveau)
|
|
detail = ", ".join(f"{c} {v:.0f} °C" for c, v in mesures)
|
|
lignes.append(f"[{ETATS[niveau]}] {f['titre']} : {detail} "
|
|
f"(seuils {f['avert']}/{f['crit']} °C, moyenne {f['jugee_sur']})")
|
|
if niveau != OK:
|
|
resume.append(f"{f['titre']} {pire:.0f} °C")
|
|
for c, v in mesures:
|
|
perf.append(f"'{f['cle']} {c}'={v:.1f};{f['avert']};{f['crit']}")
|
|
|
|
for c in cat["controles"]:
|
|
defauts = sorted({s["metric"].get("capteur", "?")
|
|
for s in requete(prom, c["expr"]) if s["metric"].get("hote") == hote})
|
|
if not defauts:
|
|
continue
|
|
niveau = CRIT if c["niveau"] == "crit" else AVERT
|
|
etat = max(etat, niveau)
|
|
lignes.append(f"[{ETATS[niveau]}] {c['titre']} : {', '.join(defauts)} — {c['raison']}")
|
|
resume.append(f"{c['titre']} ({', '.join(defauts)})")
|
|
|
|
if not perf:
|
|
print(f"INCONNU — aucun capteur de temperature relu pour {hote} : le catalogue ne "
|
|
f"correspond a rien sur cet equipement.")
|
|
return INCONNU
|
|
tete = "; ".join(resume) if resume else f"{len(perf)} capteur(s) dans les seuils, aucun defaut disque ni ventilateur"
|
|
print(f"{ETATS[etat]} — {tete} | {' '.join(perf)}")
|
|
print("\n".join(lignes))
|
|
return etat
|
|
|
|
|
|
def joignabilite_silencieuse(prom: str, hote: str) -> int | None:
|
|
series = requete(prom, f'up{{{JOBS},hote="{hote}"}}')
|
|
if not series:
|
|
print(f"INCONNU — Prometheus ne connait aucune cible nommee « {hote} ».")
|
|
return INCONNU
|
|
if not any(float(s["value"][1]) == 1 for s in series):
|
|
print(f"INCONNU — l'exportateur de {hote} ne repond pas : aucun releve a juger "
|
|
f"(le controle d'hote le dit).")
|
|
return INCONNU
|
|
return None
|
|
|
|
|
|
def main() -> int:
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("--hote", required=True)
|
|
ap.add_argument("--catalogue", required=True)
|
|
ap.add_argument("--prometheus", default="http://127.0.0.1:9090")
|
|
ap.add_argument("--joignabilite", action="store_true")
|
|
a = ap.parse_args()
|
|
try:
|
|
if a.joignabilite:
|
|
return joignabilite(a.prometheus, a.hote)
|
|
with open(a.catalogue, encoding="utf-8") as fh:
|
|
cat = json.load(fh)
|
|
return verdict(a.prometheus, a.hote, cat)
|
|
except Muette as exc:
|
|
print(f"INCONNU — {exc}")
|
|
return INCONNU
|
|
except (OSError, ValueError, KeyError) as exc:
|
|
print(f"INCONNU — catalogue illisible ou incomplet : {exc}")
|
|
return INCONNU
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|