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>
Une regle inter-tenant n'appartient ni a l'emetteur ni au recepteur : c'est le
site qui autorise un flux entre deux de ses tenants, et son runner qui prepare
le terrain.
Deux silences fermes dans le generateur de frontiere. flux_frontiere() ne
retenait que les flux `externe` -- or deux tenants vivent sur des VLAN routes
par la frontiere, leur trafic la traverse. Et un egress vers un voisin recevait
!SETOPS_INTERNES en destination : le port ouvert vers l'INTERNET. La destination
est desormais nommee.
Deux roles parce que meta/flux.yml est statique : un role unique aurait declare
l'ingress pour TOUS les caches -- maillage complet, visible au devis.
serveur_artefacts_site porte l'ingress, serveur_artefacts l'egress vers l'amont.
Debian est desormais telecharge une fois pour toute la fabric. Le cache du site
ne voit que des requetes agregees, jamais quelle machine installe quoi.
Rien n'est applique : le devis se lit avant.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Le mot `voisins_site` etait accepte par la validation mais AUCUN generateur ne
le rendait : zero regle au devis, et rien ne le signalait. Deux causes, toutes
deux fermees.
flux_frontiere() ne retenait que les flux `externe`. Or deux tenants de la meme
fabric vivent sur des VLAN distincts, routes par la frontiere : leur trafic la
traverse, donc elle doit le porter.
Et le rendu manquait : une regle inter-tenant s'attache au meme lien de transit
que le reste -- les tenants s'y distinguent par leur ALIAS SOURCE, pas par une
interface. Ma mise en garde precedente reposait sur un modele faux.
LA REGLE APPARTIENT AU SITE, pas a l'un des deux tenants : c'est son runner qui
prepare le terrain, aucun ecosysteme n'ouvre de porte chez un autre.
Les declarations de chainage restent RETIREES : le devis obtenu ouvrait plus que
voulu -- maillage complet entre tous les caches, et un egress 3142 vers
l'Internet au lieu du seul voisin. Deux raffinements a faire avant de declarer.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Le registre ne savait pas dire « les autres tenants de ma fabric ». ingress +
externe signifie DEPUIS L'INTERNET : declarer ainsi un cache partage l'aurait
publie au monde.
voisins_site rend les supernets des tenants que CE SITE heberge, en reutilisant
devis_reseau.decouvrir_du_site() plutot qu'en ecrivant un second recensement.
Ma premiere version lisait un `federe` absent comme « non federe » et excluait
Chezlepro et Technolibre en silence -- la decouverte canonique dit l'inverse.
LE CHAINAGE DES CACHES N'EST PAS LIVRE. Aucun generateur ne rend ce mot : zero
regle 3142 au devis de frontiere. Les declarations ont donc ete RETIREES plutot
que laissees a moitie -- un flux declare que personne n'applique est le piege
que ce depot traque.
La difficulte est structurelle : les regles OPNsense s'evaluent sur l'interface
d'ARRIVEE, donc une regle inter-tenant doit etre posee sur l'interface du
VOISIN. Le generateur construit tenant par tenant, sur les interfaces de ce
tenant.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Mesure avant de decider : ~21 Mo de RSS par VM pour 28 a 189 requetes servies.
Le gain de cache est negligeable ; ce qu'on recupere, c'est un demon au lieu de
cinq et un endroit a regarder au lieu de cinq.
Pas sur la frontiere : un resolveur de SITE devrait connaitre la zone interne de
chaque tenant, et un tenant dont le resolveur vit chez l'hebergeur ne peut plus
s'emanciper avec. La recursion est generique, la zone interne ne l'est pas.
L'autoritatif se replie sur 127.0.0.1:5300 -- derive de la colocalisation, pas
declare a la main. Son port devient `derive` dans meta/flux.yml : les deux lient
53 mais sur des adresses differentes.
Deux pieges. Unbound refuse d'interroger une loopback par defaut : sans lever
do-not-query-localhost, toute la zone rendait SERVFAIL. Et le plancher
/etc/hosts MASQUAIT la panne -- getent repondait, dig disait SERVFAIL. La tache
de validation du role avait raison contre moi.
client_unbound n'installant plus Unbound, son nom mentait : client_resolveur.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Le role lancait collabora/code dans Docker -- seule exception de la flotte au
principe « logiciel libre en natif ». Desormais : paquet coolwsd du depot amont,
systemd, derriere l'edge nginx. community.docker est retiree de requirements :
plus aucun role n'a besoin de Docker.
Eprouve avant d'ecrire, sur une Debian 13.6 reelle et sans rien installer :
apt-get install --simulate coolwsd resout jusqu'a « Conf coolwsd (26.04.3.1-1) »,
libgcc1 est fourni par libgcc-s1, et le depot CODE-deb est PLAT.
La configuration passe par un fragment systemd (--o:) : le coolwsd.xml livre,
439 lignes commentees, reste intact.
Trois outils du harnais ont pese. voute.py lisait les COMMENTAIRES : documenter
le nom d'une clef suffisait a l'exiger -- corrige, controle negatif fait. Et
verifier_intrants avait raison : une garde ecrite en deux morceaux promettait un
secret pour une console fermee ; reecrite en implication, elle dit le vrai
contrat.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
deriver_nomenclature(...) or {} avalait l'echec : une fonction absente rendait
un dictionnaire vide, et la machine entrait dans l'inventaire avec
ansible_host: None. La generation se declarait reussie ; la panne serait
apparue au deploiement, sous une forme incomprehensible.
Revele en portant serveur_ops dans les modeles : presence-web range son socle
en zone 1 et n'a pas de categorie 4. Le defaut n'est pas apparu en ecrivant le
role ni en le deployant chez patient 0 -- il a fallu le porter ailleurs.
serveur_ops ne nomme plus patient 0 dans ses defauts : un ecosysteme distrait
aurait clone le genome d'un autre, en silence.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Un ecosysteme pouvait DETENIR son genome sans savoir l'executer. Le poste
d'exploitation porte Ansible epingle, le genome clone depuis SA PROPRE forge, et
une cle SSH qui n'appartient qu'a lui. Il n'emporte ni la voute ni son mot de
passe : la structure se reconstruit depuis la forge, les secrets depuis la
sauvegarde. Deux sources qu'un meme incident n'atteint pas ensemble.
Une machine neuve traverse tout le moteur sans rien heriter, et revele ce qu'un
etat anterieur masquait. Deux defauts silencieux en sont sortis.
setops_plan_dir pointait le lien `instance` en dur : neuf roles lisaient donc le
plan d'une AUTRE instance. Le plancher de patient 0 portait les FQDN de
Chezlepro, et son edge publiait server_name forge.chezlepro.internal. La
variable suit desormais l'inventaire reellement charge.
Trois instances sur quatre declaraient les SAN d'exposition de leur edge ; la
quatrieme et le modele public ne les avaient pas. La forge de patient 0 etait
donc publiee derriere le certificat auto-signe de Debian, sans que rien ne le
signale -- `git clone` fut le premier a refuser, a juste titre. P42 le reclame
maintenant pour tout ecosysteme qui declare un edge.
Le harnais a ecrit la moitie de ce role : 5 echecs sur la piece neuve, aucun
n'empechait le code de tourner, tous la rendaient invisible a la carte, au
graphe et au lecteur. 42 preuves, 0 echec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
L'exploitant lance `make frontiere-appliquer` dans son terminal : « a creer 0, inchange
53 + 15 routes — la frontiere dit deja ce que le devis dit ». Elle etait DEJA conforme.
J'avais annonce quelques heures plus tot « 89 a creer, 0 inchange, donc les regles
heritees sont invisibles a l'API ». FAUX. L'intrant `opnsense_api_url` pointait sur
10.0.0.1, l'adresse d'avant la migration du boitier : chaque lecture rendait
{"_erreur": ...}, le plan lisait `.get("rows")`, n'y trouvait rien, et concluait au vide.
Avec CONFIRMER, on aurait pousse une politique entiere EN DOUBLE.
TROISIEME FOIS EN UNE SOIREE, meme confusion — « pas de reponse » pris pour « rien » :
devis_placement iterait un dict d'erreur comme une liste
devis_underlay declarait morts les reseaux qu'il ne joignait pas
appliquer_opnsense lisait un boitier injoignable comme un boitier vide
Toutes les lectures de la frontiere passent desormais par une garde qui refuse en nommant
l'hote, la cause et le piege evite. Eprouvee contre l'ancienne adresse : elle refuse.
CE QUE CA DIT DE LA METHODE : ce n'est ni le harnais ni moi qui avons trouve, c'est
l'exploitant, en lancant la commande la ou il VOIT la sortie. Mes commandes s'executent
dans ma session ; il n'en voit rien. Une commande lente ressemble a un blocage, et un
blocage a une commande lente — j'ai conclu deux fois a tort avant qu'il ne regarde.
make verifier 41/41 ; le plan reel reste « rien a faire ».
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`opnsense.yml` decrivait le monde physique depuis les group_vars d'un tenant. Le devis et
le GUI le cherchent desormais d'abord a la racine du depot de site, comme underlay.yml et
proxmox-hebergeur.yml, par la meme derivation depuis le symlink.
CE QUE LE MAUVAIS RANGEMENT A COUTE : l'adresse d'API de la frontiere y etait restee a
10.0.0.1 apres migration vers 10.17.0.1. `make frontiere-appliquer` restait suspendu sur
une adresse morte, sans aucun message — trouve par l'exploitant en lancant la commande
dans son terminal, apres que j'aie moi-meme conclu deux fois a tort.
DEUX LECONS, ecrites plutot que corrigees en silence :
- un objet range chez celui qui n'en est pas responsable derive sans que personne le voie ;
- mes commandes s'executent dans ma session : l'exploitant ne voit pas leur sortie. Une
commande lente ressemble alors a un blocage, et un blocage a une commande lente. Pour
toute ecriture longue sur du materiel, c'est a lui de la lancer.
Les anciens emplacements restent lus : un site pas encore migre continue de fonctionner.
make verifier 41/41.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Demande de l'exploitant : « l'underlay et son tenant doivent avoir chacun sa voute ».
CE QUI ETAIT FAUX. Le jeton d'API du cluster et la cle d'API de la frontiere vivaient dans
la voute de CHAQUE tenant. Patient 0 a du les recopier pour exister. Consequence : on ne
pouvait plus revoquer l'acces d'un locataire sans le revoquer pour tous — la faute des
neuf copies, appliquee aux secrets.
CE QUI EST POSE :
- `underlay.vault.yml`, chez l'hebergeur, a cote d'underlay.yml. Quatre secrets deplaces
(jeton Proxmox, cle et secret d'API OPNsense), retires des deux voutes de tenants.
- `proxmox_api.voute()` lit l'underlay APRES le tenant, donc l'hebergeur fait foi ; un site
non encore migre continue de fonctionner sur son ancienne voute.
- `appliquer_opnsense._voute()` n'a plus sa propre lecture : elle appelle celle du cluster.
La reecrire aurait fait une dixieme copie le jour ou l'on refermait les neuf autres.
- Le playbook de clonage charge la voute de l'underlay APRES celle du tenant, par la meme
derivation que proxmox-hebergeur.yml : le symlink designe deja l'hebergeur.
- `voute.py` sait que ces secrets ne sont plus attendus chez un tenant (P18).
EPROUVE SUR LE REEL, apres retrait des cles chez les deux tenants : l'API du cluster
repond, le devis de placement est conforme pour les deux ecosystemes, le devis de
frontiere se genere (86 objets), le SDN est convergent.
UNE PRECAUTION APPRISE EN CHEMIN : le premier essai a ecrit la voute EN CLAIR avant de la
chiffrer, et le chiffrement a echoue — il a fallu detruire le fichier. La sequence est
desormais l'inverse : chiffrer dans un dossier de travail, ne deposer que le resultat.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Demande de l'exploitant : « il faut vraiment faire une distinction entre l'underlay et les
tenants. Definis bien la frontiere entre les deux mondes. L'underlay et son tenant doivent
avoir chacun sa voute. »
LA DOCTRINE (docs/frontiere-physique-virtuel.md) : qui possede quoi, ou ca vit, qui
l'administre. Et la regle qui la rend operante — UN TENANT NE DETIENT JAMAIS UN SECRET DU
MONDE PHYSIQUE. Aujourd'hui chaque tenant porte le jeton d'API du cluster ; patient 0 a du
le recopier pour exister. C'est la faute des neuf copies, appliquee aux secrets : une
valeur qui vit a N endroits diverge, et on ne peut plus en revoquer une sans les autres.
L'INSTRUMENT (`make underlay-plan`) confronte le fichier au reel : l'API du cluster pour
les adresses REELLEMENT portees, une sonde TCP pour ce qui repond. N'ecrit rien.
CE QU'IL A TROUVE, des le premier passage :
management 10.0.0.0/24, stockage 10.0.1.0/24, ceph 10.0.2-3.0/24 -> PERSONNE
transit 10.0.4.0/24, vxlan 10.0.5.0/24 -> occupes (3 noeuds)
portes par les noeuds et declares NULLE PART : 192.168.11.x (la vraie gestion),
10.11.5-7.x, 192.168.50.x, 10.1.110.254
La frontiere avait deja migre vers 10.17.0.1 ; les hyperviseurs, non. Le fichier decrivait
le monde d'avant — et c'est pour cela que la regle d'admin de patient 0 atterrissait sur
`wan`, ou elle n'aurait jamais laisse passer personne.
DEUX PRECAUTIONS ECRITES DANS L'INSTRUMENT, apprises en l'ecrivant :
- l'AUTORITE DEPEND DU ROLE. L'API de Proxmox connait ses hyperviseurs, et eux seuls.
Declarer un commutateur « porte par personne » parce que le cluster l'ignore, c'est
accuser le monde de ce que l'instrument ne voit pas.
- « pas joignable d'ici » n'est pas « absent ». Les reseaux de CHEMIN (transit, VXLAN,
stockage) ne sont jamais joignables de l'exterieur, par construction (D-78). Le premier
jet les declarait morts.
Le devis a donc corrige DEUX FOIS sa propre facon de mesurer avant de rendre un verdict.
RESTE, dans l'ordre : ecrire dans underlay.yml ce qui EST ; separer les voutes ; et alors
seulement appliquer la frontiere.
make verifier 41/41.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Le controle de dependances a REFUSE le deploiement de patient 0 : client_journal requiert
serveur_loki, client_metrique requiert serveur_prometheus, serveur_forgejo requiert
serveur_postgresql et serveur_postfix. Quatre refus, une seule racine — le moteur suppose
que tout ecosysteme porte tous les services. Apres les intrants (P32) et les bases (P35),
troisieme manifestation en cinq jours. Et le mur que rencontrerait toute offre plus petite
que l'ecosysteme de reference.
UNE INTEGRATION UNIVERSELLE A BESOIN D'UN INTERLOCUTEUR. « Tout hote est mesure » est vrai
dans un ecosysteme qui porte un Prometheus ; ailleurs, la meme phrase pose sur chaque
machine un client qui n'a personne a qui parler. La regle est desormais DERIVEE : le
service central d'une integration est celui que le registre des dependances lui donne
deja. Rien de neuf a tenir a jour, donc rien de neuf a oublier.
Chezlepro -> diff VIDE (tous ses services existent, rien ne change)
patient 0 -> client_backup, client_pki, client_unbound
UNE EXIGENCE N'EST PAS TOUJOURS ABSOLUE. Deux notions manquaient au registre :
`sauf_si` l'exigence tombe sous condition (Forgejo + SQLite)
`utilise_si_present` un agrement, jamais bloquant (Forgejo notifie SI un MTA existe)
Les confondre obligeait une forge a deployer une pile courriel entiere pour exister.
ET LA LECON D'HIER A SERVI : trois lecteurs avaient besoin le meme jour de lire une
variable d'instance (P35, les clauses sauf_si, le generateur). Trois copies auraient
recommence ce qu'on venait de refermer. Il y en a UNE, dans inventory_rules.
make verifier 41/41 ; make ci 41/41 ; inventaire de Chezlepro identique.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
L'exploitant, apres le refactor : « je n'y comprends rien ». C'est la mesure qui compte —
la regle fondatrice du depot est qu'un humain pilote sans IA, et une correction qu'il ne
peut pas expliquer ne lui appartient pas.
- L'unite « La preuve » gagne une section : le defaut le plus dangereux n'est pas
l'erreur, c'est la COPIE. Neuf copies ne vieillissent pas ensemble, et la divergence ne
se voit jamais de l'interieur d'une copie. Avec le cas vecu — un devis qui repondait
CONFORME sur le mauvais ecosysteme parce que les deux avaient les memes valeurs.
- Le glossaire gagne « source unique » et « resolution d'instance » ; P39 les exige.
- Le rapprochement qui rend la chose evidente : c'est la meme lecon que
proxmox-hebergeur.yml, ou les listes du cluster recopiees chez chaque tenant avaient
deja diverge. Une source, pas N copies — pour les donnees comme pour le code.
Plan de recette regenere. make verifier 41/41.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Cinq jours, cinq defauts, tous de la meme famille : « quelle instance, quel inventaire ? »
Neuf modules portaient chacun leur reponse.
- 18 aout : P03 comparait chaque instance a l'inventaire d'une AUTRE ;
- 19 aout : verifier_ports codait `principal/` en dur ; verifier_intrants et
_frontiere_absente lisaient le symlink au lieu de la variable ;
- 20 aout : devis_placement rendait un verdict juste sur le mauvais tenant ;
- 22 aout : P35, puis P36 — la dixieme, trouvee par la preuve elle-meme.
Aucune n'etait une faute d'inattention : chacune avait ete ecrite de bonne foi, a un
moment ou le besoin semblait local. C'est le mode de panne de la duplication — pas
l'erreur, mais la DERIVE, invisible depuis l'interieur d'un fichier.
LA RESOLUTION UNIQUE. `inventory_rules` porte instance_courante(), inventaire_de(),
dossier_inventaire() et plan_de(). Trois niveaux de repli, dont le TROISIEME manquait a la
moitie des copies : un hosts.yml existant, puis un REPERTOIRE existant (instance neuve —
c'est ce qui faisait echouer `make instancier` sur le modele public), puis le defaut.
Vingt-huit modules y sont branches.
CE QUI REND CE REFACTOR SUR : avant de toucher quoi que ce soit, chaque module a ete
interroge sur ce qu'il resolvait, pour les DEUX ecosystemes. Apres refactor, meme mesure :
17 modules x 2 instances, diff VIDE. Aucune resolution n'a change — prouve, pas suppose.
P41 echoue des qu'un module reintroduit une copie. Eprouvee en negatif : une copie
replacee dans genome.py est signalee avec son numero de ligne. Trois exemptions nommees :
instances.py et inventory_gui.py manipulent le SYMLINK lui-meme (bascule d'instance), et
devis_opnsense lit deliberement quelle instance est ACTIVE. Elles parlent du lien, pas de
la resolution.
make verifier 41 OK, 0 echec, 0 saute ; make ci idem ; lint vert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Doute de l'exploitant sur patient 0 : « je doute de la pertinence de pgsql ». Mesure
plutot que discussion.
REDIS NE SERVAIT A RIEN : le role serveur_forgejo ne le mentionne ni dans son app.ini, ni
dans ses defauts, et ne declare aucun lien. Heritage du modele `forge`. Retire du plan.
POSTGRESQL ETAIT EXIGE PAR LE ROLE : DB_TYPE = postgres en dur, resoudre_base sans
condition. Le doute etait fonde, le moteur ne savait pas faire autrement.
INTERRUPTEUR `serveur_forgejo_bd: postgres|sqlite`. En sqlite la base devient un FICHIER
sous serveur_forgejo_data. Ce que ca change ailleurs : rien. Le job de sauvegarde
`serveur_forgejo` emporte deja ce dossier ; PGSSLROOTCERT etait deja conditionne au mode
TLS ; et P35 lit desormais l'interrupteur (convention `<role>_bd`, group_vars de
l'instance puis defaut du role), donc n'attend aucune entree de registre. Une valeur
inconnue est REFUSEE au debut du role plutot que de retomber en silence sur PostgreSQL.
PATIENT 0 PASSE DE SIX A QUATRE MACHINES (Dovecot, Redis, PostgreSQL et sa VM). Sur la
machine dont tout descend, chaque service en moins est une chose de moins a defendre, a
sauvegarder et a rebatir. Et l'effet depasse patient 0 : une offre `forge` pour un petit
organisme cesse d'exiger une VM PostgreSQL.
LA NEUVIEME. En verifiant P35 sur patient 0, elle a rendu un verdict JUSTE SUR LE MAUVAIS
ECOSYSTEME : `plan = RACINE / "instance" / "plan"`, le symlink en dur. Neuvieme resolution
d'instance codee en dur en cinq jours. Ce n'est plus une serie de bogues, c'est une piece
manquante : une resolution unique et partagee, a faire en une fois et de tete reposee.
Enseigne : SQLite au glossaire (P39 l'exige desormais), et le README du role documente
l'interrupteur et ce qu'il ne change pas.
make verifier 40/40 ; make ci 40/40 ; lint et syntaxe du role verts.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
L'exploitant : « j'ai une intuition : blockchain ». L'intuition visait le bon probleme —
une memoire partagee, verifiable, sans centre — mais la reponse etait deja dans git.
GIT EST DEJA UNE CHAINE DE HACHAGE : chaque commit porte l'empreinte de son parent, un
arbre de Merkle. Ce qui manquait n'etait pas la chaine mais l'AUTEUR : `user.name` est
declaratif, et toute la soiree du 20 des commits ont porte « Daniel Allaire » sans qu'aucune
preuve ne les lie a une cle (verifie : 8 commits, 0 signature, 0 etiquette).
POSE AUJOURD'HUI :
- signature par cle SSH (celle que la forge connait deja), etiquettes signees par defaut ;
premiere etiquette v2026.08.21, verifiee par `git verify-tag` ;
- `.git-allowed-signers` VERSIONNE : qui clone verifie sans rien demander a la forge, et
sans lui faire confiance. Retirer une ligne revoque pour la suite ; le passe signe reste
verifiable ;
- `scripts/genome.py` + trois cibles make : les QUATRE depots sans lesquels un ecosysteme
ne renait pas (moteur, instance, hebergeur, modeles), DERIVES et non declares ;
- `parente.yml` par ecosysteme : de quel moteur il descend, a quel commit, sous quelle
etiquette. Patient 0 descend de 742bcbf, etiquette v2026.08.21 ;
- P40 : la parente est inscrite, chaque depot se retrouve, chaque commit inscrit EXISTE
encore (une histoire reecrite se voit la), chacun porte un remote. Sautee proprement
quand l'instance n'est pas un depot git — le modele jetable de la CI.
POURQUOI PAS DE BLOCKCHAIN. Elle resout : qui ecrit ensuite, quand personne ne fait
confiance a personne et qu'il y a de l'argent en jeu. Aucun des trois ici. Et la
multiplicite qu'elle achete cher, la lignee la produit comme effet secondaire : chaque
enfant porte une copie du code dont il descend, donc reecrire l'histoire suppose de
convaincre TOUS les descendants. Le jour ou l'Alliance certifiera, ce sera un JOURNAL DE
TRANSPARENCE (Certificate Transparency, Sigstore), pas une chaine.
ENSEIGNE, pas seulement pose : nouvelle unite « Filiation, signatures et temoins » (moule
en quatre temps), onze termes au glossaire, et P39 les exige desormais.
make verifier 40 OK, 0 echec, 0 saute ; make ci idem.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Un ecosysteme Set-OPS ne se reproduit pas depuis ses machines, mais depuis QUATRE depots :
le moteur, le plan du tenant, le depot de l'hebergeur (sa fabric) et les modeles. Perdre
les VM coute du temps ; perdre ces quatre-la coute l'ecosysteme. Rien ne les nommait.
`scripts/genome.py` les DERIVE au lieu de les declarer : le moteur est ce depot,
l'instance vient de SETOPS_INSTANCE, l'hebergeur se lit du symlink underlay.yml, et les
modeles se reconnaissent a leur FORME — des plans en sous-dossiers, aucun a la racine.
Deux criteres appris d'un faux positif : sans le second, le detecteur designait le lab,
qui porte un lien `OPS-Technolibre -> ../OPS-Technolibre` que le motif traversait. Un
lien vers un frere n'est pas un contenu.
Trois cibles : `make genome` (constater), `genome-inscrire` (ecrire la parente),
`genome-verifier` (tient-elle encore ?).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Demande de l'exploitant apres une soiree passee a croiser « strophe FRR », VRF, VNet et
nexthop-vrf : cet ecosysteme doit rester pilotable par un humain, idealement un seul ;
que chaque notion sous-jacente soit ENSEIGNEE.
MESURE AVANT D'ECRIRE : 40 termes employes par le depot et absents du glossaire — LDAP
184 fois, playbook 165, underlay 106, EVPN 66, VRF 33, LMTP 25. Le glossaire expliquait le
vocabulaire propre a Set-OPS (plan, index, voute, zone) et laissait dehors tout ce qui
vient du metier. Or c'est le metier qui perd le lecteur.
CE N'EST PAS UN DEFAUT DE REDACTION. La regle fondatrice du depot est qu'un humain pilote
sans IA. Chaque mot obscur retire une personne a la liste de celles qui peuvent reprendre
le systeme : un vocabulaire non explique est un defaut de CONCEPTION.
- Glossaire reecrit : 67 termes groupes par famille (plan, machines, Ansible, reseau,
noms, confiance, identite, courriel, etat et preuve). Chaque entree dit ce que c'est ET
pourquoi ce depot s'en sert, avec renvoi vers l'unite qui developpe.
- Unite d'apprentissage manquante : « Le reseau des tenants ». Dix-sept des quarante
termes y vivaient sans domicile. Elle suit l'ordre ou les problemes se sont poses : deux
clients sur un cable -> VLAN -> ses deux limites -> encapsulation -> pourquoi 1450 ->
EVPN -> le VRF, qui n'est pas une interdiction mais une ignorance structurelle.
- Navigation : la nouvelle unite est au sidebar ; le plan de recette regenere (P22 l'a
exige des l'ajout de la page — le harnais a mordu).
P39 verifie : chaque terme du jargon a une entree ; chaque lien du glossaire mene a une
page existante ; chaque page du wiki est atteignable depuis la navigation.
LA LISTE EST DECLAREE, ET C'EST UN CHOIX MESURE. La derivation automatique a ete essayee :
153 acronymes dans le wiki et le README, dont la moitie sont des mots francais en
capitales (AUCUNE, AVANT, TOUS). Un controle qui exige une entree pour « AUCUNE » finit
desactive, et une preuve desactivee ne garde rien. La preuve dit elle-meme cet angle mort.
EPROUVEE EN NEGATIF contre le glossaire d'avant : 49 termes manquants, nommes un par un.
make verifier 39 OK, 0 echec, 0 saute ; make ci idem.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Remarque de l'exploitant en preparant patient 0 : « le pont ne me semble pas approprie du
tout, depuis qu'on cree des VNets pour des tenants ». Juste, et plus grave que cosmetique.
`make placement-plan` confrontait `proxmox_clone_pont` (vmbr1) au cluster. Ce n'est PAS la
que les VM de la flotte atterrissent : `instancier` pose dans chaque hote le pont DERIVE
de sa zone (le VNet du tenant), et `make creer-vm` le passe au clone en ecrasant ce
defaut. vmbr1 n'est que le repli des clones MANUELS, hors plan. Le devis mesurait donc un
objet qui ne sert pas, et ignorait celui qui sert.
SUR PATIENT 0 : avant, « pont vmbr1 existe -> CONFORME ». Apres, « reseaux VM : t29appl,
t29donn, t29fron, t29serv INTROUVABLE — passer `make sdn-appliquer` AVANT de creer les
VM ». Aucun de ses quatre VNets n'existe sur le cluster : le devis d'avant-vol declarait
conforme un tenant dont les VM n'auraient eu nulle part ou naitre.
D-80 avait pourtant ete corrigee le 2026-08-13 — la liaison de placement est noeud,
stockage et gabarit, le pont se derive. Le devis continuait de compter quatre objets et de
nommer le mauvais : une doctrine corrigee dans un document ne se propage pas toute seule
dans le code qui l'applique.
MESURE MAINTENANT : en `sdn`, les VNets derives confrontes a /cluster/sdn/vnets ; en
`switch`, les ponts du noeud retenu. Avec le geste correctif quand il en manque.
NON-REGRESSION sur l'ecosysteme de reference : ses six VNets existent, conforme, code 0.
Quatre tests (Cluster simule, aucun reseau touche), harnais 38/38.
Au passage, dans patient 0 : le commentaire annoncait « les QUATRE valeurs qui rattachent
un tenant a une fabric » — trois, et le pont n'en est pas.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Soir de reconstruction, VPN pas encore monte. `make placement-plan` — le devis qu'on lance
AVANT quarante minutes de deploiement — rendait `AttributeError: 'str' object has no
attribute 'get'` : dix lignes de trace Python pour dire « le nom `asgard` ne se resout pas
d'ici ».
En panne, `Cluster.__call__` rend {"_erreur": "..."} — un DICT. Le devis l'iterait comme
une liste, et un dict itere rend ses CLEFS. `Cluster.rate()` existait pour ca et n'etait
appele nulle part ici. Les quatre appels passent desormais par une garde qui nomme la
cause, l'hote interroge et le geste a tenter (resolution, VPN, jeton).
ET LE DEVIS MESURAIT LE MAUVAIS TENANT. `placement_du_tenant()` lisait `instance/` en dur :
viser patient 0 avec SETOPS_INSTANCE mesurait en silence le placement de l'instance
montee. Le verdict etait juste — pour l'autre tenant. Les deux portaient les memes quatre
valeurs, ce qui est exactement la circonstance ou l'erreur ne se voit pas. C'est la
HUITIEME resolution d'instance ou d'inventaire codee en dur trouvee en trois jours ; a ce
compte ce n'est plus une serie de bogues, c'est une piece manquante.
L'en-tete annoncait aussi « tenant instance » — le nom du lien, pas celui du tenant.
TROIS TESTS, aucun reseau touche (Cluster simule) : une panne devient un refus lisible ;
une reponse qui n'est pas une liste est refusee — c'est le cas silencieux, celui qui
franchirait la premiere garde ; le cas nominal traverse sans gene. Branches sur `make test`.
Verifie ensuite contre le cluster reel : le devis nomme « OPS-Patient0 » et confirme ses
quatre objets (asgard, TrueNAS, vmbr1, gabarit 99998).
make test OK ; make verifier 38/38 ; make ci 38/38.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Trente-huit preuves, des tests, un lint — et RIEN ne les executait sans qu'un humain tape
`make`. Le meilleur atout du depot dependait de ne pas oublier. Il a desormais une CI
(.forgejo/workflows/verifier.yml) et une cible qui la rejoue a l'identique : `make ci`.
CE QUE LA CI A TROUVE AVANT D'EXISTER. Ecrire le workflow supposait de repondre a une
question jamais posee : est-ce qu'un depot PUBLIC, seul, se tient ? Mesure sur un clone
nu : non, a cinq endroits.
- `make instancier` echouait sur le modele public — le tout premier geste du QUICKSTART.
Le Makefile forcait `principal/hosts.yml` alors que le modele vit en `production/` ; sa
precedence suit desormais celle du code (fichier, puis REPERTOIRE existant, puis defaut).
- P32 parcourait les 54 roles sans regarder ce que l'instance deploie. Elle passait sur
l'ecosysteme de reference PARCE QU'IL PORTE TOUT. Or les modeles sont des OFFRES : toute
offre plus petite que l'ecosysteme complet echouait son propre harnais, pour des services
qu'elle ne vend pas. Le perimetre se lit maintenant du plan (groupes de l'inventaire,
puis roles composes par leur playbook).
- P24 : le modele public ne declarait aucun reseau d'administration — une flotte qu'on
construit et ou l'on n'entre plus. `nftables_admin_ssh` est pose, avec le pourquoi.
- P33 : verifier_ports.py codait `instance/inventories/principal/hosts.yml` en dur.
- P32 et P24 lisaient le symlink `instance/` au lieu de SETOPS_INSTANCE.
Toutes de la MEME FAMILLE que P03 avant-hier : une resolution d'inventaire recopiee, une
variable d'environnement qui deborde de sa portee. Le depot en compte SEPT ; deux de plus
sont corrigees ici, et la septieme le dit en commentaire plutot que de le taire.
`make ci` NE TOUCHE AUCUN SYMLINK : le modele public est monte comme instance jetable,
vise par SETOPS_INSTANCE/SETOPS_UNDERLAY, detruit en sortant. Deux details mesures parce
que devines faux d'abord : l'instance jetable est un DOSSIER FRERE (la federation se
decouvre ainsi ; ailleurs, quatre preuves tombent) ; et SETOPS_UNDERLAY n'est pose QUE
pour la verification, sinon l'inventaire est ecrit avec une fabric et regenere avec une
autre — la commande fabriquait l'ecart qu'elle denonce.
RESULTAT : clone nu sans instance ni frere -> 38 OK, 0 echec, 0 saute. Depot de
l'exploitant avec ses 3 instances -> 38 OK, 0 echec, 0 saute. Aucun residu.
Et le lint du depot a refuse mon propre fichier de CI avant qu'il ne tourne une seule fois
(`on:` lu par YAML comme le booleen vrai). Le harnais mordait deja.
A AJUSTER AU PREMIER PASSAGE, ecrit en tete du workflow : l'etiquette `runs-on` doit
correspondre a un runner Forgejo enregistre, et le runner a besoin du reseau pour pip et
ansible-galaxy. Le vert de cette CI dira que le moteur et son modele public se tiennent —
pas que la flotte va bien : aucune VM jointe, aucune voute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`catalogue-services.md` est le document qu'on lit pour savoir ce que Set-OPS FAIT :
l'hebergeur d'un second site, un futur client, un mainteneur qui arrive. Verifie role par
role contre roles/, voici ce qu'il disait de faux.
- « Capacites futures encore a implementer : collaboration (Nextcloud/Collabora) et couche
web (frontal/dorsal) » — les quatre roles existent, collab-01, web-frontal-01 et
web-dorsal-01 sont ACTIFS, et les deux roles web sont codifies depuis les spikes du
2026-07-05.
- « La federation LDAP n'est pas automatisee dans le role ; Keycloak n'est pas expose » —
serveur_keycloak/tasks/federation-ldap.yml existe, et le plan declare
`expose: auth.<domaine>`.
- `infra-mail-01` : « Sendmail MTA » — c'est Dovecot ; Sendmail est retire depuis le
2026-07-04. La table des hotes datait d'avant la separation edge-mta / mailstore.
- `client_supervision` annonce comme integration — n'a JAMAIS eu ni role ni playbook. La
supervision ne pose rien sur les hotes : controles actifs depuis le coeur, resultats
passifs pousses par l'API (c'est backup-01 qui rapporte l'etat de ses depots).
- Une colonne « Role » decorative inventait des noms (`nextcloud`, `client_metriques`) : le
role porte le nom du GROUPE. Colonne retiree.
Et NEUF roles vivants ne figuraient dans aucune table — le socle, toute la pile courriel,
les sauvegardes, Icinga Web 2, oauth2-proxy, Unbound. Deux (`serveur_backup`,
`client_backup`) n'etaient nommes NULLE PART.
P38 — CE QUE P31 NE POUVAIT PAS VOIR. P31 verifie que tout est nomme et atteignable, pas
qu'un document dise vrai : une carte peut etre complete et perimee. P38 confronte le
catalogue au code dans les deux sens, et c'est la TABLE qui fait foi des deux cotes : tout
role figure dans une ligne de table (la prose ne suffit pas — la pile courriel y etait
racontee et introuvable pour qui lit un index), et tout groupe cite en table existe
reellement (role, ou playbook de groupe pour `serveur_durci`, qui en compose onze).
Deux exemptions nommees : la prose peut citer les roles RETIRES, sinon on ne peut plus
ecrire d'ou l'on vient ; et P38 ne juge pas si une description est JUSTE — cela se revoit
contre le CHANGELOG, le mecaniser serait se mentir.
EPROUVEE EN NEGATIF : rejouee contre la version d'avant, elle echoue en nommant les neuf
roles absents et les trois cases fantomes.
Le catalogue dit aussi desormais ou il s'arrete : la reconstruction prouve qu'une machine
nue atteint l'etat voulu, pas la tenue sous charge ; et l'usage reel de Nextcloud n'est pas
consigne comme preuve. Lacune nommee au passage : la dependance causale
serveur_web_frontal -> serveur_nginx n'est toujours pas declaree.
make prouver : 38 OK, 0 echec, 0 saute. make test inchange.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Suite du filtre de portee : `underlay.tenants` nomme des DOSSIERS FRERES, et une faute de
frappe y etait invisible — le tenant disparaissait des trois devis du site, qui restaient
« conformes » sur ce qu'il en restait.
Sur un site a UN SEUL tenant — le cas de la prochaine implantation — la faute rend un
devis VIDE : une frontiere sans regle, un commutateur sans VLAN. Rien dans le mot
« conforme » ne dirait qu'on vient de dessiner le vide.
L'ecart est lisible sans toucher au materiel : d'un cote une liste de noms, de l'autre
les dossiers presents. Il se dit donc a `make underlay` (D-75). Quatre situations, quatre
messages distincts : dossier absent ; dossier sans plan/nomenclature.yml ; nomenclature
non federee (index absent, categories vide, federe: false) ; plus aucun nom qui
corresponde.
CE QU'UN GABARIT NE DOIT PAS SUBIR. Un modele decrit du materiel, pas un site deploye :
sans garde, tout modele portant un exemple de `tenants` echouerait chez quiconque n'a pas
ce dossier, et P17 deviendrait rouge sur la machine du voisin. La distinction existait
deja : modeles.py passe des reperes de tenants EXPLICITES (gabarit), le site les laisse
deriver. La verification ne s'applique qu'au second cas.
Trois tests dans test_adressage_derive.py — nom introuvable, clef absente, gabarit
epargne — avec un nom absurde pour qu'aucun test ne depende des dossiers de la machine.
make test 15 + 9 ; prouver 37 OK, 0 echec, 0 saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Le commit du 2026-08-14 nommait lui-meme ce qui restait : « meme hypothese ailleurs, non
corrigee — devis_sdn et devis_reseau partent du meme decouvrir(). A traiter quand ils
serviront sur un second site. » C'est fait AVANT, pas pendant la visite.
Les trois devis equipent le MATERIEL d'un site : la frontiere (regles, routes), le
commutateur (VLAN, SVI, routes) et le SDN de l'hyperviseur (zones, VNets). Un tenant
d'ailleurs y ajoutait des objets que le materiel accepte, qui ne correspondent jamais a
rien, et que rien ne signale.
UNE SEULE FONCTION AU LIEU D'UN FILTRE RECOPIE TROIS FOIS :
`devis_reseau.decouvrir_du_site()` = decouvrir() restreint par `underlay.tenants`, la
doctrine ecrite une fois. Le filtre inline de devis_opnsense est retire au profit d'elle.
`admin_tous_tenants()` la suit : le routeur d'un site n'a pas a savoir revenir vers le
plan de gestion d'un tenant qu'il ne porte pas.
EPROUVE dans les trois situations : underlay sans la cle -> les deux tenants, comme avant ;
underlay du second site -> OPS-Technolibre seul ; nom declare qu'aucun dossier ne fournit
-> ATTENTION et le reste est retenu ; filtre qui ne retient rien -> refus, code 1.
SANS EFFET SUR LE SITE ACTUEL : l'underlay de Chezlepro ne declare pas `tenants`, et cle
absente = toute la federation (verifie : decouvrir() et decouvrir_du_site() rendent la
meme liste ici).
ET LA CLE EST ENFIN DOCUMENTEE — c'etait le vrai trou. `underlay.tenants` existait depuis
le 14 sans figurer ni dans underlay.yml.example ni dans l'annexe du runbook
d'implantation : indecouvrable pour qui monte un second site.
prouver 37 OK, 0 echec, 0 saute ; make test inchange.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Le 121 etait le total des lignes SUPPRIMEES au diff — il comptait aussi les lignes de
valeur deplacees par le retri des clefs. Compte des seules lignes de commentaire, les
quatre fichiers de l'enregistrement de 13:48 : 129 -> 35, dont ce qui reste est l'entete
que le panneau reecrit lui-meme. Soit 94.
Rien ne change au correctif ni a sa mesure (41 -> 41 lignes, 30 -> 30 commentaires, diff
d'une ligne). Un depot qui mesure ce qu'il affirme ne garde pas un chiffre approximatif.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Un enregistrement du panneau « Intrants de base », a 13:48, a emporte 121 lignes
d'explications dans quatre fichiers — dont celle qui disait POURQUOI la valeur qu'on
venait de changer avait ete choisie (le plan de gestion reste en 10.0.0.0/24 tant que la
frontiere ne sait pas classer un second CIDR, D-61). Ces phrases sont la seule trace de
raisonnements qu'aucun code ne redit. `safe_dump` les effacait toutes a chaque
sauvegarde, en retriant les clefs au passage.
LE DEPOT CONNAISSAIT DEJA LE GESTE JUSTE. `_ecrire_intrants_fabric` (underlay.yml) et
`_ecrire_index_nomenclature` remplacent LA LIGNE sans toucher au reste ; leur commentaire
dit meme « un safe_dump les effacerait toutes ». Les quatre fichiers d'intrants n'avaient
jamais recu ce traitement. Ce qui manquait pour l'etendre : savoir remplacer une valeur
de LISTE, qui tient sur plusieurs lignes.
`_fusion_chirurgicale`, trois regles :
- une clef dont la valeur ne change pas n'est PAS reecrite (zero bruit au diff) ;
- les commentaires internes a un bloc remplace sont conserves, jamais juges ;
- une clef absente du fichier est ajoutee a la fin, jamais inseree au hasard.
La deuxieme est assumee : une explication devenue fausse survit a la valeur qu'elle
explique. Corriger une phrase est un geste humain ; l'effacer parce qu'un champ a bouge,
non. Meme principe que la fusion des clefs posee le 2026-08-10.
EPROUVE SUR LE FICHIER REEL, pas sur un exemple : l'enregistrement de 13:48 rejoue sur la
version d'avant (tiree de git) rend 41 lignes -> 41, 30 commentaires -> 30, 0 perdu, et un
diff d'UNE ligne. Neuf tests dans scripts/tests/test_gui_intrants.py, branches sur
`make test`.
DEUX FOIS LE MEME GESTE DESTRUCTEUR, SUR LE MEME CHEMIN : le 10 aout ce panneau perdait
des CLEFS (dns_amorcage, amorcage_acces_courriel — une VM qui nait sans resolution), le
18 des COMMENTAIRES. La premiere fois avait valu une fusion, pas un test. C'est le test
qui manquait.
Non touche, et dit comme tel : les registres (bases, applications, serveurs, domaines)
passent toujours par safe_dump. Ils sont structures et n'ont pas perdu de commentaires au
meme enregistrement — a reprendre si l'un d'eux en porte un jour.
make test 9/9 sur le nouveau fichier ; prouver 37 OK, 0 echec, 0 saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Trouve en validant une mise a jour du CHANGELOG. Deux invocations de la meme preuve,
deux verdicts : `make prouver` -> NON CONFORME (« lab : 17 hotes avec ecart »),
`python3 scripts/prouver.py` -> CONFORME 37/37. Le lab n'avait aucun ecart.
DEUX VARIABLES DESIGNENT LA CIBLE, ET LA SECONDE GAGNE. Le Makefile exporte
SETOPS_INVENTAIRE (ligne 13), derive de l'instance ACTIVE ; instancier.py:68 lui fait
FORCER la cible par-dessus SETOPS_INSTANCE. P03 (prouver.py:505) ne redirigeait que
SETOPS_INSTANCE : elle generait le plan de CHAQUE instance federee et le comparait a
l'inventaire applique de la SEULE instance active.
LE ROUGE N'ETAIT PAS LE PROBLEME, LE VERT L'ETAIT. Sous `make`, l'inventaire applique
de lab et de Technolibre n'etait JAMAIS lu — l'angle meme pour lequel P03 a ete ecrite
le 2026-08-12 (un tenant qu'on ne regarde pas imposant ses vieilles adresses au pare-feu
partage). La preuve etait aveugle a son propre cas, par l'invocation documentee. Les
rapports du 13 et du 14 sortent de cette invocation-la. Signature visible sans lire le
code : les hosts.genere.yml de lab et de Technolibre ne bougeaient pas.
CORRECTIF, cinq sites : env.pop("SETOPS_INVENTAIRE") partout ou l'on redirige
SETOPS_INSTANCE — P03 et P15, plus les trois applicateurs (opnsense, proxmox_fw, sdn) qui
pointent vers l'HEBERGEUR. Ces trois sont sans effet tant qu'hebergeur et tenant actif
coincident, c'est-a-dire jusqu'au second site. Le geste existait deja (modeles.py:96).
GARDE, pour que la classe cesse d'etre silencieuse : inventory_rules.inventaire_force()
REFUSE une cible hors de l'instance visee, en nommant les deux valeurs. Eprouvee dans les
deux sens (contradiction -> code 1 ; cible legitime dans l'instance -> passe). Branchee
sur les quatre resolutions de _inventaire (instancier, serveurs, applications,
config_proxmox). Le GUI garde la sienne : il ne redirige jamais SETOPS_INSTANCE pour un
fils et resout par symlink a chaque requete.
make prouver : 37 OK, 0 echec, 0 saute — et les hosts.genere.yml des TROIS instances
portent l'horodatage du passage. make test 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Mesure du 2026-08-14, sur le second site. `make frontiere-plan` voulait poser sur la
frontiere de Technolibre les regles ET les routes de CHEZLEPRO : 30 objets de plus,
dont six routes vers des sous-reseaux 10.17.x qui n'existent pas la-bas.
LE DEFAUT EST DE PORTEE, ET IL EST SILENCIEUX. Les devis partaient de
`devis_reseau.decouvrir()`, qui rend TOUTE la federation — tout dossier frere portant
une nomenclature avec un index. C'etait juste tant qu'il n'y avait qu'un site :
l'hebergeur unique portait bien tous les tenants. Des le second, le boitier aurait
accepte ces trente objets, aucun n'aurait jamais correspondu a un paquet, et rien ne
l'aurait signale — une politique qui a l'air complete et ne protege rien. Encore le
chèque vert sur un perimetre vide.
CORRECTIF : l'hebergeur declare dans SON underlay les tenants qu'il porte
(`underlay.tenants`), et `devis_opnsense` s'y limite. Cle ABSENTE = ancien
comportement (toute la federation) : un site unique n'a rien a declarer, c'est le
second qui doit se nommer. Un nom declare qu'aucun dossier frere ne fournit est
signale, pas ignore.
MEME HYPOTHESE AILLEURS, non corrigee ici : `devis_sdn` et `devis_reseau` partent du
meme `decouvrir()`. A traiter quand ils serviront sur un second site.
AU PASSAGE, un defaut du document ecrit la veille : le squelette d'`underlay.yml` de
`implanter-un-tenant-sur-un-site.md` omettait `index`. Sans cette cle, `make underlay`
refuse le reseau de gestion en le prenant pour le supernet d'un AUTRE site — message
deroutant, cause triviale. Trouve en s'en servant, moins de 24 h apres l'avoir ecrit.
prouver 37/37, make test 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Mesure du 2026-08-13 sur un OPNsense 24.7 (version ANCIENNE, en cours de mise a
jour depuis) : toute ecriture du moteur y echouait. Alias, regles, NAT, routes —
`make frontiere-appliquer` n'aurait rien pose sur ce boitier.
Ce n'est donc PAS un defaut universel du moteur : il ecrit correctement sur la
frontiere de Chezlepro, plus recente. C'est un probleme de COMPATIBILITE, et le
correctif vaut surtout comme garantie de portabilite — le jour ou l'on arrive sur
un site dont on ne choisit pas le firmware, ce qui est exactement le cas ici.
LE SYMPTOME MERITE D'ETRE RETENU, lui, quelle que soit la version. Le controleur
d'OPNsense lit ses champs avec `hasPost(<racine>)`. Si le corps arrive en
`application/json` et que le boitier ne le decompose pas en variables de POST, ce
test est FAUX : reponse `{"result":"failed"}` NUE — HTTP 200, aucune redirection,
et surtout AUCUNE validation. Le controleur ne dit pas quel champ manque, parce que
de son point de vue il n'y avait aucun champ.
Trois fausses pistes avant la bonne : valeur invalide (un corps VIDE echouait
pareil), racine de payload erronee (elle etait juste), privileges de la cle (la
lecture passait). Ce qui a tranche : un corps vide aurait DU produire des
validations. Leur absence disait que le controleur n'avait rien recu.
CORRECTIF : `Frontiere` poste desormais `racine[champ]=valeur`. C'est la forme que
poste l'interface web elle-meme — aucune version d'OPNsense ne la refuse, alors que
le JSON depend du boitier. Tous les corps du moteur sont des dicts plats de chaines
(alias, rule, route) : un seul niveau d'imbrication suffit.
EPROUVE SUR LE BOITIER, dans les deux sens : addItem d'un alias sonde -> `saved`,
delItem -> `deleted`, aucune trace laissee, lecture intacte (11 alias). A re-eprouver
apres la mise a jour, pour verifier que le formulaire reste bon sur la version
recente — c'est le seul point qui reste ouvert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Question de l'exploitant apres la decouverte de vmbr3 : a quoi sert cet intrant ?
Mesure : a presque rien.
proxmox_pont 14 occurrences dans l'inventaire -> DERIVE par hote (VNet de zone)
proxmox_noeud 0 -> proxmox_clone_noeud est la vraie valeur
proxmox_stockage 0 -> proxmox_clone_stockage est la vraie valeur
instancier pose le VNet de chaque zone dans proxmox_pont, et l'hote l'emporte sur
le defaut. proxmox_clone_pont n'est donc consulte que par un `make cloner-vm`
manuel, hors flotte. C'est exactement pourquoi vmbr3 a pu y etre faux dix jours.
C'est le pire genre d'intrant : visible dans le GUI, on le corrige, on redeploie,
rien ne change. L'intitule dit desormais sa portee.
D-80 CORRIGEE : j'y avais ecrit « trois cles : noeud, stockage, pont ». Faux pour
le pont. La liaison de placement reelle est noeud, stockage et gabarit ; le pont
se derive comme le reste.
Verifier avant d'enumerer : j'avais liste les cles en lisant le fichier du tenant,
sans regarder lesquelles sont reellement consultees. Deux le sont, une ne l'est
pas — et c'est celle qui etait fausse.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
J'avais ecarte le gabarit de P37 (« objet du cluster, pas une liste declaree »).
L'exploitant a releve que ce n'etait pas une raison de ne pas le verifier : ca
deplace la question du STATIQUE vers le DEVIS.
devis_placement.py interroge le cluster et confronte les quatre valeurs : le noeud
existe ; le stockage existe ET accepte `images` ; le pont existe SUR LE NOEUD
retenu ; le gabarit existe ET porte template=1 — cloner une VM ordinaire
fonctionnerait, et produirait quatorze copies d'une machine vivante.
AU PREMIER PASSAGE il a trouve une declaration perimee : vmbr3 n'existe plus sur
aucun noeud (disparu au passage du transport VXLAN sur les interfaces VLAN), mais
proxmox-hebergeur.yml le declarait encore depuis le 3 aout — et P37 le validait,
puisqu'elle valide la DECLARATION, pas le cluster.
Invisible jusqu'ici : flotte-creer surcharge le pont avec le VNet derive de chaque
zone, donc le defaut n'aurait morde que sur un `make cloner-vm` manuel. Corrige
des deux cotes ; le defaut des trois tenants passe a vmbr1, que le cluster nomme
lui-meme « VM (5Gig) ».
P37 et ce devis ne se remplacent pas : l'un garde la coherence entre deux
declarations, l'autre confronte la declaration au reel. Il fallait les deux pour
voir un pont disparu depuis dix jours.
P31 a refuse le script tant qu'aucune cible make ne l'atteignait.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Formulation de l'exploitant, meilleure que celle du depot. Le commentaire disait
« la fabric reste celle de l'hebergeur, quel que soit le tenant actif » — vrai,
mais centre sur l'HEBERGEUR. Le cadrage juste est centre sur le TENANT : les deux
symlinks composent deux axes INDEPENDANTS (instance = quel tenant, underlay.yml =
sur quelle fabric).
Tout l'adressage derive du seed index : le plan se deplace d'une fabric a l'autre
sans y toucher. Ce qui ne se deplace pas tient en TROIS CLES —
proxmox_clone_noeud, _stockage, _pont. Elles vivent cote tenant (c'est lui qui
choisit ou se poser) mais nomment des objets de l'hebergeur. Trois, pas trente :
c'est ce qui separe « portable » de « theoriquement portable ».
P37 confronte ces trois noms aux listes de proxmox-hebergeur.yml, trouve par
derivation du symlink underlay.yml. Un nom absent est un ecart STATIQUE (D-75) —
sans quoi une faute de frappe ne se decouvre qu'au premier clone, apres quarante
minutes. Eprouvee en negatif sur les trois cles.
Non verifie et dit comme tel : le gabarit (_vmid_modele) est un objet du cluster,
pas une liste declaree.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
D-77 sortait deja le stockage de l'espace derive, mais par une EXCEPTION NOMMEE
plutot que par une regle. La question de l'exploitant — « le VLAN 40 n'a aucune
VM, pourquoi ne pas lui donner une adresse non routee ? » — a fait apparaitre le
critere juste.
Ce n'est pas « y a-t-il des machines dedans » (le VLAN de gestion n'en a pas plus
qu'un autre), c'est : ce reseau est-il jamais une DESTINATION, ou seulement un
CHEMIN ?
gestion (10) atteinte depuis ailleurs -> 10.<index>.0.0/24, UNIQUE
transit (40) prochains sauts seulement -> 192.168.40.0/24
transport VXLAN(50) VTEP <-> VTEP -> 192.168.50.0/24
stockage (20/30/31) baie <-> hyperviseurs -> 192.168.20/30/31.0/24
Sur six reseaux, CINQ cessent d'exiger la moindre coordination entre deux
hebergeurs, et « unique » redevient signifiant.
L'OS, trouve par l'exploitant avant ecriture : le VLAN 11 aurait produit
192.168.11.0/24, DEJA occupe par la gestion des hyperviseurs sur vmbr0
(passerelle .254). Le 11 se liberera quand cette gestion rejoindra 10.<index>.0.x
— mais faire dependre un plan d'adressage de l'ORDRE d'une migration est le genre
de dette qui se paie un an plus tard. Le transport passe au VLAN 50 : libre,
192.168.50.0/24 libre, et ne depend de rien. Verifie : rien ne code le 11 en dur.
underlay.yml n'est PAS modifie : il decrit le materiel tel qu'il est, et y ecrire
les nouvelles plages avant le deplacement physique le ferait mentir. Les adresses
changeront avec le materiel, dans l'ordre du runbook §6.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
P03 ne verifiait la fraicheur de l'inventaire que pour l'instance ACTIVE. Or la
frontiere nord/sud est PARTAGEE : ses alias d'hotes sont construits depuis le
hosts.yml de CHAQUE tenant. Un tenant qu'on ne regarde pas — parce qu'il n'a
aucune VM, precisement — impose donc ses adresses au pare-feu de tout le monde.
Elle boucle desormais sur les instances decouvertes, chacune verifiee via
SETOPS_INSTANCE (que instancier.py honore deja).
AU PREMIER PASSAGE, elle a trouve une troisieme instance perimee et une collision
franche :
OPS-Chezlepro-lab (index 1) applique : 10.11.18.21 <- ancienne derivation
OPS-Technolibre (index 11) derive : 10.11.x.x <- nouvelle derivation
Le lab occupait EXACTEMENT la plage desormais attribuee a Technolibre. P21 garde
les index ; rien ne gardait les inventaires APPLIQUES. La collision serait apparue
le jour ou les deux auraient tourne ensemble.
Les trois instances sont alignees : 10.1 (lab), 10.11 (Technolibre), 10.17
(Chezlepro).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
instancier.py comparer affichait l'ecart puis renvoyait TOUJOURS 0. P03
« Diff-vide du plan » ne pouvait donc pas echouer : elle est passee au vert
pendant que quatorze hotes divergeaient du plan.
Deux appelants, deux besoins — d'ou un MODE plutot qu'un changement de
comportement : `make instancier` est une INSPECTION (voir le diff avant de
decider ; un code d'erreur y transformerait la lecture en panne), P03 est une
AFFIRMATION (« le plan reproduit l'inventaire »).
prouver sort desormais a 1, et c'est EXACT : depuis le retrait du decalage de
+10, le plan derive 10.17.x.x tandis que l'inventaire applique — et les quatorze
VM qui tournent — portent 10.27.x.x. Le depot est sciemment dans cet etat
jusqu'au renumerotage.
Une preuve rouge qui dit vrai vaut mieux qu'une verte qui ne regarde rien.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Rien ne bornait `index`. supernet_de(300) rendait « 10.300.0.0/16 » : une CHAINE
qui ressemble a un reseau. Elle traverse tout le moteur sans bruit et n'echoue
qu'au premier ip_network() qui la lit, tres loin de l'index fautif.
La borne est posee A LA SOURCE (valider_index), pas dans un validateur de plan :
supernet_de, base3_de et vlan_de y passent toutes, donc aucune ne peut fabriquer
une adresse invalide — d'ou qu'on l'appelle : plan, GUI, devis ou test.
Elle protege un SECOND plafond, moins visible : a l'index 255 le VLAN vaut
3550+zone, sous les 4094 du 802.1Q. Un index a trois chiffres debordait aussi la.
Garde statique ajoutee au controle de federation (P21) : elle nomme le depot
fautif au lieu de laisser l'erreur remonter d'une bibliotheque.
LE TEST A TROUVE CE QUE LA RELECTURE N'AVAIT PAS VU : valider_index n'attrapait
que TypeError et ValueError, or int(float('inf')) leve OverflowError — un infini
faisait PLANTER la garde au lieu d'etre refuse.
test_underlay_bande_basse.py -> test_adressage_derive.py (il ne parlait plus
seulement de la bande basse). 12 cas, dont les refus.
Piege de structure au passage : les nouveaux cas, ajoutes apres le bloc
`if __name__ == "__main__"`, ne s'executaient pas — le bloc tourne avant que les
fonctions suivantes ne soient definies, et le compte affichait « 7 tests » au lieu
de 12. Un harnais qui compte ses propres tests doit etre lu.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
supernet_de rendait 10.(10+index).0.0/16. Personne ne savait plus pourquoi : ni le
commentaire de la constante, ni le wiki, ni le commit fondateur 36a882b ne le
justifiaient. Trois endroits consultes, zero raison ecrite.
Ses deux effets constates :
- il reservait 10.0-10.9 sous la plage tenant. Utile tant que l'underlay vivait
la — mais D-77 l'a fait entrer dans la bande basse de son propre /16, ce qui a
vide cette reserve de son role la veille ;
- il eloignait le premier tenant de 10.0.0.0/16, la plage la plus repandue en
reseau domestique. Ce risque revient donc aux index bas, et c'est ASSUME.
En echange l'index se lit directement dans l'adresse (17 -> 10.17.x.x) et le
plafond passe de 245 a 255 ecosystemes federes.
Chezlepro (17) 10.27.0.0/16 -> 10.17.0.0/16
Technolibre (11) 10.21.0.0/16 -> 10.11.0.0/16
lab (1) 10.11.0.0/16 -> 10.1.0.0/16
Doc alignee : les trois pages du wiki, multi-instances.md (plafond et exemple
devenus faux arithmetiquement), sdn-evpn.md, le libelle de la GUI, la docstring
d'underlay.py, D-77, et le document de preparation d'un site hebergeur. Les
CONSTATS DE TERRAIN dates sont laisses tels quels : ce sont des mesures.
CE COMMIT NE RENUMEROTE RIEN. Il change ce que le plan DERIVE ; l'inventaire
applique porte toujours 10.27.x.x et les quatorze VM tournent dessus. Appliquer
sans reconstruire rendrait la flotte injoignable — le renumerotage est une
operation a part, a mener a froid.
Au passage, retire un debris : une copie de conflit Nextcloud de
serveur_powerdns/defaults/main.yml, IDENTIQUE a l'original, commitee par accident
dans 1295eea et jamais chargee par Ansible.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
D-77 disait ou l'underlay doit vivre ; rien ne le verifiait. Une convention qu'on
n'outille pas pourrit en silence (D-70) — c'est ce qui a laisse la sauvegarde vide
pendant un mois.
Le controle disait l'INVERSE de la decision : underlay.py refusait tout
chevauchement avec un supernet tenant. Rendu plus FIN, pas plus strict :
son propre supernet, bande basse -> conforme (la regle)
son propre supernet, bande haute -> REFUS (collision avec ses zones)
supernet d'un AUTRE site -> REFUS (jamais reliables)
hors de tout supernet -> conforme (heritage 10.0.x, stockage 192.168.x)
La frontiere derive d'OCTET_ZONE, jamais ecrite en dur. Le site declare son `index`
dans underlay.yml ; sans lui on retombe sur la regle stricte d'avant D-77, sure —
Chezlepro reste donc conforme en 10.0.x tant qu'il n'a pas migre.
LE PIEGE, attrape par le test : un prefixe peut COMMENCER dans la bande basse et
deborder — 10.21.0.0/19 couvre les octets 0 a 31. La borne est evaluee sur toute
l'etendue du prefixe, pas sur son premier octet.
Deux de mes propres cas d'epreuve etaient mal choisis (10.21.14.0/23 et
10.21.12.0/21 se normalisent dans la bande basse) : il a fallu un prefixe qui
franchit reellement la frontiere pour eprouver la garde.
test_underlay_bande_basse.py : 7 cas, cable dans make test.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Le gabarit portait le nom du mauvais proprietaire. Les TROIS instances
(Chezlepro, Technolibre, lab) le declaraient et pointaient deja toutes sur le
MEME VMID 99998 — alors que le commentaire affirmait « chaque tenant a SON
golden template ». Faux depuis longtemps, et invisible en lisant un seul fichier.
Renomme sur le cluster et dans les trois instances. Le gabarit est un artefact du
MOTEUR, pas d'un tenant ; le nom d'un tenant sur le gabarit d'un autre etait un
piege qui n'attendait qu'un troisieme hebergeur. model_creer.py et
config_proxmox.py proposaient encore modele-debian13 : alignes.
Sans risque : le clonage se fait par VMID depuis le 2026-08-10, le nom ne sert
plus qu'a l'affichage.
NOUVELLE PORTE dans l'aiguillage : docs/preparer-un-site-hebergeur.md, pour
quelqu'un qui prete son materiel sans rien connaitre de Set-OPS. Ecrit a partir
du depot — VLAN et MTU d'underlay.yml, bloc par tenant de
inventory_rules.supernet_de(), privileges du jeton de config-proxmox.md,
dimensionnement (~460 Go, ~37 Go de RAM) mesure sur la flotte vivante.
Deux avertissements y figurent parce qu'ils ont deja coute cher ici : un gabarit
personnalise recopie son identite dans chaque clone, et un blocage contourne en
silence se paie en heures.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Correction : l'entree precedente attribuait le defaut a la reconstruction
from-zero. C'est faux — le commit fondateur 7476a54 (2026-07-03) disait lui-meme
« Reste : jobs Tier 1 (pg_dump/slapcat/vmail/forgejo) ». Ils n'ont jamais ete
ecrits, et infra-pki-01 a ensuite perdu son integration client_backup.
Le role qui POSSEDE la donnee dit comment la sortir : client_backup_jobs est
l'intersection du catalogue et des group_names du noeud. Un tenant qui deplace un
service emporte sa sauvegarde avec lui. On sauvegarde l'etat NON REGENERABLE :
ni zones PowerDNS ni tableaux Grafana, ils se redeploient.
L'unite qui ment est RETIREE, pas rendue bloquante : refuser le deploiement aurait
casse infra-edge-01, infra-dns-01 et mon-01, qui ne detiennent legitimement rien.
Le defaut etait le timer qui echouait chaque nuit en donnant l'apparence d'une
sauvegarde.
P36 (D-75) lit les groupes detenteurs dans le catalogue : ajouter un role au
catalogue etend la preuve du meme geste. Elle a attrape infra-pki-01 — les cles
de l'AC — corrige au plan.
Mesure hors-noeud : collab-01 64,0 MiB/272, edge-mta-01 4,4 MiB/139,
data-sql-01 1,0 MiB (pg_dumpall complet), forge-01 26,4 KiB/68,
infra-pki-01 20,1 KiB/21, idm-01 2,3 KiB/5 (slapcat). 9 hotes, 9 success.
Reste : rien ne surveille l'unite — c'est ce silence qui a laisse le defaut
vivre un mois.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Six majeures d'un coup, mais la decouverte importante est ailleurs.
LA « ROTATION DE CLE » N'EN ETAIT PAS UNE. Quatre versions, trois signataires
differents — 10.0.0 par B3B1F60A, 12.0.0 par D0A82005, 14.0.0 et 16.0.2 par
C4186DF6. Ce ne sont pas des cles distinctes : ce sont des SOUS-CLES de
signature sous une primaire stable depuis 2022 (EB114F5E...C5923710, « Forgejo
<contact@forgejo.org> »). La sous-cle 0F527CF9...0E1609E5 est bien celle qui
avait signe la 12.0.0.
D'ou une correction du verificateur : il comparait l'empreinte du SIGNATAIRE,
donc une sous-cle, et aurait echoue a chaque rotation LEGITIME — on aurait
appris a lever la garde pour avancer, ce qui est la pire chose qui puisse
arriver a un controle. Il accepte desormais la cle primaire (dernier champ de
VALIDSIG), qui survit aux rotations et refuse quand meme une cle etrangere.
FORGEJO A L'ANCRE QUE KEYCLOAK N'A PAS. forgejo.org/download publie
l'empreinte, et le binaire vient de codeberg.org : la source de confiance est
INDEPENDANTE du canal de livraison. Le projet annonce lui-meme la rotation
(« the GPG key is updated on a regular basis »), ce qui confirme qu'epingler la
primaire est le bon choix. Somme sha256 egalement publiee et verifiee conforme.
Eprouve dans les deux sens : nominal 0 ; binaire altere d'un octet 1 ;
empreinte de Keycloak appliquee a Forgejo 1 ; signature d'un autre artefact 1 ;
et Keycloak ne regresse pas apres modification du comparateur.
Verifie : versions-mesurer 0 en retard, role applique sur forge-01,
ansible-lint production sur 50 fichiers, prouver.py 35 OK (code lu sans tube).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
En ajoutant la verification de signature, P31 a declare
`verifier_signature.py` « injoignable » — alors qu'il tourne a chaque
deploiement de Keycloak. Elle ne regardait que le Makefile et les autres
scripts, jamais les roles ni les playbooks.
Une preuve qui ignore un chemin d'appel reel accuse du code sain, et on
apprend a passer outre : le pire sort possible pour une garde.
ET J'AI POUSSE AVEC CETTE PREUVE EN ECHEC. Troisieme fois de la session, et
toujours la meme faute : `prouver.py | tail -1` rend le code de sortie de
`tail`, donc le `&&` qui suit continue quoi qu'il arrive. Le commit precedent
(verification de signature Keycloak) est parti avec 34 OK / 1 echec. Corrige
ici, et la lecon vaut plus que le correctif : ne jamais lire le verdict d'une
garde a travers un tube.
Verifie : prouver.py 35 OK, code de sortie lu SANS tube.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
« J'ai besoin d'une confiance reelle. Keycloak est probablement l'element le
plus dangereux de cet ecosysteme. » C'est exact : il signe les jetons de TOUT
l'ecosysteme, une archive substituee la et l'identite entiere tombe.
CORRECTION D'ABORD. J'avais ecrit que Keycloak ne publie aucune somme de
controle. Faux, et l'exploitant l'a releve. Mesure : .sha1 et .md5 existaient
jusqu'a 26.6.2 puis ont disparu a partir de 26.7.0 ; le .asc, lui, est present
sur toutes les versions — et je l'avais rate, sans meme le chercher. Une somme
prouve qu'un fichier n'est pas corrompu ; une signature prouve QUI l'a produit.
ETABLI : la meme cle 861AB50E...6FD6EEBA a signe 26.0.7 (alors en production),
26.3.0, 26.6.2 et 26.7.1. NON ETABLI : aucune source independante ne publie
cette empreinte — ni keycloak.org, ni SECURITY.md, ni un fichier KEYS ; absente
de keys.openpgp.org, trouvee sur keyserver.ubuntu.com qui n'est pas une
autorite. On prouve la continuite, pas l'origine. L'ancre reste une decision
humaine — desormais ecrite, versionnee, et verifiee a chaque telechargement.
scripts/verifier_signature.py impose trois choses, chacune contre un
contournement precis : la cle publique vit DANS LE DEPOT (aucun serveur de
cles au deploiement) ; l'empreinte est EPINGLEE, donc une rotation amont
devient un echec bruyant ; trousseau JETABLE, donc le resultat ne depend pas
du trousseau personnel. Il lit VALIDSIG et compare l'empreinte du signataire
REEL — « bonne signature » seule laisserait passer une signature valide faite
par une autre cle du trousseau.
Eprouve sur cinq cas : nominal 0 ; artefact altere d'un octet 1 ; empreinte
differente 1 ; cle du depot corrompue 1 ; signature absente 1.
Ce que ca ne prouve PAS : que l'empreinte epinglee soit la bonne. Aucune
machine ne peut l'etablir ; le script garantit qu'on ne s'en ecarte plus sans
le voir.
Verifie : role applique de bout en bout sur idm-01, ansible-lint production,
prouver.py 35 OK.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Question de l'exploitant : « ne devrait-on pas prendre les versions les plus
recentes ? » Non — resoudre « la derniere » au deploiement detruirait la
reproductibilite qu'on vient de prouver en rasant et remontant deux
ecosystemes, et ferait de chaque reconstruction une loterie. Six majeures de
Forgejo ne s'avalent pas en effet de bord d'un `make`.
Mais il avait raison de tirer le fil : L'ECART ETAIT INVISIBLE. Il a fallu
quatre requetes a la main pour decouvrir Forgejo 10.0.0 contre v16.0.2 publie,
Keycloak 26.0.7 contre 26.7.1, Nextcloud 34.0.1 contre 34.0.2, oauth2-proxy a
jour.
Le devis ne juge pas, il RENSEIGNE : un retard n'est pas une faute, il sort
donc en 0. Monter de version reste un acte explicite.
Ce qui le distingue d'une liste ecrite a la main : les versions epinglees sont
DERIVEES du depot. La source amont, elle, ne peut pas se deriver — elle depend
de l'editeur — et vit dans une table. TOUTE VERSION EPINGLEE SANS ENTREE DANS
CETTE TABLE FAIT SORTIR EN ERREUR : sans cette garde, un epinglage ajoute
demain vieillirait sans que personne ne le voie. Une exemption reste possible,
mais ecrite et motivee (deux le sont : la serie PHP de Debian, la version
PostgreSQL vide).
Eprouve dans les deux sens : un serveur_redis_version ajoute temporairement
fait sortir en 1 en le nommant ; retire, retour a 0.
Et il rappelle ce qu'on n'epingle PAS. Grafana n'a aucune version dans le
depot — le 13.1.1 vu dans l'historique apt etait ce que son depot servait ce
jour-la. Debian, Grafana, smallstep et Icinga prennent ce qu'on leur donne.
Deux regimes coexistent dans la meme flotte, et les taire donnerait l'illusion
que tout est maitrise.
Verifie : versions-mesurer code 0, garde eprouvee, prouver.py 35 OK.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Le defaut note hier est corrige. `raser` concluait au succes sur la reponse
immediate de l'API : le DELETE rend un UPID et la main tout de suite, la
destruction se fait en tache de fond, et elle peut echouer APRES. Le
2026-08-10, six VM ont ete rapportees « detruites » alors qu'elles etaient
toujours la — la tache sortait sur « VM is locked (clone) », verrou laisse par
des clonages interrompus.
Confondre « demande acceptee » et « travail fait » est le pire mensonge
possible pour la SEULE commande destructive du moteur : on croit la place
libre, on relance la construction, et rien ne se cree sans qu'on comprenne
pourquoi.
_attendre_tache() relit l'UPID, interroge l'etat jusqu'a `stopped` et rend
l'exitstatus reel. Chaque VM est annoncee detruite OU en echec, avec la cause
telle que le cluster l'a donnee ; le compte final ne ment plus.
test_raser_resultat.py fabrique la situation exacte : un faux cluster qui
accepte tout puis rend une tache terminee en erreur. EPROUVE DANS LES DEUX
SENS — avec l'ancien comportement retabli temporairement il echoue en
designant le defaut, avec le correctif il passe. Raccorde a `make test`, donc
rejoue par P02.
Meme motif que le clonage corrige une heure plus tot, dans l'autre sens : une
operation asynchrone dont on ne verifie pas l'issue. Les deux venaient du
passage a des appels d'API directs, ou plus rien n'attend a notre place.
Verifie : make test vert, prouver.py 35 OK.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Question de l'exploitant : « je ne vois nulle part un MTU a 1450 ». Fondee.
Le 1450 existait bien — MTU_OVERLAY_DEFAUT dans underlay.py, dont P23 derive
sa garde (transport >= overlay + 50), pose par le devis SDN sur chaque zone,
et verifie sur le cluster. Mais il ne se propage pas a la carte de l'invite :
les 14 VM tournaient a 1500, et cloner_vm_debian.yml n'avait aucune occurrence
de `mtu`. La VM emettait donc des trames que son propre chemin ne pouvait pas
encapsuler — la panne que le registre des flux appelle « la plus couteuse a
diagnostiquer ».
Invisible parce que les 14 VM vivent sur asgard : deux VM du meme hyperviseur
communiquent par le pont local, sans encapsulation. Le defaut serait apparu au
premier eclatement de la flotte — c'est-a-dire quand il faudra heberger les
deux tenants ensemble.
Corrige en DEUX endroits, avec `mtu=1` (« herite du pont ») plutot que 1450 en
dur : juste en SDN comme hors SDN, et encore juste le jour des trames jumbo.
Le GABARIT le porte — proposition de l'exploitant, meilleure que la mienne :
elle couvre les clonages qui ne passent pas par le playbook. Et le playbook le
repose a chaque clone, parce qu'un gabarit se RECAPTURE et que ce qui n'est pas
versionne se perd en silence.
make mtu-mesurer (7e devis) rattache chaque hote a sa zone par son pont derive
et lit le MTU attendu dans devis_sdn.py. Ecart trouve sur 14 hotes du premier
coup, puis confirme corrige.
ET J'AI CASSE LA FLOTTE EN CORRIGEANT : applique aux cartes de VM EN MARCHE,
le changement detache et rebranche la carte sans que l'invite reconfigure son
interface. 0/14 joignables pendant trois minutes. L'agent qemu, independant du
reseau invite, a permis de constater et de redemarrer par l'API. Dans le
playbook la meme tache s'execute AVANT le demarrage du clone : aucun risque.
Sur une VM en service : poser la config, puis redemarrer — une seule d'abord.
Verifie : mtu-mesurer CONFORME 14/14 a 1450, prouver.py 35 OK, ansible-lint
production.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
L'epreuve de portabilite est passee. Un second ecosysteme souverain complet,
monte depuis zero par le meme moteur : 14 hotes, 2583 taches ok, 331 changed,
0 failed. Plan distinct, voute separee, realm technolibre, sa propre AC — et
une topologie differente : LDAP et SSO sur des machines separees la ou
Chezlepro les co-localise.
Cinq devis CONFORME (identite, certificats, PostgreSQL, courriel, frontiere —
55 lignes 0 ecart). Le sixieme dit exactement la bonne chose : les 6 services
repondent depuis l'edge, aucun depuis le poste, qui ne resout pas encore
technolibre.internal (6 entrees /etc/hosts absentes — le plancher).
SIXIEME DEFAUT MOTEUR. Le devis d'identite interrogeait LDAP en `ldapi:///`
— un socket UNIX LOCAL — depuis l'hote serveur_keycloak. Cela ne marchait que
par CO-LOCATION ACCIDENTELLE. Un tenant qui separe l'annuaire du SSO echouait
sur « Failed to import python-ldap » : l'hote SSO n'a pas de client LDAP. Les
deux lectures sont deleguees a l'hote DERIVE par resoudre_annuaire.
P35 (D-75) : toute application dont le role exige une base en a une au plan.
La garde de resoudre_base existait deja, mais s'est declenchee a la 92e tache
de collab-01, apres quarante minutes, pour un ecart entierement lisible dans
le plan.
Rien n'y est code en dur : les roles concernes sont ceux qui INCLUENT
resoudre_base, et le groupe reclame est lu dans le DEFAUT de la variable
passee — jamais deduit du nom. serveur_icingaweb2 reclame la base de
serveur_icinga ; une preuve supposant « role = groupe » aurait crie sur un cas
sain.
Eprouvee dans les deux sens ET sur les deux tenants, dont les registres n'ont
pas la meme portee : base retiree -> ECHEC la nommant ; restauree -> OK.
Verifie : prouver.py 35 OK sur les deux instances, ansible-lint production.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Le socle pose l'un de DEUX fichiers dans /etc/nftables.conf : le ruleset
derive (table setops_flux) s'il existe, sinon un gabarit de repli plat (table
setops_filter) qui n'ouvre que le 22. Ce sont des ALTERNATIVES, jamais des
couches.
Mais le rechargement est `nft -f`, qui AJOUTE sans purger, et le fichier
derive retirait setops_flux — jamais setops_filter. Un hote passe du repli au
derive gardait donc DEUX chaines input sur le meme hook, toutes deux en
policy drop. Le paquet traverse les deux : seule l'intersection de leurs
accept passait.
Symptome parfaitement trompeur : AC debout, 8443 en ecoute, regle posee et
acceptante, ICMP entre les deux VM a 0,15 ms — et step ca bootstrap qui
expire. Il a fallu lire le ruleset entier pour voir la seconde table.
Le fichier derive retire desormais les DEUX tables (le repli portait deja
flush ruleset). Pas de flush ruleset cote derive : choix du depot pour ne pas
detruire de tables etrangeres, respecte.
Ce qui l'avait rendu possible : `make reconstruire` ne generait JAMAIS les
flux. `make flux` en est maintenant la premiere etape.
Et `no_log` a masque la cause trois fois dans la journee — dont au terme d'un
deploiement de 157 taches. Les taches concernees extraient desormais le
verdict a part (ce qui a echoue, quel code, quel message) sans toucher aux
en-tetes. Une garde qui protege un secret ne doit pas emporter le diagnostic.
Verifie : TLS OK depuis infra-dns-01 vers l'AC, une seule table nftables,
13/14 hotes deployes sans echec, prouver.py 34 OK, ansible-lint production.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Les deux reconstructions de la semaine rebatissaient Chezlepro sur son propre
materiel : une preuve de reproductibilite, pas de portabilite. La vraie
epreuve est un second tenant — Technolibre, index 11, plan distinct, voute
separee, meme cluster.
1. LE CLONAGE RESOLVAIT PAR NOM. proxmox_kvm cherche une VM portant le `name`
demande ; s'il en trouve une il rend `ok` et ne clone RIEN — aucune tache
cote cluster. Or les noms courts sont VOLONTAIREMENT identiques d'un tenant a
l'autre. `backup-01` de Technolibre tombait sur celui de Chezlepro. Mesure :
id-ldap-01 et sup-01 (noms absents chez Chezlepro) passaient du premier coup,
backup-01 echouait toujours. Le clonage cible desormais par VMID, via l'API.
Deux defauts de ce correctif, trouves en le mesurant : le corps assemble en
Jinja avec `>-` rendait une CHAINE et perdait le `pool` (VM nee hors de son
pool, sans un mot) ; et l'application du gabarit de calcul expirait a 5 s,
laissant la VM aux valeurs du gabarit — 2 coeurs/2 Go au lieu du plan, en
silence. Corps en mapping YAML avec omit ; six tentatives espacees.
Et `no_log` a masque la cause au moment ou elle servait. Les deux attentes
disent maintenant ce qu'elles ont constate.
2. LE GUI DETRUISAIT DES INTRANTS. Dans ecrire_intrants, `identite` etait la
seule branche sur quatre a ecrire par-dessus le disque au lieu de fusionner.
Un enregistrement a supprime dns_amorcage et amorcage_acces_courriel. Le meme
geste sur Chezlepro aurait mange les memes cles.
3. LE VERROU DE `raser` N'ETAIT PROUVE QUE POUR UN TENANT. Le faux cluster
codait en dur les VMID de Chezlepro ; monte ailleurs, le test rendait 0 au
lieu de 2. Il fabrique desormais la collision sur le plan courant.
Verifie : prouver.py 34 OK sur Technolibre, test_raser vert sur les deux
tenants, ansible-lint production sur le playbook de clonage.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>