Sur le runner d un tenant, la cle du SITE DOIT manquer : il porte la carte de la
fabric et ne doit jamais pouvoir l ouvrir. Ma premiere version sortait en erreur
des qu une cle manquait — elle presentait une SEPARATION REUSSIE comme un defaut,
et a fait echouer une chaine parfaitement saine.
Seule l absence de la cle de l INSTANCE MONTEE empeche la machine de travailler.
C est elle, et elle seule, qui decide du code de sortie ; le reste s affiche.
Constate sur ops-01 le jour de son armement :
instance OPS-Chezlepro cle presente
hebergeur SITE-Chezlepro sans cle
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
156 lines
6.2 KiB
Python
Executable file
156 lines
6.2 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
"""Les voutes de cet ecosysteme, et la cle de chacune.
|
|
|
|
UNE VOUTE, UNE CLE (2026-08-28). Un seul mot de passe ouvrait les six voutes de la
|
|
flotte — celle de la fabric comprise. Tant que c'etait vrai, poser « la » cle sur chaque
|
|
runner aurait rendu chaque runner capable d'ouvrir les autres : compromettre le plus
|
|
petit locataire, c'etait obtenir les secrets de l'hebergeur. La separation precede donc
|
|
la distribution, et cet ordre n'est pas negociable.
|
|
|
|
CE FICHIER NE LIT AUCUN SECRET. Il ne fait que dire OU chaque cle se trouve, et laisse
|
|
Ansible l'ouvrir. Les chemins sont des pointeurs, jamais des valeurs.
|
|
|
|
DEUX VOUTES DANS LA MEME EXECUTION, ET C'EST LE CAS COURANT : `cloner_vm_debian.yml`
|
|
charge celle du tenant PUIS celle de l'underlay — les identifiants Proxmox appartiennent
|
|
a l'hebergeur, le reste au locataire. Un unique `ANSIBLE_VAULT_PASSWORD_FILE` ne peut
|
|
donc plus suffire. Ansible sait tenir plusieurs secrets a la fois
|
|
(`ANSIBLE_VAULT_IDENTITY_LIST`) et les essaie tous sur un bloc chiffre sans etiquette :
|
|
c'est exactement ce qu'il nous faut, et ca n'oblige a toucher aucun des 28 appels.
|
|
|
|
CONVENTION : ~/.config/setops-vault-<dossier-en-minuscules>
|
|
OPS-Chezlepro -> ~/.config/setops-vault-ops-chezlepro
|
|
SITE-Chezlepro -> ~/.config/setops-vault-site-chezlepro
|
|
|
|
Usage :
|
|
python3 scripts/voutes.py identites # la liste pour ANSIBLE_VAULT_IDENTITY_LIST
|
|
python3 scripts/voutes.py etat # ce qui est present, ce qui manque
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
|
|
from inventory_rules import instance_courante # noqa: E402
|
|
import underlay as underlay_mod # noqa: E402
|
|
|
|
PREFIXE = "setops-vault-"
|
|
|
|
|
|
def _config() -> Path:
|
|
"""Le dossier des cles. `XDG_CONFIG_HOME` respecte, `~/.config` par defaut."""
|
|
return Path(os.environ.get("XDG_CONFIG_HOME") or (Path.home() / ".config"))
|
|
|
|
|
|
def slug(nom: str) -> str:
|
|
return nom.strip().lower()
|
|
|
|
|
|
def cle_de(nom_depot: str) -> Path:
|
|
return _config() / f"{PREFIXE}{slug(nom_depot)}"
|
|
|
|
|
|
def _freres() -> list[str]:
|
|
"""Les depots d'ecosysteme poses a cote du moteur — ceux qui portent un `plan/`.
|
|
|
|
MEME DECOUVERTE QUE LE RESTE DU DEPOT : des dossiers freres, pas de registre.
|
|
"""
|
|
racine = Path(__file__).resolve().parents[1]
|
|
return sorted(d.name for d in racine.parent.iterdir()
|
|
if d.is_dir() and (d / "plan").is_dir())
|
|
|
|
|
|
def voutes() -> list[tuple[str, str, Path]]:
|
|
"""[(role, nom du depot, chemin de la cle)] — tout ce que CETTE machine peut ouvrir.
|
|
|
|
L'ORDRE COMPTE PEU POUR ANSIBLE (il essaie tous les secrets) mais beaucoup pour un
|
|
humain qui lit la sortie : ce qu'on pilote, le terrain, puis le voisinage.
|
|
|
|
POURQUOI LE VOISINAGE Y FIGURE, ET POURQUOI CE N'EST PAS UNE FUITE. La liste nomme
|
|
des CHEMINS, pas des secrets : une cle absente du disque n'est pas listee, et la
|
|
machine ne peut ouvrir que ce qu'elle detient. C'est donc la PRESENCE des fichiers
|
|
qui borne le pouvoir, jamais cette fonction.
|
|
|
|
Sur le poste de l'exploitant, toutes les cles sont la, et c'est voulu : c'est
|
|
l'humain qui les detient toutes, et des preuves comme P03 lisent les inventaires de
|
|
TOUS les ecosystemes freres. Sur un runner, une seule cle existe — la sienne — et la
|
|
meme fonction n'en nomme qu'une. Le code est identique, le pouvoir ne l'est pas.
|
|
"""
|
|
out: list[tuple[str, str, Path]] = []
|
|
vus: set[str] = set()
|
|
|
|
def ajouter(role: str, nom: str) -> None:
|
|
if nom and nom not in vus:
|
|
vus.add(nom)
|
|
out.append((role, nom, cle_de(nom)))
|
|
|
|
try:
|
|
inst = instance_courante()
|
|
if inst is not None:
|
|
ajouter("instance", Path(inst).resolve().name)
|
|
except Exception:
|
|
pass
|
|
c = underlay_mod.chemin()
|
|
if c is not None:
|
|
ajouter("hebergeur", c.resolve().parent.name)
|
|
for nom in _freres():
|
|
ajouter("voisin", nom)
|
|
return out
|
|
|
|
|
|
def identites() -> str:
|
|
"""`etiquette@chemin,...` pour ANSIBLE_VAULT_IDENTITY_LIST — les cles PRESENTES.
|
|
|
|
Une cle absente est SAUTEE plutot que declaree : Ansible refuse de demarrer si un
|
|
fichier d'identite n'existe pas, et l'echec porterait alors sur une voute que
|
|
l'execution n'ouvre peut-etre meme pas. Ce qui manque se lit par `voutes.py etat`.
|
|
"""
|
|
return ",".join(f"{slug(nom)}@{chemin}" for _, nom, chemin in voutes() if chemin.is_file())
|
|
|
|
|
|
def etat() -> int:
|
|
"""Rapport lisible. Le CODE DE SORTIE ne juge que ce que CETTE machine doit pouvoir.
|
|
|
|
UNE CLE ABSENTE N'EST PAS UNE FAUTE — c'est souvent la bonne reponse (2026-08-28).
|
|
Sur le runner d'un tenant, la cle du SITE doit manquer : il porte la carte de la
|
|
fabric et ne doit jamais pouvoir l'ouvrir. Ma premiere version sortait en erreur des
|
|
qu'une cle manquait, ce qui faisait echouer une chaine parfaitement saine et
|
|
presentait une SEPARATION REUSSIE comme un defaut.
|
|
|
|
Seule l'absence de la cle de l'INSTANCE MONTEE empeche la machine de travailler :
|
|
c'est elle, et elle seule, qui decide du code de sortie. Le reste s'affiche.
|
|
"""
|
|
v = voutes()
|
|
if not v:
|
|
print("Aucune voute a nommer : ni instance montee, ni underlay.")
|
|
return 0
|
|
bloquant = False
|
|
for role, nom, chemin in v:
|
|
present = chemin.is_file()
|
|
if role == "instance" and not present:
|
|
bloquant = True
|
|
etiquette = "cle presente" if present else (
|
|
"CLE ABSENTE " if role == "instance" else "sans cle ")
|
|
print(f"{role:<11} {nom:<20} {etiquette} {chemin}")
|
|
if bloquant:
|
|
print("\nLa cle de l'instance montee manque — cette machine ne peut rien "
|
|
"configurer.\n umask 077; openssl rand -base64 48 | tr -d '\\n' > <chemin>")
|
|
return 1 if bloquant else 0
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
ap = argparse.ArgumentParser(description=__doc__,
|
|
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
ap.add_argument("commande", choices=("identites", "etat"))
|
|
args = ap.parse_args(argv)
|
|
if args.commande == "identites":
|
|
print(identites())
|
|
return 0
|
|
return etat()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|