Some checks are pending
verifier / verifier (push) Waiting to run
`path.open("w")` TRONQUE avant d ecrire : entre les deux, le fichier est vide.
Une exception dans yaml.safe_dump, un disque plein, un Ctrl-C, et
instance/plan/serveurs.yml reste mutile.
L asymetrie fait la gravite : hosts.yml se regenere d un
make instancier-appliquer, le PLAN ne se regenere de rien. C est la source
unique de verite. Git est le filet, mais encore faut-il savoir qu on est tombe.
TREIZE SITES, UNE SEULE FONCTION
Le defaut n etait pas dans le GUI seul : douze sites dans sept fichiers, dont
les miroirs CLI des MEMES registres. Corriger le GUI seul aurait recree la
divergence que P41 garde depuis les neuf resolutions d instance. La fonction
vit donc dans inventory_rules.py, que les sept importaient deja. Une source,
pas douze.
TROIS DETAILS QUI FONT LA DIFFERENCE ENTRE « CA MARCHE » ET « CA TIENT »
temporaire dans le MEME dossier os.replace n est atomique qu au sein d un
meme systeme de fichiers ; un /tmp sur une
autre partition casserait la garantie sans
rien dire
fsync AVANT le rename sinon le renommage peut atteindre le disque
avant le contenu : au retour d une coupure
brutale, un fichier neuf et VIDE — le defaut
qu on ferme, deplace d un cran
report des droits mkstemp cree en 0600, le plan est en 0664 et
doit rester lisible par le groupe sur les
runners
LE TEST PORTE SON PROPRE CONTROLE NEGATIF
scripts/tests/test_ecriture_atomique.py rejoue D ABORD l ancienne forme et
verifie qu elle DETRUIT. Sans ce controle, « le fichier est intact » ne
prouverait rien — il pourrait l etre parce que rien n a ete ecrit du tout. Une
garantie qu on n a jamais vue echouer n est pas une garantie, c est une
habitude.
Branche sur P02, dont le titre annoncait « inventory_host » alors qu il lance
maintenant trois tests. Corrige au passage.
LA VOUTE DU GUI : VERIFIEE, PAS DE DEFAUT
Le soupcon etait qu executer_flux pose ANSIBLE_VAULT_PASSWORD_FILE (un seul mot
de passe) alors que creer une VM ouvre DEUX voutes depuis « une voute, une cle ».
Eprouve contre deux voutes JETABLES a mots de passe distincts — jamais les
vraies. Les deux variables se CUMULENT : Ansible essaie tous les secrets, et un
PASSWORD_FILE errone n empeche rien. Confirme en sondant l environnement qu une
recette make recoit reellement : le mot de passe saisi ET les cinq cles
calculees par voutes.py.
Ce qui sauve ce chemin n est donc pas le mot de passe saisi, c est
l IDENTITY_LIST que make pose par-dessus. Chacun couvre ce que l autre ne
couvre pas — le PASSWORD_FILE sert le runner qui n a que sa cle, l IDENTITY_LIST
le poste qui les a toutes. Ecrit au-dessus du code, pour que personne ne
« simplifie » en retirant l un des deux.
make prouver : CONFORME, 59 OK, 0 echec, 1 saute.
make instancier : DIFF VIDE, quatre registres relus, droits 664 preserves.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
156 lines
6 KiB
Python
156 lines
6 KiB
Python
#!/usr/bin/env python3
|
|
"""Registre des serveurs (VM) du plan Set-OPS.
|
|
|
|
LE PLAN EST L'AUTORITE, ET CE N'EST PLUS L'INVERSE (corrige le 2026-09-06). Ce module a
|
|
longtemps annonce « Phase 2 : l'inventaire reste AUTORITE [...] la generation depuis le
|
|
plan viendra en Phase 3 ». La Phase 3 est faite depuis longtemps : `make instancier`
|
|
genere `hosts.yml` DEPUIS ce registre, et la REGLE D'OR d'AGENTS.md interdit d'editer
|
|
l'inventaire a la main. Lire l'ancienne phrase aujourd'hui, c'est croire l'exact contraire
|
|
de la doctrine.
|
|
|
|
`bootstrap` reste utile, mais c'est une manoeuvre de REPRISE, pas le flux normal : il
|
|
(re)constitue `plan/serveurs.yml` depuis un inventaire existant — le geste qu'on fait une
|
|
fois, quand on herite d'un ecosysteme dont le plan n'existe pas encore.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
|
|
import argparse
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import yaml
|
|
|
|
from inventory_gui import charger_yaml, liste_hotes
|
|
from inventory_rules import (
|
|
ecriture_atomique,
|
|
charger_nomenclature,
|
|
charger_serveurs,
|
|
fonction_seq,
|
|
integrations_universelles,
|
|
instance_courante,
|
|
inventaire_de,
|
|
reconcilier_serveur,
|
|
valider_serveurs,
|
|
)
|
|
|
|
RACINE = Path(__file__).resolve().parents[1]
|
|
INSTANCE = instance_courante()
|
|
FICHIER = INSTANCE / "plan/serveurs.yml"
|
|
FICHIER_NOMENCLATURE = INSTANCE / "plan/nomenclature.yml"
|
|
INVENTAIRE = inventaire_de(INSTANCE)
|
|
|
|
# Champs de placement / dimensionnement NON derivables (proviennent du plan).
|
|
CHAMPS_PLAN = [("noeud", "noeud"), ("stockage", "stockage"),
|
|
("disque_taille", "disque"), ("memoire", "memoire"), ("coeurs", "coeurs")]
|
|
|
|
|
|
def ecrire(registre: dict) -> None:
|
|
entete = (
|
|
"# Registre des serveurs (VM) du plan Set-OPS.\n"
|
|
"# CE FICHIER EST L'AUTORITE : l'inventaire en est GENERE (make instancier).\n"
|
|
"# Ne jamais editer instance/inventories/*/hosts.yml a la main.\n"
|
|
"# Reconstitue ici depuis un inventaire existant par `make serveurs-bootstrap` —\n"
|
|
"# manoeuvre de reprise, pas le flux normal.\n"
|
|
"# VMID/IP/VLAN/passerelle sont DERIVES de la fonction via\n"
|
|
"# instance/plan/nomenclature.yml (jamais stockes ici).\n"
|
|
"---\n"
|
|
)
|
|
with ecriture_atomique(FICHIER) as fichier:
|
|
fichier.write(entete)
|
|
yaml.safe_dump({"serveurs": registre.get("serveurs", {}) or {}},
|
|
fichier, default_flow_style=False, sort_keys=False, allow_unicode=True)
|
|
|
|
|
|
def hotes_inventaire() -> dict:
|
|
return {h["nom"]: h for h in liste_hotes(charger_yaml(INVENTAIRE)) if h.get("nom")}
|
|
|
|
|
|
def bootstrap() -> dict:
|
|
serveurs = {}
|
|
# Les universelles ne se recopient pas dans le plan (D-33) : les reinscrire ici
|
|
# reconstituerait, a chaque bootstrap, les lignes qu'on vient de retirer.
|
|
universelles = set(integrations_universelles())
|
|
for nom, h in hotes_inventaire().items():
|
|
fonction, _ = fonction_seq(nom)
|
|
srv = {"fonction": fonction, "etat": h.get("etat", "planifie")}
|
|
for cle_src, cle_dst in CHAMPS_PLAN:
|
|
v = str(h.get(cle_src, "")).strip()
|
|
if v:
|
|
srv[cle_dst] = int(v) if v.isdigit() else v
|
|
integrations = sorted(g for g in (h.get("groupes") or [])
|
|
if g.startswith("client_") and g not in universelles)
|
|
if integrations:
|
|
srv["integrations"] = integrations
|
|
serveurs[nom] = srv
|
|
return {"serveurs": serveurs}
|
|
|
|
|
|
def lister() -> None:
|
|
registre = charger_serveurs(FICHIER)
|
|
serveurs = registre.get("serveurs", {})
|
|
if not serveurs:
|
|
print("Aucun serveur. Lance: make serveurs-bootstrap")
|
|
return
|
|
nomenclature = charger_nomenclature(FICHIER_NOMENCLATURE)
|
|
hotes = hotes_inventaire()
|
|
for nom, srv in serveurs.items():
|
|
rec = reconcilier_serveur(nom, srv, hotes.get(nom), nomenclature)
|
|
d = rec["derive"]
|
|
if rec["absent_inventaire"]:
|
|
statut = "ABSENT de l'inventaire"
|
|
elif rec["divergences"]:
|
|
statut = "DIVERGENCE"
|
|
else:
|
|
statut = "reconcilie"
|
|
print(f"{nom} [fonction {srv.get('fonction')}, etat {srv.get('etat', '?')}]"
|
|
f" derive: vmid {d.get('vmid', '?')}, ip {d.get('adresse_ip', '?')}, vlan {d.get('vlan', '?')} -> {statut}")
|
|
for dv in rec["divergences"]:
|
|
print(f" ! {dv}")
|
|
|
|
|
|
def verifier() -> int:
|
|
registre = charger_serveurs(FICHIER)
|
|
nomenclature = charger_nomenclature(FICHIER_NOMENCLATURE)
|
|
valider_serveurs(registre, nomenclature)
|
|
hotes = hotes_inventaire()
|
|
divergents = []
|
|
for nom, srv in registre.get("serveurs", {}).items():
|
|
rec = reconcilier_serveur(nom, srv, hotes.get(nom), nomenclature)
|
|
if rec["divergences"] or rec["absent_inventaire"]:
|
|
divergents.append(nom)
|
|
if divergents:
|
|
print(f"Registre valide, mais {len(divergents)} serveur(s) non reconcilie(s): {', '.join(divergents)}")
|
|
else:
|
|
print("Registre des serveurs valide et reconcilie avec l'inventaire.")
|
|
return 0
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description="Registre des serveurs (VM) du plan Set-OPS.")
|
|
sub = parser.add_subparsers(dest="commande", required=True)
|
|
sub.add_parser("lister", help="Affiche les serveurs et leur reconciliation avec l'inventaire.")
|
|
sub.add_parser("verifier", help="Valide le registre et signale les divergences.")
|
|
sub.add_parser("bootstrap", help="(Re)genere instance/plan/serveurs.yml depuis l'inventaire actuel.")
|
|
|
|
args = parser.parse_args()
|
|
try:
|
|
if args.commande == "lister":
|
|
lister()
|
|
elif args.commande == "verifier":
|
|
return verifier()
|
|
elif args.commande == "bootstrap":
|
|
registre = bootstrap()
|
|
valider_serveurs(registre, charger_nomenclature(FICHIER_NOMENCLATURE))
|
|
ecrire(registre)
|
|
print(f"instance/plan/serveurs.yml genere depuis l'inventaire ({len(registre['serveurs'])} serveurs).")
|
|
except Exception as exc:
|
|
print(f"erreur: {exc}", file=sys.stderr)
|
|
return 2
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|