#!/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/.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())