deep_proxmox.py passe de 1245 à 474 lignes : tout ce qui ne connaît ni « qm »
ni pmxcfs vit désormais dans descente.py, prêt pour un second test long.
L'extraction a mis à nu ce qui protégeait un hôte qu'on n'a pas créé : rien.
a_defaire exigeait « vmid » et « parent_alias », deux clés que seule une
descente écrit — la protection tenait parce qu'aucun champ ne décrivait un
hôte emprunté. Un champ « cree », écrit à l'instant de la création, la rend
explicite et ferme trois portes : la liste de destruction, le repli par NOM de
detruire_etage1, et le retrait des entrées ~/.ssh/config de l'utilisateur.
Quatrième porte : le dossier des rapports est partagé. « deep_qemu --detruire »
aurait pris le rapport le plus récent, fût-il celui d'une descente Proxmox. Le
rapport porte son outil ; un rapport plus ancien, qui n'en a pas, est placé par
le préfixe de son nom de fichier plutôt que d'être rendu indéfaisable.
detruire_etage1 ne devine plus le nom de sa cible : il est obligatoire. Et une
sixième étape est née — « cet étage peut-il héberger le suivant ? » — parce que
le contrôle du stockage était celui du DÉBUT de l'étage suivant.
64 tests, les cinq garde-fous morts sous mutation. Au passage : la classe
LongTestMenuMixin, que mon renommage de répertoire avait rebaptisée
long_testMenuMixin sans qu'aucun test le voie.
--- EN ---
deep_proxmox.py drops from 1245 to 474 lines: everything that knows neither
"qm" nor pmxcfs now lives in descente.py, ready for a second long test.
The extraction laid bare what protected a host we did not create: nothing.
a_defaire required "vmid" and "parent_alias", two keys only a descent writes —
the protection held because no field described a borrowed host. A "cree" field,
written the instant a machine is created, makes it explicit and closes three
doors: the destroy list, detruire_etage1's fallback to the NAME, and the
removal of the user's own ~/.ssh/config entries.
Fourth door: the report directory is shared. "deep_qemu --detruire" would have
taken the most recent report, Proxmox's included. Reports now carry their tool;
an older one without it is placed by its filename prefix rather than made
undestroyable.
detruire_etage1 no longer guesses its target's name: it is mandatory. And a
sixth step is born — "can this level host the next?" — because the storage
check was the one at the START of the next level.
64 tests, all five guards die under mutation. Along the way: the class
LongTestMenuMixin, which my directory rename had turned into
long_testMenuMixin without any test noticing.
Assisted-by: claude-opus-5
(cherry picked from commit e1bc9ae3cfacd36a502bc88dc0789a4e86ce986b)
LongTest était le SEUL répertoire en CamelCase que nous ayons créé. Les deux
exceptions sous script/ — OCA_maintainer-tools, OCA_odoo-module-migrator —
sont des noms de dépôts amont tirés par Google Repo, pas les nôtres. Tout le
reste est en minuscules avec des soulignés : image_db, code_generator,
fork_github_repo, shell_script_odoo.
Le nom avait été repris tel qu'il m'avait été dicté, sans être confronté à la
convention — le contrôle même que le reste de ce travail applique partout.
50 occurrences dans 8 fichiers. Le menu TODO résout le nouveau chemin, l'essai
à blanc passe, et les 125 tests des trois fichiers touchés restent verts.
--- EN ---
LongTest was the ONLY CamelCase directory we created. The two exceptions under
script/ — OCA_maintainer-tools, OCA_odoo-module-migrator — are upstream repo
names pulled by Google Repo, not ours. Everything else is lowercase with
underscores: image_db, code_generator, fork_github_repo, shell_script_odoo.
The name had been taken as dictated, without being checked against the
convention — the very check the rest of this work applies everywhere.
50 occurrences across 8 files. The TODO menu resolves the new path, the dry run
passes, and the 125 tests in the three touched files stay green.
Assisted-by: claude-opus-5
(cherry picked from commit 170ee61e50dfeff638c84c07eadac00c62526e4d)
Le défaut promettait dix étages qu'aucune machine ne tient. Une descente
complète par ligne, sur 28 cœurs :
étage 1 : 0 s d'amorçage, 200 s d'installation, 280 s en tout
étage 2 : 37 s, 344 s, 495 s
étage 3 : 93 s, 777 s, 1 064 s
étage 4 : 15 608 s, 26 306 s, n'a pas abouti
Trois étages coûtent une demi-heure. Le quatrième a coûté 4 h 20 d'amorçage et
7 h 18 d'installation sur la même machine : tout y est 15 à 30 fois plus lent,
pas une seule étape. C'est aussi là que les fabricants s'arrêtent — le
quatrième étage est le troisième hyperviseur imbriqué, AMD en documente deux.
La profondeur reste le seul paramètre, et le README dit désormais ce qu'on
achète en la montant.
--- EN ---
The default promised ten levels no machine can hold. One full descent per row,
on 28 cores:
level 1: 0 s boot, 200 s install, 280 s total
level 2: 37 s, 344 s, 495 s
level 3: 93 s, 777 s, 1 064 s
level 4: 15 608 s, 26 306 s, did not finish
Three levels cost half an hour. The fourth cost 4 h 20 of boot and 7 h 18 of
install on the same machine: everything there is 15 to 30 times slower, not one
step. It is also where the vendors stop — level 4 is the third nested
hypervisor, and AMD documents two.
Depth remains the only parameter, and the README now says what raising it buys.
Assisted-by: claude-opus-5
(cherry picked from commit 226d6ffa3c664ba9e1a6dfdf48d52027b52eda23)
Attaqué par trois lentilles sur le code écrit, avant de le lancer pour de
vrai. Deux fautes valaient à elles seules l'exercice.
Il n'aurait JAMAIS fonctionné. L'installeur était lancé par « sh », or il
porte « set -euo pipefail » et un shebang bash : sur Debian /bin/sh est dash,
qui répond « set: Illegal option -o pipefail » et sort à la PREMIÈRE ligne.
Chaque étage aurait échoué sur l'installation, à tous les coups.
Et « --detruire » pouvait emporter une machine étrangère. Il prenait toute
entrée ssh dont le nom CONTENAIT « deep-pve », puis sur son rebond détruisait
toute VM dont le nom contenait « deep-pve » — une « deep-pve-lab » de
production tombait dedans, et « --purge » emporte les disques. Son tri « du
plus profond au plus haut » comptait les « + » de l'alias, or alias_etage
remplace le « + » du parent par un « - » : chaque alias en portait exactement
UN, le tri ne triait rien, et la destruction partait du plus HAUT — le disque
du parent emportait ses enfants sans qu'on les ait nommés. Il ignorait
« --dry-run », ne lisait aucun code de retour, concluait « ✓ défait », et le
menu le lançait d'une touche.
Il ne détruit plus que ce que le RAPPORT nomme : un couple (parent, VMID) par
étage, du plus profond d'après le niveau lu, égalité stricte du nom, arrêt
CONSTATÉ avant destruction, codes de retour lus, et une confirmation par
« OUI » après la liste.
Six autres constats, tous réels. Le redémarrage se prouve par btime et non par
le seul noyau — rejoué sur un étage déjà installé, on validait un redémarrage
qui n'avait pas eu lieu, exactement le piège corrigé la semaine dernière dans
le suivi. La sonde de disponibilité ne demande plus sudo, sinon un sudo lent
se lisait « jamais joignable en ssh ». Les délais suivent la profondeur : le
script existe pour mesurer un ralentissement de 36x, et un plafond fixe
déclarait échouée une installation qui avançait. L'adresse fixe est contrôlée
AVANT de télécharger une image et de démarrer une VM. Le DNS de l'hôte suit la
spec, sinon apt meurt sans rien expliquer. Et l'essai à blanc ne prétend plus
avoir atteint quoi que ce soit — son rapport était indiscernable d'une
réussite, JSON compris.
L'algorithme aussi : profondeur 0 rendait un plan d'UN étage, donc
« --depth 0 » créait une VM ; et sur un hôte de quatre cœurs le premier étage
recevait UN vCPU quand son invité en recevait deux — un parent plus étroit que
son enfant.
Les tests mordent, prouvé par mutation : remplacer le calcul du premier étage
par la valeur imbriquée les laissait verts.
--- EN ---
Attacked by three lenses on the written code, before running it for real. Two
faults alone justified the exercise.
It would NEVER have worked. The installer was run by "sh", yet it carries "set
-euo pipefail" and a bash shebang: on Debian /bin/sh is dash, which answers
"set: Illegal option -o pipefail" and exits on the FIRST line. Every level
would have failed at install, every time.
And "--detruire" could take a stranger's machine. It took every ssh entry
whose name CONTAINED "deep-pve", then on its jump host destroyed every VM
whose name contained "deep-pve" — a production "deep-pve-lab" fell in, and
"--purge" takes the disks. Its "deepest first" sort counted the "+" in the
alias, yet alias_etage replaces the parent's "+" with a "-": every alias had
exactly ONE, the sort sorted nothing, and destruction started from the TOP —
the parent's disk took its children with it, unnamed. It ignored "--dry-run",
read no return code, concluded "✓ done", and the menu fired it on one key.
It now destroys only what the REPORT names: a (parent, VMID) pair per level,
deepest first by the recorded level, strict name equality, shutdown VERIFIED
before destruction, return codes read, and a "OUI" confirmation after the
list.
Six more findings, all real. The reboot is proven by btime, not by the kernel
alone — replayed on an already-installed level, we validated a reboot that
never happened, exactly the trap fixed last week in the monitor. The liveness
probe no longer asks for sudo, or a slow sudo read as "never reachable by
ssh". Timeouts follow the depth: the script exists to measure a 36x slowdown,
and a fixed ceiling declared failed an install that was progressing. The
static address is checked BEFORE downloading an image and starting a VM. The
host's DNS follows the spec, or apt dies explaining nothing. And the dry run
no longer claims to have reached anything — its report was indistinguishable
from a success, JSON included.
The algorithm too: depth 0 returned a ONE-level plan, so "--depth 0" created a
VM; and on a four-core host the first level got ONE vCPU while its guest got
two — a parent narrower than its child.
The tests bite, proven by mutation: replacing the first level's computation
with the nested value left them green.
Assisted-by: Claude Opus 5
(cherry picked from commit 64b8e5063bd7f420cdeb27b88f94043190b5ecd4)
La profondeur d'imbrication praticable ne se déduit pas, elle se mesure. Une
mesure à la main a trouvé, au quatrième étage, un invité 36 fois plus lent que
le temps réel — 583 secondes d'horloge pour 16 secondes de temps invité,
chaque ligne d'ACPI prenant une seconde — puis un noyau gelé au MÊME octet
quelles que soient les ressources. Un chiffre obtenu une fois, sur une
machine, n'est pas un chiffre.
D'où trois choses.
L'algorithme, en fonctions pures. Deux ressources s'épuisent en descendant :
la mémoire, chaque étage gardant de quoi faire tourner ses propres démons, et
le disque, celui de l'enfant vivant DANS celui du parent. Une troisième se
dégrade, et elle borne le vCPU à deux au-delà du premier étage : douze ont
gelé le noyau invité, les mêmes deux avançaient. La mémoire n'est PAS bornée —
la même VM gelait au même octet avec 9 Go et avec 2 Go, donc la rogner ne
gagnerait rien et priverait l'étage du dessous. Le plan est annoncé avant
toute création, et jamais au-delà de ce qui tient.
Le garde-fou dans l'écran. Il lisait la capacité de l'HÔTE et l'offrait en
entier : sur un troisième étage à 14 cœurs, il a proposé 12 vCPU à une VM qui
n'a jamais démarré. Le nombre n'était pas absurde pour la machine ; il l'était
pour sa profondeur, que l'écran ignorait. Elle se compte maintenant sur la
chaîne de ProxyJump — un rebond par étage, et c'est nous qui écrivons ces
entrées.
Le test long, dans LongTest/ et non dans test/ : le lanceur unitaire doit
rester lançable en quelques secondes, partout, y compris sans virtualisation.
La descente est uniforme — créer, attendre le ssh, installer, redémarrer et
vérifier le noyau, remettre pmxcfs debout, contrôler le stockage — et s'arrête
au premier étage qui échoue en NOMMANT l'étape. Il envoie notre
install_proxmox.sh par scp plutôt que de laisser la VM cloner le dépôt : c'est
notre code qu'on éprouve, et un correctif absent du distant a fait revenir le
même défaut sur trois VM.
--- EN ---
The practicable nesting depth cannot be deduced, only measured. A manual
measurement found, at the fourth level, a guest 36 times slower than real time
— 583 seconds of wall clock for 16 seconds of guest time, each ACPI line
taking a second — then a kernel frozen at the SAME byte whatever the
resources. A number obtained once, on one machine, is not a number.
Hence three things.
The algorithm, in pure functions. Two resources run out going down: memory,
each level keeping what its own daemons need, and disk, the child's living
INSIDE the parent's. A third degrades, and it caps the vCPU at two beyond the
first level: twelve froze the guest kernel, the same two progressed. Memory is
NOT capped — the same VM froze at the same byte with 9 GB and with 2 GB, so
trimming it would gain nothing and starve the level below. The plan is
announced before anything is created, and never beyond what fits.
The guard in the screen. It read the HOST's capacity and offered all of it: on
a third level with 14 cores it proposed 12 vCPU to a VM that never booted. The
number was not absurd for the machine; it was for its depth, which the screen
did not know. It is now counted on the ProxyJump chain — one hop per level,
and we are the ones writing those entries.
The long test, in LongTest/ and not test/: the unit runner must stay runnable
in seconds, anywhere, including without virtualisation. The descent is uniform
— create, wait for ssh, install, reboot and check the kernel, bring pmxcfs
back, check the storage — and stops at the first level that fails, NAMING the
step. It sends our install_proxmox.sh over scp instead of letting the VM clone
the repository: it is our code being exercised, and a fix absent from the
remote made the same defect return on three VMs.
Assisted-by: Claude Opus 5
(cherry picked from commit 4f70c461330cac6f46783a60e0f33052a979fa23)