Set-OPS-Public/scripts/fiche_role.py
Daniel Allaire a4a8fc6065 fiche_role : la fiche d un role, generee depuis ce qu il declare
68 roles. Autant de pages faites a la main seraient perimees avant la fin du
mois — c est le defaut que ce depot traque partout sous le nom une liste qui
suit une autre prend du retard.

La fiche lit meta/flux, supervision, metriques, empreinte, authentification. Un
schema mermaid au centre, puis les tableaux, et chaque ligne porte la RAISON que
le role a declaree.

Une section vide est une information, pas un defaut de la fiche : elle devient
la carte de ce qui reste a faire, tenue a jour toute seule.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-14 13:12:06 -04:00

157 lines
6.6 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"
SORTIE = RACINE / "docs" / "roles"
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)
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"{r}.md").write_text(fiche(r) + "\n", encoding="utf-8")
print(f"{len(noms)} fiche(s) dans {SORTIE.relative_to(RACINE)}/")