2026-06-24 20:17:46 -04:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
"""Generateur d'inventaire Set-OPS (Phase 3) : plan -> hosts.yml.
|
|
|
|
|
|
2026-06-30 16:08:25 -04:00
|
|
|
NON destructif : ecrit hosts.genere.yml a cote de l'inventaire de l'instance et compare
|
2026-06-24 20:17:46 -04:00
|
|
|
SEMANTIQUEMENT (via ansible-inventory --list) avec l'inventaire actuel. Aucune
|
|
|
|
|
bascule tant que la comparaison n'est pas vide et validee.
|
|
|
|
|
|
|
|
|
|
Derivation :
|
|
|
|
|
- host vars : ansible_host/ansible_user + proxmox_* (IP/VMID/VLAN/passerelle
|
|
|
|
|
derives de la nomenclature ; placement/taille depuis instance/plan/serveurs.yml) ;
|
|
|
|
|
- groupes : socle (serveur_debian/durcis) + groupes de service derives des
|
|
|
|
|
applications de l'hote + groupe d'etat (hotes_actifs/hotes_planifies).
|
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 integrations client_* : les universelles viennent de la POLITIQUE du role
|
|
|
|
|
(roles/client_*/meta/integration.yml), les facultatives de la liste du plan.
|
|
|
|
|
Voir inventory_rules.integrations_de() — source unique des trois lecteurs
|
|
|
|
|
(inventaire, voute, GUI).
|
2026-06-24 20:17:46 -04:00
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import os
|
|
|
|
|
|
|
|
|
|
import json
|
|
|
|
|
import subprocess
|
|
|
|
|
import sys
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
import yaml
|
|
|
|
|
|
|
|
|
|
from inventory_rules import (
|
|
|
|
|
charger_applications,
|
2026-07-05 15:30:22 -04:00
|
|
|
charger_domaines,
|
2026-06-24 20:17:46 -04:00
|
|
|
charger_nomenclature,
|
|
|
|
|
charger_serveurs,
|
|
|
|
|
deriver_nomenclature,
|
2026-06-30 10:07:04 -04:00
|
|
|
deriver_ressources,
|
2026-07-05 15:30:22 -04:00
|
|
|
expositions_des_applications,
|
2026-06-24 20:17:46 -04:00
|
|
|
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_de,
|
|
|
|
|
integrations_universelles,
|
2026-07-22 21:32:42 -04:00
|
|
|
liens_acceptes,
|
2026-06-24 20:17:46 -04:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
RACINE = Path(__file__).resolve().parents[1]
|
2026-06-30 10:07:04 -04:00
|
|
|
ROLES = RACINE / "roles"
|
2026-06-24 20:17:46 -04:00
|
|
|
INSTANCE = Path(os.environ.get("SETOPS_INSTANCE") or (RACINE / "instance"))
|
2026-06-30 16:08:25 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def _inventaire(instance: Path, *noms: str) -> Path:
|
|
|
|
|
for nom in noms:
|
|
|
|
|
p = instance / "inventories" / nom / "hosts.yml"
|
|
|
|
|
if p.exists():
|
|
|
|
|
return p
|
Mise en conformité prouvable : registre d'affirmations + make prouver
Le dépôt fait / explique / prouve ce qu'il affirme, vérifiable en une commande.
- Phase 1 : docs/audit/affirmations.md — 54 affirmations publiques tracées vers
une commande de preuve et un statut (✅/🟡/❌/⚪).
- Phase 2 : CLAUDE.md réduit à un pointeur mince ; contradiction SSH levée (le
code applique déjà PasswordAuthentication no + AuthenticationMethods publickey,
conforme à AGENTS.md) ; section AGENTS « Codex » → « agents IA ».
- Phase 3 : parcours démarrage réparé (QUICKSTART renvoyait à un modèle absent,
chemins de voûte faux, commandes make périmées) ; make verifier vert
(ansible-lint 33 → 0 : site.yml généré nommé, pipefail, name[template]) ;
voûte Proxmox unifiée lue par le clonage (all/vault.yml).
- Phase 4 : make prouver → docs/audit/preuve-<date>.md, harnais rejouable qui
rappelle l'outillage existant (aucune validation réimplémentée).
- Phase 5 : parcours QUICKSTART prouvé hors-ligne sur le socle ; modèle socle
rendu valide (autorite interne → auto-heberge) ; split-brain d'inventaire
corrigé (repli sur le répertoire existant, pas principal/).
make prouver : 15 OK, 0 échec, 1 sautée (voûte). ansible-lint : 0 failure.
Écarts découverts en cours de traitement (AFF-097..100) : tous résolus.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 19:53:18 -04:00
|
|
|
# Aucun hosts.yml encore genere : viser le repertoire d'inventaire deja present
|
|
|
|
|
# (ses group_vars y sont), sinon le premier nom par defaut. Evite d'ecrire dans
|
|
|
|
|
# 'principal/' alors que l'instance (ex. modele socle) est en 'production/'.
|
|
|
|
|
for nom in noms:
|
|
|
|
|
if (instance / "inventories" / nom).is_dir():
|
|
|
|
|
return instance / "inventories" / nom / "hosts.yml"
|
2026-06-30 16:08:25 -04:00
|
|
|
return instance / "inventories" / noms[0] / "hosts.yml"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Inventaire unique de l'instance (rétro-compat : principal > production).
|
|
|
|
|
# SETOPS_INVENTAIRE force la cible.
|
|
|
|
|
_force = os.environ.get("SETOPS_INVENTAIRE")
|
|
|
|
|
INVENTAIRE = Path(_force) if _force else _inventaire(INSTANCE, "principal", "production")
|
|
|
|
|
GENERE = INVENTAIRE.with_name("hosts.genere.yml")
|
2026-06-24 20:17:46 -04:00
|
|
|
FICHIER_SERVEURS = INSTANCE / "plan/serveurs.yml"
|
|
|
|
|
FICHIER_APPLICATIONS = INSTANCE / "plan/applications.yml"
|
|
|
|
|
FICHIER_NOMENCLATURE = INSTANCE / "plan/nomenclature.yml"
|
2026-07-05 15:30:22 -04:00
|
|
|
FICHIER_DOMAINES = INSTANCE / "plan/domaines.yml"
|
2026-06-24 20:17:46 -04:00
|
|
|
|
|
|
|
|
GROUPES_SOCLE = ["serveur_debian", "serveur_durci"]
|
|
|
|
|
PLACEMENT = [("noeud", "proxmox_noeud"), ("stockage", "proxmox_stockage"),
|
|
|
|
|
("disque", "proxmox_disque_taille"), ("memoire", "proxmox_memoire"),
|
|
|
|
|
("coeurs", "proxmox_coeurs")]
|
|
|
|
|
|
|
|
|
|
|
2026-07-03 08:33:30 -04:00
|
|
|
# --- Bindings (liens) : relations declaratives entre applications -----------------
|
|
|
|
|
# Une application declare ses 'liens' ([{vers, role}]) dans plan/applications.yml ;
|
|
|
|
|
# chaque role decrit les liens qu'il accepte dans roles/<groupe>/meta/liens.yml
|
|
|
|
|
# (setops_liens.accepte). instancier resout la cible (FQDN interne derive) et injecte
|
|
|
|
|
# les variables en host_vars du consommateur. Cf. docs/bindings-conception.md.
|
|
|
|
|
|
|
|
|
|
def _domaine_interne() -> str:
|
|
|
|
|
"""Lit domaine_interne dans group_vars/all de l'instance (keystone, non chiffre)."""
|
|
|
|
|
base = INVENTAIRE.parent / "group_vars" / "all"
|
|
|
|
|
if base.is_dir():
|
|
|
|
|
for fichier in sorted(base.glob("*.yml")):
|
|
|
|
|
try:
|
|
|
|
|
data = yaml.safe_load(fichier.read_text(encoding="utf-8")) or {}
|
|
|
|
|
except yaml.YAMLError:
|
|
|
|
|
continue
|
|
|
|
|
if isinstance(data, dict) and str(data.get("domaine_interne", "")).strip():
|
|
|
|
|
return str(data["domaine_interne"]).strip()
|
|
|
|
|
return ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _attributs_cible(vers: str, apps: dict, serveurs: dict, nomenclature: dict, domaine: str) -> dict | None:
|
|
|
|
|
"""Resout une cible de lien en attributs substituables. Phase 1 : applications."""
|
|
|
|
|
if vers in apps:
|
|
|
|
|
hote = apps[vers].get("hote")
|
|
|
|
|
srv = serveurs.get(hote, {})
|
|
|
|
|
_, seq = fonction_seq(str(hote))
|
|
|
|
|
d = deriver_nomenclature(str(srv.get("fonction", "")), seq, nomenclature) or {}
|
|
|
|
|
return {
|
|
|
|
|
"genre": "application",
|
|
|
|
|
"hote": hote,
|
|
|
|
|
"fqdn": f"{hote}.{domaine}" if domaine else str(hote),
|
|
|
|
|
"ip": d.get("adresse_ip"),
|
|
|
|
|
}
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _substituer(valeur, attrs: dict):
|
|
|
|
|
"""Remplace {cible.X} dans une chaine ; laisse bool/int tels quels."""
|
|
|
|
|
if not isinstance(valeur, str):
|
|
|
|
|
return valeur
|
|
|
|
|
for cle, val in attrs.items():
|
|
|
|
|
valeur = valeur.replace("{cible." + cle + "}", str(val))
|
|
|
|
|
return valeur
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def resoudre_liens(apps: dict, serveurs: dict, nomenclature: dict) -> dict:
|
|
|
|
|
"""Derive {hote: {var: valeur}} depuis les 'liens' des applications (bindings)."""
|
|
|
|
|
domaine = _domaine_interne()
|
|
|
|
|
injections: dict = {}
|
|
|
|
|
for nom_app, app in apps.items():
|
|
|
|
|
accepte = None
|
|
|
|
|
for lien in (app.get("liens") or []):
|
|
|
|
|
vers, role = lien.get("vers"), lien.get("role")
|
|
|
|
|
if accepte is None:
|
2026-07-22 21:32:42 -04:00
|
|
|
accepte = liens_acceptes(str(app.get("groupe", "")), RACINE)
|
2026-07-03 08:33:30 -04:00
|
|
|
spec = accepte.get(role)
|
|
|
|
|
if not spec:
|
|
|
|
|
raise ValueError(
|
|
|
|
|
f"application '{nom_app}': lien role '{role}' non accepte par "
|
|
|
|
|
f"'{app.get('groupe')}' (roles/{app.get('groupe')}/meta/liens.yml).")
|
|
|
|
|
attrs = _attributs_cible(str(vers), apps, serveurs, nomenclature, domaine)
|
|
|
|
|
if attrs is None:
|
|
|
|
|
raise ValueError(f"application '{nom_app}': cible de lien '{vers}' introuvable.")
|
|
|
|
|
attendu = spec.get("cible")
|
|
|
|
|
if attendu and attendu != attrs["genre"]:
|
|
|
|
|
raise ValueError(
|
|
|
|
|
f"application '{nom_app}': lien '{role}' attend une cible '{attendu}', "
|
|
|
|
|
f"'{vers}' est '{attrs['genre']}'.")
|
|
|
|
|
sur_conso = spec.get("injecte_sur", "consommateur") == "consommateur"
|
|
|
|
|
cible_hote = app.get("hote") if sur_conso else attrs["hote"]
|
|
|
|
|
for var, gabarit in (spec.get("variables") or {}).items():
|
|
|
|
|
injections.setdefault(cible_hote, {})[var] = _substituer(gabarit, attrs)
|
|
|
|
|
return injections
|
|
|
|
|
|
|
|
|
|
|
2026-06-24 20:17:46 -04:00
|
|
|
def generer() -> dict:
|
|
|
|
|
serveurs = charger_serveurs(FICHIER_SERVEURS).get("serveurs", {})
|
|
|
|
|
apps = charger_applications(FICHIER_APPLICATIONS).get("applications", {})
|
|
|
|
|
nomenclature = charger_nomenclature(FICHIER_NOMENCLATURE)
|
2026-07-03 08:33:30 -04:00
|
|
|
injections = resoudre_liens(apps, serveurs, nomenclature)
|
2026-07-05 15:30:22 -04:00
|
|
|
# Expositions du plan (champ 'expose') -> pour deriver les SANs de chaque edge.
|
|
|
|
|
domaines = charger_domaines(FICHIER_DOMAINES)
|
|
|
|
|
expositions = expositions_des_applications({"applications": apps}, domaines)
|
2026-06-24 20:17:46 -04:00
|
|
|
|
|
|
|
|
services_par_hote: dict = {}
|
|
|
|
|
for app in apps.values():
|
|
|
|
|
services_par_hote.setdefault(app.get("hote"), set()).add(app.get("groupe"))
|
|
|
|
|
|
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
|
|
|
# Politique d'integration : lue une fois, appliquee a tous (voir D-33).
|
|
|
|
|
universelles = integrations_universelles(RACINE)
|
|
|
|
|
|
2026-06-24 20:17:46 -04:00
|
|
|
children: dict = {
|
|
|
|
|
"modeles_vm": {"hosts": {}},
|
|
|
|
|
"hotes_actifs": {"hosts": {}},
|
|
|
|
|
"hotes_planifies": {"hosts": {}},
|
|
|
|
|
}
|
|
|
|
|
for nom, srv in serveurs.items():
|
|
|
|
|
_, seq = fonction_seq(nom)
|
|
|
|
|
d = deriver_nomenclature(str(srv.get("fonction", "")), seq, nomenclature) or {}
|
|
|
|
|
hostvars = {
|
|
|
|
|
"ansible_host": d.get("adresse_ip"),
|
|
|
|
|
"ansible_user": "ansible",
|
|
|
|
|
"proxmox_cidr": d.get("cidr"),
|
|
|
|
|
"proxmox_passerelle": d.get("passerelle"),
|
|
|
|
|
"proxmox_vlan": d.get("vlan"),
|
|
|
|
|
"proxmox_vmid": int(d["vmid"]) if d.get("vmid") else None,
|
|
|
|
|
}
|
|
|
|
|
for cle_srv, cle_var in PLACEMENT:
|
|
|
|
|
if str(srv.get(cle_srv, "")).strip():
|
|
|
|
|
hostvars[cle_var] = srv[cle_srv]
|
2026-06-30 10:07:04 -04:00
|
|
|
# Ressources derivees des logiciels (groupes de service) + socle SE.
|
|
|
|
|
# setdefault : une valeur explicite du plan (PLACEMENT ci-dessus) gagne.
|
|
|
|
|
ressources = deriver_ressources(sorted(services_par_hote.get(nom, set())), ROLES)
|
|
|
|
|
hostvars.setdefault("proxmox_coeurs", ressources["coeurs"])
|
|
|
|
|
hostvars.setdefault("proxmox_memoire", ressources["memoire"])
|
|
|
|
|
hostvars.setdefault("proxmox_disque_taille", ressources["disque"])
|
2026-07-03 08:33:30 -04:00
|
|
|
# Variables derivees des liens (bindings) declares par les applications.
|
|
|
|
|
for var, val in injections.get(nom, {}).items():
|
|
|
|
|
hostvars[var] = val
|
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
|
|
|
services = services_par_hote.get(nom, set())
|
|
|
|
|
groupes = set(GROUPES_SOCLE) | services | integrations_de(srv, services, universelles)
|
2026-07-05 15:30:22 -04:00
|
|
|
# SANs d'exposition : les FQDN que cet edge sert (derives du plan) -> cert edge
|
|
|
|
|
# (client_pki_sans), au lieu d'une liste manuelle. Vide si l'hote n'est pas un edge.
|
|
|
|
|
sans = sorted({e["fqdn"] for e in expositions if e.get("edge") in groupes})
|
|
|
|
|
if sans:
|
|
|
|
|
hostvars["sans_exposition"] = sans
|
2026-06-24 20:17:46 -04:00
|
|
|
etat = "hotes_actifs" if srv.get("etat") == "actif" else "hotes_planifies"
|
|
|
|
|
children[etat]["hosts"][nom] = hostvars
|
|
|
|
|
for groupe in sorted(groupes):
|
|
|
|
|
children.setdefault(groupe, {"hosts": {}})["hosts"][nom] = None
|
|
|
|
|
return {"all": {"children": children}}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def ecrire(path: Path = GENERE) -> None:
|
|
|
|
|
entete = ("# Inventaire GENERE depuis le plan (make instancier / instancier-appliquer).\n"
|
|
|
|
|
"# NE PAS editer a la main : edite instance/plan/serveurs.yml + instance/plan/applications.yml.\n"
|
|
|
|
|
"# Source : instance/plan/serveurs.yml + instance/plan/applications.yml + instance/plan/nomenclature.yml.\n")
|
|
|
|
|
with path.open("w", encoding="utf-8") as fichier:
|
|
|
|
|
fichier.write(entete)
|
|
|
|
|
yaml.safe_dump(generer(), fichier, default_flow_style=False, sort_keys=True, allow_unicode=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _resolu(fichier: Path) -> tuple[dict, dict]:
|
2026-07-01 22:17:45 -04:00
|
|
|
cmd = ["ansible-inventory", "-i", str(fichier), "--list"]
|
|
|
|
|
# Une voute chiffree (group_vars/all/vault.yml) fait echouer ansible-inventory sans
|
|
|
|
|
# mot de passe (exit 4). Si ANSIBLE_VAULT_PASSWORD_FILE n'est pas deja fourni, on
|
|
|
|
|
# tente le fichier conventionnel Set-OPS ~/.config/setops-vault-pass.
|
|
|
|
|
if not os.environ.get("ANSIBLE_VAULT_PASSWORD_FILE"):
|
|
|
|
|
conv = Path.home() / ".config" / "setops-vault-pass"
|
|
|
|
|
if conv.is_file():
|
|
|
|
|
cmd += ["--vault-password-file", str(conv)]
|
|
|
|
|
sortie = subprocess.run(cmd, capture_output=True, text=True, check=True).stdout
|
2026-06-24 20:17:46 -04:00
|
|
|
data = json.loads(sortie)
|
|
|
|
|
hostvars = data.get("_meta", {}).get("hostvars", {})
|
|
|
|
|
groupes: dict = {}
|
|
|
|
|
for groupe, info in data.items():
|
|
|
|
|
if groupe in ("_meta", "all", "ungrouped") or not isinstance(info, dict):
|
|
|
|
|
continue
|
|
|
|
|
for hote in (info.get("hosts") or []):
|
|
|
|
|
groupes.setdefault(hote, set()).add(groupe)
|
|
|
|
|
return hostvars, groupes
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def compter_ecarts(reference: Path, genere: Path) -> int:
|
|
|
|
|
hv_act, grp_act = _resolu(reference)
|
|
|
|
|
hv_gen, grp_gen = _resolu(genere)
|
|
|
|
|
hotes = sorted(set(hv_act) | set(hv_gen))
|
|
|
|
|
ecarts = 0
|
|
|
|
|
for hote in hotes:
|
|
|
|
|
if hote not in hv_gen:
|
|
|
|
|
print(f" - {hote} : ABSENT du genere"); ecarts += 1; continue
|
|
|
|
|
if hote not in hv_act:
|
|
|
|
|
print(f" - {hote} : EN TROP dans le genere"); ecarts += 1; continue
|
|
|
|
|
g_manquants = grp_act.get(hote, set()) - grp_gen.get(hote, set())
|
|
|
|
|
g_surplus = grp_gen.get(hote, set()) - grp_act.get(hote, set())
|
|
|
|
|
vars_diff = [k for k in set(hv_act[hote]) | set(hv_gen[hote])
|
|
|
|
|
if hv_act[hote].get(k) != hv_gen[hote].get(k)]
|
|
|
|
|
if g_manquants or g_surplus or vars_diff:
|
|
|
|
|
ecarts += 1
|
|
|
|
|
print(f" - {hote} :")
|
|
|
|
|
if g_manquants:
|
|
|
|
|
print(f" groupes non reproduits : {', '.join(sorted(g_manquants))}")
|
|
|
|
|
if g_surplus:
|
|
|
|
|
print(f" groupes en trop : {', '.join(sorted(g_surplus))}")
|
|
|
|
|
if vars_diff:
|
|
|
|
|
details = ", ".join(f"{k} (inv={hv_act[hote].get(k)!r} gen={hv_gen[hote].get(k)!r})" for k in sorted(vars_diff))
|
|
|
|
|
print(f" vars differentes : {details}")
|
|
|
|
|
return ecarts
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def comparer() -> int:
|
|
|
|
|
ecrire(GENERE)
|
|
|
|
|
ecarts = compter_ecarts(INVENTAIRE, GENERE)
|
|
|
|
|
if ecarts == 0:
|
|
|
|
|
print("DIFF VIDE : le plan reproduit exactement l'inventaire actuel. Bascule possible.")
|
|
|
|
|
else:
|
|
|
|
|
print(f"\n{ecarts} hote(s) avec ecart.")
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def appliquer(force: bool = False) -> int:
|
|
|
|
|
"""Ecrit hosts.yml depuis le plan. Refuse si le diff n'est pas vide (sauf --force)."""
|
|
|
|
|
ecrire(GENERE)
|
|
|
|
|
ecarts = compter_ecarts(INVENTAIRE, GENERE)
|
|
|
|
|
if ecarts and not force:
|
|
|
|
|
print(f"\nDiff NON vide ({ecarts} hote(s)). Revois 'make instancier'. "
|
|
|
|
|
"Utilise FORCE=1 pour appliquer un changement intentionnel du plan.")
|
|
|
|
|
return 1
|
|
|
|
|
ecrire(INVENTAIRE)
|
|
|
|
|
print(f"\n{os.path.relpath(INVENTAIRE, RACINE)} (re)genere depuis le plan. "
|
|
|
|
|
"git est le filet : git diff / git checkout pour revenir.")
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main() -> int:
|
|
|
|
|
import argparse
|
|
|
|
|
parser = argparse.ArgumentParser(description="Generateur d'inventaire Set-OPS (Phase 3).")
|
|
|
|
|
sub = parser.add_subparsers(dest="commande", required=True)
|
|
|
|
|
sub.add_parser("generer", help="Ecrit hosts.genere.yml depuis le plan.")
|
|
|
|
|
sub.add_parser("comparer", help="Compare le genere a l'inventaire actuel (semantique).")
|
|
|
|
|
pa = sub.add_parser("appliquer", help="Ecrit hosts.yml depuis le plan (refuse si diff non vide).")
|
|
|
|
|
pa.add_argument("--force", action="store_true", help="Applique meme si le diff n'est pas vide.")
|
|
|
|
|
args = parser.parse_args()
|
|
|
|
|
try:
|
|
|
|
|
if args.commande == "generer":
|
|
|
|
|
ecrire()
|
|
|
|
|
print(f"{os.path.relpath(GENERE, RACINE)} genere depuis le plan.")
|
|
|
|
|
elif args.commande == "comparer":
|
|
|
|
|
return comparer()
|
|
|
|
|
elif args.commande == "appliquer":
|
|
|
|
|
return appliquer(force=args.force)
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
print(f"erreur: {exc}", file=sys.stderr)
|
|
|
|
|
return 2
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
raise SystemExit(main())
|