frontiere : appliquee, et la contrainte de nommage d'OPNsense consignee

Devis clos : 0 a creer, 0 a retirer, 65 inchange + 15 routes. Flotte verifiee
apres coup -- DNS interne, Internet et apt sans erreur sur les cinq hotes.

OPNsense refuse un alias de 32 caracteres ou plus. La contrainte n'etait ecrite
nulle part et se manifestait a l'APPLICATION, pas au devis. nom_alias abrege
desormais (SERVEUR_ -> SRV_, comme le pare-feu est-ouest) et REFUSE bruyamment
si le nom deborde encore : un devis qui promet un objet que la cible rejettera
n'est pas un devis. Le role devient serveur_cache_site.

« RIEN N'EST APPLIQUE » signifiait « pas encore recharge », pas « rien ecrit » :
les objets etaient dans la config, le pare-feu en marche les ignorait. J'avais
lu le message a l'envers.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Daniel Allaire 2026-08-24 17:51:29 -04:00
parent fa7e656844
commit a1997774be
10 changed files with 87 additions and 8 deletions

View file

@ -1,5 +1,42 @@
# CHANGELOG — Set-OPS # CHANGELOG — Set-OPS
## 2026-08-24 — La frontière appliquée, et une contrainte qui n'était écrite nulle part
`a creer : 0 | a retirer : 0 | inchange : 65 + 15 routes` — le devis est clos. La flotte de
patient 0 vérifiée après coup : DNS interne, Internet et `apt` sans erreur sur les cinq.
### OPNsense refuse un nom d'alias de 32 caractères ou plus
Le schéma `SETOPS_<tenant>_<ROLE>` ne laisse que 17 caractères au rôle :
```
SETOPS_PATI29_SERVEUR_ARTEFACTS_SITE 36 refusé
SETOPS_PATI29_SRV_ARTEFACTS_SITE 32 refusé encore, d'un caractère
SETOPS_PATI29_SRV_CACHE_SITE 28 passe
```
La contrainte n'était **écrite nulle part**, et elle s'est manifestée à l'**application**,
pas au devis. Un devis qui promet un objet que la cible rejettera n'est pas un devis :
`nom_alias` abrège désormais (`SERVEUR_` → `SRV_`, comme le pare-feu est-ouest) et **refuse
bruyamment** si le nom déborde encore. Le rôle est renommé `serveur_cache_site`.
### « RIEN N'EST APPLIQUÉ » voulait dire autre chose que ce que j'ai lu
Le premier essai a signalé cinq échecs et affiché *« RIEN N'EST APPLIQUÉ, la config reste
en attente »*. J'ai d'abord compris que le boîtier n'avait pas été touché. Le devis
suivant disait autre chose : 62 objets inchangés au lieu de 49.
Les objets **étaient bien écrits** dans la configuration ; c'est le **rechargement** qui
n'avait pas eu lieu — « en attente » au sens d'OPNsense. Une différence qui compte : entre
les deux, la configuration contenait des objets que le pare-feu en marche ignorait encore.
### Et la garde du boîtier injoignable a servi
Une tentative a échoué sur une poignée TLS expirée — le premier paquet après une pause,
mesuré tout au long de la journée. Le code a **refusé de lire un boîtier injoignable comme
un boîtier vide**, au lieu de proposer de tout recréer. C'est exactement ce pour quoi cette
garde avait été écrite.
## 2026-08-24 — Le chaînage des caches, et la règle qui appartient au SITE ## 2026-08-24 — Le chaînage des caches, et la règle qui appartient au SITE
Le runner de SITE est le seul à toucher la configuration système du matériel. Sa Le runner de SITE est le seul à toucher la configuration système du matériel. Sa

View file

