[FIX] deep_qemu : listes apt, sous-réseau par étage, étape muette

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)
This commit is contained in:
Mathieu Benoit 2026-08-28 03:52:21 -04:00
parent 032556544e
commit 5af6c94c0a
11 changed files with 558 additions and 5 deletions

View file

@ -41,6 +41,9 @@ Recréer l'environnement virtuel, utiliser le guide d'installation depuis l'outi
## Ajouté
<!-- [en] -->
- `long_test/` — tests that create real machines and take hours, kept out of `test/` so the unit runner stays runnable in seconds. `deep_proxmox.py` stacks Proxmox in Proxmox, `deep_qemu.py` stacks QEMU in QEMU, and they share one engine. Measured on 28 cores: three levels cost 34 minutes, the fourth 4 h 20 of boot plus 7 h 18 of install — everything there is 15 to 30 times slower, and that is where the vendors stop documenting nesting. The depth is a parameter and defaults to three, because three works
- deep_qemu proves KVM at every level instead of assuming it: `deploy_qemu.py` never passes `--cpu host-passthrough` and, when /dev/kvm is missing, it does not fail — it sets `--virt-type qemu` and creates a fully EMULATED VM, seven and a half minutes to boot, with no exit code to say so. Unguarded, the descent would measure stacked TCG while believing it measured nesting. Each level must show `/dev/kvm`, `nested=Y` and a child domain in `type='kvm'`; what was not read counts as NO
- Both long tests take `--hote` to start from a machine you already own, rather than creating a head VM to host a hypervisor you have on hand — that costs five minutes AND one level of nesting. The plan is then sized on the ROOT, read over ssh; the delays count ABSOLUTE depth; and the root is never a level reached, never destroyed, and its ~/.ssh/config entry is never removed
- Support Odoo migration database and module with TODO
- Support multi version odoo switch on same workspace
- Script for hardening the installation
@ -122,6 +125,9 @@ Recréer l'environnement virtuel, utiliser le guide d'installation depuis l'outi
<!-- [fr] -->
- `long_test/` — des tests qui créent de vraies machines et durent des heures, tenus hors de `test/` pour que le lanceur unitaire reste lançable en quelques secondes. `deep_proxmox.py` empile des Proxmox dans des Proxmox, `deep_qemu.py` des QEMU dans des QEMU, et les deux partagent un moteur. Mesuré sur 28 cœurs : trois étages coûtent 34 minutes, le quatrième 4 h 20 d'amorçage plus 7 h 18 d'installation — tout y est 15 à 30 fois plus lent, et c'est là que les fabricants cessent de documenter l'imbrication. La profondeur est un paramètre et vaut trois par défaut, parce que trois marche
- deep_qemu PROUVE KVM à chaque étage au lieu de le supposer : `deploy_qemu.py` ne passe jamais `--cpu host-passthrough` et, quand /dev/kvm manque, il n'échoue pas — il pose `--virt-type qemu` et crée une VM entièrement ÉMULÉE, sept minutes et demie de démarrage, sans qu'aucun code de retour ne le dise. Sans garde, la descente mesurerait de la TCG empilée en croyant mesurer de l'imbrication. Chaque étage doit montrer `/dev/kvm`, `nested=Y` et un domaine enfant en `type='kvm'` ; ce qui n'a pas été lu vaut NON
- Les deux tests longs acceptent `--hote` pour partir d'une machine qu'on possède déjà, au lieu de créer une VM de tête pour héberger un hyperviseur qu'on a sous la main — cela coûte cinq minutes ET un étage d'imbrication. Le plan se dimensionne alors sur la RACINE, lue par ssh ; les délais comptent la profondeur ABSOLUE ; et la racine n'est jamais un étage atteint, jamais détruite, et son entrée ~/.ssh/config n'est jamais retirée
- Support de la migration de base de données et de modules Odoo avec TODO
- Support du changement multi-version Odoo sur le même espace de travail
- Script pour le renforcement de la sécurité de l'installation

View file

