Some checks are pending
verifier / verifier (push) Waiting to run
Les six machines du SITE — racine de l AC, forge, cache, resolveur, depot de sauvegarde, runner — ne recevaient que serveur_debian. Ni auditd, ni fail2ban, ni apparmor, ni sysctl, ni pare-feu. Le commentaire au-dessus de GROUPE_SOCLE affirmait pourtant le contraire, ce qui rendait l ecart invisible. Le pare-feu du site n avait aucune des trois pieces qui le rendent applicable : - aucune regle generee (resoudre_flux lit un hosts.yml ; le site a un inventaire dynamique) -> commande nftables-site, branchee sur make flux - le chemin du jeu de regles sortait du depot (inventory_dir vaut scripts/ pour un inventaire dynamique) -> host_var explicite - aucun nftables_admin_ssh : l exploitant administre PAR REBOND, la connexion arrive avec l adresse de la patte de frontiere DANS LA ZONE VISEE. Le generateur refuse desormais de produire des regles sans cet intrant. Defaut attrape en LISANT le jeu de regles avant de l appliquer : la regle DNS de site-dns-01 omettait 10.17.0.0/16. _supernets_voisins retire l instance montee — juste chez un tenant, faux du cote du site, ou ca retire le locataire qu on pilote. L appliquer aurait prive de DNS les quinze machines reconstruites la veille. MaxStartups du depot passe en host_var : dans le meta du role, il ne portait que dans son play, et le passage de serveur_durci le remettait au defaut sans rien dire. Un reglage qui depend de l ordre des plays revient en arriere. Verifie depuis un locataire a travers le nouveau pare-feu : cache, forge, depot (2 snapshots) et resolveur repondent ; l AC du site refuse, et c est correct. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
434 lines
23 KiB
Python
Executable file
434 lines
23 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 yaml # noqa: E402
|
|
import underlay as U # noqa: E402
|
|
from inventory_rules import integrations_universelles # 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.
|
|
#
|
|
# CE COMMENTAIRE DISAIT VRAI, LE CODE N'EN FAISAIT QUE LA MOITIE (2026-09-02).
|
|
#
|
|
# Seul `serveur_debian` etait pose. `serveur_durci` — auditd, fail2ban, apparmor,
|
|
# sysctl, unattended-upgrades, journald, ssh_hardening, nftables — ne l'etait pas. Un
|
|
# inventaire de tenant met CHAQUE machine dans les deux (`inventory_host.py`) ; celui du
|
|
# site n'en mettait aucune dans le second.
|
|
#
|
|
# Ce que ca laissait sans protection : la racine de l'autorite de certification, la
|
|
# forge qui porte le genome, le cache d'artefacts par ou passe chaque paquet installe
|
|
# sur chaque machine de chaque locataire, et le depot ou tous deposent leur etat. Les
|
|
# machines les plus consequentes de l'installation etaient les moins durcies — et
|
|
# personne ne pouvait le voir, puisque le commentaire affirmait le contraire.
|
|
#
|
|
# TROUVE EN CHERCHANT AUTRE CHOSE : un depot de sauvegarde refusait une cle valide, et
|
|
# le fichier de durcissement SSH fautif datait d'une version abandonnee le 2026-08-09.
|
|
# Rien ne l'avait jamais remplace parce que rien n'appliquait le role qui le remplace.
|
|
GROUPE_SOCLE = ["serveur_debian", "serveur_durci"]
|
|
|
|
# LE PLANCHER `/etc/hosts` SE DERIVE DE CE GROUPE, et de nul autre.
|
|
#
|
|
# `hosts_statiques` construit sa liste depuis `hotes_actifs` + `hotes_planifies` — des
|
|
# groupes qu'un inventaire de tenant fabrique, et que celui du site ne fabriquait pas. Le
|
|
# role tournait donc sans erreur et ecrivait un plancher VIDE : `localhost`, et rien
|
|
# d'autre. Un succes qui n'a rien fait, la pire forme d'echec.
|
|
#
|
|
# Le site n'a pas d'hotes « planifies » — une machine y est declaree ou elle n'existe pas.
|
|
# Elles vont donc toutes dans `hotes_actifs`.
|
|
GROUPE_FLOTTE = "hotes_actifs"
|
|
|
|
|
|
def plan_dir() -> Path | None:
|
|
"""Le `plan/` du SITE, a cote de son `underlay.yml` — derive du symlink, jamais redit.
|
|
|
|
UN SITE A BIEN UN PLAN (2026-08-25). Ce qui le distingue d'un tenant n'est pas
|
|
l'absence de plan mais l'absence de DERIVATION : un tenant tire son adressage de son
|
|
`index`, un site le declare, parce qu'il EST le terrain. Il ne passe donc pas par
|
|
`instancier` — mais il a bien des machines, des services et des intrants a declarer.
|
|
|
|
Faute d'avoir fait cette distinction, ce plan a d'abord ete reconstruit morceau par
|
|
morceau dans `underlay.yml`, sans etre nomme : chaque manque est arrive par surprise
|
|
au lieu d'etre previsible. `underlay.yml` decrit ce qui est MESURE (la fabric) ; le
|
|
plan decrit ce qui est VOULU.
|
|
"""
|
|
c = U.chemin()
|
|
return (c.resolve().parent / "plan") if c else None
|
|
|
|
|
|
def _charger(nom: str) -> dict:
|
|
d = plan_dir()
|
|
f = (d / nom) if d else None
|
|
if not f or not f.is_file():
|
|
return {}
|
|
return yaml.safe_load(f.read_text(encoding="utf-8")) or {}
|
|
|
|
|
|
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": {}}}
|
|
|
|
intrants = _charger("10-intrants.yml")
|
|
serveurs = (_charger("serveurs.yml") or {}).get("serveurs") or {}
|
|
applications = (_charger("applications.yml") or {}).get("applications") or {}
|
|
if not serveurs:
|
|
return {"_meta": {"hostvars": {}}}
|
|
|
|
# LES SUPERNETS DES TENANTS QUE CE SITE HEBERGE.
|
|
#
|
|
# LA MEME SOURCE QUE LES FLUX, UN FILTRE DIFFERENT — et la difference compte.
|
|
#
|
|
# `resoudre_flux._supernets_voisins()` EXCLUT l'instance montee : il sert a declarer
|
|
# les flux d'un tenant vers ses VOISINS, et personne n'est son propre voisin. Le site,
|
|
# lui, sert TOUS ceux qu'il heberge — y compris celui qu'on pilote au moment ou l'on
|
|
# genere cet inventaire. Reutiliser la fonction telle quelle aurait prive de
|
|
# resolution le seul tenant qu'on est en train de deployer, et la panne serait apparue
|
|
# chez lui seul (constate en la branchant, le 2026-08-30 : Chezlepro manquait).
|
|
#
|
|
# On reutilise donc la DECOUVERTE partagee (`decouvrir_du_site`, lecon de P41) sans
|
|
# son filtre : deux recensements de tenants finiraient par diverger, deux filtres non.
|
|
#
|
|
# Rend [] si la carte de l'hebergeur n'est pas montee : le site se decrit alors comme
|
|
# avant, sans preter son resolveur. Degrader, jamais deviner.
|
|
try:
|
|
import devis_reseau
|
|
from inventory_rules import supernet_de
|
|
_supernets_tenants = sorted({supernet_de(int(n["index"]))
|
|
for _nom, _p, n in devis_reseau.decouvrir_du_site()
|
|
if n.get("index") is not None})
|
|
except Exception:
|
|
_supernets_tenants = []
|
|
|
|
# LES LOCATAIRES DU DEPOT DE SAUVEGARDE — UN COMPTE UNIX CHACUN.
|
|
#
|
|
# Le depot du site recoit l'etat de PLUSIEURS ecosystemes. `serveur_backup`, concu
|
|
# pour le depot d'un tenant, n'a qu'un compte et qu'une cle : la ou il n'y a qu'un
|
|
# locataire, ca suffit. Ici, ca ne suffit plus — une seule cle partagee donnerait a
|
|
# chaque tenant la lecture des instantanes de tous les autres. Une sauvegarde qui
|
|
# expose ce qu'elle protege ne protege rien.
|
|
#
|
|
# D'ou un compte par locataire, chacun avec SON home en 0700 et SA cle. L'isolation
|
|
# est alors celle du noyau, pas une convention de nommage de repertoires.
|
|
#
|
|
# La cle PUBLIQUE d'un tenant n'est pas un secret : elle vit deja en clair dans son
|
|
# `group_vars/serveur_backup.yml`. C'est la MOITIE PRIVEE qui reste chez lui, dans sa
|
|
# voute, que le site ne detient pas et n'a pas a detenir. Le site heberge des
|
|
# octets chiffres par restic cote client : il ne peut pas les lire non plus.
|
|
#
|
|
# Meme decouverte que ci-dessus (`decouvrir_du_site`) : le site sert les tenants
|
|
# qu'il heberge, et cesse de les servir quand ils s'emancipent.
|
|
def _locataires_sauvegarde() -> list:
|
|
out = []
|
|
try:
|
|
import devis_reseau
|
|
racine_instances = devis_reseau.DOSSIER_INSTANCES
|
|
decouverts = devis_reseau.decouvrir_du_site()
|
|
except Exception:
|
|
return out
|
|
for nom_depot, prefixe, _n in decouverts:
|
|
gv = (racine_instances / nom_depot / "inventories" / "principal"
|
|
/ "group_vars" / "serveur_backup.yml")
|
|
try:
|
|
cle = str((yaml.safe_load(gv.read_text(encoding="utf-8")) or {})
|
|
.get("serveur_backup_pubkey") or "").strip()
|
|
except OSError:
|
|
cle = ""
|
|
# Un locataire sans cle publique declaree n'est pas une erreur du site :
|
|
# c'est un tenant qui n'a pas encore de sauvegarde. On ne lui ouvre pas de
|
|
# compte, et surtout on n'en ouvre pas un SANS cle — il accepterait alors
|
|
# tout ce que le compte `restic` accepte deja.
|
|
if cle:
|
|
out.append({"nom": nom_depot,
|
|
"compte": "restic-" + prefixe.lower(),
|
|
"pubkey": cle})
|
|
return out
|
|
|
|
_locataires_backup = _locataires_sauvegarde()
|
|
|
|
# Les groupes d'une machine du site, tels que `plan/applications.yml` les declare.
|
|
def _groupes_de(nom_hote: str) -> set:
|
|
return {str(a.get("groupe")) for a in applications.values()
|
|
if str(a.get("hote")) == nom_hote and a.get("groupe")}
|
|
|
|
par_reseau = {r.get("nom"): r for r in U.reseaux(u)}
|
|
# Les sous-reseaux ou vivent REELLEMENT des machines du site : l'etendue de
|
|
# l'ecosysteme, telle que le plan la dessine.
|
|
_zones_du_site = sorted({str((par_reseau.get(s.get("reseau")) or {}).get("sous_reseau"))
|
|
for s in serveurs.values()
|
|
if str(s.get("etat", "actif")) == "actif"
|
|
and (par_reseau.get(s.get("reseau")) or {}).get("sous_reseau")})
|
|
|
|
# LE RESOLVEUR D'AMORCAGE SE DERIVE, IL NE S'ECRIT PAS (2026-08-25).
|
|
#
|
|
# Il a ete ecrit en dur deux fois, et il a eu tort les deux fois : d'abord la frontiere
|
|
# (`10.0.3.1`) alors que le site avait son DNS, puis `10.0.3.51` — juste jusqu'a ce que
|
|
# le decoupage en zones deplace la machine en `10.0.34.11`.
|
|
#
|
|
# Une adresse ecrite a la main est une copie ; une copie se perime. Celle-ci se lit
|
|
# desormais la ou elle est vraie : l'hote qui porte `serveur_resolveur` dans ce plan.
|
|
# Le plan peut encore la surcharger — pour l'amorcage d'un site tout neuf, ou le
|
|
# resolveur n'existe pas encore — mais ce n'est plus le cas normal.
|
|
_resolveur = next((str(s.get("ip")) for nom, s in serveurs.items()
|
|
if any(a.get("hote") == nom and a.get("groupe") == "serveur_resolveur"
|
|
for a in applications.values())
|
|
and str(s.get("etat", "actif")) == "actif"), "")
|
|
|
|
# Ce que TOUTE machine du site recoit, des intrants du plan.
|
|
communes: dict = {}
|
|
if _resolveur:
|
|
communes["dns_amorcage"] = _resolveur
|
|
for cle in ("domaine_interne", "organisation", "dns_amorcage"):
|
|
if intrants.get(cle):
|
|
communes[cle] = str(intrants[cle])
|
|
# L'identite de sauvegarde DU SITE (moitie publique). Le depot du site l'autorise
|
|
# pour ses propres machines ; les locataires ont chacun la leur, ailleurs.
|
|
if intrants.get("backup_pubkey"):
|
|
communes["serveur_backup_pubkey"] = str(intrants["backup_pubkey"])
|
|
if intrants.get("cle_ssh"):
|
|
communes["ansible_ssh_private_key_file"] = intrants["cle_ssh"]
|
|
if intrants.get("rebond"):
|
|
communes["ansible_ssh_common_args"] = (
|
|
f"-o ProxyJump={intrants['rebond']} -o StrictHostKeyChecking=no")
|
|
|
|
# Les integrations universelles, moins celles que le site ne peut pas honorer.
|
|
exemptees = dict(intrants.get("integrations_exemptes") or {})
|
|
universelles = [r for r in integrations_universelles() if r not in exemptees]
|
|
|
|
# LES EXPOSITIONS : le nom du SERVICE, pas celui de la machine. Un nom de service
|
|
# survit au demenagement du service ; un nom de machine, non.
|
|
#
|
|
# `edge` nomme le GROUPE qui sert ce FQDN — c'est ce qu'attend `hosts_statiques`.
|
|
# Chez un tenant c'est l'edge nginx, qui termine le TLS pour tout le monde. Le site
|
|
# n'a pas d'edge : chaque service se sert lui-meme, donc le groupe est le sien. La
|
|
# forme reste la meme, ce qui la remplit change.
|
|
expositions: list[dict] = []
|
|
for nom_app, app in applications.items():
|
|
for fqdn in (app.get("expose") or []):
|
|
expositions.append({"fqdn": str(fqdn), "edge": str(app.get("groupe") or ""),
|
|
"application": nom_app})
|
|
|
|
# --- LES GARDES DU PLAN, LA OU LE PLAN EST LU ----------------------------
|
|
#
|
|
# Elles vivaient dans `underlay.valider()` tant que les machines habitaient la carte.
|
|
# Le plan les emporte avec lui : une garde loin de ce qu'elle garde finit par garder
|
|
# autre chose. Elles refusent ici, avant que quoi que ce soit ne soit clone.
|
|
erreurs: list[str] = []
|
|
noeuds = {h.get("nom") for h in U.hotes(u) if h.get("role") == "hyperviseur"}
|
|
vus_ip: dict[str, str] = {str(h.get("ip")): str(h.get("nom")) for h in U.hotes(u)}
|
|
vus_vmid: dict[int, str] = {}
|
|
for nom, srv in serveurs.items():
|
|
if str(srv.get("etat", "actif")) != "actif":
|
|
continue
|
|
r = par_reseau.get(srv.get("reseau"))
|
|
if not r:
|
|
erreurs.append(f"serveur '{nom}': reseau '{srv.get('reseau')}' inconnu de "
|
|
f"`underlay.yml` — la fabric est la source de ces valeurs")
|
|
continue
|
|
if not str(r.get("pont", "") or "").strip():
|
|
erreurs.append(f"serveur '{nom}': le reseau '{r.get('nom')}' ne declare aucun "
|
|
f"`pont:` — aucun pont d'hyperviseur ne le porte, une VM y "
|
|
f"naitrait sourde")
|
|
if srv.get("noeud") not in noeuds:
|
|
erreurs.append(f"serveur '{nom}': noeud '{srv.get('noeud')}' n'est pas un "
|
|
f"hyperviseur declare dans `underlay.hotes`")
|
|
ip = str(srv.get("ip") or "")
|
|
if ip in vus_ip:
|
|
erreurs.append(f"serveur '{nom}': IP {ip} deja prise par '{vus_ip[ip]}'")
|
|
vus_ip[ip] = nom
|
|
vmid = srv.get("vmid")
|
|
if not isinstance(vmid, int):
|
|
erreurs.append(f"serveur '{nom}': `vmid` absent ou non entier")
|
|
elif vmid in vus_vmid:
|
|
erreurs.append(f"serveur '{nom}': vmid {vmid} en double avec "
|
|
f"'{vus_vmid[vmid]}'")
|
|
else:
|
|
vus_vmid[vmid] = nom
|
|
for nom_app, app in applications.items():
|
|
if app.get("hote") not in serveurs:
|
|
erreurs.append(f"application '{nom_app}': hote '{app.get('hote')}' n'est pas "
|
|
f"declare dans `plan/serveurs.yml`")
|
|
sans_service = [n for n in serveurs
|
|
if not any(a.get("hote") == n for a in applications.values())]
|
|
if sans_service:
|
|
erreurs.append("serveur(s) sans aucune application : " + ", ".join(sorted(sans_service))
|
|
+ " — une machine du site existe POUR un role")
|
|
if erreurs:
|
|
raise SystemExit("plan du site refuse :\n - " + "\n - ".join(erreurs))
|
|
|
|
hostvars: dict[str, dict] = {}
|
|
groupes: dict[str, list[str]] = {}
|
|
|
|
for nom, srv in serveurs.items():
|
|
if str(srv.get("etat", "actif")) != "actif":
|
|
continue
|
|
r = par_reseau.get(srv.get("reseau")) or {}
|
|
hostvars[nom] = {
|
|
"ansible_host": str(srv["ip"]),
|
|
"ansible_user": srv.get("utilisateur", UTILISATEUR_DEFAUT),
|
|
"site_reseau": srv.get("reseau"),
|
|
"site_pont": r.get("pont"),
|
|
"site_noeud": srv.get("noeud"),
|
|
"proxmox_vmid": srv.get("vmid"),
|
|
# L'espace d'adressage de cet ecosysteme. Un tenant le derive de son index ;
|
|
# le site le tient du reseau ou vivent ses machines. Meme sens, autre source.
|
|
# `serveur_resolveur` s'en sert pour savoir QUI a le droit de l'interroger.
|
|
"setops_supernet": str(r.get("sous_reseau") or ""),
|
|
# QUI A LE DROIT D'INTERROGER LE RESOLVEUR — TOUTES LES ZONES DU SITE.
|
|
#
|
|
# `serveur_resolveur` derive sa liste d'autorisation de `setops_supernet`, qui
|
|
# vaut le sous-reseau de la machine. C'etait juste tant que le site etait un
|
|
# `/24` plat. Decoupe en zones, le resolveur n'autorisait plus que la SIENNE :
|
|
# les trois autres se faisaient refuser, et la panne ressemble a un DNS mort
|
|
# alors que c'est une ACL.
|
|
#
|
|
# Un resolveur ouvert au monde est un relais d'amplification ; celui-ci est
|
|
# ouvert a son ecosysteme, et a rien d'autre.
|
|
# LE SITE PRETE SON RESOLVEUR PENDANT LA JEUNESSE DE SES LOCATAIRES
|
|
# (2026-08-30). L'hote qui porte `serveur_resolveur_site` admet EN PLUS les
|
|
# supernets des tenants que ce site heberge.
|
|
#
|
|
# POURQUOI ICI ET PAS DANS LE ROLE : l'inventaire connait le plan du site ET
|
|
# le registre des tenants ; le role, lui, tourne sur une machine qui n'a
|
|
# aucune raison de lire la carte de l'hebergeur. La derivation appartient a
|
|
# qui detient les deux sources.
|
|
#
|
|
# CE QUE CA A COUTE DE NE PAS L'AVOIR : `infra-pki-01`, premiere machine a
|
|
# porter l'autorite de Chezlepro, ne pouvait resoudre aucun nom. `apt` s'en
|
|
# sortait par le mandataire du cache, qui resout a sa place — tout le reste
|
|
# (`get_url`, un depot tiers en HTTPS, un `git clone`) restait aveugle.
|
|
"serveur_resolveur_reseaux_autorises": (
|
|
_zones_du_site + _supernets_tenants
|
|
if "serveur_resolveur_site" in _groupes_de(nom) else _zones_du_site),
|
|
# LE PARE-FEU DE MACHINE, EN DEUX TEMPS (2026-09-02).
|
|
#
|
|
# `serveur_durci` porte `nftables_baseline`, qui refuse tout ce qui n'est pas
|
|
# declare. Chez un tenant, trois pieces le rendent applicable : un registre de
|
|
# flux resolu POUR SES HOTES, un chemin ou lire le jeu de regles genere, et un
|
|
# `nftables_admin_ssh` qui dit par ou l'administration entre.
|
|
#
|
|
# Le site n'en avait aucune : `resoudre_flux.py nftables` n'ecrit que pour
|
|
# l'instance montee, et le chemin par defaut (`inventory_dir/../../`) sort du
|
|
# depot pour un inventaire DYNAMIQUE, dont le `inventory_dir` est `scripts/`.
|
|
#
|
|
# Poser le pare-feu avant ces pieces, ce serait appliquer une politique
|
|
# `drop` sans aucune regle : les six machines du site tomberaient d'un coup,
|
|
# et le deploiement qui les repare passe justement par SSH. On pose donc les
|
|
# neuf autres roles d'abord — ils ne coupent rien — et le pare-feu ensuite,
|
|
# quand il aura de quoi laisser passer.
|
|
"nftables_baseline_enabled": True,
|
|
# LE CHEMIN DU JEU DE REGLES, DIT ICI PARCE QUE LE DEFAUT NE PEUT PAS LE DIRE.
|
|
#
|
|
# `nftables_baseline` derive son chemin de `inventory_dir` — juste pour un
|
|
# tenant, dont l'inventaire est un FICHIER sous `instance/inventories/...`.
|
|
# Le site a un inventaire DYNAMIQUE : son `inventory_dir` est `scripts/`, et
|
|
# le defaut pointait donc un cran AU-DESSUS du depot, sur un chemin qui
|
|
# n'existe pas. Le role serait tombe sur son repli — une politique `drop`
|
|
# sans les regles resolues.
|
|
#
|
|
# Les regles du site vivent a cote de son plan, comme sa carte et sa voute :
|
|
# `make flux` les y ecrit (`resoudre_flux.py nftables-site`).
|
|
"nftables_baseline_ruleset_genere": str(
|
|
(U.chemin().resolve().parent / "flux-genere" / f"{nom}.nft")
|
|
if U.chemin() else ""),
|
|
# Ce que le marqueur verifiera : la liste ci-dessus les admet-elle vraiment ?
|
|
"serveur_resolveur_site_supernets_tenants": _supernets_tenants,
|
|
# `MaxStartups` COMPTE LES CONNEXIONS NON AUTHENTIFIEES, et un depot de SITE
|
|
# en voit la somme de tous ses locataires — dont les minuteurs se reveillent
|
|
# a la meme heure. Au-dela du seuil, sshd coupe AVANT la banniere : le client
|
|
# rapporte `kex_exchange_identification: Connection reset by peer`, qui
|
|
# accuse le reseau pour un refus applicatif. Douze sauvegardes simultanees,
|
|
# une perdue (2026-09-01).
|
|
#
|
|
# EN host_var, ET NON EN PARAMETRE DE DEPENDANCE (regression du 2026-09-02).
|
|
# La valeur vivait dans le `meta` de `serveur_backup_site` : elle ne portait
|
|
# donc que dans le play de CE role. Le jour ou `serveur_durci` a apporte
|
|
# `ssh_hardening` a toutes les machines du site, son passage — le dernier — a
|
|
# remis le depot a la valeur par defaut, sans rien signaler. Un reglage qui
|
|
# depend de l'ordre des plays est un reglage qui reviendra en arriere.
|
|
#
|
|
# Ici, tout play qui applique `ssh_hardening` a cet hote la voit.
|
|
# Les ecosystemes qui deposent leur etat ici, un compte Unix chacun.
|
|
"serveur_backup_site_locataires": (
|
|
_locataires_backup
|
|
if "serveur_backup_site" in _groupes_de(nom) else []),
|
|
# Un role partage peut avoir besoin de savoir de quel cote il tourne.
|
|
"setops_site": True,
|
|
# Le plan du site, pour les roles qui lisent des registres.
|
|
"setops_plan_dir": str(plan_dir() or ""),
|
|
"hosts_statiques_expositions": expositions,
|
|
**communes,
|
|
# En dernier : ce qu'une machine declare d'elle-meme prime sur ce que le
|
|
# plan declare pour toutes.
|
|
**(srv.get("variables") or {}),
|
|
}
|
|
for _socle in GROUPE_SOCLE:
|
|
groupes.setdefault(_socle, []).append(nom)
|
|
if "serveur_backup_site" in _groupes_de(nom):
|
|
hostvars[nom]["ssh_hardening_max_startups"] = "60:30:200"
|
|
groupes.setdefault(GROUPE_FLOTTE, []).append(nom)
|
|
for integ in universelles:
|
|
groupes.setdefault(integ, []).append(nom)
|
|
for integ in (srv.get("integrations") or []):
|
|
groupes.setdefault(str(integ), []).append(nom)
|
|
|
|
# LES GROUPES VIENNENT DU REGISTRE DES APPLICATIONS, comme chez un tenant.
|
|
for app in applications.values():
|
|
hote, groupe = app.get("hote"), app.get("groupe")
|
|
if hote in hostvars and groupe:
|
|
groupes.setdefault(str(groupe), []).append(hote)
|
|
|
|
out: dict = {g: {"hosts": sorted(set(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())
|