trois reconstructions : trouver, verifier, prouver — 37 min 24 au tour 3
Tour 1 8 passages ~2 h 30 16 defauts Tour 2 3 passages 53 min 2 defauts Tour 3 2 passages 37 min 0 LE TOUR 2 EST CELUI QUI COMPTE LE PLUS. J avais annonce deux passages ; il en fallait trois, parce que les deux blocages connus sont EN SERIE : on ne peut pas amorcer une forge qui ne tourne pas, et elle ne tourne qu apres la convergence de la cle. Un runbook ecrit d apres un recit de depannage contenait une erreur qu aucune relecture n aurait montree — il fallait le SUIVRE pour la voir. DEUX MURS DE PLUS, invisibles au tour 1. site-creer rend la main avant que les machines repondent (3 min 20 mesurees). Et les cles d hote changent a chaque reconstruction : accept-new couvre la premiere rencontre, jamais un changement. LE CYCLE DE LA CLE EST DENOUE. Aucun ordre de couches n y pouvait rien — les certificats doivent venir tot. C est la propriete du geste qui a change de main : serveur_forgejo revendique la cle au moment ou il cree le groupe. client_pki garde la sienne et la repose a chaque passage, parce que step reecrit la cle a chaque renouvellement. Gain : un passage entier et 300 secondes d attente perdue. forge-amorcer purge desormais l entree perimee de l hote que GIT va contacter, lu dans git remote get-url — pas celle de l API, qui peut etre un tunnel. Et il rend les quatre premieres lignes de stderr : n afficher que la derniere m a coute un aller-retour, git terminant toujours par un message generique. Le runbook porte les six etapes du tour 3 avec leurs durees reelles et les dix-huit murs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
This commit is contained in:
parent
bef0655112
commit
b9b0d87db2
4 changed files with 193 additions and 17 deletions
63
CHANGELOG.md
63
CHANGELOG.md
|
|
@ -1,5 +1,68 @@
|
|||
# CHANGELOG — Set-OPS
|
||||
|
||||
## 2026-09-12 (3) — Trois reconstructions : trouver, verifier, prouver
|
||||
|
||||
La premiere reconstruction avait trouve seize defauts. Deux tours de plus ont ete faits, et
|
||||
c'est le second qui compte le plus : **un runbook ecrit d'apres un recit de depannage
|
||||
contenait une erreur qu'aucune relecture n'aurait montree.**
|
||||
|
||||
Tour 1 8 passages ~2 h 30 16 defauts trouves
|
||||
Tour 2 3 passages 53 min 2 defauts trouves
|
||||
Tour 3 2 passages 37 min 24 0
|
||||
|
||||
### Ce que le tour 2 a corrige dans le runbook
|
||||
|
||||
J'avais annonce DEUX passages. Il en fallait TROIS, et pour une raison instructive : les
|
||||
deux blocages connus — la cle de Forgejo et la forge vide — sont **en serie**. On ne peut
|
||||
pas amorcer une forge qui ne tourne pas, et elle ne tourne qu'apres la convergence de la
|
||||
cle. Je les croyais franchissables dans le meme passage.
|
||||
|
||||
Il fallait SUIVRE le runbook pour le voir.
|
||||
|
||||
### Deux murs de plus, invisibles au tour 1
|
||||
|
||||
**#17 — `site-creer` rend la main avant que les machines repondent.** Mesure : 3 min 20 au
|
||||
tour 3. Le chemin locataire a `_attendre-flotte` ; le site n'a pas d'equivalent. Un
|
||||
enchainement automatique echouerait sur `UNREACHABLE` qui n'est qu'une impatience.
|
||||
|
||||
**#18 — les cles d'hote changent a chaque reconstruction.** `known_hosts` porte l'ancienne,
|
||||
`git` refuse — a juste titre — et sans terminal il ne peut rien demander. Le message qu'il
|
||||
rend parle de droits d'acces et de depot inexistant.
|
||||
|
||||
`accept-new` accepte une PREMIERE rencontre, jamais un CHANGEMENT : il ne suffit donc pas
|
||||
pour une machine reconstruite sur une adresse deja connue. `forge-amorcer` purge desormais
|
||||
l'entree perimee — celle de l'hote que **git** va contacter, lu dans `git remote get-url`,
|
||||
et non celle de l'API : quand l'amorcage passe par un tunnel, les deux different et purger
|
||||
la mauvaise ne se verrait qu'au push.
|
||||
|
||||
### Le cycle de la cle, denoue
|
||||
|
||||
`client_pki` posait les droits de la cle d'hote pour le groupe `git`, que le paquet de
|
||||
Forgejo cree dans une couche POSTERIEURE. Aucun ordre de couches ne denoue ce cycle : les
|
||||
certificats doivent venir tot, tout le monde en depend.
|
||||
|
||||
C'est la PROPRIETE DU GESTE qui a change de main. `serveur_forgejo` revendique la cle au
|
||||
moment ou il cree le groupe ; `client_pki` garde la sienne et la repose a chaque passage,
|
||||
parce que `step` reecrit la cle a chaque renouvellement. Les deux se recouvrent, et c'est
|
||||
voulu : l'une amorce, l'autre entretient.
|
||||
|
||||
Gain mesure : un passage entier, et 300 secondes d'attente perdue.
|
||||
|
||||
### Ce que mon propre outil masquait
|
||||
|
||||
`forge-amorcer` n'affichait que la DERNIERE ligne de `stderr` — or `git` termine toujours
|
||||
par « assurez-vous que le depot existe », quelle que soit la cause. Un aller-retour de
|
||||
diagnostic pour une cle d'hote. Il rend maintenant les quatre premieres lignes.
|
||||
|
||||
C'est le meme defaut que ceux trouves dans le moteur, commis par moi : **un message qui
|
||||
pointe ailleurs que sa cause**.
|
||||
|
||||
### La sequence est desormais mesuree, pas reconstituee
|
||||
|
||||
`docs/runbooks-exploitation.md` §9 porte les six etapes du tour 3, avec leurs durees
|
||||
reelles et les dix-huit murs. Le troisieme tour n'a rien trouve de nouveau : c'est le
|
||||
premier ou reconstruire le site est une PROCEDURE et non une enquete.
|
||||
|
||||
## 2026-09-12 (2) — Le site est reconstruit depuis zero, et il a fallu seize corrections
|
||||
|
||||
`SITE-Chezlepro` n'avait jamais ete rase. Il avait ete monte par ajouts successifs, sur des
|
||||
|
|
|
|||
|
|
@ -327,15 +327,27 @@ laissé le premier renommage, jusqu'au passage de `client_pki`.
|
|||
> la connexion. Les zones du SITE ne sont pas routées depuis le plan d'administration du
|
||||
> locataire — le mur est la frontière, pas le vhost.
|
||||
|
||||
## 9. Le premier jour d'un site — la séquence, et les seize murs
|
||||
## 9. Le premier jour d'un site — la séquence, et les dix-huit murs
|
||||
|
||||
> **Écrit le 2026-09-12**, au sortir de la première reconstruction d'un site depuis zéro.
|
||||
> Avant elle, `SITE-Chezlepro` n'avait jamais été rasé : il avait été monté par ajouts
|
||||
> successifs, sur des semaines, avec un service déjà debout à chaque étape.
|
||||
>
|
||||
> La limite qu'on répétait — « l'infrastructure d'accueil n'a jamais été reconstruite
|
||||
> depuis zéro » — se lisait comme de la prudence. C'était **seize défauts** que rien
|
||||
> depuis zéro » — se lisait comme de la prudence. C'était **dix-huit défauts** que rien
|
||||
> d'autre n'aurait pu révéler.
|
||||
>
|
||||
> **Trois reconstructions complètes** ont été nécessaires : la première pour les trouver,
|
||||
> la deuxième pour vérifier les correctifs — elle en a révélé deux de plus, invisibles
|
||||
> tant que l'état n'était pas assez neuf — et la troisième pour prouver la séquence.
|
||||
>
|
||||
> | | Passages | Durée | Défauts trouvés |
|
||||
> |---|---|---|---|
|
||||
> | Tour 1 | 8 | ~2 h 30 | 16 |
|
||||
> | Tour 2 | 3 | 53 min | 2 |
|
||||
> | Tour 3 | **2** | **37 min 24** | **0** |
|
||||
>
|
||||
> La séquence ci-dessous est celle du troisième tour. Elle est mesurée, pas reconstituée.
|
||||
|
||||
### Ce qui rend un site différent d'un locataire
|
||||
|
||||
|
|
@ -352,31 +364,48 @@ C'est de là que viennent onze des quinze murs.
|
|||
# 0. AVANT TOUT — l'état sort du bâtiment
|
||||
make depot-hors-site VERS=<support hors site>
|
||||
|
||||
# 1. Les VM, depuis l'underlay
|
||||
make site-creer CONFIRMER=true # ~12 min pour 7 machines
|
||||
|
||||
# 2. Le résolveur d'amorçage, DÉCLARÉ dans le plan du site
|
||||
# 1. Le résolveur d'amorçage, DÉCLARÉ dans le plan du site AVANT de créer quoi que ce soit
|
||||
# plan/10-intrants.yml : dns_amorcage: <patte de la frontière>
|
||||
# Sans lui, les machines pointent sur le DNS du site — qui est l'une d'elles.
|
||||
# Et `nftables_admin_ssh: [<plan d'administration>]`, sans quoi l'exploitant ne
|
||||
# pourra pas atteindre ce qu'il vient de construire.
|
||||
|
||||
# 3. Le déploiement, DEUX FOIS au minimum
|
||||
ansible-playbook -i scripts/site_inventaire.py playbooks/site.yml \
|
||||
-e "@$(dirname "$(readlink -f underlay.yml)")/underlay.vault.yml"
|
||||
# 2. Les VM, depuis l'underlay ~9 min
|
||||
make site-creer CONFIRMER=true
|
||||
|
||||
# 4. Entre les deux passages : amorcer la forge
|
||||
# 3. ATTENDRE QU'ELLES REPONDENT — `site-creer` ne le fait pas ~3 min
|
||||
until ansible -i scripts/site_inventaire.py all,'!<hyperviseurs>' -m ping >/dev/null 2>&1
|
||||
do sleep 10; done
|
||||
|
||||
# 4. Passage 1 — va jusqu'à `serveur_ops`, s'arrête sur la forge vide ~20 min
|
||||
V="$(dirname "$(readlink -f underlay.yml)")/underlay.vault.yml"
|
||||
ansible-playbook -i scripts/site_inventaire.py playbooks/site.yml -e "@$V"
|
||||
|
||||
# 5. Amorcer la forge — elle tourne, elle est vide ~45 s
|
||||
export SETOPS_FORGE_MDP=… # jamais en argument de ligne de commande
|
||||
make forge-amorcer CONFIRMER=true
|
||||
|
||||
# 5. Rejouer jusqu'à 0 échec
|
||||
# 6. Passage 2 — doit finir à 0 échec ~4 min
|
||||
ansible-playbook -i scripts/site_inventaire.py playbooks/site.yml -e "@$V"
|
||||
```
|
||||
|
||||
**Total mesuré : 37 min 24** pour sept machines, de rien du tout à un site complet.
|
||||
|
||||
**La voûte se passe en `-e @`** — `make site-appliquer` la dérive du symlink `underlay.yml`,
|
||||
mais il n'existe aucune cible qui déploie le site EN ENTIER. Un `ansible-playbook` direct
|
||||
l'oublie, et l'échec parle d'une assertion, jamais d'un fichier manquant.
|
||||
|
||||
**Deux passages, au minimum, et ce n'est pas un contournement.** `client_pki` pose les
|
||||
droits d'une clé pour un consommateur qu'une couche ultérieure crée : au premier passage le
|
||||
groupe `git` n'existe pas, la clé reste fermée, et Forgejo ne démarre qu'au second.
|
||||
**Deux passages, et le second ne sert qu'à la forge.** Le premier va jusqu'à la couche 45
|
||||
sur 46 ; seul `serveur_ops` manque, faute de génome dans une forge neuve.
|
||||
|
||||
> **Il en fallait TROIS avant le 2026-09-12.** `client_pki` posait les droits de la clé
|
||||
> d'hôte pour le groupe `git`, que le paquet de Forgejo crée dans une couche postérieure :
|
||||
> au premier passage la clé restait fermée, Forgejo ne démarrait pas — après **300 secondes
|
||||
> d'attente perdue** — et il fallait un passage entier pour qu'il démarre, un autre pour la
|
||||
> forge. Aucun ordre de couches ne dénoue ce cycle ; c'est la **propriété du geste** qui a
|
||||
> changé de main : `serveur_forgejo` revendique désormais la clé au moment où il crée le
|
||||
> groupe. `client_pki` garde la sienne et la repose à chaque passage, parce que `step`
|
||||
> réécrit la clé à chaque renouvellement.
|
||||
|
||||
### Les quinze murs, et ce que chacun enseigne
|
||||
|
||||
|
|
@ -398,10 +427,12 @@ groupe `git` n'existe pas, la clé reste fermée, et Forgejo ne démarre qu'au s
|
|||
| 14 | Un flux à deux paires n'obtient qu'une branche | la chaîne de `elif` rangeait `[flotte, admin]` dans un seul cas |
|
||||
| 15 | Dépôts créés privés, runner anonyme | `could not read Username` — un message qui pointe ailleurs que sa cause |
|
||||
| 16 | Le wiki n'existe pas sur une forge neuve | Forgejo ne crée `<dépôt>.wiki.git` qu'à la **première page**, posée à la main dans l'interface |
|
||||
| 17 | `site-creer` rend la main avant que les machines répondent | mesuré à **3 min 20** — un enchaînement automatique échouerait sur `UNREACHABLE` |
|
||||
| 18 | Les clés d'hôte changent à chaque reconstruction | `accept-new` couvre la première rencontre, **jamais un changement** : `forge-amorcer` purge l'entrée périmée de l'hôte que `git` va contacter |
|
||||
|
||||
### Le motif
|
||||
|
||||
Onze des seize sont **du code juste en régime établi**, faux le premier jour : un résolveur
|
||||
Douze des dix-huit sont **du code juste en régime établi**, faux le premier jour : un résolveur
|
||||
qui se pointe sur lui-même, une clé dont le consommateur n'existe pas encore, un répertoire
|
||||
créé par une couche ultérieure, une forge vide qu'on croit remplie.
|
||||
|
||||
|
|
|
|||
|
|
@ -85,6 +85,34 @@
|
|||
# read operation timed out ». Un deploiement de flotte echouait parce qu'un serveur tiers
|
||||
# etait lent — sur une plateforme qui se veut souveraine, c'est une dependance de trop
|
||||
# sur le chemin critique.
|
||||
# LE ROLE QUI CREE LE GROUPE REVENDIQUE LA CLE (2026-09-12).
|
||||
#
|
||||
# `client_pki` (couche `pki_client`) pose les droits de la cle d'hote pour le groupe qui
|
||||
# doit la lire — ici `git`. Mais ce groupe est cree JUSTE AU-DESSUS, dans une couche
|
||||
# POSTERIEURE. Au premier jour d'un site, l'ordre se mord la queue :
|
||||
#
|
||||
# client_pki a besoin du groupe git <- cree par ce role
|
||||
# forgejo a besoin de la cle lisible <- posee par client_pki
|
||||
#
|
||||
# Mesure sur deux reconstructions completes : le passage 1 laisse la cle en `root:root`,
|
||||
# Forgejo ne demarre pas, et l'attente du premier demarrage coute 300 SECONDES pour rien.
|
||||
# Il fallait un passage entier de plus.
|
||||
#
|
||||
# AUCUN ORDRE DE COUCHES NE DENOUE CE CYCLE — les certificats doivent venir tot, tout le
|
||||
# monde en depend. Ce qui se deplace, c'est la PROPRIETE DU GESTE : le role qui cree le
|
||||
# groupe est celui qui peut, a cet instant, donner l'acces.
|
||||
#
|
||||
# `client_pki` garde la sienne et la REPOSE a chaque passage — `step` reecrit la cle a
|
||||
# chaque renouvellement, et ne regler les droits qu'ici les perdrait des le premier.
|
||||
# Les deux taches se recouvrent, et c'est voulu : l'une amorce, l'autre entretient.
|
||||
- name: Donner acces a la cle d'hote au groupe git (amorcage)
|
||||
ansible.builtin.file:
|
||||
path: "{{ client_pki_cle | default('/etc/step/certs/' ~ (ansible_fqdn | default(ansible_hostname)) ~ '.key') }}"
|
||||
owner: root
|
||||
group: "{{ serveur_forgejo_utilisateur }}"
|
||||
mode: "{{ client_pki_cle_mode | default('0640') }}"
|
||||
failed_when: false # pas encore de certificat : `client_pki` passera apres
|
||||
|
||||
- name: Le binaire de cette version est-il deja pose ?
|
||||
ansible.builtin.stat:
|
||||
path: "/usr/local/bin/forgejo-{{ serveur_forgejo_version }}"
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ import subprocess
|
|||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from urllib.parse import urlsplit
|
||||
from pathlib import Path
|
||||
|
||||
RACINE = Path(__file__).resolve().parent.parent
|
||||
|
|
@ -132,6 +133,35 @@ def main() -> int:
|
|||
f"Relancer avec : make forge-amorcer CONFIRMER=true")
|
||||
return 0
|
||||
|
||||
# PURGER L'ENTREE PERIMEE DE L'HOTE QU'ON S'APPRETE A CONTACTER (2026-09-12).
|
||||
#
|
||||
# `accept-new` accepte une PREMIERE rencontre, jamais un CHANGEMENT — et c'est bien
|
||||
# ainsi. Mais une forge reconstruite porte une cle d'hote inedite sur une adresse
|
||||
# DEJA CONNUE : ssh refuse, `git` ne peut rien demander sans terminal, et le message
|
||||
# qu'il rend parle de droits d'acces et de depot inexistant. Mesure au tour 2 : un
|
||||
# aller-retour de diagnostic complet pour une cle d'hote.
|
||||
#
|
||||
# CE QUE CA N'AFFAIBLIT PAS. La purge ne vaut que pour l'hote de CETTE forge, au
|
||||
# moment ou l'exploitant amorce une machine qu'il vient de creer lui-meme par une API
|
||||
# authentifiee. On ne touche a aucune autre entree, et `accept-new` reprend ensuite
|
||||
# son role : la cle notee ici sera defendue comme les autres au prochain contact.
|
||||
# LA CIBLE EST L'HOTE QUE `git` VA CONTACTER, pas celui de l'API : quand l'amorcage
|
||||
# passe par un tunnel, `--forge` vaut `127.0.0.1` alors que le push part vers le nom
|
||||
# du remote. Purger le mauvais ne servirait a rien, et on ne s'en apercevrait qu'au
|
||||
# push.
|
||||
hotes_git = set()
|
||||
for _nom, local, _br in prets:
|
||||
url = subprocess.run(["git", "-C", str(local), "remote", "get-url", "origin"],
|
||||
capture_output=True, text=True, timeout=20).stdout.strip()
|
||||
h = urlsplit(url).hostname if "://" in url else (
|
||||
url.split("@", 1)[-1].split(":", 1)[0] if "@" in url else "")
|
||||
if h and h not in ("127.0.0.1", "localhost"):
|
||||
hotes_git.add(h)
|
||||
for cible in sorted(hotes_git):
|
||||
r = subprocess.run(["ssh-keygen", "-R", cible], capture_output=True, text=True, timeout=20)
|
||||
if r.returncode == 0 and "not found" not in (r.stderr or "").lower():
|
||||
print(f" known_hosts : entree perimee de {cible} retiree")
|
||||
|
||||
mdp = os.environ.get("SETOPS_FORGE_MDP", "")
|
||||
if not mdp:
|
||||
print("\nREFUS : mot de passe de l'administrateur de la forge absent.\n"
|
||||
|
|
@ -196,12 +226,36 @@ def main() -> int:
|
|||
continue
|
||||
# `--force` JAMAIS : si le depot porte deja quelque chose, on veut le savoir,
|
||||
# pas l'ecraser. Un amorcage n'a de sens que sur une forge vide.
|
||||
# LA FORGE EST NEUVE, SA CLE D'HOTE AUSSI (2026-09-12). Une reconstruction donne
|
||||
# a chaque machine une cle d'hote inedite : `known_hosts` porte l'ancienne, `git`
|
||||
# refuse — a juste titre — et sans terminal il ne peut rien demander :
|
||||
#
|
||||
# Host key verification failed.
|
||||
# ssh_askpass: exec(/usr/bin/ssh-askpass): No such file or directory
|
||||
#
|
||||
# `accept-new` ACCEPTE UNE PREMIERE RENCONTRE, JAMAIS UN CHANGEMENT : si une cle
|
||||
# connue differe, ssh refuse toujours. C'est la posture juste ici — l'exploitant
|
||||
# vient de creer cette machine lui-meme, il y a quelques minutes, par une API
|
||||
# authentifiee. Exiger qu'il ait deja vu la cle d'un hote qui n'existait pas
|
||||
# rendrait l'amorcage impossible.
|
||||
#
|
||||
# NE PAS confondre avec `StrictHostKeyChecking=no`, qui avalerait aussi un
|
||||
# changement — c'est-a-dire exactement l'attaque contre laquelle il protege.
|
||||
env = dict(os.environ, GIT_SSH_COMMAND="ssh -o StrictHostKeyChecking=accept-new")
|
||||
r = subprocess.run(["git", "-C", str(local), "push", "origin", branche],
|
||||
capture_output=True, text=True, timeout=180)
|
||||
capture_output=True, text=True, timeout=180, env=env)
|
||||
if r.returncode == 0:
|
||||
print(f" {nom:22} pousse ({branche})")
|
||||
else:
|
||||
print(f" {nom:22} ECHEC push : {(r.stderr or '').strip().splitlines()[-1][:90]}")
|
||||
# LA DERNIERE LIGNE DE `stderr` EST LA PLUS GENERIQUE (2026-09-12). `git`
|
||||
# termine par « assurez-vous que le depot existe » quelle que soit la cause :
|
||||
# cle d'hote changee, droits, depot absent. N'afficher qu'elle a coute un
|
||||
# aller-retour de diagnostic — on rend les premieres lignes, qui portent le
|
||||
# motif reel.
|
||||
_err = [l for l in (r.stderr or "").strip().splitlines() if l.strip()]
|
||||
print(f" {nom:22} ECHEC push :")
|
||||
for _l in _err[:4]:
|
||||
print(f" {_l[:110]}")
|
||||
echecs += 1
|
||||
|
||||
print(f"\n{len(prets) - echecs}/{len(prets)} depot(s) amorces.")
|
||||
|
|
|
|||
Loading…
Reference in a new issue