Machinerie d'exposition : SAN auto-dérivés + vhost WebSocket-aware

É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 <noreply@anthropic.com>
This commit is contained in:
Daniel Allaire 2026-07-05 15:30:22 -04:00
parent da4976aa98
commit 0d2bc2c2e9
3 changed files with 26 additions and 1 deletions

View file

@ -6,6 +6,10 @@
# amont = http://<IP interne de l'hote de l'application>:<port>. 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 %}

View file

@ -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}}

View file

@ -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