Set-OPS-Public/scripts/serveurs.py
Daniel Allaire 4462c13b3d P03 : la preuve comparait chaque instance a l'inventaire d'UNE SEULE
Trouve en validant une mise a jour du CHANGELOG. Deux invocations de la meme preuve,
deux verdicts : `make prouver` -> NON CONFORME (« lab : 17 hotes avec ecart »),
`python3 scripts/prouver.py` -> CONFORME 37/37. Le lab n'avait aucun ecart.

DEUX VARIABLES DESIGNENT LA CIBLE, ET LA SECONDE GAGNE. Le Makefile exporte
SETOPS_INVENTAIRE (ligne 13), derive de l'instance ACTIVE ; instancier.py:68 lui fait
FORCER la cible par-dessus SETOPS_INSTANCE. P03 (prouver.py:505) ne redirigeait que
SETOPS_INSTANCE : elle generait le plan de CHAQUE instance federee et le comparait a
l'inventaire applique de la SEULE instance active.

LE ROUGE N'ETAIT PAS LE PROBLEME, LE VERT L'ETAIT. Sous `make`, l'inventaire applique
de lab et de Technolibre n'etait JAMAIS lu — l'angle meme pour lequel P03 a ete ecrite
le 2026-08-12 (un tenant qu'on ne regarde pas imposant ses vieilles adresses au pare-feu
partage). La preuve etait aveugle a son propre cas, par l'invocation documentee. Les
rapports du 13 et du 14 sortent de cette invocation-la. Signature visible sans lire le
code : les hosts.genere.yml de lab et de Technolibre ne bougeaient pas.

CORRECTIF, cinq sites : env.pop("SETOPS_INVENTAIRE") partout ou l'on redirige
SETOPS_INSTANCE — P03 et P15, plus les trois applicateurs (opnsense, proxmox_fw, sdn) qui
pointent vers l'HEBERGEUR. Ces trois sont sans effet tant qu'hebergeur et tenant actif
coincident, c'est-a-dire jusqu'au second site. Le geste existait deja (modeles.py:96).

GARDE, pour que la classe cesse d'etre silencieuse : inventory_rules.inventaire_force()
REFUSE une cible hors de l'instance visee, en nommant les deux valeurs. Eprouvee dans les
deux sens (contradiction -> code 1 ; cible legitime dans l'instance -> passe). Branchee
sur les quatre resolutions de _inventaire (instancier, serveurs, applications,
config_proxmox). Le GUI garde la sienne : il ne redirige jamais SETOPS_INSTANCE pour un
fils et resout par symlink a chaque requete.

make prouver : 37 OK, 0 echec, 0 saute — et les hosts.genere.yml des TROIS instances
portent l'horodatage du passage. make test 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 14:35:01 -04:00

155 lines
5.7 KiB
Python

#!/usr/bin/env python3
"""Registre des serveurs (VM) du plan Set-OPS.
Phase 2 : l'inventaire reste AUTORITE. Ce registre est bootstrape depuis lui
(reconciliation en lecture). La generation de l'inventaire depuis le plan
viendra en Phase 3 (make instancier).
"""
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 (
charger_nomenclature,
charger_serveurs,
fonction_seq,
integrations_universelles,
inventaire_force,
reconcilier_serveur,
valider_serveurs,
)
RACINE = Path(__file__).resolve().parents[1]
INSTANCE = Path(os.environ.get("SETOPS_INSTANCE") or (RACINE / "instance"))
FICHIER = INSTANCE / "plan/serveurs.yml"
FICHIER_NOMENCLATURE = INSTANCE / "plan/nomenclature.yml"
def _inventaire(instance: Path, *noms: str) -> Path:
forced = inventaire_force(instance) # refuse une cible hors de `instance`
if forced:
return forced
for nom in noms:
p = instance / "inventories" / nom / "hosts.yml"
if p.exists():
return p
return instance / "inventories" / noms[0] / "hosts.yml"
INVENTAIRE = _inventaire(INSTANCE, "principal", "production")
# 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"
"# Bootstrape depuis l'inventaire (make serveurs-bootstrap). L'inventaire\n"
"# reste autorite en Phase 2 ; VMID/IP/VLAN/passerelle sont DERIVES de la\n"
"# fonction via instance/plan/nomenclature.yml (non stockes ici).\n"
"---\n"
)
with FICHIER.open("w", encoding="utf-8") 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())