This repository has been archived on 2026-06-26. You can view files and clone it, but cannot push or open issues or pull requests.
Set-OPS/scripts/instancier.py
Daniel Allaire 7ddc2fedad Moteur : robustesse pour une instance hors depot (os.path.relpath)
Path.relative_to(RACINE) levait quand l'instance est externe
(SETOPS_INSTANCE = ../OPS-laPreuve par ex.). Remplace par os.path.relpath
dans instancier.py / inventory_gui.py / config_proxmox.py.

Revele par une instance d'essai agnostique (lapreuve.local, 10.9.0.0/16) :
le moteur genere un inventaire distinct, zero fuite Chezlepro. Cas normal
(OPS-Chezlepro via symlink) inchange : diff vide, lint 0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 10:12:03 -04:00

184 lines
7.5 KiB
Python

#!/usr/bin/env python3
"""Generateur d'inventaire Set-OPS (Phase 3) : plan -> hosts.yml.
NON destructif : ecrit dans instance/inventories/production/hosts.genere.yml et compare
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).
- Les integrations clients_* ne sont PAS derivables du plan a ce stade : la
comparaison les revele (lacune a combler en Phase 3b).
"""
from __future__ import annotations
import os
import json
import subprocess
import sys
from pathlib import Path
import yaml
from inventory_rules import (
charger_applications,
charger_nomenclature,
charger_serveurs,
deriver_nomenclature,
fonction_seq,
)
RACINE = Path(__file__).resolve().parents[1]
INSTANCE = Path(os.environ.get("SETOPS_INSTANCE") or (RACINE / "instance"))
INVENTAIRE = INSTANCE / "inventories/production/hosts.yml"
GENERE = INSTANCE / "inventories/production/hosts.genere.yml"
FICHIER_SERVEURS = INSTANCE / "plan/serveurs.yml"
FICHIER_APPLICATIONS = INSTANCE / "plan/applications.yml"
FICHIER_NOMENCLATURE = INSTANCE / "plan/nomenclature.yml"
GROUPES_SOCLE = ["serveur_debian", "serveur_durci"]
PLACEMENT = [("noeud", "proxmox_noeud"), ("stockage", "proxmox_stockage"),
("disque", "proxmox_disque_taille"), ("memoire", "proxmox_memoire"),
("coeurs", "proxmox_coeurs")]
def generer() -> dict:
serveurs = charger_serveurs(FICHIER_SERVEURS).get("serveurs", {})
apps = charger_applications(FICHIER_APPLICATIONS).get("applications", {})
nomenclature = charger_nomenclature(FICHIER_NOMENCLATURE)
services_par_hote: dict = {}
for app in apps.values():
services_par_hote.setdefault(app.get("hote"), set()).add(app.get("groupe"))
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]
etat = "hotes_actifs" if srv.get("etat") == "actif" else "hotes_planifies"
children[etat]["hosts"][nom] = hostvars
groupes = set(GROUPES_SOCLE) | services_par_hote.get(nom, set()) | set(srv.get("integrations") or [])
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]:
sortie = subprocess.run(["ansible-inventory", "-i", str(fichier), "--list"],
capture_output=True, text=True, check=True).stdout
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())