diff --git a/CHANGELOG.md b/CHANGELOG.md index 178ca6e..07b8467 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,21 @@ chacun respecte sa contrainte, et le préfixe reste commun. Vérifié : aucune collision d'IPSet, et **tout renvoi `+X` d'une règle pointe vers un IPSet qui existe** — 58 IPSets, 34 groupes, aucun orphelin. +### Corrigé — des listes d'adresses en dur, et 48 IPSets inutilisés +Six règles par tenant portaient **quatorze adresses en dur** : les mots-clés `flotte` et +`edge` n'avaient pas droit à un IPSet, seuls les rôles en avaient. `flotte` en reçoit un +désormais, et `edge` renvoie à celui de nginx. **36 des 40 règles** se lisent maintenant +`-source +t17-…`. + +Et le devis listait **28 à 30 IPSets par tenant dont la moitié n'était référencée nulle +part** : un opérateur en aurait créé 58 pour n'en utiliser qu'une douzaine. Seuls les IPSets +réellement référencés sont émis — **6 par tenant**. Un devis crée ce qu'il liste. + +Restent quatre règles en liste explicite : celles dont la source est **plusieurs rôles à la +fois** (LDAPS ouvert à Keycloak, Dovecot, Icinga Web et Postfix). Aucun IPSet unique ne les +couvre et Proxmox n'accepte qu'une référence par règle ; les éclater gonflerait le devis pour +un gain discutable. + ### Corrigé — l'affectation variait selon l'état du tenant Elle partait de `hotes_actifs`, avec un repli sur « tous » quand il n'y en avait aucun. Deux tenants donnaient donc deux comportements : Technolibre listait ses 14 VM (zéro actif → repli), diff --git a/scripts/devis_proxmox_fw.py b/scripts/devis_proxmox_fw.py index f123fcf..1f621d1 100644 --- a/scripts/devis_proxmox_fw.py +++ b/scripts/devis_proxmox_fw.py @@ -33,6 +33,7 @@ RACINE = Path(__file__).resolve().parents[1] sys.path.insert(0, str(RACINE / "scripts")) from resoudre_flux import ( # noqa: E402 + GROUPE_EDGE, charger_flux, est_groupe_operationnel, _enfants, @@ -111,7 +112,13 @@ def construire(tenants: list[tuple[str, str, dict]]) -> dict: ips = _ip_par_hote(data) # Un IPSet par role porte dans ce tenant : les membres sont ses hotes. + # `flotte` en recoit un aussi : sans lui, six regles par tenant portaient + # quatorze adresses en dur, illisibles et impossibles a mettre a jour d'un geste. ipsets: dict[str, dict] = {} + if ips: + ipsets[f"t{n['index']}-flotte"] = { + "role": "flotte (tous les hotes du tenant)", "membres": sorted(set(ips.values())), + } for groupe in sorted(_enfants(data)): if not est_groupe_operationnel(groupe): continue @@ -123,6 +130,7 @@ def construire(tenants: list[tuple[str, str, dict]]) -> dict: groupes: list[dict] = [] sans_source: list[str] = [] + utilises: set[str] = set() for groupe in sorted(_enfants(data)): if not est_groupe_operationnel(groupe): continue @@ -141,10 +149,17 @@ def construire(tenants: list[tuple[str, str, dict]]) -> dict: if not srcs: sans_source.append(f"{groupe}:{','.join(_ports(fl))}") continue - # Source par IPSet quand le pair est UN role : une seule liste a tenir. - source = (f"+{nom_ipset(pairs[0], n['index'])}" - if len(pairs) == 1 and nom_ipset(pairs[0], n["index"]) in ipsets - else ",".join(srcs)) + # Source par IPSet des que possible : une seule liste a tenir, et une + # regle qui se lit. Les mots-cles `flotte` et `edge` y ont droit comme + # les roles — `edge` designe le role nginx. + source = ",".join(srcs) + if len(pairs) == 1: + cle = {"flotte": f"t{n['index']}-flotte", + "edge": nom_ipset(GROUPE_EDGE, n["index"])}.get( + pairs[0], nom_ipset(pairs[0], n["index"])) + if cle in ipsets: + source = f"+{cle}" + utilises.add(cle) for port in _ports(fl): regles.append({ "action": "ACCEPT", "sens": "IN", @@ -179,7 +194,8 @@ def construire(tenants: list[tuple[str, str, dict]]) -> dict: blocs.append({ "tenant": nom, "etiquette": etiquette, - "ipsets": ipsets, "groupes": groupes, + "ipsets": {k: v for k, v in ipsets.items() if k in utilises}, + "groupes": groupes, "affectations": affect, "sans_source": sorted(set(sans_source)), "admin": admin_de(nom), })