@ -15,6 +15,9 @@ Recréer l'environnement virtuel, utiliser le guide d'installation depuis l'outi
## Ajouté
- `long_test/` — des tests qui créent de vraies machines et durent des heures, tenus hors de `test/` pour que le lanceur unitaire reste lançable en quelques secondes. `deep_proxmox.py` empile des Proxmox dans des Proxmox, `deep_qemu.py` des QEMU dans des QEMU, et les deux partagent un moteur. Mesuré sur 28 cœurs : trois étages coûtent 34 minutes, le quatrième 4 h 20 d'amorçage plus 7 h 18 d'installation — tout y est 15 à 30 fois plus lent, et c'est là que les fabricants cessent de documenter l'imbrication. La profondeur est un paramètre et vaut trois par défaut, parce que trois marche
- deep_qemu PROUVE KVM à chaque étage au lieu de le supposer : `deploy_qemu.py` ne passe jamais `--cpu host-passthrough` et, quand /dev/kvm manque, il n'échoue pas — il pose `--virt-type qemu` et crée une VM entièrement ÉMULÉE, sept minutes et demie de démarrage, sans qu'aucun code de retour ne le dise. Sans garde, la descente mesurerait de la TCG empilée en croyant mesurer de l'imbrication. Chaque étage doit montrer `/dev/kvm`, `nested=Y` et un domaine enfant en `type='kvm'` ; ce qui n'a pas été lu vaut NON
- Les deux tests longs acceptent `--hote` pour partir d'une machine qu'on possède déjà, au lieu de créer une VM de tête pour héberger un hyperviseur qu'on a sous la main — cela coûte cinq minutes ET un étage d'imbrication. Le plan se dimensionne alors sur la RACINE, lue par ssh ; les délais comptent la profondeur ABSOLUE ; et la racine n'est jamais un étage atteint, jamais détruite, et son entrée ~/.ssh/config n'est jamais retirée
- Support de la migration de base de données et de modules Odoo avec TODO
- Support du changement multi-version Odoo sur le même espace de travail
- Script pour le renforcement de la sécurité de l'installation

View file

@ -15,6 +15,9 @@ Recreating the virtual environment, use installation guide from tool `make`.
## Added
- `long_test/` — tests that create real machines and take hours, kept out of `test/` so the unit runner stays runnable in seconds. `deep_proxmox.py` stacks Proxmox in Proxmox, `deep_qemu.py` stacks QEMU in QEMU, and they share one engine. Measured on 28 cores: three levels cost 34 minutes, the fourth 4 h 20 of boot plus 7 h 18 of install — everything there is 15 to 30 times slower, and that is where the vendors stop documenting nesting. The depth is a parameter and defaults to three, because three works
- deep_qemu proves KVM at every level instead of assuming it: `deploy_qemu.py` never passes `--cpu host-passthrough` and, when /dev/kvm is missing, it does not fail — it sets `--virt-type qemu` and creates a fully EMULATED VM, seven and a half minutes to boot, with no exit code to say so. Unguarded, the descent would measure stacked TCG while believing it measured nesting. Each level must show `/dev/kvm`, `nested=Y` and a child domain in `type='kvm'`; what was not read counts as NO
- Both long tests take `--hote` to start from a machine you already own, rather than creating a head VM to host a hypervisor you have on hand — that costs five minutes AND one level of nesting. The plan is then sized on the ROOT, read over ssh; the delays count ABSOLUTE depth; and the root is never a level reached, never destroyed, and its ~/.ssh/config entry is never removed
- Support Odoo migration database and module with TODO
- Support multi version odoo switch on same workspace
- Script for hardening the installation

View file

@ -25,6 +25,10 @@ Version Odoo par défaut : **18.0** (support officiel ERPLibre 1.6.0)
- Pour les commits : suivre le format `[TYPE] description` (ex: `[FIX]`, `[UPD]`, `[ADD]`, `[REM]`)
- Pour la documentation : modifier les `.base.md`, jamais les `.md` ou `.fr.md` directement
- Outil mmg disponible via `source .venv.erplibre/bin/activate && mmg`
- Les tests qui créent de VRAIES machines vivent dans `long_test/` et non dans
`test/` : le lanceur unitaire balaie `test/test_*.py` et doit rester lançable
en quelques secondes, même sans virtualisation. Ils durent des heures et se
défont par `--detruire` — voir `long_test/README.md`
## Core Principles

View file

