#!/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())