genome : la forge du SITE fait autorite (D-81), et make genome-etat le verifie
Some checks are pending
verifier / verifier (push) Waiting to run

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 <noreply@anthropic.com>
This commit is contained in:
Daniel Allaire 2026-08-26 09:17:24 -04:00
parent 6a5a49f924
commit 87716cefc7
7 changed files with 180 additions and 2 deletions

View file

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

View file

@ -766,6 +766,14 @@ site-appliquer: ## Applique un role aux machines du site — GROUPE=<serveur_ops
#
# CE QUI EST POUSSE SE DERIVE de `serveur_ops_depots` (au plan du site) : on pousse ceux
# dont une copie locale existe a cote du moteur, et rien d'autre.
.PHONY: genome-etat
genome-etat: ## Ce que la forge du site porte, face au poste — ne corrige rien, regarde
@set -e; \
voute="$$(dirname "$$(readlink -f underlay.yml)")/underlay.vault.yml"; \
supp=(); [[ -f "$$voute" ]] && supp+=( -e "@$$voute" ); \
ansible-playbook -i scripts/site_inventaire.py \
playbooks/maintenance/genome_etat.yml "$${supp[@]}" $(ARGS)
.PHONY: genome-pousser
genome-pousser: ## Le runner du site pousse le genome sur la forge du site — DEPOT=<nom> pour un seul
@set -e; \

View file

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

View file

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

View file

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

View file

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

View file

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