Set-OPS-Public/scripts/serveurs.py

156 lines
5.7 KiB
Python
Raw Normal View History

#!/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,
intégrations : le rôle déclare sa politique ; le cluster passe à l'hébergeur Deux corrections de propriété, l'une dans le plan, l'autre dans les intrants. 1. Intégrations universelles (D-33/D-34, P26) Le plan portait 57 lignes d'intégration écrites à la main, dont 28 disaient oui à quelque chose de vrai pour tous les hôtes. Elles n'existaient que pour être oubliées — et elles l'avaient été : dans Chezlepro, backup-01 et infra-pki-01 n'étaient ni supervisés, ni journalisés, ni certifiés. Le rôle déclare désormais sa politique une fois, dans meta/integration.yml ; le plan ne garde que les vrais choix et refuse la recopie. Les exemptions se dérivent du service rendu (sauf_role), jamais d'un nom d'hôte : l'AC ne s'enrôle pas auprès d'elle-même, et l'exemption suit step-ca si on le déplace. Une seule fonction de résolution — integrations_de() — lue par l'inventaire, la voûte et le panneau. Sans le passage par la voûte, les secrets des intégrations universelles auraient cessé d'être exigés et P18 serait passé au vert sur une voûte incomplète. Vérifié : diff vide sur Technolibre (la politique reproduit exactement les 41 lignes retirées) ; sur Chezlepro, exactement les groupes manquants, et pas client_pki sur infra-pki-01. 2. Vue Intégrations : la matrice La fiche montrait les intégrations d'UN serveur ; le trou de Chezlepro n'a pas été trouvé par le panneau mais par le devis de pare-feu. Matrice serveurs x intégrations : colonnes de politique en lecture seule, facultatives cochables sur place, ligne de couverture n/N qui rend le motif visible sans le juger. 3. Propriété des intrants (D-35/D-36, P27) Le cluster Proxmox appartient à l'hébergeur, comme sa fabric et sa frontière. Recopié chez chaque tenant, son inventaire avait déjà divergé : deux listes de stockages contradictoires pour le même matériel. API/nœuds/stockages/ponts vont dans proxmox-hebergeur.yml, à côté d'underlay.yml, dont le chemin se dérive — l'hébergeur reste non déclaré (D-17). Restent au tenant son golden template et ses défauts de placement. Le panneau nomme désormais le propriétaire de chaque section : éditer une section « hébergeur » vaut pour tous ses tenants, et l'écran ne le disait pas. 26 preuves OK, 0 échec. --syntax-check des deux playbooks Proxmox. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 14:09:22 -04:00
integrations_universelles,
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
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:
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
forced = inventaire_force(instance) # refuse une cible hors de `instance`
if forced:
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
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 = {}
intégrations : le rôle déclare sa politique ; le cluster passe à l'hébergeur Deux corrections de propriété, l'une dans le plan, l'autre dans les intrants. 1. Intégrations universelles (D-33/D-34, P26) Le plan portait 57 lignes d'intégration écrites à la main, dont 28 disaient oui à quelque chose de vrai pour tous les hôtes. Elles n'existaient que pour être oubliées — et elles l'avaient été : dans Chezlepro, backup-01 et infra-pki-01 n'étaient ni supervisés, ni journalisés, ni certifiés. Le rôle déclare désormais sa politique une fois, dans meta/integration.yml ; le plan ne garde que les vrais choix et refuse la recopie. Les exemptions se dérivent du service rendu (sauf_role), jamais d'un nom d'hôte : l'AC ne s'enrôle pas auprès d'elle-même, et l'exemption suit step-ca si on le déplace. Une seule fonction de résolution — integrations_de() — lue par l'inventaire, la voûte et le panneau. Sans le passage par la voûte, les secrets des intégrations universelles auraient cessé d'être exigés et P18 serait passé au vert sur une voûte incomplète. Vérifié : diff vide sur Technolibre (la politique reproduit exactement les 41 lignes retirées) ; sur Chezlepro, exactement les groupes manquants, et pas client_pki sur infra-pki-01. 2. Vue Intégrations : la matrice La fiche montrait les intégrations d'UN serveur ; le trou de Chezlepro n'a pas été trouvé par le panneau mais par le devis de pare-feu. Matrice serveurs x intégrations : colonnes de politique en lecture seule, facultatives cochables sur place, ligne de couverture n/N qui rend le motif visible sans le juger. 3. Propriété des intrants (D-35/D-36, P27) Le cluster Proxmox appartient à l'hébergeur, comme sa fabric et sa frontière. Recopié chez chaque tenant, son inventaire avait déjà divergé : deux listes de stockages contradictoires pour le même matériel. API/nœuds/stockages/ponts vont dans proxmox-hebergeur.yml, à côté d'underlay.yml, dont le chemin se dérive — l'hébergeur reste non déclaré (D-17). Restent au tenant son golden template et ses défauts de placement. Le panneau nomme désormais le propriétaire de chaque section : éditer une section « hébergeur » vaut pour tous ses tenants, et l'écran ne le disait pas. 26 preuves OK, 0 échec. --syntax-check des deux playbooks Proxmox. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 14:09:22 -04:00
# 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
intégrations : le rôle déclare sa politique ; le cluster passe à l'hébergeur Deux corrections de propriété, l'une dans le plan, l'autre dans les intrants. 1. Intégrations universelles (D-33/D-34, P26) Le plan portait 57 lignes d'intégration écrites à la main, dont 28 disaient oui à quelque chose de vrai pour tous les hôtes. Elles n'existaient que pour être oubliées — et elles l'avaient été : dans Chezlepro, backup-01 et infra-pki-01 n'étaient ni supervisés, ni journalisés, ni certifiés. Le rôle déclare désormais sa politique une fois, dans meta/integration.yml ; le plan ne garde que les vrais choix et refuse la recopie. Les exemptions se dérivent du service rendu (sauf_role), jamais d'un nom d'hôte : l'AC ne s'enrôle pas auprès d'elle-même, et l'exemption suit step-ca si on le déplace. Une seule fonction de résolution — integrations_de() — lue par l'inventaire, la voûte et le panneau. Sans le passage par la voûte, les secrets des intégrations universelles auraient cessé d'être exigés et P18 serait passé au vert sur une voûte incomplète. Vérifié : diff vide sur Technolibre (la politique reproduit exactement les 41 lignes retirées) ; sur Chezlepro, exactement les groupes manquants, et pas client_pki sur infra-pki-01. 2. Vue Intégrations : la matrice La fiche montrait les intégrations d'UN serveur ; le trou de Chezlepro n'a pas été trouvé par le panneau mais par le devis de pare-feu. Matrice serveurs x intégrations : colonnes de politique en lecture seule, facultatives cochables sur place, ligne de couverture n/N qui rend le motif visible sans le juger. 3. Propriété des intrants (D-35/D-36, P27) Le cluster Proxmox appartient à l'hébergeur, comme sa fabric et sa frontière. Recopié chez chaque tenant, son inventaire avait déjà divergé : deux listes de stockages contradictoires pour le même matériel. API/nœuds/stockages/ponts vont dans proxmox-hebergeur.yml, à côté d'underlay.yml, dont le chemin se dérive — l'hébergeur reste non déclaré (D-17). Restent au tenant son golden template et ses défauts de placement. Le panneau nomme désormais le propriétaire de chaque section : éditer une section « hébergeur » vaut pour tous ses tenants, et l'écran ne le disait pas. 26 preuves OK, 0 échec. --syntax-check des deux playbooks Proxmox. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 14:09:22 -04:00
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())