104 lines
4.1 KiB
Python
104 lines
4.1 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
"""Devis des expositions web : chaque `expose:` du plan répond-il vraiment ?
|
||
|
|
|
||
|
|
Lit le relevé déposé par `playbooks/maintenance/devis-expositions.yml`. Aucun accès réseau
|
||
|
|
ici. Sort en code 1 s'il y a un écart.
|
||
|
|
|
||
|
|
**Une vraie requête, jamais un `connect()`.** À travers l'OPNsense (anti-spoofing), toute
|
||
|
|
connexion TCP réussit — y compris vers une adresse où aucune machine n'existe. Et « lire
|
||
|
|
des données après connexion » ne vaut rien en TLS, où c'est le *client* qui parle en
|
||
|
|
premier : le port 443 d'un edge sain se comporte exactement comme un port mort. Seul le
|
||
|
|
code de retour d'une requête HTTPS complète tranche.
|
||
|
|
|
||
|
|
**Deux points de vue, et c'est leur différence qui diagnostique :**
|
||
|
|
|
||
|
|
depuis l'edge — edge + dorsal, sans la frontière
|
||
|
|
depuis le poste — DNS + frontière + edge + dorsal, le chemin d'un vrai utilisateur
|
||
|
|
|
||
|
|
L'un répond et pas l'autre : ce n'est pas le service, c'est le chemin.
|
||
|
|
"""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import json
|
||
|
|
import sys
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
RACINE = Path(__file__).resolve().parent.parent
|
||
|
|
RELEVE = RACINE / "instance" / "devis-expositions.json"
|
||
|
|
|
||
|
|
# Un service VIVANT peut legitimement repondre autre chose que 200 : une redirection vers
|
||
|
|
# l'IdP, ou un 401/403 quand l'authentification est exigee. Ce qui compte est qu'il ait
|
||
|
|
# repondu quelque chose d'intelligible.
|
||
|
|
VIVANT = {200, 301, 302, 303, 307, 401, 403}
|
||
|
|
|
||
|
|
|
||
|
|
def _lire(paires: list) -> dict[str, dict]:
|
||
|
|
"""Le relevé associe chaque résultat à son FQDN (zip côté Ansible)."""
|
||
|
|
out = {}
|
||
|
|
for resultat, fqdn in paires:
|
||
|
|
out[fqdn] = resultat
|
||
|
|
return out
|
||
|
|
|
||
|
|
|
||
|
|
def _verdict(r: dict) -> tuple[int, bool, str]:
|
||
|
|
"""Rend (code affiché, vivant ?, message).
|
||
|
|
|
||
|
|
Un code n'est pas un verdict : un 502 ou un 504 est une reponse, et pourtant le
|
||
|
|
service est mort derriere. Le premier jet ne testait que la PRESENCE d'un code et
|
||
|
|
laissait donc passer un dorsal tombe — trouve par le test negatif, pas par la
|
||
|
|
lecture. Le 2026-08-08.
|
||
|
|
"""
|
||
|
|
code = r.get("status", 0)
|
||
|
|
if not isinstance(code, int) or code <= 0:
|
||
|
|
return 0, False, (r.get("msg") or "aucune réponse")[:90]
|
||
|
|
return code, code in VIVANT, (r.get("msg") or "")[:90]
|
||
|
|
|
||
|
|
|
||
|
|
def analyser(releve: dict) -> list[str]:
|
||
|
|
edge, poste, ecarts = _lire(releve["depuis_edge"]), _lire(releve["poste"]), []
|
||
|
|
largeur = max((len(f) for f in releve["expositions"]), default=10)
|
||
|
|
|
||
|
|
print(f" {'exposition':<{largeur}} edge poste")
|
||
|
|
for fqdn in releve["expositions"]:
|
||
|
|
ce, ve, me = _verdict(edge.get(fqdn, {}))
|
||
|
|
cp, vp, mp = _verdict(poste.get(fqdn, {}))
|
||
|
|
print(f" {fqdn:<{largeur}} {ce or '---':<6} {cp or '---'}")
|
||
|
|
|
||
|
|
if not ve and not vp:
|
||
|
|
ecarts.append(f"{fqdn} : en echec des DEUX cotes ({ce or 'muet'}) — le service "
|
||
|
|
f"lui-meme ne repond pas. {me}")
|
||
|
|
elif ve and not vp:
|
||
|
|
ecarts.append(f"{fqdn} : repond depuis l'edge ({ce}) mais PAS depuis le poste "
|
||
|
|
f"({cp or 'muet'}) — ce n'est pas le service, c'est le chemin "
|
||
|
|
f"(DNS ou frontiere). {mp}")
|
||
|
|
elif vp and not ve:
|
||
|
|
ecarts.append(f"{fqdn} : repond depuis le poste ({cp}) mais pas depuis l'edge "
|
||
|
|
f"({ce or 'muet'}) — la resolution interne diverge de la publique. {me}")
|
||
|
|
return ecarts
|
||
|
|
|
||
|
|
|
||
|
|
def main(argv: list[str] | None = None) -> int:
|
||
|
|
chemin = Path(argv[0]) if argv else RELEVE
|
||
|
|
if not chemin.exists():
|
||
|
|
print("Relevé absent. Lancer d'abord : make expositions-plan", file=sys.stderr)
|
||
|
|
return 2
|
||
|
|
|
||
|
|
releve = json.loads(chemin.read_text(encoding="utf-8"))
|
||
|
|
print(f"Devis des expositions — {len(releve['expositions'])} déclarée(s) au plan, "
|
||
|
|
f"edge {releve['edge']}\n")
|
||
|
|
ecarts = analyser(releve)
|
||
|
|
|
||
|
|
if not ecarts:
|
||
|
|
print("\nCONFORME : toute exposition déclarée répond, des deux points de vue.")
|
||
|
|
return 0
|
||
|
|
print()
|
||
|
|
for e in ecarts:
|
||
|
|
print(f" [écart] {e}")
|
||
|
|
print(f"\nÉCART : {len(ecarts)} exposition(s) en difficulté.")
|
||
|
|
return 1
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
raise SystemExit(main(sys.argv[1:]))
|