Le tableau des panneaux vivait DANS la condition de l exportateur. Le premier role a declarer des panneaux sans exportateur — client_metrique, dont le job node est universel — affichait « aucun exportateur declare » et cachait ses quatre panneaux. Une fiche qui tait ce qu un role declare est pire qu une fiche absente : elle affirme que rien n existe. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
227 lines
11 KiB
Python
Executable file
227 lines
11 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
"""La fiche d'un role : comment il est branche dans l'ecosysteme.
|
|
|
|
GENEREE, JAMAIS ECRITE — ET C'EST TOUT L'INTERET.
|
|
|
|
Soixante-huit roles. Autant de pages faites a la main seraient perimees avant la fin du
|
|
mois : c'est le defaut que ce depot traque partout ailleurs sous le nom « une liste qui
|
|
suit une autre prend du retard ». Une soixante-neuvieme liste ne ferait pas exception.
|
|
|
|
CE QUE LA FICHE LIT, ELLE NE L'INVENTE PAS. Chaque rôle DECLARE deja ce qu'il faudrait
|
|
dessiner, et chaque declaration porte sa RAISON :
|
|
|
|
meta/flux.yml qui lui parle, sur quel port, et pourquoi
|
|
meta/authentification.yml qui il laisse entrer
|
|
meta/empreinte.yml ce qu'il coute
|
|
meta/supervision.yml quel verdict il rend
|
|
meta/metriques.yml quelles series il expose
|
|
|
|
UNE SECTION VIDE EST UNE INFORMATION, PAS UN DEFAUT DE LA FICHE. Un role qui ne declare
|
|
aucune supervision l'affiche ; la fiche devient alors la carte de ce qui reste a faire,
|
|
tenue a jour toute seule.
|
|
|
|
CE QU'ELLE N'EST PAS : les pages de flux ecrites a la main racontent « par ou passe
|
|
l'heure, un certificat, un courriel » — un mecanisme qui traverse plusieurs machines, et
|
|
qu'on LIT pour comprendre. Cette fiche-ci repond a « ce role, il est branche comment ? »,
|
|
et on la CONSULTE pour agir. Aucune ne remplace l'autre.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import yaml
|
|
|
|
RACINE = Path(__file__).resolve().parents[1]
|
|
ROLES = RACINE / "roles"
|
|
# UN SEUL DOMICILE, ET C'EST LE WIKI. Ces fiches sont faites pour etre LUES, et le wiki
|
|
# est l'endroit ou l'on lit — « on lit la-bas, on ecrit ici ». Les garder aussi dans
|
|
# `docs/` en ferait deux copies du meme contenu, donc deux listes dont l'une prendrait du
|
|
# retard sur l'autre. On connait la suite.
|
|
#
|
|
# LE PREFIXE GROUPE. Vingt-sept pages redigees a la main vivent deja ici ; soixante-huit
|
|
# fiches generees les noieraient dans l'index de la forge. `Role-` les rassemble, et
|
|
# `Roles.md` sert de porte d'entree.
|
|
SORTIE = RACINE / "wiki"
|
|
PREFIXE = "Rôle-"
|
|
|
|
|
|
def lire(role: str, nom: str):
|
|
f = ROLES / role / "meta" / f"{nom}.yml"
|
|
if not f.is_file():
|
|
return None
|
|
try:
|
|
return yaml.safe_load(f.read_text(encoding="utf-8"))
|
|
except yaml.YAMLError as e:
|
|
return {"_erreur": str(e)}
|
|
|
|
|
|
def court(r: str) -> str:
|
|
"""`serveur_postgresql` -> `postgresql`. Les diagrammes se lisent mieux courts."""
|
|
return r.split("_", 1)[1] if "_" in r else r
|
|
|
|
|
|
def diagramme(role: str, flux, supervision, metriques) -> list[str]:
|
|
"""Le role au centre ; ce qui entre a gauche, ce qu'il rend a droite.
|
|
|
|
MERMAID, PAS UNE IMAGE. Une image se regenere a la main et diverge ; un bloc mermaid
|
|
vit dans le fichier, se relit en diff, et se rend aussi bien sur la forge que dans une
|
|
page publiee.
|
|
"""
|
|
L = ["```mermaid", "graph LR"]
|
|
moi = court(role)
|
|
L.append(f' R["<b>{moi}</b>"]')
|
|
|
|
entrants = []
|
|
for fl in (flux or {}).get("flux", []):
|
|
if fl.get("sens") != "ingress":
|
|
continue
|
|
ports = fl["port"] if isinstance(fl["port"], list) else [fl["port"]]
|
|
for p in (fl.get("pair") if isinstance(fl.get("pair"), list) else [fl.get("pair")]):
|
|
if p:
|
|
entrants.append((court(str(p)), "/".join(str(x) for x in ports),
|
|
str(fl.get("chiffrement", ""))))
|
|
for i, (src, ports, chif) in enumerate(sorted(set(entrants))):
|
|
sur = f"{ports}" + (f" · {chif}" if chif and chif not in ("n-a", "aucun") else "")
|
|
L.append(f' E{i}["{src}"] -->|"{sur}"| R')
|
|
|
|
for s in (supervision or {}).get("sondes", []):
|
|
L.append(f' R -.->|"sonde « {s["nom"]} »"| ICINGA[["Icinga"]]')
|
|
expo = (metriques or {}).get("exportateur")
|
|
if expo:
|
|
L.append(f' R -.->|"{expo.get("port")} · {expo.get("job")}"| PROM[["Prometheus"]]')
|
|
|
|
L.append("```")
|
|
return L
|
|
|
|
|
|
def fiche(role: str) -> str:
|
|
flux = lire(role, "flux")
|
|
sup = lire(role, "supervision")
|
|
met = lire(role, "metriques")
|
|
emp = lire(role, "empreinte")
|
|
aut = lire(role, "authentification")
|
|
|
|
L = [f"# Rôle `{role}`", "",
|
|
"> **Généré** par `scripts/fiche_role.py` depuis les `meta/` de ce rôle.",
|
|
"> Ne pas éditer à la main : corriger la déclaration, puis régénérer.", "",
|
|
"> **Pour qui :** celui qui doit agir sur ce rôle et veut savoir, avant de "
|
|
"toucher quoi que ce soit, qui lui parle, ce qu'il rend, et ce qu'il coûte.", ""]
|
|
|
|
L += diagramme(role, flux, sup, met) + [""]
|
|
|
|
L += ["## Qui lui parle", ""]
|
|
entrants = [f for f in (flux or {}).get("flux", []) if f.get("sens") == "ingress"]
|
|
if entrants:
|
|
L += ["| port | depuis | chiffrement | pourquoi |", "|---|---|---|---|"]
|
|
for f in entrants:
|
|
pairs = f.get("pair") if isinstance(f.get("pair"), list) else [f.get("pair")]
|
|
L.append(f"| `{f['port']}` | {', '.join(f'`{p}`' for p in pairs if p) or '—'} "
|
|
f"| {f.get('chiffrement', '—')} | {f.get('raison', '').strip()} |")
|
|
else:
|
|
L.append("*Aucun flux entrant déclaré — `meta/flux.yml` absent ou sans `ingress`.*")
|
|
L.append("")
|
|
|
|
L += ["## Ce qu'il rend à la supervision", ""]
|
|
if (sup or {}).get("sondes"):
|
|
L += ["| sonde | TTL | ce qu'elle voit |", "|---|---|---|"]
|
|
for s in sup["sondes"]:
|
|
L.append(f"| `{s['nom']}` | {s.get('ttl', '—')} s | {s.get('raison', '').strip()} |")
|
|
else:
|
|
L.append("*Aucune sonde déclarée. Un service qui porte de l'état et n'en déclare "
|
|
"aucune mérite qu'on se demande pourquoi — voir `docs/supervision-conception.md`.*")
|
|
L.append("")
|
|
|
|
# LES DEUX MOITIES DE `metriques.yml` SONT INDEPENDANTES, ET LA FICHE LES LISAIT
|
|
# IMBRIQUEES (corrige le 2026-09-14). Le tableau des panneaux vivait DANS la condition
|
|
# de l'exportateur : un role qui declare des panneaux SANS exportateur — comme
|
|
# `client_metrique`, dont le job `node` est universel et ecrit ailleurs — affichait
|
|
# « aucun exportateur declare » et cachait ses quatre panneaux.
|
|
#
|
|
# Une fiche qui tait ce qu'un role declare est pire qu'une fiche absente : elle
|
|
# affirme que rien n'existe.
|
|
L += ["## Ce qu'il expose en séries", ""]
|
|
expo = (met or {}).get("exportateur")
|
|
panneaux = (met or {}).get("panneaux") or []
|
|
if expo:
|
|
L += [f"Exportateur `{expo.get('paquet')}` — port **{expo.get('port')}**, "
|
|
f"job `{expo.get('job')}`.", ""]
|
|
elif panneaux:
|
|
L += ["*Aucun exportateur propre : les séries viennent d'un collecteur universel "
|
|
"(`node_exporter`), déclaré une seule fois.*", ""]
|
|
else:
|
|
L.append("*Aucun exportateur déclaré — voir `docs/metriques-conception.md`.*")
|
|
if panneaux:
|
|
L += ["| panneau | unité | pourquoi on le regarde |", "|---|---|---|"]
|
|
for p in panneaux:
|
|
L.append(f"| {p['titre']} | {p.get('unite', '—')} | {p.get('raison', '').strip()} |")
|
|
elif expo:
|
|
L.append("*Séries collectées, aucun panneau déclaré : personne ne les regarde "
|
|
"encore.*")
|
|
L.append("")
|
|
|
|
L += ["## Ce qu'il coûte, et qui entre", ""]
|
|
e = (emp or {}).get("setops_empreinte")
|
|
L.append(f"- **Empreinte** : {e['coeurs']} cœur(s), {e['memoire_mo']} Mo, "
|
|
f"{e['disque_go']} Go." if e else "- **Empreinte** : non déclarée.")
|
|
a = (aut or {}).get("authentification")
|
|
L.append(f"- **Authentification** : `{a['portee']}` — {a.get('raison', '').strip()}"
|
|
if a else "- **Authentification** : non déclarée.")
|
|
L.append("")
|
|
return "\n".join(L)
|
|
|
|
|
|
def index(noms: list[str]) -> None:
|
|
"""La porte d'entree des fiches, avec le compte de ce qui manque.
|
|
|
|
CE TABLEAU EST LA VRAIE VALEUR DE L'ENSEMBLE. Une fiche dit comment UN role est
|
|
branche ; l'index dit ce que la flotte ne declare pas encore — et il se recompte a
|
|
chaque generation, sans que personne ait a tenir la liste.
|
|
"""
|
|
lignes = [
|
|
"# Les rôles, un par un", "",
|
|
"> **Généré** par `make fiches`. Ne pas éditer à la main : corriger la "
|
|
"déclaration du rôle, puis régénérer.", "",
|
|
"> **Pour qui :** celui qui doit agir sur un rôle et veut savoir, avant de toucher "
|
|
"quoi que ce soit, qui lui parle, ce qu'il rend et ce qu'il coûte.", "",
|
|
"Chaque fiche est construite depuis ce que le rôle **déclare lui-même** — "
|
|
"`meta/flux.yml`, `meta/supervision.yml`, `meta/metriques.yml`, "
|
|
"`meta/empreinte.yml`, `meta/authentification.yml`. Aucune n'est écrite à la main : "
|
|
"soixante-huit pages rédigées seraient périmées avant la fin du mois.", "",
|
|
"**Une colonne vide est une information**, pas un trou dans la fiche. C'est la "
|
|
"carte de ce qui reste à déclarer, tenue à jour toute seule.", "",
|
|
"| rôle | flux entrants | sonde | métriques |", "|---|---|---|---|",
|
|
]
|
|
manque = {"flux": 0, "sonde": 0, "metriques": 0}
|
|
for r in noms:
|
|
fl = lire(r, "flux") or {}
|
|
entrants = len([f for f in fl.get("flux", []) if f.get("sens") == "ingress"])
|
|
sondes = len((lire(r, "supervision") or {}).get("sondes", []))
|
|
expo = 1 if (lire(r, "metriques") or {}).get("exportateur") else 0
|
|
for cle, v in (("flux", entrants), ("sonde", sondes), ("metriques", expo)):
|
|
if not v:
|
|
manque[cle] += 1
|
|
lignes.append(f"| [`{r}`]({PREFIXE}{r}) | {entrants or '—'} | {sondes or '—'} "
|
|
f"| {'oui' if expo else '—'} |")
|
|
lignes += ["", "## Ce qui reste à déclarer", "",
|
|
f"Sur **{len(noms)}** rôles :", "",
|
|
f"- **{manque['flux']}** ne déclarent aucun flux entrant",
|
|
f"- **{manque['sonde']}** ne déclarent aucune sonde",
|
|
f"- **{manque['metriques']}** ne déclarent aucune métrique", "",
|
|
"Ces nombres ne sont pas tous des dettes : un rôle de durcissement n'a "
|
|
"rien à exposer, et beaucoup de `client_*` n'ont aucun verdict à rendre. "
|
|
"Mais un service qui porte de l'état et ne déclare rien mérite qu'on se "
|
|
"demande pourquoi.", ""]
|
|
(SORTIE / "Rôles.md").write_text("\n".join(lignes) + "\n", encoding="utf-8")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
noms = sys.argv[1:] or sorted(d.name for d in ROLES.iterdir() if d.is_dir())
|
|
SORTIE.mkdir(parents=True, exist_ok=True)
|
|
for r in noms:
|
|
if not (ROLES / r).is_dir():
|
|
print(f"Refus: role inconnu « {r} ».", file=sys.stderr)
|
|
raise SystemExit(2)
|
|
(SORTIE / f"{PREFIXE}{r}.md").write_text(fiche(r) + "\n", encoding="utf-8")
|
|
index(noms)
|
|
print(f"{len(noms)} fiche(s) + index dans {SORTIE.relative_to(RACINE)}/")
|