@ -132,6 +132,61 @@ The plan is printed **before** anything is created, and the script never
promises a depth it knows will not fit — better to announce six levels and
reach six than to promise ten and die at the seventh without knowing why.
## deep_qemu.py — how deep does QEMU-in-QEMU go?
The same descent, a different stack — and the pair is the point. The fourth
level's slowdown comes from the **processor**: what a VM exit costs under
nested paging. The per-level *cost*, though, comes from what you install. A
Proxmox node lays down a kernel, corosync, ceph and a web UI; a libvirt host
lays down `libvirtd` and `qemu-kvm`. Measured together, the two separate what
is due to the hardware from what is due to the stack — two things the Proxmox
measurement alone confounds.
### What this test must prove before it measures anything
`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, and no
exit code says so.
Unguarded, this script would measure stacked TCG while believing it measured
nesting — and return a more flattering number that means nothing. So every
level must prove, not assume:
* `/dev/kvm` is readable;
* `/sys/module/kvm_amd|kvm_intel/parameters/nested` reads `Y`;
* the child's domain is `<domain type='kvm'>`, checked right after creation.
**What was not read counts as NO.** An absent `/sys/module` file means an
unloaded module, not a permissions problem. A level that fails these stops the
descent instead of prolonging it into the void.
## Starting from a host you already have
Both scripts take `--hote`. 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.
```
./long_test/deep_proxmox.py --hote root@10.0.0.5 # an existing Proxmox
./long_test/deep_qemu.py --hote erplibre@10.0.0.7 # an existing libvirt host
```
Three things follow, and they are not decorative:
* 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;
* the delays count **absolute** depth: a level-1 child placed in a root that
is already at the third level is really at the fourth;
* the root is **never** a level reached, and **never** destroyed. A borrowed
host has no local libvirt UUID, so `--detruire` refuses to fall back on its
name — `virsh undefine --remove-all-storage` erases a disk for good.
The menu offers the host already chosen without searching for it, and undoes
each stack separately: they share the report directory, but each knows only
its own reports.
<!-- [fr] -->
# long_test — des tests qui créent de vraies machines
@ -269,3 +324,60 @@ Le plan est affiché **avant** que quoi que ce soit ne soit créé, et le script
ne promet jamais une profondeur qu'il sait irréalisable — mieux vaut annoncer
six étages et en réussir six que d'en promettre dix et mourir au septième sans
savoir pourquoi.
## deep_qemu.py — jusqu'à quel étage une QEMU dans une QEMU tient-elle ?
La même descente, une autre pile — et c'est le couple qui compte. Le
ralentissement du quatrième étage vient du **processeur** : de ce que coûte une
sortie de VM sous pagination imbriquée. Le *coût* par étage, lui, vient de ce
qu'on installe. Un nœud Proxmox pose un noyau, corosync, ceph et une interface
web ; un hôte libvirt pose `libvirtd` et `qemu-kvm`. Mesurées ensemble, les
deux séparent ce qui tient au matériel de ce qui tient à la pile — deux choses
que la seule mesure Proxmox confond.
### Ce que ce test doit prouver avant de mesurer quoi que ce soit
`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, et aucun code de retour ne le dit.
Sans garde, ce script mesurerait de la TCG empilée en croyant mesurer de
l'imbrication — et rendrait un chiffre plus flatteur qui ne veut rien dire.
Chaque étage doit donc prouver, et non supposer :
* `/dev/kvm` est lisible ;
* `/sys/module/kvm_amd|kvm_intel/parameters/nested` vaut `Y` ;
* le domaine de l'enfant est `<domain type='kvm'>`, vérifié juste après sa
création.
**Ce qui n'a pas été lu vaut NON.** Un fichier `/sys/module` absent, c'est un
module non chargé, pas un problème de permission. Un étage qui échoue à cela
arrête la descente au lieu de la prolonger dans le vide.
## Partir d'un hôte qu'on possède déjà
Les deux scripts acceptent `--hote`. Créer une VM de tête pour héberger un
hyperviseur qu'on a sous la main coûte cinq minutes *et* un étage
d'imbrication — donc de la lenteur, puisque c'est justement elle qu'on mesure.
```
./long_test/deep_proxmox.py --hote root@10.0.0.5 # un Proxmox existant
./long_test/deep_qemu.py --hote erplibre@10.0.0.7 # un hôte libvirt existant
```
Trois choses en découlent, et elles ne sont pas décoratives :
* 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 ;
* la racine n'est **jamais** un étage atteint, et **jamais** détruite. Un hôte
emprunté n'a pas d'UUID libvirt local, donc `--detruire` refuse de se rabattre
sur son nom — `virsh undefine --remove-all-storage` efface un disque pour de
bon.
Le menu propose l'hôte déjà retenu sans le rechercher, et défait chaque pile
séparément : elles partagent le dossier des rapports, mais chacune ne connaît
que les siens.

