From 87716cefc79e4fd99721df2b44112772e46125ec Mon Sep 17 00:00:00 2001 From: Daniel Allaire Date: Wed, 26 Aug 2026 09:17:24 -0400 Subject: [PATCH] genome : la forge du SITE fait autorite (D-81), et `make genome-etat` le verifie MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Decision de l'exploitant : la forge du SITE fait autorite pour le genome. Toute autre copie — y compris celle d'ou le moteur a ete pousse jusqu'ici — est un MIROIR. Un ecosysteme se reproduit depuis la forge de son site : c'est de la qu'il clone son moteur, ses plans, ses modeles. Si l'autorite est ailleurs, cette forge devient un cache qu'on croit a jour — et le 2026-08-26 elle etait quatre commits en arriere sans que rien ne le signale, dont le correctif qui desarme le pare-feu Proxmox. UNE AUTORITE QU'ON NE VERIFIE PAS EST UNE AUTORITE QU'ON SUPPOSE. `make genome-etat` confronte, depot par depot, ce que le poste porte a ce que la forge porte. Il REFUSE en cas d'ecart plutot que de le signaler : un ecart connu et tolere redevient un ecart oublie, et la commande qui le corrige tient en trois mots. Il dit aussi quand la copie locale n'est pas propre — des commits pas encore faits sont une autre forme de retard. Mesure au passage, et traitee plutot qu'ignoree : le premier contact avec la forge echoue une fois sur six — poignee TLS expiree, puis cinq reponses de suite. Ce n'est pas le chemin, qui est prouve ; c'est l'acceptation TLS apres un temps d'inactivite. Les deux cibles reessaient. Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 17 ++++ Makefile | 8 ++ docs/audit/preuve-2026-08-26.md | 4 +- docs/decisions-architecture.md | 1 + playbooks/maintenance/genome_etat.yml | 117 +++++++++++++++++++++++ playbooks/maintenance/genome_pousser.yml | 7 ++ scripts/genome_colis.py | 28 ++++++ 7 files changed, 180 insertions(+), 2 deletions(-) create mode 100644 playbooks/maintenance/genome_etat.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index bdb795a..89865af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,6 +59,23 @@ qu'elle porte, et on refuse si ça diffère de ce que porte le runner. sans que rien ne le signale — dont le correctif qui désarme le pare-feu Proxmox. Un écosystème qui se reproduit depuis une forge en retard reproduit ses défauts.* +### D-81 — la forge du site fait autorité, et `make genome-etat` le vérifie + +Décision de l'exploitant : **la forge du SITE fait autorité pour le génome.** Toute autre +copie — y compris celle d'où le moteur a été poussé jusqu'ici — est un **miroir**. + +Une autorité qu'on ne vérifie pas est une autorité qu'on suppose. `make genome-etat` +confronte donc, dépôt par dépôt, ce que le poste porte à ce que la forge porte — et +**refuse** en cas d'écart plutôt que de le signaler. *Un écart connu et toléré redevient un +écart oublié, et la commande qui le corrige tient en trois mots.* Il dit aussi quand la +copie locale n'est pas propre : des commits pas encore faits sont une autre forme de +retard. + +Mesuré au passage, et traité plutôt qu'ignoré : **le premier contact avec la forge échoue +une fois sur six** — poignée TLS expirée, puis cinq réponses de suite. Ce n'est pas le +chemin, qui est prouvé ; c'est l'acceptation TLS après un temps d'inactivité. Les deux +cibles réessaient. + ## 2026-08-25 — Quatre zones inverses, et rien de plus **47 preuves.** Le site ne servait aucun PTR. `serveur_powerdns_zone_inverse` dérivait diff --git a/Makefile b/Makefile index 281a406..497c77d 100644 --- a/Makefile +++ b/Makefile @@ -766,6 +766,14 @@ site-appliquer: ## Applique un role aux machines du site — GROUPE= pour un seul @set -e; \ diff --git a/docs/audit/preuve-2026-08-26.md b/docs/audit/preuve-2026-08-26.md index 5d0bbfe..7a24e08 100644 --- a/docs/audit/preuve-2026-08-26.md +++ b/docs/audit/preuve-2026-08-26.md @@ -43,10 +43,10 @@ | P28 | Pools Proxmox : un par tenant, sans collision | AFF-110 | ✅ OK | CONFORME : 3 pool(s) Proxmox, 35 VM placee(s), aucun nom ni VMID en collision. | | P29 | Authentification : chaque role declare sa position | AFF-111 | ✅ OK | 28 role(s) serveur declares (interne-sans-auth 2, ldap-direct 2, sans-auth-humaine 17, socle-identite 2, web-sso 5) ; 2 lacune(s) nommee(s) : serveur_loki, serv | | P30 | SDN EVPN : zones, VNets et sous-reseaux derives | AFF-112 | ✅ OK | CONFORME : SDN EVPN, 3 zone(s), 15 VNet(s), 15 sous-reseau(x), aucune collision. | -| P31 | Documentation : tout ce que le depot FAIT est nomme | — | ✅ OK | 50 scripts expliques et atteignables, 103 cibles make documentees, 60 roles avec README. | +| P31 | Documentation : tout ce que le depot FAIT est nomme | — | ✅ OK | 50 scripts expliques et atteignables, 104 cibles make documentees, 60 roles avec README. | | P32 | Intrants exiges par les roles : tous fournis | — | ✅ OK | CONFORME : 14 exigence(s) de role, toutes satisfaites (39 cle(s) declaree(s) par l'instance). | | P33 | Aucune collision de port entre roles co-localises | — | ✅ OK | CONFORME : 33 revendication(s) de port, aucune collision entre roles co-localises (16 groupes). | -| P34 | Chaque document declare son lecteur | — | ✅ OK | 42 document(s) declarent leur lecteur (23 genere(s) exempte(s)). | +| P34 | Chaque document declare son lecteur | — | ✅ OK | 42 document(s) declarent leur lecteur (24 genere(s) exempte(s)). | | P35 | Toute application exigeant une base en a une au plan | — | ✅ OK | 0 application(s) exigeant une base l'ont toutes (0 entree(s) au registre). | | P36 | Tout detenteur d'etat porte une sauvegarde | — | ✅ OK | 2 hote(s) detiennent de l'etat, tous porteurs de `client_backup` (9 groupe(s) au catalogue). | | P37 | Le placement du tenant existe chez son hebergeur | — | ✅ OK | placement confronte a l'hebergeur monte (SITE-Chezlepro) : noeud, stockage, pont — tous offerts. | diff --git a/docs/decisions-architecture.md b/docs/decisions-architecture.md index 1452e8f..7fcc1a6 100644 --- a/docs/decisions-architecture.md +++ b/docs/decisions-architecture.md @@ -55,6 +55,7 @@ sont les seules vérifiables. | # | Décision | Pourquoi | Détail | Garde | |---|---|---|---|---| +| **D-81** | **La forge du SITE fait autorité pour le génome.** Toute autre copie — y compris celle d'où le moteur a été poussé jusqu'ici — est un **miroir**. Le poste de l'exploitant ne route pas jusqu'à elle : c'est le **runner du site** qui publie, par `make genome-pousser` | un écosystème se reproduit depuis la forge de son site : c'est de là qu'il clone son moteur, ses plans, ses modèles. Si l'autorité est ailleurs, cette forge devient un cache qu'on croit à jour — et le 2026-08-26 elle était **quatre commits en arrière** sans que rien ne le signale, dont le correctif qui désarme le pare-feu Proxmox. **Un écosystème qui se reproduit depuis une forge en retard reproduit ses défauts.** Le poste n'a de patte que sur l'administration, et on ne perce pas de chemin pour lui : le runner existe pour ce travail | `playbooks/maintenance/genome_pousser.yml`, `scripts/genome_colis.py`, `Makefile` §genome-pousser | — | | **D-13** | Un **hébergeur** sert plusieurs **tenants** et a son tenant par défaut | Chezlepro est les deux à la fois, ce qui masquait la distinction | `frontiere-opnsense.md` §2 | — | | **D-14** | `underlay.yml` appartient à l'**hébergeur**, monté par symlink | ce sont ses commutateurs, ses câbles ; le moteur est générique, un tenant n'en possède pas | `sdn-evpn.md`, `underlay.yml.example` | — | | **D-15** | Ce symlink **ne suit pas** `make instance-utiliser` | basculer le tenant actif ne change pas la fabric | `frontiere-opnsense.md` §2 | — | diff --git a/playbooks/maintenance/genome_etat.yml b/playbooks/maintenance/genome_etat.yml new file mode 100644 index 0000000..78bd6f2 --- /dev/null +++ b/playbooks/maintenance/genome_etat.yml @@ -0,0 +1,117 @@ +--- +# CE QUE LA FORGE DU SITE PORTE, FACE A CE QUE LE POSTE PORTE. +# +# La forge du SITE fait autorite pour le genome (D-81). Toute autre copie est un miroir. +# Encore faut-il que cette autorite soit A JOUR — et la derive ne se voit pas toute seule. +# +# Mesure du 2026-08-26 : la forge etait restee quatre commits derriere le poste sans que +# rien ne le signale. Le runner clonait donc un moteur perime, dont le correctif qui +# desarme le pare-feu Proxmox. Un ecosysteme qui se reproduit depuis une forge en retard +# reproduit ses defauts. +# +# Cette cible ne corrige rien : elle REGARDE. `make genome-pousser` corrige. +# +# make genome-etat + +- name: Ce que la forge du site porte, face au poste + hosts: serveur_ops_site + gather_facts: false + + vars: + genome_racine: "{{ serveur_ops_racine | default('/opt/setops') }}" + genome_freres: "{{ playbook_dir | dirname | dirname | dirname }}" + genome_moteur: "{{ (serveur_ops_depots | selectattr('role', 'eq', 'moteur') | first).dest }}" + + tasks: + - name: Voir lesquels de ces dépôts existent sur le contrôleur + ansible.builtin.stat: + path: "{{ genome_freres }}/{{ item.dest }}/.git" + delegate_to: localhost + become: false + loop: "{{ serveur_ops_depots | default([]) }}" + loop_control: + label: "{{ item.dest }}" + register: genome_locaux + + - name: Retenir ceux qu'on peut comparer + ansible.builtin.set_fact: + genome_compares: >- + {{ genome_locaux.results | selectattr('stat.exists') + | map(attribute='item') | list }} + + - name: Ce que le poste porte + ansible.builtin.command: + argv: + - python3 + - "{{ genome_freres }}/{{ genome_moteur }}/scripts/genome_colis.py" + - etat + - "{{ genome_freres }}/{{ item.dest }}" + - --branche + - "{{ item.branche | default('main') }}" + delegate_to: localhost + become: false + loop: "{{ genome_compares }}" + loop_control: + label: "{{ item.dest }}" + register: genome_ici + changed_when: false + + - name: Ce que la forge porte + ansible.builtin.uri: + url: >- + https://forge.{{ domaine_interne }}/api/v1/repos/{{ serveur_ops_forge_organisation + | default('genome') }}/{{ item.depot }}/branches/{{ item.branche | default('main') }} + validate_certs: false + status_code: [200, 404] + loop: "{{ genome_compares }}" + loop_control: + label: "{{ item.depot }}" + register: genome_la_bas + # LE PREMIER CONTACT AVEC LA FORGE RATE PARFOIS (mesure du 2026-08-26 : une fois sur + # six, poignee TLS expiree, puis cinq reponses de suite). Ce n'est pas le chemin — + # il est prouve — c'est l'acceptation TLS apres un temps d'inactivite. On reessaie + # plutot que de rendre un verdict sur un alea. + retries: 4 + delay: 3 + until: genome_la_bas is succeeded + changed_when: false + + - name: L'état, dépôt par dépôt + ansible.builtin.debug: + msg: >- + {{ item.depot }} : + poste {{ (genome_ici.results[idx].stdout | from_json).courte }} + {{ '=' if genome_la_bas.results[idx].status == 200 + and genome_la_bas.results[idx].json.commit.id + == (genome_ici.results[idx].stdout | from_json).tete + else '≠' }} + forge {{ genome_la_bas.results[idx].json.commit.id[:7] + if genome_la_bas.results[idx].status == 200 else '(absente)' }} + {{ '— copie locale NON PROPRE, des commits ne sont pas faits' + if not (genome_ici.results[idx].stdout | from_json).propre else '' }} + loop: "{{ genome_compares }}" + loop_control: + label: "{{ item.depot }}" + index_var: idx + + # ON REFUSE PLUTOT QUE DE SIGNALER. Un ecart connu et tolere redevient un ecart oublie ; + # et la commande qui le corrige tient en trois mots. + - name: La forge fait-elle autorité sur une copie à jour ? + ansible.builtin.assert: + that: + - genome_la_bas.results[idx].status == 200 + - genome_la_bas.results[idx].json.commit.id + == (genome_ici.results[idx].stdout | from_json).tete + fail_msg: >- + {{ item.depot }} : la forge du site porte + {{ genome_la_bas.results[idx].json.commit.id[:7] + if genome_la_bas.results[idx].status == 200 else '(rien — dépôt absent)' }}, + le poste porte {{ (genome_ici.results[idx].stdout | from_json).courte }}. + La forge du site FAIT AUTORITÉ (D-81) : tant qu'elle est en retard, tout + écosystème qui s'y reproduit reproduit un moteur périmé. + Corriger avec `make genome-pousser`. + success_msg: "{{ item.depot }} : la forge est à jour." + loop: "{{ genome_compares }}" + loop_control: + label: "{{ item.depot }}" + index_var: idx diff --git a/playbooks/maintenance/genome_pousser.yml b/playbooks/maintenance/genome_pousser.yml index 10751bf..2afaa34 100644 --- a/playbooks/maintenance/genome_pousser.yml +++ b/playbooks/maintenance/genome_pousser.yml @@ -226,6 +226,13 @@ loop_control: label: "{{ item.depot }}" register: genome_forge + # LE PREMIER CONTACT AVEC LA FORGE RATE PARFOIS (mesure du 2026-08-26 : une fois sur + # six, poignee TLS expiree, puis cinq reponses de suite). Ce n'est pas le chemin — + # il est prouve — c'est l'acceptation TLS apres un temps d'inactivite. On reessaie + # plutot que de rendre un verdict sur un alea. + retries: 4 + delay: 3 + until: genome_forge is succeeded changed_when: false - name: La forge porte-t-elle exactement ce que le runner porte ? diff --git a/scripts/genome_colis.py b/scripts/genome_colis.py index 241f884..f1db9b1 100644 --- a/scripts/genome_colis.py +++ b/scripts/genome_colis.py @@ -25,6 +25,9 @@ DEUX GESTES, DEUX COTES : Sur le RUNNER. Verifie le colis, avance `main` EN AVANCE RAPIDE SEULEMENT, puis pousse. Rend du JSON : {avant, apres, pousse}. + genome_colis.py etat + Des deux cotes. Ce que cette copie porte, pour le confronter a la forge. + `--ff-only` est le garde-fou. Si les deux histoires ont diverge, on s'arrete plutot que de fabriquer une fusion que personne n'a decidee — et le message de git nomme alors exactement ce qu'il faut reconcilier. @@ -62,6 +65,25 @@ def emballer(depot: Path, colis: Path, branche: str) -> int: return 0 +def etat(depot: Path, branche: str) -> int: + """Ce que cette copie porte — pour le confronter a ce que la forge porte. + + LA DERIVE NE SE VOIT PAS TOUTE SEULE. Le 2026-08-26, la forge du site etait restee + quatre commits derriere le poste sans que rien ne le signale ; le runner clonait donc + un moteur perime, dont le correctif qui desarme le pare-feu Proxmox. On rend donc + l'ecart LISIBLE, plutot que d'esperer s'en souvenir. + """ + if not (depot / ".git").exists(): + raise SystemExit(f"{depot} n'est pas un depot git.") + print(json.dumps({ + "tete": _git(depot, "log", "-1", "--format=%H", branche), + "courte": _git(depot, "log", "-1", "--format=%h", branche), + "sujet": _git(depot, "log", "-1", "--format=%s", branche)[:80], + "propre": _git(depot, "status", "--porcelain") == "", + })) + return 0 + + def recevoir(depot: Path, colis: Path, pushurl: str, cle: str | None, branche: str) -> int: if not (depot / ".git").exists(): @@ -119,7 +141,13 @@ def main(argv: list[str]) -> int: r.add_argument("--cle", default=None, help="cle SSH du runner") r.add_argument("--branche", default="main") + d = sous.add_parser("etat", help="ce que cette copie porte") + d.add_argument("depot") + d.add_argument("--branche", default="main") + a = p.parse_args(argv) + if a.geste == "etat": + return etat(Path(a.depot), a.branche) if a.geste == "emballer": return emballer(Path(a.depot), Path(a.colis), a.branche) return recevoir(Path(a.depot), Path(a.colis), a.pushurl, a.cle, a.branche)