2026-08-24 23:11:32 -04:00
|
|
|
#!/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"
|
|
|
|
|
|
frontiere : le devis voit le site — `fabric`, alias et regles
Un mot manquait au vocabulaire des flux. Le runner de SITE declarait son API Proxmox en
`externe`, qui se rend par !SETOPS_INTERNES — or les hyperviseurs SONT en RFC 1918 : la
regle les aurait exclus tout en ayant l'air d'ouvrir le flux. D'ou `fabric`.
Deux flux manquaient :
- serveur_cache_site n'avait aucun egress : la chaine de caches se terminait sur un
cache vide, et ca ne se serait vu qu'au premier apt update d'un ecosysteme neuf ;
- serveur_ops_site n'avait que l'API : le shell des noeuds et l'API de la frontiere
sont deux autres pouvoirs, declares a part.
Le devis ignorait les machines du site — dans aucun plan de tenant, donc invisibles a sa
boucle, zero regle rendue SANS RIEN SIGNALER. Il emet desormais SETOPS_SITE,
SETOPS_FABRIC, SETOPS_ADMIN_SITE et un alias par role, sur opt2 (arrivee reelle du
trafic) et lan pour le SSH, plus le NAT sortant du site.
Le socle s'applique au site : site_inventaire.py range ses machines dans serveur_debian.
Patient 0 ne declare plus serveur_ops_site ni serveur_cache_site : il est un tenant
comme les autres, c'est meme tout ce qu'il prouve. Inventaire regenere.
42 preuves vertes. Devis : 26 regles a creer, 5 a retirer.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 00:49:04 -04:00
|
|
|
# LE SOCLE S'APPLIQUE AUSSI AU SITE. Une machine de l'hebergeur n'est pas d'une autre
|
|
|
|
|
# espece : elle veut le meme durcissement SSH, les memes horloges, le meme pare-feu que
|
|
|
|
|
# n'importe quelle machine de la flotte. L'oublier lui aurait donne un SSH par defaut et
|
|
|
|
|
# aucune regle de frontiere — la machine la plus puissante du site, la moins protegee.
|
|
|
|
|
GROUPE_SOCLE = "serveur_debian"
|
|
|
|
|
|
2026-08-24 23:11:32 -04:00
|
|
|
|
|
|
|
|
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))
|
|
|
|
|
|
2026-08-25 09:34:02 -04:00
|
|
|
# L'IDENTITE DU SITE. Un tenant tire `domaine_interne` et `organisation` de son plan,
|
|
|
|
|
# via `instancier`. Un site n'a pas de plan : il les declare dans sa carte, et c'est
|
|
|
|
|
# ici qu'ils deviennent des variables d'inventaire. Sans eux, un role partage comme
|
|
|
|
|
# `serveur_forgejo` construirait des noms d'hote a partir de rien.
|
|
|
|
|
site = u.get("site") or {}
|
|
|
|
|
communes: dict = {}
|
|
|
|
|
if site.get("domaine"):
|
|
|
|
|
communes["domaine_interne"] = site["domaine"]
|
|
|
|
|
if site.get("organisation"):
|
|
|
|
|
communes["organisation"] = site["organisation"]
|
|
|
|
|
# LE REBOND, DECLARE PLUTOT QUE PASSE EN LIGNE DE COMMANDE. Le poste n'a pas de route
|
|
|
|
|
# vers le segment du site ; la frontiere y est adjacente. Le jour ou une route directe
|
|
|
|
|
# existe, on retire `rebond:` de la carte et rien d'autre ne bouge.
|
|
|
|
|
# Le socle ne pose `/etc/resolv.conf` que si cette variable existe. Sans elle, les
|
|
|
|
|
# machines naissent sans resolveur — et l'echec ressemble a un probleme de reseau.
|
|
|
|
|
if site.get("dns_amorcage"):
|
|
|
|
|
communes["dns_amorcage"] = str(site["dns_amorcage"])
|
|
|
|
|
if site.get("cle_ssh"):
|
|
|
|
|
communes["ansible_ssh_private_key_file"] = site["cle_ssh"]
|
|
|
|
|
if site.get("rebond"):
|
|
|
|
|
communes["ansible_ssh_common_args"] = (
|
|
|
|
|
f"-o ProxyJump={site['rebond']} -o StrictHostKeyChecking=no")
|
|
|
|
|
|
2026-08-24 23:11:32 -04:00
|
|
|
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,
|
2026-08-25 09:34:02 -04:00
|
|
|
**communes,
|
2026-08-24 23:11:32 -04:00
|
|
|
}
|
frontiere : le devis voit le site — `fabric`, alias et regles
Un mot manquait au vocabulaire des flux. Le runner de SITE declarait son API Proxmox en
`externe`, qui se rend par !SETOPS_INTERNES — or les hyperviseurs SONT en RFC 1918 : la
regle les aurait exclus tout en ayant l'air d'ouvrir le flux. D'ou `fabric`.
Deux flux manquaient :
- serveur_cache_site n'avait aucun egress : la chaine de caches se terminait sur un
cache vide, et ca ne se serait vu qu'au premier apt update d'un ecosysteme neuf ;
- serveur_ops_site n'avait que l'API : le shell des noeuds et l'API de la frontiere
sont deux autres pouvoirs, declares a part.
Le devis ignorait les machines du site — dans aucun plan de tenant, donc invisibles a sa
boucle, zero regle rendue SANS RIEN SIGNALER. Il emet desormais SETOPS_SITE,
SETOPS_FABRIC, SETOPS_ADMIN_SITE et un alias par role, sur opt2 (arrivee reelle du
trafic) et lan pour le SSH, plus le NAT sortant du site.
Le socle s'applique au site : site_inventaire.py range ses machines dans serveur_debian.
Patient 0 ne declare plus serveur_ops_site ni serveur_cache_site : il est un tenant
comme les autres, c'est meme tout ce qu'il prouve. Inventaire regenere.
42 preuves vertes. Devis : 26 regles a creer, 5 a retirer.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 00:49:04 -04:00
|
|
|
groupes.setdefault(GROUPE_SOCLE, []).append(nom)
|
2026-08-24 23:11:32 -04:00
|
|
|
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())
|