Set-OPS-Public/scripts/orchestrer.py
Daniel Allaire 835f8ab6d0 Mise en conformité prouvable : registre d'affirmations + make prouver
Le dépôt fait / explique / prouve ce qu'il affirme, vérifiable en une commande.

- Phase 1 : docs/audit/affirmations.md — 54 affirmations publiques tracées vers
  une commande de preuve et un statut (/🟡//).
- Phase 2 : CLAUDE.md réduit à un pointeur mince ; contradiction SSH levée (le
  code applique déjà PasswordAuthentication no + AuthenticationMethods publickey,
  conforme à AGENTS.md) ; section AGENTS « Codex » → « agents IA ».
- Phase 3 : parcours démarrage réparé (QUICKSTART renvoyait à un modèle absent,
  chemins de voûte faux, commandes make périmées) ; make verifier vert
  (ansible-lint 33 → 0 : site.yml généré nommé, pipefail, name[template]) ;
  voûte Proxmox unifiée lue par le clonage (all/vault.yml).
- Phase 4 : make prouver → docs/audit/preuve-<date>.md, harnais rejouable qui
  rappelle l'outillage existant (aucune validation réimplémentée).
- Phase 5 : parcours QUICKSTART prouvé hors-ligne sur le socle ; modèle socle
  rendu valide (autorite interne → auto-heberge) ; split-brain d'inventaire
  corrigé (repli sur le répertoire existant, pas principal/).

make prouver : 15 OK, 0 échec, 1 sautée (voûte). ansible-lint : 0 failure.
Écarts découverts en cours de traitement (AFF-097..100) : tous résolus.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 19:53:18 -04:00

217 lines
8.5 KiB
Python

#!/usr/bin/env python3
"""Orchestrateur Set-OPS : couches + graphe -> playbooks/site.yml ordonne.
Genere le point d'entree general (site.yml) comme une SEQUENCE ORDONNEE
d'`import_playbook` des groupes, dans l'ordre de reconstruction :
couche (docs/couches-deploiement.yml, cle primaire)
puis tri topologique INTRA-couche (docs/dependances-groupes.yml).
C'est un artefact du MOTEUR (deterministe : univers des playbooks de groupe
x couches x graphe) — aucune donnee d'instance. Ansible saute tout seul les
groupes sans hote actif, donc le meme site.yml sert toute instance.
Deux gardes de coherence (le generateur REFUSE sinon) :
1. bijection : chaque groupe deployable (playbooks/groupes/<g>.yml operationnel)
figure dans exactement une couche, et chaque groupe d'une couche a un playbook ;
2. pas d'arete « en arriere » : un prerequis (graphe) doit vivre dans une couche
<= celle du groupe qui en depend (sinon la classification est fausse).
Usage :
python3 scripts/orchestrer.py verifier # valide, n'ecrit rien (sortie != 0 si erreur)
python3 scripts/orchestrer.py ordre # imprime l'ordre (couche : groupes)
python3 scripts/orchestrer.py ecrire # (re)genere playbooks/site.yml
"""
from __future__ import annotations
import sys
from pathlib import Path
import yaml
from inventory_rules import charger_dependances, est_groupe_operationnel
RACINE = Path(__file__).resolve().parents[1]
DOSSIER_GROUPES = RACINE / "playbooks" / "groupes"
FICHIER_COUCHES = RACINE / "docs" / "couches-deploiement.yml"
FICHIER_DEPENDANCES = RACINE / "docs" / "dependances-groupes.yml"
SITE = RACINE / "playbooks" / "site.yml"
class ErreurOrchestration(Exception):
"""Incoherence entre l'univers des groupes, les couches et le graphe."""
def univers_groupes() -> set[str]:
"""Les groupes deployables : un playbooks/groupes/<g>.yml operationnel."""
return {
p.stem
for p in DOSSIER_GROUPES.glob("*.yml")
if est_groupe_operationnel(p.stem)
}
def charger_couches(path: Path = FICHIER_COUCHES) -> list[dict]:
if not path.exists():
raise ErreurOrchestration(f"Registre des couches introuvable : {path}")
data = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
couches = data.get("couches")
if not isinstance(couches, list) or not couches:
raise ErreurOrchestration(f"{path} doit contenir une liste 'couches' non vide.")
for couche in couches:
if not isinstance(couche, dict) or "nom" not in couche:
raise ErreurOrchestration(f"Couche invalide (nom requis) dans {path}.")
if not isinstance(couche.get("groupes"), list):
raise ErreurOrchestration(f"Couche {couche.get('nom')!r} : 'groupes' doit etre une liste.")
return couches
def indice_couche(couches: list[dict]) -> dict[str, int]:
"""groupe -> indice de sa couche (0 = premiere deployee)."""
indice: dict[str, int] = {}
for i, couche in enumerate(couches):
for groupe in couche["groupes"]:
if groupe in indice:
raise ErreurOrchestration(
f"Groupe {groupe!r} classe dans deux couches "
f"({couches[indice[groupe]]['nom']} et {couche['nom']})."
)
indice[groupe] = i
return indice
def valider(couches: list[dict], dependances: dict) -> dict[str, int]:
"""Bijection univers<->couches + aucune arete en arriere. Renvoie groupe->couche."""
univers = univers_groupes()
indice = indice_couche(couches)
classes = set(indice)
non_classes = sorted(univers - classes)
if non_classes:
raise ErreurOrchestration(
"Groupes deployables non classes dans une couche : "
+ ", ".join(non_classes)
+ f"\n -> ajouter chacun a une couche dans {FICHIER_COUCHES.name}."
)
fantomes = sorted(classes - univers)
if fantomes:
raise ErreurOrchestration(
"Couches referencant des groupes sans playbook operationnel : "
+ ", ".join(fantomes)
+ f"\n -> playbooks/groupes/<groupe>.yml attendu, ou retirer du registre."
)
# Arete « en arriere » : n requiert m, mais m est deploye APRES n.
arrieres: list[str] = []
for groupe, config in dependances.items():
if groupe not in indice:
continue
for requis in config.get("requiert_groupes_actifs", []):
if requis in indice and indice[requis] > indice[groupe]:
arrieres.append(
f"{groupe} (couche {couches[indice[groupe]]['nom']}) "
f"requiert {requis} (couche {couches[indice[requis]]['nom']}, plus tardive)"
)
if arrieres:
raise ErreurOrchestration(
"Dependances incoherentes avec les couches (prerequis deploye trop tard) :\n - "
+ "\n - ".join(arrieres)
)
return indice
def _tri_topo_intra(groupes: list[str], dependances: dict) -> list[str]:
"""Ordonne des groupes d'UNE MEME couche : un prerequis (present ici) d'abord.
Depart stable = l'ordre du registre (couches-deploiement.yml).
"""
ensemble = set(groupes)
rang = {g: i for i, g in enumerate(groupes)} # ordre du registre
# aretes intra-couche : m -> n (m requis par n) ; on veut m avant n.
restant = {
n: {m for m in dependances.get(n, {}).get("requiert_groupes_actifs", []) if m in ensemble}
for n in groupes
}
ordre: list[str] = []
while restant:
prets = sorted((g for g, deps in restant.items() if not deps), key=lambda g: rang[g])
if not prets:
raise ErreurOrchestration(
"Cycle de dependances intra-couche : " + ", ".join(sorted(restant))
)
for g in prets:
ordre.append(g)
del restant[g]
for deps in restant.values():
deps.discard(g)
return ordre
def ordre_deploiement(couches: list[dict] | None = None, dependances: dict | None = None) -> list[tuple[str, list[str]]]:
"""Renvoie [(nom_couche, [groupes ordonnes]), ...] dans l'ordre de deploiement."""
couches = couches or charger_couches()
dependances = dependances if dependances is not None else charger_dependances(FICHIER_DEPENDANCES)
valider(couches, dependances)
resultat: list[tuple[str, list[str]]] = []
for couche in couches:
ordonnes = _tri_topo_intra(list(couche["groupes"]), dependances)
resultat.append((couche["nom"], ordonnes))
return resultat
def rendre_site(ordre: list[tuple[str, list[str]]]) -> str:
lignes = [
"---",
"# GENERE par scripts/orchestrer.py (make site) — NE PAS EDITER A LA MAIN.",
"# Point d'entree ordonne : deploie l'ecosysteme couche par couche, dans",
"# l'ordre de reconstruction. Regenerer apres tout changement de couches",
"# (docs/couches-deploiement.yml) ou du graphe (docs/dependances-groupes.yml).",
"# Ansible saute les groupes sans hote actif : le meme fichier sert toute instance.",
"",
]
for nom, groupes in ordre:
lignes.append(f"# --- couche {nom} ---")
for groupe in groupes:
lignes.append(f"- name: Couche {nom} — groupe {groupe}")
lignes.append(f" import_playbook: groupes/{groupe}.yml")
lignes.append("")
return "\n".join(lignes).rstrip() + "\n"
def ecrire(path: Path = SITE) -> None:
contenu = rendre_site(ordre_deploiement())
path.write_text(contenu, encoding="utf-8")
print(f"{path.relative_to(RACINE)} (re)genere. git est le filet : git diff / git checkout.")
def imprimer_ordre() -> None:
for i, (nom, groupes) in enumerate(ordre_deploiement()):
print(f"{i}. couche {nom}")
for groupe in groupes:
print(f" {groupe}")
def main(argv: list[str]) -> int:
commande = argv[0] if argv else "verifier"
try:
if commande == "verifier":
ordre_deploiement() # leve si incoherent
total = sum(len(g) for _, g in ordre_deploiement())
print(f"Orchestration coherente : {total} groupes classes, aucun cycle, aucune arete en arriere.")
elif commande == "ordre":
imprimer_ordre()
elif commande == "ecrire":
ecrire()
else:
print(f"Commande inconnue : {commande} (verifier | ordre | ecrire)", file=sys.stderr)
return 2
except ErreurOrchestration as erreur:
print(f"ERREUR orchestration :\n{erreur}", file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))