Set-OPS-Public/scripts/tests/test_parite_tls_site.py

108 lines
4.4 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
"""Ce qu'un locataire chiffre par ses group_vars, le site le chiffre aussi.
POURQUOI (2026-10-12). Trois fois le meme ecart : un interrupteur TLS que chaque locataire
pose dans ses `group_vars`, et que l'inventaire du site — dynamique, sans group_vars — ne
posait nulle part. PostgreSQL servait le certificat auto-signe de Debian (2026-09-12),
l'edge aussi (meme jour), et node_exporter et Loki servaient en HTTP (2026-10-12) alors
que le registre des flux les dit `tls`. Chaque fois, le certificat etait la, inutilise.
Le test lit les `group_vars/<groupe>.yml` des locataires montes a cote du depot, retient
chaque cle qui contient `tls` et vaut vrai, et exige la meme valeur dans les hostvars des
machines du site qui portent ce groupe.
"""
from __future__ import annotations
import sys
from pathlib import Path
import yaml
RACINE = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(RACINE / "scripts"))
import site_inventaire as si # noqa: E402
# CE QUI RESTE A FAIRE, NOMME — pas une tolerance muette. Chaque entree dit pourquoi le
# site differe encore, et disparait le jour ou l'ecart se ferme.
RESTE = {
("client_metrique", "client_metrique_tls_actif", "hyperviseurs"):
"node_exporter des hyperviseurs : il leur faudrait un certificat a eux "
"(pas de client_pki sur la fabric) — collecte encore en HTTP, chantier suivant.",
}
# CE QUE LE SITE OBTIENT AUTREMENT, avec la CONDITION qui le prouve dans ses hostvars.
# Pas une dispense : si la condition tombe, l'ecart redevient un echec.
AUTREMENT = {
("serveur_icinga", "serveur_icinga_db_tls"): (
lambda v: v.get("serveur_postgresql_tls_force") is True,
"derive par le role de `serveur_postgresql_tls_force` (resoudre_base), vrai au site"),
("serveur_postfix", "serveur_postfix_lmtp_tls_verify"): (
lambda v: v.get("serveur_postfix_mode") == "relais",
"sans objet : le relais du site ne remet rien par LMTP (`serveur_postfix_mode: relais`)"),
}
def interrupteurs_des_locataires() -> dict[tuple[str, str], list[str]]:
"""{(groupe, cle): [locataires]} pour chaque cle *tls* vraie d'un group_vars."""
vus: dict[tuple[str, str], list[str]] = {}
for gv in sorted(RACINE.parent.glob("OPS-*/inventories/principal/group_vars/*.yml")):
locataire = gv.parents[3].name
if locataire.endswith("-lab"):
continue
try:
donnees = yaml.safe_load(gv.read_text(encoding="utf-8")) or {}
except yaml.YAMLError:
continue
if not isinstance(donnees, dict):
continue
for cle, val in donnees.items():
if "tls" in str(cle) and val is True:
vus.setdefault((gv.stem, str(cle)), []).append(locataire)
return vus
def main() -> int:
attendus = interrupteurs_des_locataires()
if not attendus:
print("OK : aucun locataire monte a cote du depot — rien a comparer.")
return 0
try:
inv = si.inventaire()
except SystemExit as e: # plan du site refuse ou absent
print(f"OK : aucun site monte ({e}) — rien a comparer.")
return 0
except Exception as e: # noqa: BLE001
print(f"OK : aucun site monte ({type(e).__name__}) — rien a comparer.")
return 0
hv = inv["_meta"]["hostvars"]
hyperviseurs = set((inv.get("hyperviseurs") or {}).get("hosts") or [])
echecs, restes = [], []
for (groupe, cle), qui in sorted(attendus.items()):
for hote in (inv.get(groupe) or {}).get("hosts") or []:
if hv.get(hote, {}).get(cle) is True:
continue
autre = AUTREMENT.get((groupe, cle))
if autre and autre[0](hv.get(hote, {})):
continue
classe = "hyperviseurs" if hote in hyperviseurs else "vm"
raison = RESTE.get((groupe, cle, classe))
if raison:
restes.append(f"{hote} : {cle} — {raison}")
else:
echecs.append(f"{hote} ({groupe}) : `{cle}` vaut {hv.get(hote, {}).get(cle)!r} au site, "
f"true chez {', '.join(qui)}")
for r in sorted(set(restes)):
print(f"RESTE : {r}")
for e in echecs:
print(f"ECHEC : {e}")
if not echecs:
print(f"OK : les {len(attendus)} interrupteurs TLS des locataires valent aussi au site.")
return 1 if echecs else 0
if __name__ == "__main__":
raise SystemExit(main())