From 0d2bc2c2e9f0c9af429946575317575486529b88 Mon Sep 17 00:00:00 2001 From: Daniel Allaire Date: Sun, 5 Jul 2026 15:30:22 -0400 Subject: [PATCH] =?UTF-8?q?Machinerie=20d'exposition=20:=20SAN=20auto-d?= =?UTF-8?q?=C3=A9riv=C3=A9s=20+=20vhost=20WebSocket-aware?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Élimine 3 gotchas du spike Nextcloud/Collabora : - instancier dérive sans_exposition par edge (FQDN exposés du plan) → le cert edge (client_pki_sans) se met à jour tout seul, plus de liste manuelle en group_vars ; - champ 'websocket' sur une application → le vhost d'exposition ajoute la map $http_upgrade + en-têtes Upgrade/Connection + timeout long (Collabora, éditeurs live) ; - (plancher /etc/hosts : le mécanisme d'alias existait déjà, publier_expositions=true). Prouvé sur le lab : cert edge couvre les 6 expositions, bureau 200 en WebSocket. Co-Authored-By: Claude Opus 4.8 --- roles/serveur_nginx/templates/expositions.conf.j2 | 11 +++++++++++ scripts/instancier.py | 13 ++++++++++++- scripts/inventory_rules.py | 3 +++ 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/roles/serveur_nginx/templates/expositions.conf.j2 b/roles/serveur_nginx/templates/expositions.conf.j2 index c8344b5..f6c9e89 100644 --- a/roles/serveur_nginx/templates/expositions.conf.j2 +++ b/roles/serveur_nginx/templates/expositions.conf.j2 @@ -6,6 +6,10 @@ # amont = http://:. Une exposition # non resolvable (application sans hote actif, ou sans port) est listee mais # NON publiee (binding declare, pas encore cablable). +{% if serveur_nginx_expositions | default([]) | selectattr('websocket', 'defined') | selectattr('websocket') | list | length > 0 %} +# Une exposition au moins est WebSocket-aware (ex. Collabora) : map d'upgrade. +map $http_upgrade $connection_upgrade { default upgrade; "" close; } +{% endif %} {% for expo in serveur_nginx_expositions | default([]) %} {% set ns = namespace(amont='') %} {% if expo.hote in hostvars and hostvars[expo.hote].ansible_host is defined and expo.port %} @@ -37,6 +41,13 @@ server { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; +{% if expo.websocket | default(false) %} + # Collabora & co : editeur en direct via WebSocket (upgrade + longue tenue). + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_read_timeout 3600s; +{% endif %} } } {% else %} diff --git a/scripts/instancier.py b/scripts/instancier.py index b8f027e..bafdb61 100644 --- a/scripts/instancier.py +++ b/scripts/instancier.py @@ -27,10 +27,12 @@ import yaml from inventory_rules import ( charger_applications, + charger_domaines, charger_nomenclature, charger_serveurs, deriver_nomenclature, deriver_ressources, + expositions_des_applications, fonction_seq, ) @@ -55,6 +57,7 @@ GENERE = INVENTAIRE.with_name("hosts.genere.yml") FICHIER_SERVEURS = INSTANCE / "plan/serveurs.yml" FICHIER_APPLICATIONS = INSTANCE / "plan/applications.yml" FICHIER_NOMENCLATURE = INSTANCE / "plan/nomenclature.yml" +FICHIER_DOMAINES = INSTANCE / "plan/domaines.yml" GROUPES_SOCLE = ["serveur_debian", "serveur_durci"] PLACEMENT = [("noeud", "proxmox_noeud"), ("stockage", "proxmox_stockage"), @@ -151,6 +154,9 @@ def generer() -> dict: apps = charger_applications(FICHIER_APPLICATIONS).get("applications", {}) nomenclature = charger_nomenclature(FICHIER_NOMENCLATURE) injections = resoudre_liens(apps, serveurs, nomenclature) + # Expositions du plan (champ 'expose') -> pour deriver les SANs de chaque edge. + domaines = charger_domaines(FICHIER_DOMAINES) + expositions = expositions_des_applications({"applications": apps}, domaines) services_par_hote: dict = {} for app in apps.values(): @@ -184,9 +190,14 @@ def generer() -> dict: # Variables derivees des liens (bindings) declares par les applications. for var, val in injections.get(nom, {}).items(): hostvars[var] = val + groupes = set(GROUPES_SOCLE) | services_par_hote.get(nom, set()) | set(srv.get("integrations") or []) + # SANs d'exposition : les FQDN que cet edge sert (derives du plan) -> cert edge + # (client_pki_sans), au lieu d'une liste manuelle. Vide si l'hote n'est pas un edge. + sans = sorted({e["fqdn"] for e in expositions if e.get("edge") in groupes}) + if sans: + hostvars["sans_exposition"] = sans etat = "hotes_actifs" if srv.get("etat") == "actif" else "hotes_planifies" children[etat]["hosts"][nom] = hostvars - groupes = set(GROUPES_SOCLE) | services_par_hote.get(nom, set()) | set(srv.get("integrations") or []) for groupe in sorted(groupes): children.setdefault(groupe, {"hosts": {}})["hosts"][nom] = None return {"all": {"children": children}} diff --git a/scripts/inventory_rules.py b/scripts/inventory_rules.py index c7fa77d..cb276f8 100644 --- a/scripts/inventory_rules.py +++ b/scripts/inventory_rules.py @@ -200,6 +200,9 @@ def expositions_des_applications(applications: dict, domaines: dict, edge: str | "fqdn": fqdn, "application": app_id, "groupe": app.get("groupe"), "hote": app.get("hote"), "port": app.get("port"), "edge": edge_dom, "domaine": dom, + # Exposition WebSocket-aware (ex. Collabora) : le vhost ajoute + # alors l'upgrade Upgrade/Connection. Declare par l'app (websocket: true). + "websocket": bool(app.get("websocket")), }) return resultat