@ -53,7 +53,7 @@ Ce que la reconstruction couvre, par capacité :
| Source d'artefacts | `serveur_artefacts`, `client_artefacts` | cache apt de l'ecosysteme (apt-cacher-ng) : les paquets viennent de chez soi, pas de six serveurs etrangers — **mode hors ligne** pour prouver ce que le cache detient vraiment | | Source d'artefacts | `serveur_artefacts`, `client_artefacts` | cache apt de l'ecosysteme (apt-cacher-ng) : les paquets viennent de chez soi, pas de six serveurs etrangers — **mode hors ligne** pour prouver ce que le cache detient vraiment |
| Runner de site | `serveur_ops_site` | le pouvoir de **materialiser** : creer et detruire des VM sur la fabric. Detient la voute du SITE, chiffree, et n'entre JAMAIS chez un tenant — reserve a l'ecosysteme de l'hebergeur | | Runner de site | `serveur_ops_site` | le pouvoir de **materialiser** : creer et detruire des VM sur la fabric. Detient la voute du SITE, chiffree, et n'entre JAMAIS chez un tenant — reserve a l'ecosysteme de l'hebergeur |
| Resolution | `serveur_resolveur`, `client_resolveur` | UN resolveur recursif par tenant (Unbound), qui recurse depuis la racine et delegue la zone souveraine a PowerDNS. Remplace les N demons locaux d'avant le 2026-08-24 | | Resolution | `serveur_resolveur`, `client_resolveur` | UN resolveur recursif par tenant (Unbound), qui recurse depuis la racine et delegue la zone souveraine a PowerDNS. Remplace les N demons locaux d'avant le 2026-08-24 |
| Cache du site | `serveur_artefacts_site` | designe LE cache que les ecosystemes voisins prennent comme amont : Debian telecharge une fois pour toute la fabric, et le cache ne voit que des requetes agregees. Reserve a l'ecosysteme de l'hebergeur | | Cache du site | `serveur_cache_site` | designe LE cache que les ecosystemes voisins prennent comme amont : Debian telecharge une fois pour toute la fabric, et le cache ne voit que des requetes agregees. Reserve a l'ecosysteme de l'hebergeur |
| Agents de flotte | `client_metrique`, `client_journal`, `client_smtp` | collecte et relais sur toute la flotte | | Agents de flotte | `client_metrique`, `client_journal`, `client_smtp` | collecte et relais sur toute la flotte |
*Rôles retirés (2026-07-04, supersédés ou hors conception)* : `serveur_sendmail` *Rôles retirés (2026-07-04, supersédés ou hors conception)* : `serveur_sendmail`

View file

@ -19,7 +19,7 @@ flux:
# LE CHAINAGE, cote consommateur. Un seul port, et seulement vers un CACHE — jamais # LE CHAINAGE, cote consommateur. Un seul port, et seulement vers un CACHE — jamais
# d'une VM vers le cache d'un autre tenant. # d'une VM vers le cache d'un autre tenant.
# #
# L'INGRESS CORRESPONDANT N'EST PAS ICI : il appartient a `serveur_artefacts_site`, le # L'INGRESS CORRESPONDANT N'EST PAS ICI : il appartient a `serveur_cache_site`, le
# cache designe du site. Sans cette separation, chaque cache aurait accepte chaque # cache designe du site. Sans cette separation, chaque cache aurait accepte chaque
# autre — un maillage complet, visible au devis du 2026-08-24. # autre — un maillage complet, visible au devis du 2026-08-24.
- sens: egress - sens: egress

View file

@ -1,4 +1,4 @@
# serveur_artefacts_site # serveur_cache_site
**Le cache d'artefacts du SITE** : celui que les écosystèmes voisins prennent comme amont, **Le cache d'artefacts du SITE** : celui que les écosystèmes voisins prennent comme amont,
pour que Debian ne soit téléchargé qu'une fois pour toute la fabric. pour que Debian ne soit téléchargé qu'une fois pour toute la fabric.

View file

