inter-sites : le second serveur de noms, sur l'autre site, par le tunnel
- underlay.py : sites pairs lus dans leur depot frere (sites_pairs_actifs, sur_le_site, dns_public_des_pairs). - Contrat du site : dns_public_pairs ; serveur_powerdns notifie et admet le pair ; resoudre_flux l'admet sur 5300. - devis_opnsense : opnsense_liens_sites (regles et routes selon ou vit chaque locataire) ; P24 juge les routes du lien a part ; noms d'alias < 32 caracteres gardes ; le refus du groupe wireguard ne tranche plus (quick=0), et la garde le verifie. - appliquer_opnsense : quick=0 transmis et dans l'identite d'une regle. - proxmox_api : SETOPS_PROXMOX_API pour joindre l'API par un rebond. Temoin depuis l'Internet : les deux zones repondent sur 51.79.45.193, signees. 95 preuves (make verifier), make test passe. CHANGELOG 149. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
65a4268871
commit
968caed3b1
10 changed files with 298 additions and 5 deletions
42
CHANGELOG.md
42
CHANGELOG.md
|
|
@ -1,3 +1,45 @@
|
|||
## 2026-10-14 (149) — Le second serveur de noms, sur l'autre site, par le tunnel
|
||||
|
||||
**95 preuves.** `make verifier` passe (site de Chezlepro), `make test` passe. Témoin de bout en
|
||||
bout, depuis l'Internet : `chezlepro.ca` et `technolibre.ca` répondent sur l'adresse publique du
|
||||
site de TechnoLibre (`51.79.45.193:53`, par le relais), signées DNSSEC, à la série du dernier
|
||||
transfert ; une zone inconnue reçoit `REFUSED`. Un NOTIFY forcé des primaires fait retirer les
|
||||
deux zones par le secondaire une seconde plus tard.
|
||||
|
||||
**La réplication du DNS public passe par le lien entre les sites (phase 2).**
|
||||
`dns_public_pairs` est `actif` des deux côtés.
|
||||
- `underlay.py` lit un site PAIR dans son dépôt frère par les mêmes fonctions qu'il emploie pour
|
||||
lui-même (`sites_pairs_actifs`, `sur_le_site`, `dns_public_des_pairs`) — pas de seconde
|
||||
lecture du plan qui finirait par diverger.
|
||||
- Le contrat du site porte `dns_public_pairs` ; les primaires des locataires le notifient
|
||||
(`only-notify`, `also-notify`) et le laissent frapper sur 5300 (pare-feu de la machine et
|
||||
pare-feu Proxmox).
|
||||
- `opnsense_liens_sites` : le devis dérive, selon OÙ VIT chaque locataire (la fiche qu'il
|
||||
porte), l'entrée du transfert par le tunnel et la sortie des NOTIFY là où il vit, les routes
|
||||
/32 vers ses primaires et l'entrée des NOTIFY là où il est répliqué. P24 juge ces routes à
|
||||
part : chacune doit désigner une adresse qu'une règle du lien utilise.
|
||||
- `bifrost-1` : `wg0` assigné (`opt11`), passerelle `INTERSITES_GW` ; `requin` : sa passerelle.
|
||||
- Témoin : le devis de Chezlepro ne change que par le lien (deux alias, quatre règles, une
|
||||
route) ; chez les locataires, seule `infra-dns-01` change.
|
||||
|
||||
**Trois règles d'OPNsense apprises sur la frontière de production.**
|
||||
- **Le groupe `wireguard` contient toutes les interfaces WireGuard, assignées comprises, et ses
|
||||
règles passent avant les leurs** : le refus audible `quick` du groupe (posé pour les tunnels
|
||||
d'administration) a rejeté le transfert avant les règles d'`opt11` — et aurait rejeté tout
|
||||
ce qui arrive par le relais sur `requin`. Il ne tranche plus (`quick=0`) ; le devis refuse un
|
||||
refus `quick` sur ce groupe dès qu'une interface WireGuard assignée porte ses règles.
|
||||
- **Un nom d'alias fait moins de 32 caractères** : `SETOPS_DNS_PUBLIC_SITE_TECHNOLIBRE` (34) a
|
||||
été refusé, et ses quatre règles avec. Gardé au devis, avant tout envoi.
|
||||
- **Enregistrer une instance WireGuard avec un seul champ vide ses relations** (`peers`).
|
||||
|
||||
**Le pare-feu Proxmox de Chezlepro**, rattrapé au passage : la règle `srv-dovecot` du port
|
||||
12345 (SASL supprimé par `63f5fda`) n'avait jamais été retirée ; commentaires de `srv-loki` et
|
||||
`srv-postgresql` à jour. Hors du site, l'API n'est joignable que par un rebond :
|
||||
`SETOPS_PROXMOX_API=hote:port` le désigne le temps d'une commande, sans toucher à la carte.
|
||||
|
||||
**Reste** : un nom pour ce second serveur (`dns_public_nom` du site de TechnoLibre), puis les
|
||||
enregistrements NS des zones et du registre ; la phase 3, les sauvegardes croisées.
|
||||
|
||||
## 2026-10-14 (148) — Le lien entre les sites, et ce qu'il a montré en ne devant rien ouvrir
|
||||
|
||||
**95 preuves.** `make verifier` passe (site de Chezlepro), `make test` passe. Témoin : avec le lien
|
||||
|
|
|
|||
|
|
@ -95,6 +95,9 @@ serveur_powerdns_sonde_tolerance: 900
|
|||
# tire les zones, et l'adresse publique vers laquelle elles pointent. Le locataire ne les
|
||||
# recopie pas — il les recoit, comme ses adresses d'amorcage.
|
||||
serveur_powerdns_dns_public_site: "{{ dns_public_site | default('') }}"
|
||||
# LES SERVEURS PUBLICS DES SITES PAIRS (2026-10-14) — le second serveur de noms, sur un autre
|
||||
# site, joint par le tunnel entre les sites. Du contrat du site, comme `dns_public_site`.
|
||||
serveur_powerdns_dns_public_pairs: "{{ dns_public_pairs | default([]) }}"
|
||||
serveur_powerdns_ip_publique: "{{ ip_publique_site | default('') }}"
|
||||
# L'ADRESSE DES NOMS EXPOSES : celle DU LOCATAIRE (2026-09-29), que le site lui attribue et
|
||||
# que la frontiere redirige vers son web frontal. `ip_publique_site` ne porte plus que les
|
||||
|
|
|
|||
|
|
@ -75,6 +75,7 @@
|
|||
# `allow-axfr-ips` SANS signature — mesure le 2026-09-16. Le serveur public du site
|
||||
# ne doit donc jamais y figurer.
|
||||
- serveur_powerdns_dns_public_site not in (serveur_powerdns_allow_axfr_ips | default([]))
|
||||
- serveur_powerdns_dns_public_pairs | intersect(serveur_powerdns_allow_axfr_ips | default([])) | length == 0
|
||||
- (vault_tsig_dns_public | default('')) | length > 0
|
||||
# LE NOM DU SERVEUR DE NOMS VIENT DU SITE. Sans lui, NS et SOA seraient vides.
|
||||
- serveur_powerdns_dns_public_nom is search('^[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)+\.?$')
|
||||
|
|
|
|||
|
|
@ -45,8 +45,10 @@ allow-axfr-ips=127.0.0.1/32
|
|||
# Par defaut, un primaire notifie AUSSI les adresses de ses enregistrements NS. Ici, ce
|
||||
# sont les adresses PUBLIQUES des serveurs de noms : le primaire enverrait des
|
||||
# notifications vers Internet depuis l'interieur du locataire. `only-notify` les borne.
|
||||
only-notify={{ serveur_powerdns_dns_public_site }}/32
|
||||
also-notify={{ serveur_powerdns_dns_public_site }}
|
||||
# ET CEUX DES SITES PAIRS (2026-10-14) : le second serveur de noms tire la zone par le tunnel
|
||||
# entre les sites, et doit apprendre qu'elle a change comme le premier.
|
||||
only-notify={{ ([serveur_powerdns_dns_public_site] + serveur_powerdns_dns_public_pairs) | map('regex_replace', '$', '/32') | join(',') }}
|
||||
also-notify={{ ([serveur_powerdns_dns_public_site] + serveur_powerdns_dns_public_pairs) | join(',') }}
|
||||
|
||||
version-string=anonymous
|
||||
security-poll-suffix=
|
||||
|
|
|
|||
|
|
@ -142,7 +142,8 @@ def cle_regle(r: dict) -> str:
|
|||
# n'a plus la cle du devis : elle est recreee, au lieu de passer pour conforme alors que
|
||||
# ses reponses partent par le WAN et se perdent.
|
||||
via = (f"|reply:{r['replyto']}" if r.get("replyto") else "") + \
|
||||
(f"|via:{r['passerelle']}" if r.get("passerelle") else "")
|
||||
(f"|via:{r['passerelle']}" if r.get("passerelle") else "") + \
|
||||
("|lent" if r.get("rapide") is False else "")
|
||||
return (f"setops:{marque}{r['tenant']}:{r['interface']}:{r['sens']}:{r['protocole']}:"
|
||||
f"{r['source']}->{r['destination']}:{ports}{via}")
|
||||
|
||||
|
|
@ -306,6 +307,10 @@ def _corps_regle(r: dict, k: str) -> dict:
|
|||
corps["replyto"] = r["replyto"]
|
||||
if r.get("passerelle"):
|
||||
corps["gateway"] = r["passerelle"]
|
||||
# `quick=0` : la regle ne tranche pas, le dernier verdict qui correspond l'emporte (voir le
|
||||
# refus du groupe `wireguard` dans le devis).
|
||||
if r.get("rapide") is False:
|
||||
corps["quick"] = "0"
|
||||
return corps
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1491,6 +1491,102 @@ def construire(tenants: list[tuple[str, str, dict]]) -> dict:
|
|||
"raison": str(s.get("motif") or "bruit de journal, deja refuse par defaut"),
|
||||
})
|
||||
|
||||
# LE LIEN ENTRE LES SITES (2026-10-14) — la replication du DNS public par le tunnel.
|
||||
#
|
||||
# `opnsense_liens_sites` declare, par site pair, l'interface du tunnel ; le prochain saut
|
||||
# est la `frontiere_pair` de ce site dans la carte. Rien n'est emis tant que le pair n'est
|
||||
# pas ACTIF des deux cotes (`dns_public_pairs`, P82).
|
||||
#
|
||||
# OU TOURNE UN LOCATAIRE ? Les deux cartes le declarent — l'une parce qu'elle l'heberge,
|
||||
# l'autre pour pouvoir le reprendre. La reponse est la fiche qu'il porte : le site qui la
|
||||
# lui a remise est celui ou il vit. Les regles s'inversent selon elle :
|
||||
# - la ou il vit : le transfert ENTRE par le tunnel (5300 vers ses primaires), et ses
|
||||
# NOTIFY SORTENT vers le serveur public du pair, route par le tunnel ;
|
||||
# - la ou il est replique : ses primaires sont ROUTES par le tunnel (/32 — ils priment
|
||||
# sur les routes de reprise), et ses NOTIFY ENTRENT par le tunnel.
|
||||
_liens_sites = [l for l in (_intr.get("opnsense_liens_sites") or [])
|
||||
if str(l.get("site") or "") in underlay_mod.sites_pairs_actifs()]
|
||||
|
||||
def _site_reel(_nom_t: str) -> str:
|
||||
try:
|
||||
import devis_reseau as _dr
|
||||
_f = _dr.DOSSIER_INSTANCES / _nom_t / "fiche-site.yml"
|
||||
return str((yaml.safe_load(_f.read_text(encoding="utf-8")) or {}).get("site") or "")
|
||||
except (OSError, ImportError):
|
||||
return ""
|
||||
|
||||
def _primaires_publiees(_nom_t: str) -> list[str]:
|
||||
_face = face_publiee(_nom_t)
|
||||
if _face is not None and not (_face.get("dns_public") or {}).get("zones"):
|
||||
return []
|
||||
return cibles_du_tenant("serveur_powerdns", _nom_t)
|
||||
|
||||
_ici = underlay_mod.chemin().resolve().parent.name if underlay_mod.chemin() else ""
|
||||
for _lien in _liens_sites:
|
||||
_pair = str(_lien["site"])
|
||||
_if_lien = str(_lien.get("interface") or "")
|
||||
_saut = next((str(h["ip"]) for h in underlay_mod.hotes(_u)
|
||||
if str(h.get("role")) == "frontiere_pair" and str(h.get("site")) == _pair
|
||||
and h.get("ip")), "")
|
||||
if not _if_lien or not _saut:
|
||||
print(f"note : lien vers {_pair} sans interface ou sans `frontiere_pair` dans la "
|
||||
f"carte — aucune regle emise.", file=sys.stderr)
|
||||
continue
|
||||
with underlay_mod.sur_le_site(_pair):
|
||||
_dns_pair = sorted(underlay_mod.adresses_site_portant("serveur_dns_public"))
|
||||
_ici_vivent = sorted({ip for _n, _p, _x in tenants if _site_reel(_n) == _ici
|
||||
for ip in _primaires_publiees(_n)})
|
||||
_la_bas = sorted({ip for _n, _p, _x in tenants if _site_reel(_n) == _pair
|
||||
for ip in _primaires_publiees(_n)})
|
||||
# MOINS DE 32 CARACTERES, regle d'OPNsense : `SETOPS_DNS_PUBLIC_SITE_TECHNOLIBRE` (34)
|
||||
# a ete refuse par le boitier le 2026-10-14. Le nom du pair, sans son prefixe `SITE-`.
|
||||
_court = _pair.upper().replace("-", "_").removeprefix("SITE_")
|
||||
_a_dns_pair = f"SETOPS_DNSPUB_{_court}"
|
||||
if _ici_vivent and _dns_pair:
|
||||
alias[_a_dns_pair] = {"type": "host", "contenu": _dns_pair,
|
||||
"description": f"Serveur DNS public du site pair {_pair}"}
|
||||
alias["SETOPS_DNS_PRIMAIRES_ICI"] = {
|
||||
"type": "host", "contenu": _ici_vivent,
|
||||
"description": "Primaires DNS des locataires qui vivent sur ce site"}
|
||||
for _proto in ("tcp", "udp"):
|
||||
regles.append({
|
||||
"sens": "in", "interface": _if_lien, "protocole": _proto,
|
||||
"source": _a_dns_pair, "destination": "SETOPS_DNS_PRIMAIRES_ICI",
|
||||
"ports": ["5300"], "chiffrement": "clair", "role": "serveur_powerdns",
|
||||
"tenant": "INTERSITES",
|
||||
"raison": f"Le serveur public de {_pair} tire les zones par le tunnel (AXFR "
|
||||
f"signe TSIG, SOA avant). La cle decide qui lit ; ceci, qui frappe.",
|
||||
})
|
||||
regles.append({
|
||||
"sens": "out", "interface": if_transit, "protocole": _proto,
|
||||
"source": "SETOPS_DNS_PRIMAIRES_ICI", "destination": _a_dns_pair,
|
||||
"ports": ["53"], "chiffrement": "clair", "role": "serveur_powerdns",
|
||||
"tenant": "INTERSITES",
|
||||
"raison": f"NOTIFY des primaires vers le serveur public de {_pair} : une zone "
|
||||
f"publique a change. Son contenu n'a rien de confidentiel.",
|
||||
})
|
||||
for _ip in _dns_pair:
|
||||
routes.append({"reseau": f"{_ip}/32", "prochain_saut": _saut, "tenant": "INTERSITES",
|
||||
"description": f"Serveur DNS public de {_pair}, par le tunnel"})
|
||||
if _la_bas and "SETOPS_SITE_SERVEUR_DNS_PUBLIC" in alias:
|
||||
alias["SETOPS_DNS_PRIMAIRES_DISTANTS"] = {
|
||||
"type": "host", "contenu": _la_bas,
|
||||
"description": f"Primaires DNS des locataires qui vivent sur {_pair}"}
|
||||
for _proto in ("tcp", "udp"):
|
||||
regles.append({
|
||||
"sens": "in", "interface": _if_lien, "protocole": _proto,
|
||||
"source": "SETOPS_DNS_PRIMAIRES_DISTANTS",
|
||||
"destination": "SETOPS_SITE_SERVEUR_DNS_PUBLIC",
|
||||
"ports": ["53"], "chiffrement": "clair", "role": "serveur_dns_public",
|
||||
"tenant": "INTERSITES",
|
||||
"raison": f"NOTIFY des primaires de {_pair}, par le tunnel : une zone "
|
||||
f"publique a change, le second serveur de noms la retire.",
|
||||
})
|
||||
for _ip in _la_bas:
|
||||
routes.append({"reseau": f"{_ip}/32", "prochain_saut": _saut, "tenant": "INTERSITES",
|
||||
"description": f"Primaire DNS d'un locataire de {_pair}, par le "
|
||||
f"tunnel (prime sur la route de reprise)"})
|
||||
|
||||
# L'INTERIEUR REFUSE A VOIX HAUTE, LA FRONTIERE AUSSI (2026-10-01).
|
||||
#
|
||||
# Les machines refusaient deja en le disant (nftables : `reject` admin-prohibited ;
|
||||
|
|
@ -1527,6 +1623,14 @@ def construire(tenants: list[tuple[str, str, dict]]) -> dict:
|
|||
"chiffrement": "sans objet",
|
||||
"raison": ("Refus par defaut AUDIBLE sur une interface interne : un flux non "
|
||||
"declare echoue tout de suite au lieu d'expirer. Le WAN reste muet."),
|
||||
# LE GROUPE `wireguard` NE TRANCHE PAS (2026-10-14). OPNsense y range TOUTES les
|
||||
# interfaces WireGuard, assignees comprises (relais, lien entre les sites), et
|
||||
# evalue les regles de groupe AVANT celles des interfaces : un refus `quick` sur le
|
||||
# groupe a rejete le transfert DNS du site pair avant que les regles d'`opt11` ne
|
||||
# le laissent passer (mesure sur `bifrost-1`, regle n° 71). Non definitif, il ne
|
||||
# sert que si rien d'autre ne correspond — les tunnels d'administration gardent
|
||||
# leur refus audible, les interfaces assignees decident pour elles-memes.
|
||||
**({"rapide": False} if _itf == if_vpn else {}),
|
||||
})
|
||||
|
||||
# DEUX DECLARATIONS JUSTES NE FONT QU'UNE REGLE (2026-09-10).
|
||||
|
|
@ -1816,6 +1920,7 @@ def construire(tenants: list[tuple[str, str, dict]]) -> dict:
|
|||
**({"if_publique": if_public, "passerelle_publique": gw_public} if gw_public else {}),
|
||||
"if_transit": _ou_marqueur(intrants, "opnsense_if_transit", IF_TRANSIT),
|
||||
"if_gestion": _ou_marqueur(intrants, "opnsense_if_gestion", IF_GESTION),
|
||||
"if_vpn": if_vpn,
|
||||
"reseaux_gestion": reseaux_gestion,
|
||||
"alias": alias,
|
||||
"routes": routes,
|
||||
|
|
@ -2051,7 +2156,49 @@ def verifier(devis: dict) -> tuple[bool, list[str]]:
|
|||
# `connect()` vers une adresse inexistante aboutissait toujours. Un alias plus etroit
|
||||
# que les routes est la panne symetrique : la frontiere achemine vers un reseau que
|
||||
# le filtre refuse. Les deux se voient ici, pas a l'usage.
|
||||
routes_declarees = {str(r["reseau"]) for r in devis.get("routes") or []}
|
||||
# LE GROUPE `wireguard` NE TRANCHE PAS quand une interface WireGuard assignee porte ses
|
||||
# propres regles (2026-10-14) : son refus `quick`, evalue avant elles, les rendrait mortes.
|
||||
_vpn = str(devis.get("if_vpn") or "wireguard")
|
||||
_assignees = {str(devis.get("if_publique") or "")} | {
|
||||
str(r["interface"]) for r in devis["regles"] if r.get("tenant") == "INTERSITES"}
|
||||
_assignees.discard("")
|
||||
if any(r["interface"] in _assignees and str(r.get("action") or "pass") == "pass"
|
||||
for r in devis["regles"]):
|
||||
for r in devis["regles"]:
|
||||
if (r["interface"] == _vpn and str(r.get("action")) in ("reject", "block")
|
||||
and r.get("rapide") is not False and str(r.get("source")) == "any"):
|
||||
erreurs.append(f"refus `quick` sur le groupe `{_vpn}` : evalue avant les regles "
|
||||
f"des interfaces WireGuard assignees ({', '.join(sorted(_assignees))}), "
|
||||
f"il les rendrait mortes.")
|
||||
|
||||
# UN NOM D'ALIAS QU'OPNSENSE ACCEPTE (2026-10-14) : il commence par une lettre ou un seul
|
||||
# souligne, fait MOINS de 32 caracteres, et ne contient que lettres, chiffres et
|
||||
# soulignes. Le boitier refusait `SETOPS_DNS_PUBLIC_SITE_TECHNOLIBRE` (34) — et avec lui
|
||||
# les quatre regles qui le citaient. La garde le dit ici, avant tout envoi.
|
||||
import re as _re
|
||||
for _n in sorted(devis["alias"]):
|
||||
if not _re.fullmatch(r"(?:[A-Za-z]|_(?!_))[A-Za-z0-9_]{0,30}", _n):
|
||||
erreurs.append(f"alias `{_n}` ({len(_n)} caracteres) : OPNsense le refuserait "
|
||||
"(moins de 32 caracteres, lettres, chiffres et soulignes).")
|
||||
|
||||
# LES ROUTES DU LIEN ENTRE LES SITES (2026-10-14) se jugent a part : elles visent des
|
||||
# adresses d'un autre site (serveur public du pair, primaires distants), jamais un
|
||||
# reseau de locataire. Meme principe, en /32 : chacune doit designer une adresse d'un
|
||||
# alias du lien, et cet alias doit servir a une regle — rien de route que le filtre
|
||||
# refuserait.
|
||||
_lien = [r for r in devis.get("routes") or [] if r.get("tenant") == "INTERSITES"]
|
||||
_alias_lien = {n for n in devis["alias"] if n.startswith("SETOPS_DNSPUB_")
|
||||
or n == "SETOPS_DNS_PRIMAIRES_DISTANTS"}
|
||||
_servis = {n for n in _alias_lien for r in devis["regles"]
|
||||
if n in (str(r["source"]).lstrip("!"), str(r["destination"]).lstrip("!"))}
|
||||
_adresses_lien = {f"{c}/32" for n in _servis for c in devis["alias"][n]["contenu"]}
|
||||
for r in _lien:
|
||||
if str(r["reseau"]) not in _adresses_lien:
|
||||
erreurs.append(f"{r['reseau']} est route par le lien entre les sites, mais aucune "
|
||||
"regle du lien ne la designe : la frontiere y acheminerait des "
|
||||
"paquets que le filtre refuserait.")
|
||||
routes_declarees = {str(r["reseau"]) for r in devis.get("routes") or []
|
||||
if r.get("tenant") != "INTERSITES"}
|
||||
autorises: set[str] = set()
|
||||
for nom, a in devis["alias"].items():
|
||||
if nom.startswith("SETOPS_TENANT_"):
|
||||
|
|
|
|||
|
|
@ -92,6 +92,15 @@ class Cluster:
|
|||
base = hebergeur()
|
||||
heb = yaml.safe_load((base / "proxmox-hebergeur.yml").read_text(encoding="utf-8")) or {}
|
||||
v = voute(base)
|
||||
# LE TEMPS D'UNE COMMANDE, PAR UN REBOND (2026-10-14). Hors du site, l'API du cluster
|
||||
# n'est joignable que par une redirection SSH a travers la frontiere ; la carte, elle,
|
||||
# continue de dire l'adresse reelle. `SETOPS_PROXMOX_API=hote:port` la remplace pour
|
||||
# cette seule invocation — jamais ecrit nulle part.
|
||||
_rebond = os.environ.get("SETOPS_PROXMOX_API", "").strip()
|
||||
if _rebond:
|
||||
_h, _, _p = _rebond.rpartition(":")
|
||||
return cls(_h, int(_p), heb.get("proxmox_api_user"), v["proxmox_api_token_id"],
|
||||
v["proxmox_api_token_secret"]), base
|
||||
return cls(heb.get("proxmox_api_host"), heb.get("proxmox_api_port", 8006),
|
||||
heb.get("proxmox_api_user"), v["proxmox_api_token_id"],
|
||||
v["proxmox_api_token_secret"]), base
|
||||
|
|
|
|||
|
|
@ -539,7 +539,10 @@ def _adresses_dns_public_site() -> list[str]:
|
|||
"""
|
||||
try:
|
||||
import underlay as underlay_mod
|
||||
return underlay_mod.adresses_site_portant("serveur_dns_public")
|
||||
# Et ceux des sites PAIRS actifs (2026-10-14) : le second serveur de noms frappe par
|
||||
# le tunnel entre les sites. Vide tant que la replication n'est pas activee.
|
||||
return sorted(set(underlay_mod.adresses_site_portant("serveur_dns_public"))
|
||||
| set(underlay_mod.dns_public_des_pairs()))
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
|
|
|||
|
|
@ -160,6 +160,10 @@ def contrat() -> dict:
|
|||
# QUI TIRE SES ZONES PUBLIQUES — le seul serveur a qui le primaire notifie, et le
|
||||
# seul que son pare-feu laisse frapper. Une ADRESSE : c'est un pair de flux.
|
||||
"dns_public_site": _une("serveur_dns_public"),
|
||||
# ET LES SERVEURS PUBLICS DES SITES PAIRS (2026-10-14) — le second serveur de noms, sur
|
||||
# un autre site, par le tunnel entre les sites. Vide tant que `dns_public_pairs` n'est
|
||||
# pas `actif` des deux cotes : rien ne change pour le locataire avant.
|
||||
"dns_public_pairs": underlay_mod.dns_public_des_pairs(),
|
||||
# VERS OU POINTENT SES NOMS PUBLICS — l'adresse publique de la frontiere. Un fait
|
||||
# du site : le locataire la recoit, il ne la recopie pas.
|
||||
"ip_publique_site": _ip_publique_site(),
|
||||
|
|
@ -189,6 +193,7 @@ OU_LE_LOCATAIRE_LE_DIT = {
|
|||
"serveur_ops_forge_cle_hote": ("serveur_ops.yml", "serveur_ops_forge_cle_hote"),
|
||||
"client_backup_cible": ("client_backup.yml", "client_backup_cible"),
|
||||
"dns_public_site": ("all/10-intrants.yml", "dns_public_site"),
|
||||
"dns_public_pairs": ("all/10-intrants.yml", "dns_public_pairs"),
|
||||
"ip_publique_site": ("all/10-intrants.yml", "ip_publique_site"),
|
||||
"ip_publique": ("all/10-intrants.yml", "ip_publique"),
|
||||
"dns_public_nom": ("all/10-intrants.yml", "dns_public_nom"),
|
||||
|
|
|
|||
|
|
@ -217,6 +217,82 @@ def adresses_site_portant(groupe: str) -> list[str]:
|
|||
if m.get("ip") and groupe in svc.get(m["nom"], [])})
|
||||
|
||||
|
||||
# --- LES SITES PAIRS (2026-10-14) ------------------------------------------------------
|
||||
#
|
||||
# Deux sites relies par un tunnel se doivent la replication du DNS public (et, plus tard, les
|
||||
# sauvegardes croisees). Ce qu'un site sait de son pair, il le LIT dans le depot frere du pair
|
||||
# (`../SITE-<nom>`), par les memes fonctions qu'il emploie pour lui-meme : une seconde lecture
|
||||
# du plan finirait par diverger de la premiere. Un pair est ACTIF quand `dns_public_pairs` le
|
||||
# dit — declare des deux cotes, garde par P82.
|
||||
|
||||
def sites_pairs_actifs() -> list[str]:
|
||||
"""Les sites avec lesquels CE site replique son DNS public, `etat: actif` seulement."""
|
||||
liens = (lire_plan_site("10-intrants.yml") or {}).get("dns_public_pairs") or []
|
||||
return sorted({str(x.get("site")) for x in liens
|
||||
if isinstance(x, dict) and str(x.get("etat", "actif")) == "actif"
|
||||
and x.get("site")})
|
||||
|
||||
|
||||
def carte_du_site(nom: str) -> Path | None:
|
||||
"""La carte d'un site FRERE (`../<nom>/underlay.yml`, a cote du depot de ce site)."""
|
||||
c = chemin()
|
||||
if not c:
|
||||
return None
|
||||
p = c.resolve().parent.parent / nom / "underlay.yml"
|
||||
return p if p.is_file() else None
|
||||
|
||||
|
||||
class sur_le_site:
|
||||
"""Le temps d'un bloc, `chemin()` designe la carte d'un autre site (frere).
|
||||
|
||||
Toutes les lectures du plan passent par `chemin()` ; les rediriger ainsi garantit qu'on
|
||||
lit le pair EXACTEMENT comme on se lit soi-meme. Restaure l'etat d'avant en sortant,
|
||||
exception comprise.
|
||||
"""
|
||||
|
||||
def __init__(self, nom: str):
|
||||
self.carte = carte_du_site(nom)
|
||||
if self.carte is None:
|
||||
raise FileNotFoundError(f"site pair `{nom}` : aucune carte a cote de ce site")
|
||||
|
||||
def __enter__(self):
|
||||
self._avant = os.environ.get("SETOPS_UNDERLAY")
|
||||
os.environ["SETOPS_UNDERLAY"] = str(self.carte)
|
||||
return self
|
||||
|
||||
def __exit__(self, *_):
|
||||
if self._avant is None:
|
||||
os.environ.pop("SETOPS_UNDERLAY", None)
|
||||
else:
|
||||
os.environ["SETOPS_UNDERLAY"] = self._avant
|
||||
return False
|
||||
|
||||
|
||||
def dns_public_des_pairs() -> list[str]:
|
||||
"""Les adresses du DNS public de chaque site pair ACTIF — derivees de leur plan.
|
||||
|
||||
Un pair sans carte a cote de ce site ne rend rien et ne fait pas echouer : le lien est
|
||||
declare, mais son vis-a-vis n'est pas lisible d'ici (poste qui n'a pas ce depot).
|
||||
"""
|
||||
out: set[str] = set()
|
||||
for nom in sites_pairs_actifs():
|
||||
try:
|
||||
with sur_le_site(nom):
|
||||
out.update(adresses_site_portant("serveur_dns_public"))
|
||||
except FileNotFoundError:
|
||||
continue
|
||||
return sorted(out)
|
||||
|
||||
|
||||
def tenants_heberges_par(nom: str) -> list[str]:
|
||||
"""Les locataires que le site `nom` heberge (sa carte, `tenants:`)."""
|
||||
try:
|
||||
with sur_le_site(nom):
|
||||
return sorted((charger() or {}).get("tenants") or {})
|
||||
except FileNotFoundError:
|
||||
return []
|
||||
|
||||
|
||||
def reseaux(underlay: dict | None) -> list[dict]:
|
||||
return (underlay or {}).get("reseaux", []) or []
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue