Set-OPS-Public/scripts/verifier_ports.py

113 lines
4.5 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
"""Deux rôles co-localisés revendiquent-ils le même port ?
Un port n'appartient à personne : le premier service démarré le prend, et l'autre boucle
sur `bind: address already in use`. Rien ne vérifiait qu'un hôte ne porte pas deux rôles
qui veulent le même.
Le défaut n'est pas théorique. Sur `infra-mail-01`, l'écoute SASL de Dovecot (12345,
choix délibéré de Set-OPS) et l'interface HTTP d'Alloy (12345, défaut amont) se disputent
le port depuis le premier jour. En exploitation courante Alloy le tenait, et c'est Dovecot
qui échouait **en silence**. Il a fallu une reconstruction from-zero, qui inverse l'ordre
de démarrage, pour que ça devienne audible (2026-08-08).
**La condition pour que ce contrôle existe** : que tout port lié soit DÉCLARÉ. Celui
d'Alloy ne l'était pas c'était un défaut amont subi, donc invisible. Il l'est désormais.
**`partage: true`** distingue les deux situations que le registre confondait :
- un rôle qui **ouvre** une écoute il revendique le port ;
- un rôle qui **décrit** l'écoute d'un autre pour la documenter ou l'autoriser
(`serveur_backup` empruntant le sshd de `serveur_debian`) pas une revendication.
Sans cette distinction, la seule co-location légitime de la flotte serait signalée à tort,
et on apprendrait à ignorer la preuve ce qui est pire que de ne pas l'avoir.
"""
from __future__ import annotations
import argparse
import collections
import sys
from pathlib import Path
import yaml
RACINE = Path(__file__).resolve().parent.parent
def _groupes(inventaire: Path) -> dict[str, set[str]]:
"""Groupe -> hotes, en descendant les `children` de l'inventaire YAML."""
inv = yaml.safe_load(inventaire.read_text(encoding="utf-8")) or {}
trouves: dict[str, set[str]] = collections.defaultdict(set)
def marcher(noeud, nom):
if not isinstance(noeud, dict):
return
if nom and isinstance(noeud.get("hosts"), dict):
trouves[nom] |= set(noeud["hosts"])
for enfant, sous in (noeud.get("children") or {}).items():
marcher(sous, enfant)
marcher(inv.get("all") or {}, "all")
return trouves
def _revendications() -> dict[tuple[str, str], set[str]]:
"""(protocole, port) -> roles qui OUVRENT cette ecoute."""
par_port: dict[tuple[str, str], set[str]] = collections.defaultdict(set)
for f in sorted(RACINE.glob("roles/*/meta/flux.yml")):
role = f.parts[-3]
d = yaml.safe_load(f.read_text(encoding="utf-8")) or {}
for fl in d.get("flux") or []:
if fl.get("sens") != "ingress" or fl.get("partage"):
continue
port = str(fl.get("port"))
if not port.isdigit(): # `derive`, codes ICMP : pas une ecoute fixe
continue
par_port[(str(fl.get("protocole", "tcp")).lower(), port)].add(role)
return par_port
def main(argv: list[str] | None = None) -> int:
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
ap.add_argument("--inventaire", default=None)
a = ap.parse_args(argv)
inventaire = Path(a.inventaire) if a.inventaire else \
(RACINE / "instance" / "inventories" / "principal" / "hosts.yml")
if not inventaire.is_file():
print(f"Inventaire introuvable : {inventaire}", file=sys.stderr)
return 2
groupes = _groupes(inventaire)
par_port = _revendications()
conflits: list[str] = []
for (proto, port), roles in sorted(par_port.items()):
if len(roles) < 2:
continue
hotes = set().union(*[groupes.get(r, set()) for r in roles])
for hote in sorted(hotes):
ici = sorted(r for r in roles if hote in groupes.get(r, set()))
if len(ici) > 1:
conflits.append(f"{hote} : {proto}/{port} revendique par " + " et ".join(ici))
if conflits:
print(f"{len(conflits)} collision(s) de port entre roles co-localises :\n")
for c in conflits:
print(f" {c}")
print("\nLe premier service demarre prendra le port ; l'autre echouera, peut-etre\n"
"en silence. Deplacer l'un des deux, ou marquer `partage: true` si les deux\n"
"decrivent la MEME ecoute.")
return 1
total = sum(len(r) for r in par_port.values())
print(f"CONFORME : {total} revendication(s) de port, aucune collision "
f"entre roles co-localises ({len(groupes)} groupes).")
return 0
if __name__ == "__main__":
raise SystemExit(main())