@ -1,5 +1,16 @@
--- ---
# LE CACHE D'ARTEFACTS DU SITE — celui que les autres écosystèmes prennent comme amont. # LE CACHE D'ARTEFACTS DU SITE — nomme `cache_site` et non `artefacts_site`.
#
# POURQUOI CE NOM COURT (2026-08-24). OPNsense refuse un nom d'alias de 32 caracteres ou
# plus, et le schema `SETOPS_<tenant>_<ROLE>` ne laisse que 17 caracteres au role :
#
# SETOPS_PATI29_SERVEUR_ARTEFACTS_SITE 36 refuse a l'application
# SETOPS_PATI29_SRV_ARTEFACTS_SITE 32 refuse encore, d'un caractere
# SETOPS_PATI29_SRV_CACHE_SITE 28 passe
#
# La contrainte n'etait ecrite nulle part et s'est manifestee a l'APPLICATION, pas au
# devis. Elle l'est desormais, et le devis REFUSE plutot que de promettre un objet que la
# cible rejettera. — celui que les autres écosystèmes prennent comme amont.
# #
# Même patron que `serveur_ops_site` : le rôle n'installe rien de plus, il **attribue une # Même patron que `serveur_ops_site` : le rôle n'installe rien de plus, il **attribue une
# responsabilité** et la rend lisible dans le plan. `serveur_artefacts` fait le travail ; # responsabilité** et la rend lisible dans le plan. `serveur_artefacts` fait le travail ;
@ -14,4 +25,4 @@
# tenant ordinaire qui le déclarerait s'offrirait à servir ses voisins sans y être invité. # tenant ordinaire qui le déclarerait s'offrirait à servir ses voisins sans y être invité.
# #
# Le cache du site va DIRECTEMENT chez Debian : il est le bout de la chaîne. # Le cache du site va DIRECTEMENT chez Debian : il est le bout de la chaîne.
serveur_artefacts_site_amont_attendu: "" serveur_cache_site_amont_attendu: ""

View file

@ -7,7 +7,7 @@
- name: Exiger que le cache du site aille directement à l'amont - name: Exiger que le cache du site aille directement à l'amont
ansible.builtin.assert: ansible.builtin.assert:
that: that:
- (serveur_artefacts_amont | default('')) == serveur_artefacts_site_amont_attendu - (serveur_artefacts_amont | default('')) == serveur_cache_site_amont_attendu
fail_msg: >- fail_msg: >-
Ce cache est déclaré cache du SITE : il doit aller directement chez Debian. Ce cache est déclaré cache du SITE : il doit aller directement chez Debian.
`serveur_artefacts_amont` doit rester vide — sinon les caches se chaînent en boucle. `serveur_artefacts_amont` doit rester vide — sinon les caches se chaînent en boucle.

View file

@ -261,9 +261,40 @@ def cibles_par_role(role: str, inventaire: Path) -> list[str]:
return sorted({ips[h] for h in _hotes_du_groupe(data, role) if h in ips}) return sorted({ips[h] for h in _hotes_du_groupe(data, role) if h in ips})
# OPNsense refuse un nom d'alias de 32 caracteres ou plus. La contrainte n'etait nulle
# part, et elle s'est manifestee a l'APPLICATION — pas au devis :
#
# SETOPS_PATI29_SERVEUR_ARTEFACTS_SITE (36)
# « The name must start with a letter [...] and be less than 32 characters »
#
# L'outil a bien refuse d'appliquer quoi que ce soit, mais le devis, lui, annoncait un
# objet impossible. Un devis qui promet ce que la cible refusera n'est pas un devis.
LONGUEUR_MAX_ALIAS = 31
# Meme abreviation que le pare-feu est-ouest (`devis_proxmox_fw` : serveur_ -> srv-), pour
# qu'un seul schema de lecture serve aux deux couches.
ABREGES = (("SERVEUR_", "SRV_"), ("CLIENT_", "CLI_"))
def nom_alias(role: str, etiquette: str) -> str: def nom_alias(role: str, etiquette: str) -> str:
"""Alias d'hotes, PREFIXE par le tenant : deux tenants ont chacun leur nginx.""" """Alias d'hotes, PREFIXE par le tenant : deux tenants ont chacun leur nginx.
return f"SETOPS_{etiquette}_{role.upper()}"
Abrege SEULEMENT si le nom complet depasse — un nom court reste lisible tel quel, et
la flotte existante n'est pas renommee pour la commodite d'un seul role.
"""
nom = f"SETOPS_{etiquette}_{role.upper()}"
if len(nom) <= LONGUEUR_MAX_ALIAS:
return nom
court = role.upper()
for long, bref in ABREGES:
court = court.replace(long, bref)
nom = f"SETOPS_{etiquette}_{court}"
if len(nom) > LONGUEUR_MAX_ALIAS:
# Mieux vaut un devis qui refuse qu'un devis qui promet l'impossible.
raise SystemExit(
f"Nom d'alias trop long pour OPNsense ({len(nom)} > {LONGUEUR_MAX_ALIAS}) : {nom}.\n"
f"Raccourcir le nom du role `{role}`.")
return nom
def _instance_active() -> tuple[str, dict] | tuple[None, None]: def _instance_active() -> tuple[str, dict] | tuple[None, None]: