Set-OPS-Public/scripts/site_inventaire.py
Daniel Allaire 6e126dd8a9 site : le chemin qui materialise les machines de l'hebergeur
- site_machines.py : traduit une declaration d'underlay en SETOPS_*, que cloner-vm
  consomme deja. Le clone reste le seul chemin eprouve.
- site_inventaire.py : inventaire DYNAMIQUE. Un site ne derivant de rien, sa declaration
  est deja sa forme finale — un hosts.yml genere ne rendrait rien plus inspectable.
  Les groupes sont les services : playbooks/groupes/<role>.yml trouve ses hotes seul.
- cibles site-decrire / site-inventaire / site-creer (CONFIRMER=true) / site-appliquer,
  toutes independantes d'une instance montee : le site existe avant tout tenant.
- playbook de groupe manquant pour serveur_cache_site, et son classement en couche.

Le premier garde-fou de site-appliquer refusait un GROUPE vide : il ne pouvait jamais
se declencher (GROUPE a un defaut global). Le vrai risque, observe en le testant : un
playbook de tenant contre l'inventaire du site ne matche aucun hote et sort avec 0 — un
succes qui n'a rien fait. Refus desormais de tout groupe absent de cet inventaire.

42 preuves vertes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 23:11:32 -04:00

93 lines
3.7 KiB
Python
Executable file

#!/usr/bin/env python3
"""Inventaire dynamique des machines de l'HEBERGEUR, lu dans `underlay.yml`.
ansible-playbook -i scripts/site_inventaire.py playbooks/groupes/serveur_ops_site.yml
POURQUOI DYNAMIQUE, ET PAS UN `hosts.yml` GENERE.
La regle du depot est qu'un inventaire de tenant est un ARTEFACT GENERE, jamais edite a
la main : on edite le plan, puis `make instancier`. Cette regle existe parce qu'un tenant
DERIVE tout de son index — le fichier intermediaire est le prix a payer pour rendre la
derivation inspectable.
Un site ne derive de rien. Sa declaration EST deja sa forme finale : `underlay.yml` dit
l'adresse, le nœud, le pont, les services. Materialiser un second fichier qui repete la
meme chose ne rendrait rien plus inspectable — ca creerait seulement un artefact de plus
a garder en phase, et une occasion de plus de le laisser deriver de sa source.
LES GROUPES SONT LES SERVICES. Une machine qui declare `services: [serveur_ops_site]`
atterrit dans le groupe `serveur_ops_site`, et `playbooks/groupes/serveur_ops_site.yml`
la trouve sans qu'on ait rien a cabler. C'est la meme convention que pour les tenants :
ce qui se partage entre un site et un tenant, ce sont les ROLES, pas la forme du plan.
"""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
import underlay as U # noqa: E402
# L'inventaire des tenants passe par ce compte ; le site n'a aucune raison d'en differer.
UTILISATEUR_DEFAUT = "ansible"
def inventaire() -> dict:
u = U.charger()
if u is None:
# Aucun underlay monte : un inventaire VIDE, pas une erreur. Ansible doit pouvoir
# etre lance sans site sans que ca ressemble a une panne.
return {"_meta": {"hostvars": {}}}
# La carte est-elle seulement valide ? Un inventaire tire d'un underlay fautif
# produirait des machines plausibles et fausses — le pire des deux mondes.
erreurs = [e for e in U.valider(u) if e.startswith("machine")]
if erreurs:
raise SystemExit("underlay.yml refuse :\n - " + "\n - ".join(erreurs))
par_reseau = {r.get("nom"): r for r in U.reseaux(u)}
hostvars: dict[str, dict] = {}
groupes: dict[str, list[str]] = {}
for m in U.machines(u):
nom = m["nom"]
r = par_reseau.get(m.get("reseau"), {})
hostvars[nom] = {
"ansible_host": str(m["ip"]),
"ansible_user": m.get("utilisateur", UTILISATEUR_DEFAUT),
# De quoi qu'un role puisse se situer sans redemander la carte.
"site_reseau": m.get("reseau"),
"site_pont": r.get("pont"),
"site_noeud": m.get("noeud"),
"proxmox_vmid": m.get("vmid"),
# CE QUI DISTINGUE CES MACHINES DE TOUTES LES AUTRES. Un role partage entre
# un site et un tenant peut avoir besoin de savoir de quel cote il tourne —
# par exemple pour ne PAS chercher un plan la ou il n'y en a pas.
"setops_site": True,
}
for service in m.get("services") or []:
groupes.setdefault(service, []).append(nom)
out: dict = {g: {"hosts": h} for g, h in groupes.items()}
out["site"] = {"hosts": sorted(hostvars)}
out["_meta"] = {"hostvars": hostvars}
return out
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("--list", action="store_true")
ap.add_argument("--host")
a = ap.parse_args()
if a.host:
print(json.dumps(inventaire().get("_meta", {}).get("hostvars", {}).get(a.host, {})))
else:
print(json.dumps(inventaire(), indent=2, sort_keys=True))
return 0
if __name__ == "__main__":
raise SystemExit(main())