Generees depuis ce que chaque role declare, avec un schema mermaid et la raison de chaque ligne. Une section vide est une information : l index recompte ce que la flotte ne declare pas — 36 sans flux, 40 sans sonde, 67 sans metrique. La charte du wiki disait de ne pas recopier le depot, pour eviter la derive. Le motif ne vaut pas pour une page qui relit sa source ; l exception est nommee plutot que prise en silence. La navigation exigeait une citation DIRECTE alors que son motif parle d atteignabilite. L exigence litterale interdisait toute page d index — la garde forcait a degrader ce qu elle protegeait. Elle suit maintenant les liens de proche en proche, et son motif reconnait le souligne : un motif trop etroit ne rend pas une garde prudente, il la rend aveugle. Et le compte du wiki serait passe de 27 a 96 : une fiche generee n est pas une unite d apprentissage. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
211 lines
9.8 KiB
Python
Executable file
211 lines
9.8 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("")
|
|
|
|
L += ["## Ce qu'il expose en séries", ""]
|
|
if (met or {}).get("exportateur"):
|
|
e = met["exportateur"]
|
|
L += [f"Exportateur `{e.get('paquet')}` — port **{e.get('port')}**, "
|
|
f"job `{e.get('job')}`.", "",
|
|
"| panneau | unité | pourquoi on le regarde |", "|---|---|---|"]
|
|
for p in met.get("panneaux", []):
|
|
L.append(f"| {p['titre']} | {p.get('unite', '—')} | {p.get('raison', '').strip()} |")
|
|
else:
|
|
L.append("*Aucun exportateur déclaré — voir `docs/metriques-conception.md`.*")
|
|
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)}/")
|