[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:
parent
032556544e
commit
5af6c94c0a
11 changed files with 558 additions and 5 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
|
|
|||
Loading…
Reference in a new issue