View file

@ -134,4 +134,61 @@ mêmes 32 Mio, c'est-à-dire simplement la taille des fichiers d'amorçage.
Le plan est affiché **avant** que quoi que ce soit ne soit créé, et le script
ne promet jamais une profondeur qu'il sait irréalisable — mieux vaut annoncer
six étages et en réussir six que d'en promettre dix et mourir au septième sans
savoir pourquoi.
savoir pourquoi.
## deep_qemu.py — jusqu'à quel étage une QEMU dans une QEMU tient-elle ?
La même descente, une autre pile — et c'est le couple qui compte. Le
ralentissement du quatrième étage vient du **processeur** : de ce que coûte une
sortie de VM sous pagination imbriquée. Le *coût* par étage, lui, vient de ce
qu'on installe. Un nœud Proxmox pose un noyau, corosync, ceph et une interface
web ; un hôte libvirt pose `libvirtd` et `qemu-kvm`. Mesurées ensemble, les
deux séparent ce qui tient au matériel de ce qui tient à la pile — deux choses
que la seule mesure Proxmox confond.
### Ce que ce test doit prouver avant de mesurer quoi que ce soit
`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, et aucun code de retour ne le dit.
Sans garde, ce script mesurerait de la TCG empilée en croyant mesurer de
l'imbrication — et rendrait un chiffre plus flatteur qui ne veut rien dire.
Chaque étage doit donc prouver, et non supposer :
* `/dev/kvm` est lisible ;
* `/sys/module/kvm_amd|kvm_intel/parameters/nested` vaut `Y` ;
* le domaine de l'enfant est `<domain type='kvm'>`, vérifié juste après sa
création.
**Ce qui n'a pas été lu vaut NON.** Un fichier `/sys/module` absent, c'est un
module non chargé, pas un problème de permission. Un étage qui échoue à cela
arrête la descente au lieu de la prolonger dans le vide.
## Partir d'un hôte qu'on possède déjà
Les deux scripts acceptent `--hote`. Créer une VM de tête pour héberger un
hyperviseur qu'on a sous la main coûte cinq minutes *et* un étage
d'imbrication — donc de la lenteur, puisque c'est justement elle qu'on mesure.
```
./long_test/deep_proxmox.py --hote root@10.0.0.5 # un Proxmox existant
./long_test/deep_qemu.py --hote erplibre@10.0.0.7 # un hôte libvirt existant
```
Trois choses en découlent, et elles ne sont pas décoratives :
* 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 ;
* la racine n'est **jamais** un étage atteint, et **jamais** détruite. Un hôte
emprunté n'a pas d'UUID libvirt local, donc `--detruire` refuse de se rabattre
sur son nom — `virsh undefine --remove-all-storage` efface un disque pour de
bon.
Le menu propose l'hôte déjà retenu sans le rechercher, et défait chaque pile
séparément : elles partagent le dossier des rapports, mais chacune ne connaît
que les siens.

View file

@ -126,3 +126,58 @@ size of the boot files.
The plan is printed **before** anything is created, and the script never
promises a depth it knows will not fit — better to announce six levels and
reach six than to promise ten and die at the seventh without knowing why.
## deep_qemu.py — how deep does QEMU-in-QEMU go?
The same descent, a different stack — and the pair is the point. The fourth
level's slowdown comes from the **processor**: what a VM exit costs under
nested paging. The per-level *cost*, though, comes from what you install. A
Proxmox node lays down a kernel, corosync, ceph and a web UI; a libvirt host
lays down `libvirtd` and `qemu-kvm`. Measured together, the two separate what
is due to the hardware from what is due to the stack — two things the Proxmox
measurement alone confounds.
### What this test must prove before it measures anything
`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, and no
exit code says so.
Unguarded, this script would measure stacked TCG while believing it measured
nesting — and return a more flattering number that means nothing. So every
level must prove, not assume:
* `/dev/kvm` is readable;
* `/sys/module/kvm_amd|kvm_intel/parameters/nested` reads `Y`;
* the child's domain is `<domain type='kvm'>`, checked right after creation.
**What was not read counts as NO.** An absent `/sys/module` file means an
unloaded module, not a permissions problem. A level that fails these stops the
descent instead of prolonging it into the void.
## Starting from a host you already have
Both scripts take `--hote`. 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.
```
./long_test/deep_proxmox.py --hote root@10.0.0.5 # an existing Proxmox
./long_test/deep_qemu.py --hote erplibre@10.0.0.7 # an existing libvirt host
```
Three things follow, and they are not decorative:
* 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;
* the delays count **absolute** depth: a level-1 child placed in a root that
is already at the third level is really at the fourth;
* the root is **never** a level reached, and **never** destroyed. A borrowed
host has no local libvirt UUID, so `--detruire` refuses to fall back on its
name — `virsh undefine --remove-all-storage` erases a disk for good.
The menu offers the host already chosen without searching for it, and undoes
each stack separately: they share the report directory, but each knows only
its own reports.

