Trois mesures faites sur une descente à cinq étages, et une conclusion de ma
part corrigée par le contre-essai.
Le bail DHCP se fait attendre. L'étage 3 était créé, en type='kvm', et
« domifaddr » ne rendait rien : l'invité n'avait pas encore demandé son
adresse — 87 s puis 94 s selon les tours, quand deploy_qemu s'accorde 90 s et
rend 0 sans l'avoir trouvée. Lu une fois, cela ne prouvait rien.
Un redémarrage demandé à l'invité peut le laisser bloqué dans son
micrologiciel : RIP immobile 46 minutes, pas un octet lu, trois vCPU à fond.
J'ai d'abord conclu que c'était la taille de la mémoire, parce que la même
machine à 2 Go démarrait. Le contre-essai à 4 Go l'a réfuté : elle démarre
aussi, à froid. La différence est le REDÉMARRAGE, pas la mémoire — à chaud
elle reste dans l'UEFI, à froid elle charge son noyau en 60 à 90 s, à 2, 3 et
4 Go. La descente rallume donc une fois par le parent, et une seule : une
boucle de rallumage cacherait un vrai échec.
La mémoire de la pile QEMU est doublée pour une autre raison, mesurée elle
aussi : l'étage 2 avec 5 Go hébergeait un invité de 4 Go et n'avait plus que
127 Mo de libre. Ce n'est pas le plancher qui compte, c'est l'écart.
--- EN ---
Three measurements from a five-level descent, and a conclusion of mine
refuted by the counter-test.
The DHCP lease takes its time. Level 3 was created, type='kvm', and
"domifaddr" returned nothing: the guest had not yet asked for its address —
87 s then 94 s depending on the run, while deploy_qemu allows itself 90 s and
returns 0 without having found it. Read once, that proved nothing.
A reboot asked of the guest can leave it stuck in its firmware: static RIP for
46 minutes, not a byte read, three vCPU at full tilt. I first concluded it was
the memory size, because the same machine booted at 2 GB. The counter-test at
4 GB refuted it: it boots too, cold. The difference is the REBOOT, not the
memory — warm it stays in UEFI, cold it loads its kernel in 60 to 90 s, at 2,
3 and 4 GB. The descent therefore power-cycles once through the parent, and
only once: a restart loop would hide a real failure.
The QEMU stack's memory is doubled for another, also measured reason: level 2
with 5 GB hosted a 4 GB guest and had 127 MB left. It is not the floor that
matters, it is the gap.
Assisted-by: claude-opus-5
(cherry picked from commit e3f60e3ddf066eb76d434bbfe6b01f2271fb1874)
Trois défauts trouvés en une heure par le premier lancement réel — c'est ce
qu'un test d'intégration doit produire.
1. « --setup-host » a échoué en ZÉRO seconde sur « Unable to locate package
qemu-system-x86 », alors que le paquet existe : la VM venait de démarrer et
ses listes ne portaient que bookworm-security. Le message envoyait chercher
des paquets, pas des listes. Même parade qu'install_proxmox.sh — arrêter
apt-daily, puis réessayer.
2. Le réseau « default » de libvirt sert 192.168.122.0/24 à TOUS les étages.
L'étage 2, dont l'adresse VENAIT de ce réseau, voyait son propre net-start
refusé : « Network is already in use by interface enp1s0 ». Un invité qui
vit dans un réseau ne peut pas servir le même. Chaque étage prend le sien,
déduit de sa profondeur absolue, et le REDÉFINIT avant de le démarrer.
3. Le mien : l'extraction du moteur avait coupé preparer_systeme sur le
« return False » de sa boucle, sans son « return True ». La fonction rendait
None, donc l'étape échouait SANS RIEN DIRE, et les deux piles étaient
cassées. L'essai à blanc ne pouvait pas le voir — il sort avant. Un test
d'AST interdit désormais qu'une étape retombe sur None.
long_test/ était introuvable hors du menu : une ligne dans CLAUDE.md, trois
entrées au CHANGELOG, deux sections au README.
--- EN ---
Three defects found in one hour by the first real run — which is what an
integration test is for.
1. "--setup-host" failed in ZERO seconds on "Unable to locate package
qemu-system-x86" though the package exists: the VM had just booted and its
lists carried only bookworm-security. The message sent us looking for
packages, not for lists. Same remedy as install_proxmox.sh — stop
apt-daily, then retry.
2. libvirt's "default" network serves 192.168.122.0/24 at EVERY level. Level 2,
whose own address CAME from that network, had its net-start refused:
"Network is already in use by interface enp1s0". A guest living inside a
network cannot serve the same one. Each level takes its own, derived from
its absolute depth, and REDEFINES it before starting it.
3. Mine: extracting the engine had cut preparer_systeme at its loop's "return
False", without the final "return True". The function returned None, so the
step failed SAYING NOTHING, and both stacks were broken. The dry run could
not see it — it exits earlier. An AST test now forbids a step from falling
through to None.
long_test/ was undiscoverable outside the menu: one line in CLAUDE.md, three
CHANGELOG entries, two README sections.
Assisted-by: claude-opus-5
(cherry picked from commit e46bad408143f7511a04ffdc6a20efdb785f4b5e)
Créer une VM de tête pour héberger un hyperviseur qu'on possède déjà coûte
cinq minutes ET un étage d'imbrication — donc de la lenteur, puisque c'est
elle qu'on mesure. « --hote » part d'un hôte existant ; le menu le propose
sans le rechercher, l'hôte Proxmox déjà retenu étant lu par _pve_host(ask=False).
Trois conséquences que le code ne tirait pas :
- le plan se dimensionne sur la RACINE, lue par ssh. Le dimensionner sur la
machine locale quand les étages vivent ailleurs annoncerait des étages qui
ne tiennent pas ;
- les délais comptent la profondeur ABSOLUE. Un enfant de niveau 1 posé dans
une racine déjà au troisième étage est en réalité au quatrième, et héritait
de délais quatre fois trop courts — le défaut même que « delai » raconte
avoir corrigé ;
- la racine n'est pas un étage atteint. L'y compter décalait de un le total et
le code de sortie ; elle va dans une clé à part, et jamais « cree ».
« sudo » est DÉDUIT de « id -u » et non supposé, et une racine illisible fait
renoncer au lieu d'inventer une capacité.
Le menu offre les deux piles et défait chacune séparément — elles partagent le
dossier des rapports mais chacune ne connaît que les siens. Un test vérifie que
toute entrée affichée a son branchement : ils sont couplés par position, sans
garde.
80 tests, quatre garde-fous morts sous mutation.
--- EN ---
Creating a head VM to host a hypervisor you already own costs five minutes AND
one level of nesting — that is, slowness, which is the very thing being
measured. "--hote" starts from an existing host; the menu offers it without
searching, reading the already-chosen Proxmox host via _pve_host(ask=False).
Three consequences the code did not draw:
- the plan is sized on the ROOT, read over ssh. Sizing it on the local machine
while the levels live elsewhere would announce levels that do not fit;
- delays count ABSOLUTE depth. A level-1 child placed in a root already at the
third level is really at the fourth, and inherited delays four times too
short — the very defect "delai" recounts having fixed;
- the root is not a level reached. Counting it shifted the total and the exit
code by one; it goes in its own key, and never as "cree".
"sudo" is DEDUCED from "id -u" rather than assumed, and an unreadable root
makes us give up instead of inventing a capacity.
The menu offers both stacks and undoes each separately — they share the report
directory but each knows only its own. A test checks that every displayed entry
has its branch: they are coupled by position, with no guard.
80 tests, four guards die under mutation.
Assisted-by: claude-opus-5
(cherry picked from commit c2ab1a968346458925f55fc95619eb4ebd9d3efa)
Le pendant de deep_proxmox : des QEMU dans des QEMU. Le ralentissement du
quatrième étage vient du PROCESSEUR, mais le coût par étage vient de ce qu'on
installe — un nœud Proxmox pose un noyau, corosync, ceph et une interface web
là où un hôte libvirt pose libvirtd. Les deux mesures ensemble séparent ce qui
tient au matériel de ce qui tient à la pile.
Ce test ne peut pas se contenter de descendre. deploy_qemu.py ne passe jamais
« --cpu host-passthrough » et, quand /dev/kvm manque, il n'échoue PAS : il pose
« --virt-type qemu », avertit sur une ligne et crée une VM entièrement ÉMULÉE —
sept minutes et demie de démarrage, aucun code de retour pour le dire. Sans
garde, la descente mesurerait de la TCG empilée en croyant mesurer de
l'imbrication, et rendrait un chiffre plus flatteur et faux.
Chaque étage doit donc PROUVER : /dev/kvm lisible, « nested » à Y, et le
domaine de l'enfant en type='kvm'. Ce qui n'a pas été lu vaut NON — un
/sys/module absent, c'est un module non chargé, pas une permission.
nesting_plan reçoit ses coûts : les constantes vCPU décrivent la physique de
l'imbrication et valent pour les deux piles, les six nombres qui chiffrent un
Proxmox non. Un étage QEMU demande 2 Go et 20 Go, contre 4 et 25.
26 tests, six garde-fous morts sous mutation.
--- EN ---
The counterpart to deep_proxmox: QEMU inside QEMU. The fourth level's slowdown
comes from the PROCESSOR, but the per-level cost comes from what you install —
a Proxmox node lays down a kernel, corosync, ceph and a web UI where a libvirt
host lays down libvirtd. Together the two measurements separate what is due to
the hardware from what is due to the stack.
This test cannot merely descend. deploy_qemu.py never passes "--cpu
host-passthrough" and, when /dev/kvm is missing, it does NOT fail: it sets
"--virt-type qemu", warns on one line and creates a fully EMULATED VM — seven
and a half minutes to boot, no exit code to say so. Unguarded, the descent
would measure stacked TCG while believing it measured nesting, and return a
more flattering, false number.
Every level must therefore PROVE: /dev/kvm readable, "nested" at Y, and the
child's domain type='kvm'. What was not read counts as NO — an absent
/sys/module means an unloaded module, not a permission problem.
nesting_plan takes its costs: the vCPU constants describe the physics of
nesting and hold for both stacks, the six numbers that price a Proxmox do not.
A QEMU level asks 2 GB and 20 GB against 4 and 25.
26 tests, six guards die under mutation.
Assisted-by: claude-opus-5
(cherry picked from commit 39682cb1ce9648db91261387cae88c40c2a67837)
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)