2026-06-24 20:17:46 -04:00
|
|
|
#!/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,
|
resolution d'instance : une seule, partagee — au lieu de neuf copies
Cinq jours, cinq defauts, tous de la meme famille : « quelle instance, quel inventaire ? »
Neuf modules portaient chacun leur reponse.
- 18 aout : P03 comparait chaque instance a l'inventaire d'une AUTRE ;
- 19 aout : verifier_ports codait `principal/` en dur ; verifier_intrants et
_frontiere_absente lisaient le symlink au lieu de la variable ;
- 20 aout : devis_placement rendait un verdict juste sur le mauvais tenant ;
- 22 aout : P35, puis P36 — la dixieme, trouvee par la preuve elle-meme.
Aucune n'etait une faute d'inattention : chacune avait ete ecrite de bonne foi, a un
moment ou le besoin semblait local. C'est le mode de panne de la duplication — pas
l'erreur, mais la DERIVE, invisible depuis l'interieur d'un fichier.
LA RESOLUTION UNIQUE. `inventory_rules` porte instance_courante(), inventaire_de(),
dossier_inventaire() et plan_de(). Trois niveaux de repli, dont le TROISIEME manquait a la
moitie des copies : un hosts.yml existant, puis un REPERTOIRE existant (instance neuve —
c'est ce qui faisait echouer `make instancier` sur le modele public), puis le defaut.
Vingt-huit modules y sont branches.
CE QUI REND CE REFACTOR SUR : avant de toucher quoi que ce soit, chaque module a ete
interroge sur ce qu'il resolvait, pour les DEUX ecosystemes. Apres refactor, meme mesure :
17 modules x 2 instances, diff VIDE. Aucune resolution n'a change — prouve, pas suppose.
P41 echoue des qu'un module reintroduit une copie. Eprouvee en negatif : une copie
replacee dans genome.py est signalee avec son numero de ligne. Trois exemptions nommees :
instances.py et inventory_gui.py manipulent le SYMLINK lui-meme (bascule d'instance), et
devis_opnsense lit deliberement quelle instance est ACTIVE. Elles parlent du lien, pas de
la resolution.
make verifier 41 OK, 0 echec, 0 saute ; make ci idem ; lint vert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 14:19:55 -04:00
|
|
|
instance_courante,
|
|
|
|
|
inventaire_de,
|
2026-06-24 20:17:46 -04:00
|
|
|
reconcilier_serveur,
|
|
|
|
|
valider_serveurs,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
RACINE = Path(__file__).resolve().parents[1]
|
resolution d'instance : une seule, partagee — au lieu de neuf copies
Cinq jours, cinq defauts, tous de la meme famille : « quelle instance, quel inventaire ? »
Neuf modules portaient chacun leur reponse.
- 18 aout : P03 comparait chaque instance a l'inventaire d'une AUTRE ;
- 19 aout : verifier_ports codait `principal/` en dur ; verifier_intrants et
_frontiere_absente lisaient le symlink au lieu de la variable ;
- 20 aout : devis_placement rendait un verdict juste sur le mauvais tenant ;
- 22 aout : P35, puis P36 — la dixieme, trouvee par la preuve elle-meme.
Aucune n'etait une faute d'inattention : chacune avait ete ecrite de bonne foi, a un
moment ou le besoin semblait local. C'est le mode de panne de la duplication — pas
l'erreur, mais la DERIVE, invisible depuis l'interieur d'un fichier.
LA RESOLUTION UNIQUE. `inventory_rules` porte instance_courante(), inventaire_de(),
dossier_inventaire() et plan_de(). Trois niveaux de repli, dont le TROISIEME manquait a la
moitie des copies : un hosts.yml existant, puis un REPERTOIRE existant (instance neuve —
c'est ce qui faisait echouer `make instancier` sur le modele public), puis le defaut.
Vingt-huit modules y sont branches.
CE QUI REND CE REFACTOR SUR : avant de toucher quoi que ce soit, chaque module a ete
interroge sur ce qu'il resolvait, pour les DEUX ecosystemes. Apres refactor, meme mesure :
17 modules x 2 instances, diff VIDE. Aucune resolution n'a change — prouve, pas suppose.
P41 echoue des qu'un module reintroduit une copie. Eprouvee en negatif : une copie
replacee dans genome.py est signalee avec son numero de ligne. Trois exemptions nommees :
instances.py et inventory_gui.py manipulent le SYMLINK lui-meme (bascule d'instance), et
devis_opnsense lit deliberement quelle instance est ACTIVE. Elles parlent du lien, pas de
la resolution.
make verifier 41 OK, 0 echec, 0 saute ; make ci idem ; lint vert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 14:19:55 -04:00
|
|
|
INSTANCE = instance_courante()
|
2026-06-24 20:17:46 -04:00
|
|
|
FICHIER = INSTANCE / "plan/serveurs.yml"
|
|
|
|
|
FICHIER_NOMENCLATURE = INSTANCE / "plan/nomenclature.yml"
|
resolution d'instance : une seule, partagee — au lieu de neuf copies
Cinq jours, cinq defauts, tous de la meme famille : « quelle instance, quel inventaire ? »
Neuf modules portaient chacun leur reponse.
- 18 aout : P03 comparait chaque instance a l'inventaire d'une AUTRE ;
- 19 aout : verifier_ports codait `principal/` en dur ; verifier_intrants et
_frontiere_absente lisaient le symlink au lieu de la variable ;
- 20 aout : devis_placement rendait un verdict juste sur le mauvais tenant ;
- 22 aout : P35, puis P36 — la dixieme, trouvee par la preuve elle-meme.
Aucune n'etait une faute d'inattention : chacune avait ete ecrite de bonne foi, a un
moment ou le besoin semblait local. C'est le mode de panne de la duplication — pas
l'erreur, mais la DERIVE, invisible depuis l'interieur d'un fichier.
LA RESOLUTION UNIQUE. `inventory_rules` porte instance_courante(), inventaire_de(),
dossier_inventaire() et plan_de(). Trois niveaux de repli, dont le TROISIEME manquait a la
moitie des copies : un hosts.yml existant, puis un REPERTOIRE existant (instance neuve —
c'est ce qui faisait echouer `make instancier` sur le modele public), puis le defaut.
Vingt-huit modules y sont branches.
CE QUI REND CE REFACTOR SUR : avant de toucher quoi que ce soit, chaque module a ete
interroge sur ce qu'il resolvait, pour les DEUX ecosystemes. Apres refactor, meme mesure :
17 modules x 2 instances, diff VIDE. Aucune resolution n'a change — prouve, pas suppose.
P41 echoue des qu'un module reintroduit une copie. Eprouvee en negatif : une copie
replacee dans genome.py est signalee avec son numero de ligne. Trois exemptions nommees :
instances.py et inventory_gui.py manipulent le SYMLINK lui-meme (bascule d'instance), et
devis_opnsense lit deliberement quelle instance est ACTIVE. Elles parlent du lien, pas de
la resolution.
make verifier 41 OK, 0 echec, 0 saute ; make ci idem ; lint vert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 14:19:55 -04:00
|
|
|
INVENTAIRE = inventaire_de(INSTANCE)
|
2026-06-24 20:17:46 -04:00
|
|
|
|
|
|
|
|
# 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())
|
2026-06-24 20:17:46 -04:00
|
|
|
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)
|
2026-06-24 20:17:46 -04:00
|
|
|
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())
|