Le commit dc92f01 justifiait le retrait des roles de site de patient 0 par « patient 0
est un tenant comme les autres, c'est meme tout ce qu'il prouve ». C'est FAUX.
Un tenant ordinaire existe POUR SES GENS : Chezlepro heberge de l'identite, du courriel,
de la collaboration. Patient 0 n'heberge que LA LIGNEE. Il est l'ecosysteme d'origine et
le detenteur du genome, et il le reste.
Ce qui le quitte, ce sont deux responsabilites de SITE qu'il tenait faute d'un SITE
capable de les porter — a l'epoque un site n'etait qu'un fichier de carte, sans machines.
Le SITE existe desormais comme objet a part entiere : le geste etait donc juste, seule sa
raison ne l'etait pas.
Le texte est corrige plutot qu'efface, dans le CHANGELOG comme dans les fichiers : une
doctrine juste appuyee sur une raison fausse finit toujours par se retourner.
42 preuves vertes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
124 lines
5.4 KiB
Python
Executable file
124 lines
5.4 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"
|
|
|
|
# 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"
|
|
|
|
|
|
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))
|
|
|
|
# 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")
|
|
|
|
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,
|
|
**communes,
|
|
}
|
|
groupes.setdefault(GROUPE_SOCLE, []).append(nom)
|
|
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())
|