View file

@ -89,6 +89,60 @@ CONTROLE_CMD = (
" | tail -1 | sed s/^/DISQUE=/"
)
# Les listes apt AVANT toute installation. Constaté au premier lancement
# réel : « --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, ses listes ne portaient que « bookworm-security », et un
# apt-get update les a complétées d'un coup.
#
# Deux causes, une seule parade : cloud-init n'a pas fini de composer
# /etc/apt, et apt-daily tient le verrou des listes au premier démarrage.
# install_proxmox.sh a la même parade, et pour la même raison — arrêter les
# minuteries, puis réessayer.
PREPARE_APT_CMD = (
"systemctl stop apt-daily.service apt-daily-upgrade.service"
" apt-daily.timer apt-daily-upgrade.timer >/dev/null 2>&1;"
" i=1; while [ $i -le 12 ]; do"
" DEBIAN_FRONTEND=noninteractive apt-get update && exit 0;"
" echo APT-RETRY=$i; sleep 15; i=$((i+1)); done; exit 1"
)
# Le réseau « default » de libvirt sert 192.168.122.0/24, à TOUS les étages.
# Constaté au premier essai réel : l'étage 2, dont l'adresse était
# 192.168.122.45 — servie par le « default » de son parent — a vu son propre
# « net-start default » refusé net :
#
# error: internal error: 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
# reçoit donc son propre sous-réseau, déduit de sa PROFONDEUR : deux étages ne
# peuvent pas tomber sur le même, et rien n'est à deviner.
#
# 131 et au-delà : 122 est celui de libvirt et 123 celui de la machine où ce
# test a été écrit. Les éviter tous les deux coûte un octet.
RESEAU_BASE = 131
def cidr_pour(profondeur):
"""Le troisième octet du sous-réseau d'un étage. Déterminé, jamais tiré."""
return f"192.168.{RESEAU_BASE + max(0, int(profondeur) - 1)}"
def reseau_xml(prefixe, nom="default"):
"""Le réseau NAT d'un étage, en une ligne — pas de heredoc.
Une seule ligne parce qu'elle traverse deux couches de quoting pour
atterrir dans dash : un heredoc n'y survivrait pas.
"""
return (
f"<network><name>{nom}</name><forward mode='nat'/>"
f"<bridge name='virbr0' stp='on' delay='0'/>"
f"<ip address='{prefixe}.1' netmask='255.255.255.0'>"
f"<dhcp><range start='{prefixe}.10' end='{prefixe}.200'/></dhcp>"
f"</ip></network>"
)
RESEAU_CMD = (
"virsh -c qemu:///system net-info default 2>&1 | sed s/^/NET:/; "
"systemctl is-active libvirtd 2>/dev/null | sed s/^/UNITE:/"
@ -208,6 +262,18 @@ class Descente(descente.Descente):
return True
if not self._envoyer_cli(hote):
return False
# Les listes d'abord. Sans elles, « --setup-host » échoue en zéro
# seconde sur des paquets qui existent — et le message parle de
# paquets introuvables, pas de listes vides.
code, _o = self.executer(
hote,
PREPARE_APT_CMD,
self.delai("install"),
"apt-get update",
)
if code:
self.dire(" ✗ listes apt : le verrou reste tenu")
return False
code, _o = self.executer(
hote,
f"python3 {DISTANT_CLI} --setup-host --assume-yes",
@ -234,16 +300,23 @@ class Descente(descente.Descente):
if self.dry_run:
print(" libvirtd + réseau default")
return True
# Idempotent : sur un hôte déjà en ordre, les deux commandes ne font
# rien et rendent 0.
# Son sous-réseau à LUI, sinon « net-start default » se heurte à
# l'adresse que son parent lui a servie.
profondeur = self.profondeur_racine + max(1, self.niveau_courant)
xml = reseau_xml(cidr_pour(profondeur))
self.executer(
hote,
"systemctl enable --now libvirtd 2>/dev/null;"
" virsh -c qemu:///system net-start default 2>/dev/null;"
" virsh -c qemu:///system net-autostart default 2>/dev/null; true",
" virsh -c qemu:///system net-destroy default 2>/dev/null;"
" virsh -c qemu:///system net-undefine default 2>/dev/null;"
f" printf '%s' {shlex.quote(xml)} > /tmp/reseau.xml;"
" virsh -c qemu:///system net-define /tmp/reseau.xml;"
" virsh -c qemu:///system net-start default;"
" virsh -c qemu:///system net-autostart default; true",
self.delai("reparation"),
"libvirtd",
)
self.dire(f" réseau {cidr_pour(profondeur)}.0/24")
_c, out = self.executer(
hote, RESEAU_CMD, self.delai("controle"), "réseau"
)

View file

@ -409,6 +409,11 @@ class Descente:
if code or "-KO" in pve.strip_ssh_noise(sortie):
self.dire(f" ✗ {etiquette}")
return False
# Ce « return True » manquait, et l'étape échouait donc SANS RIEN
# DIRE : « ✗ étage 1 systeme » et pas une ligne de cause. Une fonction
# qui rend None là où l'appelant attend un booléen ne ment pas à
# moitié — elle dit « non ».
return True
def creer_etage1(self, res):
"""Une VM locale, par la CLI QEMU/KVM. Le seul étage sur du métal."""

View file

@ -110,6 +110,153 @@ class TestUnEtageQuiNeSaitPasHeberger(unittest.TestCase):
self.assertNotIn(interdit, deep_qemu.CONTROLE_CMD, interdit)
class TestLesListesAptAvantToute(unittest.TestCase):
"""Constaté au premier lancement réel : « --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, ses listes ne portaient que
« bookworm-security », et un apt-get update les a complétées d'un coup.
Le message parlait de paquets introuvables, pas de listes vides : c'est
exactement le genre de diagnostic qui envoie chercher au mauvais endroit.
"""
def setUp(self):
self.d = deep_qemu.Descente.__new__(deep_qemu.Descente)
self.d.dry_run = False
self.d.journal = None
self.d.niveau_courant = 1
self.d._envoyer_cli = lambda hote: True
self.faits = []
def _repond(self, code_apt=0):
def executer(hote, cmd, delai, etiquette="", **k):
self.faits.append(etiquette)
if etiquette == "apt-get update":
return code_apt, ""
return 0, ""
self.d.executer = executer
def test_the_lists_are_refreshed_before_the_install(self):
self._repond()
with contextlib.redirect_stdout(io.StringIO()):
self.assertTrue(self.d.installer({"target": "h"}))
self.assertEqual(
self.faits, ["apt-get update", "deploy_qemu --setup-host"]
)
def test_an_apt_lock_that_never_lets_go_stops_the_level(self):
"""Installer sur des listes vides donnerait « paquet introuvable » —
un diagnostic qui envoie chercher au mauvais endroit."""
self._repond(code_apt=1)
with contextlib.redirect_stdout(io.StringIO()) as sortie:
self.assertFalse(self.d.installer({"target": "h"}))
self.assertIn("verrou reste tenu", sortie.getvalue())
self.assertNotIn("deploy_qemu --setup-host", self.faits)
def test_the_daily_timers_are_stopped_first(self):
"""apt-daily tient le verrou des listes au premier démarrage.
install_proxmox.sh a la même parade, et pour la même raison."""
self.assertIn("apt-daily.timer", deep_qemu.PREPARE_APT_CMD)
self.assertIn("apt-daily.service", deep_qemu.PREPARE_APT_CMD)
def test_it_retries_rather_than_giving_up_at_once(self):
self.assertIn("while", deep_qemu.PREPARE_APT_CMD)
self.assertIn("sleep", deep_qemu.PREPARE_APT_CMD)
def test_the_apt_probe_is_written_for_dash(self):
for interdit in ("[[", "pipefail", "seq "):
self.assertNotIn(interdit, deep_qemu.PREPARE_APT_CMD, interdit)
class TestChaqueEtageSonSousReseau(unittest.TestCase):
"""Le « default » de libvirt sert 192.168.122.0/24 à TOUS les étages.
Constaté au premier essai réel : l'étage 2, dont l'adresse était
192.168.122.45 — servie par le « default » de son parent — a vu son propre
« net-start default » refusé net :
error: internal error: Network is already in use by interface enp1s0
Un invité qui vit DANS un réseau ne peut pas servir le même."""
def test_two_levels_never_share_a_subnet(self):
vus = [deep_qemu.cidr_pour(p) for p in range(1, 11)]
self.assertEqual(len(set(vus)), 10, vus)
def test_it_avoids_libvirts_own_and_the_hosts(self):
"""122 est celui de libvirt, 123 celui de la machine où ce test a été
écrit : tomber sur l'un ou l'autre recréerait la collision."""
for profondeur in range(1, 11):
prefixe = deep_qemu.cidr_pour(profondeur)
self.assertNotIn(prefixe, ("192.168.122", "192.168.123"))
def test_the_subnet_is_derived_not_drawn(self):
# Deux appels pour la même profondeur donnent le même : rien de tiré
# au hasard, sinon --detruire et le diagnostic ne se retrouveraient pas.
self.assertEqual(deep_qemu.cidr_pour(3), deep_qemu.cidr_pour(3))
def test_a_depth_of_zero_or_less_still_gives_a_subnet(self):
for profondeur in (0, -1):
self.assertTrue(deep_qemu.cidr_pour(profondeur).startswith("192."))
def test_the_network_xml_is_one_line(self):
"""Elle traverse deux couches de quoting pour atterrir dans dash : un
heredoc n'y survivrait pas."""
xml = deep_qemu.reseau_xml("192.168.131")
self.assertNotIn("\n", xml)
self.assertIn("<name>default</name>", xml)
self.assertIn("192.168.131.1", xml)
self.assertIn("mode='nat'", xml)
def test_the_dhcp_range_lives_in_its_own_subnet(self):
xml = deep_qemu.reseau_xml("192.168.137")
self.assertIn("start='192.168.137.10'", xml)
self.assertIn("end='192.168.137.200'", xml)
# Et la passerelle n'est pas dans la plage servie.
self.assertIn("address='192.168.137.1'", xml)
def test_the_level_redefines_before_starting(self):
"""« net-start » sur un réseau dont le sous-réseau collisionne échoue :
il faut le REDÉFINIR, pas seulement le démarrer."""
faits = []
d = deep_qemu.Descente.__new__(deep_qemu.Descente)
d.dry_run = False
d.journal = None
d.niveau_courant = 2
d.profondeur_racine = 0
def executer(hote, cmd, delai, etiquette="", **k):
faits.append(cmd)
return 0, "NET: Active: yes\nUNITE:active\n"
d.executer = executer
with contextlib.redirect_stdout(io.StringIO()) as sortie:
self.assertTrue(d.remettre_debout({"target": "h"}))
pose = faits[0]
self.assertLess(pose.index("net-undefine"), pose.index("net-define"))
self.assertLess(pose.index("net-define"), pose.index("net-start"))
# Le sous-réseau de CET étage, pas un autre.
self.assertIn("192.168.132", pose)
self.assertIn("192.168.132.0/24", sortie.getvalue())
def test_a_borrowed_root_shifts_every_subnet(self):
"""Partir d'une racine déjà au troisième étage : le premier enfant est
au quatrième, et doit prendre le sous-réseau du quatrième."""
faits = []
d = deep_qemu.Descente.__new__(deep_qemu.Descente)
d.dry_run = False
d.journal = None
d.niveau_courant = 1
d.profondeur_racine = 3
d.executer = lambda h, c, delai, e="", **k: (
faits.append(c) or (0, "NET: Active: yes\nUNITE:active\n")
)
with contextlib.redirect_stdout(io.StringIO()):
d.remettre_debout({"target": "h"})
self.assertIn(deep_qemu.cidr_pour(4), faits[0])
class TestLeControleArreteLaDescente(unittest.TestCase):
"""Un étage sans KVM ne casse pas : il bascule en émulation et continue.
C'est ce silence-là que le contrôle doit rompre."""

View file

@ -1142,6 +1142,94 @@ class TestLeMenuDesDeuxTests(unittest.TestCase):
self.assertEqual(args, " --hote erplibre@10.0.0.7")
class TestAucuneEtapeNeRepondNone(unittest.TestCase):
"""Une étape qui rend None dit « non » à l'appelant, sans dire pourquoi.
Vécu : l'extraction du moteur avait coupé `preparer_systeme` sur le
« return False » de sa boucle, sans son « return True » final. La descente
affichait « ✗ étage 1 systeme » et pas une ligne de cause — et l'essai à
blanc ne pouvait pas le voir, puisqu'il sort avant. Les deux piles étaient
cassées, aucun test ne l'a vu."""
CROCHETS = (
"preparer_parent",
"creer_enfant",
"installer",
"noyau_convient",
"remettre_debout",
"controler",
"preparer_systeme",
"redemarrer_et_verifier",
)
def test_no_step_can_fall_through_to_none(self):
"""Par l'AST, sur les trois fichiers : le dernier énoncé du corps d'une
étape doit être un return ou un raise, jamais une boucle ou un if dont
on peut sortir."""
import ast
chutes = []
for chemin in (
"long_test/descente.py",
"long_test/deep_proxmox.py",
"long_test/deep_qemu.py",
):
arbre = ast.parse(
open(os.path.join(RACINE, chemin), encoding="utf-8").read()
)
for noeud in ast.walk(arbre):
if (
isinstance(noeud, ast.FunctionDef)
and noeud.name in self.CROCHETS
):
dernier = noeud.body[-1]
if isinstance(
dernier, (ast.For, ast.While, ast.If, ast.Try)
):
chutes.append(f"{chemin}:{noeud.lineno} {noeud.name}")
self.assertEqual(chutes, [])
def test_the_real_path_of_preparer_systeme_answers_true(self):
"""Éprouvé sur le vrai chemin, pas seulement à blanc : c'est l'essai à
blanc qui masquait le défaut, en sortant avant."""
d = moteur.Descente.__new__(moteur.Descente)
d.dry_run = False
d.journal = None
d.niveau_courant = 1
d.executer = lambda h, c, delai, etiquette="", **k: (0, "OK")
vrai = moteur.pve.run
moteur.pve.run = lambda h, r, t=120: (0, "10.0.0.2 22 10.0.0.1 22")
self.addCleanup(setattr, moteur.pve, "run", vrai)
with contextlib.redirect_stdout(io.StringIO()):
self.assertIs(d.preparer_systeme({"target": "h"}), True)
def test_a_failing_repair_answers_false_and_says_which(self):
d = moteur.Descente.__new__(moteur.Descente)
d.dry_run = False
d.journal = None
d.niveau_courant = 1
d.executer = lambda h, c, delai, etiquette="", **k: (0, "hosts-KO")
vrai = moteur.pve.run
moteur.pve.run = lambda h, r, t=120: (0, "10.0.0.2 22 10.0.0.1 22")
self.addCleanup(setattr, moteur.pve, "run", vrai)
with contextlib.redirect_stdout(io.StringIO()) as sortie:
self.assertIs(d.preparer_systeme({"target": "h"}), False)
# Et il DIT laquelle : un « ✗ » sans cause envoie chercher partout.
self.assertIn("gel cloud-init", sortie.getvalue())
def test_an_unknown_access_address_is_named(self):
d = moteur.Descente.__new__(moteur.Descente)
d.dry_run = False
d.journal = None
d.niveau_courant = 1
vrai = moteur.pve.run
moteur.pve.run = lambda h, r, t=120: (0, "")
self.addCleanup(setattr, moteur.pve, "run", vrai)
with contextlib.redirect_stdout(io.StringIO()) as sortie:
self.assertFalse(d.preparer_systeme({"target": "h"}))
self.assertIn("adresse d'accès inconnue", sortie.getvalue())
class TestPartirDunHoteExistant(unittest.TestCase):
"""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."""