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>
217 lines
8.5 KiB
Python
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:]))
|