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)
564 lines
21 KiB
Python
564 lines
21 KiB
Python
#!/usr/bin/env python3
|
|
# © 2026 TechnoLibre (http://www.technolibre.ca)
|
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
|
"""Jusqu'à quel étage une QEMU dans une QEMU tient-elle ?
|
|
|
|
Le pendant de `deep_proxmox.py`, et sa raison d'être : le ralentissement du
|
|
quatrième étage vient du PROCESSEUR — de ce que coûte une sortie de VM sous
|
|
pagination imbriquée — mais 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 nu pose libvirtd et qemu-kvm. Les deux mesures ensemble 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
|
|
|
|
`deploy_qemu.py` ne passe jamais « --cpu host-passthrough » : il s'en remet au
|
|
défaut de virt-install. 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.
|
|
Un étage émulé démarre en sept minutes et demie au lieu de quelques secondes,
|
|
et rien dans le code de retour ne le dit.
|
|
|
|
Sans garde, ce script mesurerait donc de la TCG empilée en croyant mesurer de
|
|
la virtualisation imbriquée — et rendrait un chiffre plus flatteur, et faux.
|
|
D'où le contrôle de chaque étage : /dev/kvm lisible, « nested » à Y, et le
|
|
domaine de l'enfant en type='kvm' avec un CPU host-passthrough. Un étage qui
|
|
échoue à cela arrête la descente au lieu de la prolonger dans le vide.
|
|
|
|
./long_test/deep_qemu.py # trois étages
|
|
./long_test/deep_qemu.py --depth 5 # en demander plus, sciemment
|
|
./long_test/deep_qemu.py --dry-run # le plan, rien de créé
|
|
./long_test/deep_qemu.py --detruire # défaire ce qui a été posé
|
|
"""
|
|
|
|
import os
|
|
import re
|
|
import shlex
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
|
|
RACINE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
sys.path.insert(0, RACINE)
|
|
sys.path.insert(0, os.path.join(RACINE, "long_test"))
|
|
|
|
from script.proxmox import nesting # noqa: E402
|
|
from script.proxmox import proxmox_deploy as pve # noqa: E402
|
|
|
|
import descente # noqa: E402
|
|
from descente import ( # noqa: E402,F401
|
|
DELAIS,
|
|
Famille,
|
|
a_defaire,
|
|
alias_etage as _alias_etage,
|
|
autre_descente,
|
|
capacite_hote,
|
|
cle_publique,
|
|
dernier_rapport,
|
|
descente_vivante,
|
|
detruire,
|
|
detruire_etage1,
|
|
dire,
|
|
identite_de,
|
|
mener,
|
|
module_qemu,
|
|
nom_etage as _nom_etage,
|
|
retirer_alias,
|
|
)
|
|
|
|
# Une Debian nue : c'est elle qui recevra libvirt et qemu-kvm.
|
|
DISTRO = "debian"
|
|
NOM_BASE = "deep-qemu"
|
|
OUTIL = "deep_qemu"
|
|
|
|
# Le script est envoyé par scp, comme install_proxmox.sh l'est pour Proxmox :
|
|
# c'est NOTRE code qu'on veut éprouver, et il n'importe que la bibliothèque
|
|
# standard, donc il tourne dans un invité nu sans rien d'autre.
|
|
LOCAL_CLI = "script/qemu/deploy_qemu.py"
|
|
DISTANT_CLI = "/tmp/deploy_qemu.py"
|
|
CLE_DISTANTE = "/root/.ssh/longtest.pub"
|
|
|
|
# Trois faits, trois lignes, et l'ABSENCE d'une ligne vaut « non ». Écrit pour
|
|
# dash : /bin/sh sur Debian n'est pas bash.
|
|
CONTROLE_CMD = (
|
|
"if [ -r /dev/kvm ]; then echo KVM=oui; else echo KVM=non; fi; "
|
|
"cat /sys/module/kvm_amd/parameters/nested 2>/dev/null"
|
|
" | sed s/^/NESTED=/; "
|
|
"cat /sys/module/kvm_intel/parameters/nested 2>/dev/null"
|
|
" | sed s/^/NESTED=/; "
|
|
"df --output=avail -BG /var/lib/libvirt/images 2>/dev/null"
|
|
" | 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:/"
|
|
)
|
|
|
|
|
|
def nom_etage(niveau):
|
|
return _nom_etage(niveau, NOM_BASE)
|
|
|
|
|
|
def alias_etage(niveau, parent_alias):
|
|
return _alias_etage(niveau, parent_alias, NOM_BASE)
|
|
|
|
|
|
def parse_controle(texte):
|
|
"""Ce que le contrôle a VU. Ce qui n'a pas été lu vaut « non ».
|
|
|
|
L'absence d'une ligne n'est jamais un oui : si
|
|
/sys/module/kvm_amd/parameters/nested n'existe pas, c'est que le module
|
|
n'est pas chargé, et l'étage suivant serait émulé.
|
|
"""
|
|
propre = pve.strip_ssh_noise(texte or "")
|
|
nested = re.search(r"^NESTED=(\S+)", propre, re.M)
|
|
disque = re.search(r"^DISQUE=\s*(\d+)", propre, re.M)
|
|
return {
|
|
"kvm": bool(re.search(r"^KVM=oui\s*$", propre, re.M)),
|
|
# « Y » ou « 1 » selon les versions du module.
|
|
"nested": bool(nested and nested.group(1).strip() in ("Y", "1")),
|
|
"disque_go": int(disque.group(1)) if disque else 0,
|
|
}
|
|
|
|
|
|
def parse_reseau(texte):
|
|
"""Le réseau libvirt « default » est-il actif, et libvirtd debout ?"""
|
|
propre = pve.strip_ssh_noise(texte or "")
|
|
actif = re.search(r"^NET:\s*Active:\s*(\S+)", propre, re.M | re.I)
|
|
unite = re.search(r"^UNITE:(\S+)", propre, re.M)
|
|
return {
|
|
"reseau": bool(actif and actif.group(1).lower() == "yes"),
|
|
"libvirtd": bool(unite and unite.group(1).strip() == "active"),
|
|
}
|
|
|
|
|
|
def parse_domifaddr(texte):
|
|
"""La première adresse IPv4 d'un domaine, sans son masque, ou "".
|
|
|
|
virsh écrit un tableau ; on ne prend que les lignes qui annoncent « ipv4 »,
|
|
et jamais la ligne d'en-tête ni les tirets.
|
|
"""
|
|
for ligne in pve.strip_ssh_noise(texte or "").splitlines():
|
|
champs = ligne.split()
|
|
if len(champs) >= 4 and champs[2].lower() == "ipv4":
|
|
return champs[3].split("/")[0]
|
|
return ""
|
|
|
|
|
|
def parse_domaine(xml):
|
|
"""Le domaine tourne-t-il sous KVM, et son CPU passe-t-il l'hôte ?
|
|
|
|
Les deux comptent, et pour la même raison : un domaine type='qemu' est
|
|
ÉMULÉ, et un CPU qui ne passe pas les drapeaux de l'hôte ne porte pas la
|
|
virtualisation — l'étage suivant serait émulé à son tour, sept minutes et
|
|
demie de démarrage, sans qu'aucun code de retour ne le dise.
|
|
"""
|
|
propre = pve.strip_ssh_noise(xml or "")
|
|
domaine = re.search(r"<domain[^>]*\btype=['\"](\w+)['\"]", propre)
|
|
cpu = re.search(r"<cpu[^>]*\bmode=['\"]([\w-]+)['\"]", propre)
|
|
return {
|
|
"type": domaine.group(1) if domaine else "",
|
|
"cpu": cpu.group(1) if cpu else "",
|
|
}
|
|
|
|
|
|
class Descente(descente.Descente):
|
|
"""Les verbes de QEMU/KVM. Le reste est dans `descente.Descente`."""
|
|
|
|
OUTIL = OUTIL
|
|
NOM_BASE = NOM_BASE
|
|
DISTRO = DISTRO
|
|
|
|
# ------------------------------------------------------------------ #
|
|
def _envoyer_cli(self, hote):
|
|
"""Pose NOTRE deploy_qemu.py sur l'hôte. Rend True s'il y est.
|
|
|
|
Refait à chaque besoin plutôt qu'une fois : /tmp est vidé au
|
|
démarrage sur bien des systèmes, et l'installation redémarre.
|
|
"""
|
|
local = os.path.join(RACINE, LOCAL_CLI)
|
|
if self.dry_run:
|
|
print(f" scp {local} <hôte>:{DISTANT_CLI}")
|
|
return True
|
|
argv = pve.ssh_argv(hote, "")[:-1] # les options, sans la commande
|
|
cible = argv[-1]
|
|
options = argv[1:-1]
|
|
res = subprocess.run(
|
|
["scp", "-q"] + options + [local, f"{cible}:{DISTANT_CLI}"],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=300,
|
|
)
|
|
if res.returncode:
|
|
self.dire(f" ✗ scp : {res.stderr.strip()[:200]}")
|
|
return False
|
|
return True
|
|
|
|
def installer(self, hote):
|
|
"""libvirt et qemu-kvm, par « --setup-host ».
|
|
|
|
Le code de retour ne prouve RIEN ici : « --setup-host » rend 0 même
|
|
quand il s'est contenté de PROGRAMMER un redémarrage. C'est l'étape
|
|
suivante — redémarrer et constater que la machine est revenue — qui
|
|
prouve quelque chose, et le contrôle de fin d'étage qui prouve que KVM
|
|
est là.
|
|
"""
|
|
if self.dry_run:
|
|
print(f" {DISTANT_CLI} --setup-host")
|
|
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",
|
|
self.delai("install"),
|
|
"deploy_qemu --setup-host",
|
|
montrer=True,
|
|
)
|
|
return code == 0
|
|
|
|
def noyau_convient(self, noyau):
|
|
"""Tout noyau convient : c'est le REDÉMARRAGE qui compte, pas ce
|
|
qu'on redémarre.
|
|
|
|
Il charge les modules KVM et applique l'appartenance au groupe
|
|
libvirt, qui ne prend effet qu'à la SESSION SUIVANTE — sans lui,
|
|
virt-install retombe sur qemu:///session, où le réseau « default »
|
|
n'existe pas. Le moteur prouve déjà que la machine a vraiment
|
|
redémarré en comparant l'instant de démarrage.
|
|
"""
|
|
return bool(noyau)
|
|
|
|
def remettre_debout(self, hote):
|
|
"""libvirtd actif et le réseau « default » démarré."""
|
|
if self.dry_run:
|
|
print(" libvirtd + réseau default")
|
|
return True
|
|
# 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-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"
|
|
)
|
|
vu = parse_reseau(out)
|
|
self.dire(
|
|
f" libvirtd {'actif' if vu['libvirtd'] else 'ABSENT'},"
|
|
f" réseau default {'actif' if vu['reseau'] else 'ABSENT'}"
|
|
)
|
|
return vu["libvirtd"] and vu["reseau"]
|
|
|
|
def rallumer_a_froid(self, parent, nom):
|
|
"""« virsh destroy » puis « start » : un processus QEMU neuf.
|
|
|
|
Mesuré sur la machine bloquée : à chaud elle restait 46 minutes au
|
|
même pointeur d'instruction, dans son micrologiciel ; à froid elle a
|
|
chargé son noyau en 60 à 90 secondes, trois fois de suite, à 2, 3 et
|
|
4 Go. Ce n'est donc pas la taille de la mémoire — c'est la façon de
|
|
redémarrer.
|
|
"""
|
|
if self.dry_run or not parent:
|
|
return False
|
|
self.executer(
|
|
parent,
|
|
f"virsh -c qemu:///system destroy {nom} 2>/dev/null; true",
|
|
self.delai("controle"),
|
|
"extinction",
|
|
)
|
|
code, _o = self.executer(
|
|
parent,
|
|
f"virsh -c qemu:///system start {nom}",
|
|
self.delai("controle"),
|
|
"rallumage",
|
|
)
|
|
return code == 0
|
|
|
|
def controler(self, hote):
|
|
"""CET étage peut-il héberger le suivant SANS l'émuler ?
|
|
|
|
Le contrôle qui donne son sens à la mesure. Sans lui, un étage sans
|
|
KVM ne casse pas : il bascule en émulation et continue. La descente
|
|
irait plus « profond » en mesurant tout autre chose — de la TCG
|
|
empilée, pas de la virtualisation imbriquée.
|
|
"""
|
|
if self.dry_run:
|
|
print(" /dev/kvm + nested=Y + place disque")
|
|
return True
|
|
code, out = self.executer(
|
|
hote, CONTROLE_CMD, self.delai("controle"), "kvm"
|
|
)
|
|
if code:
|
|
self.dire(" ✗ contrôle KVM illisible : rien conclu")
|
|
return False
|
|
vu = parse_controle(out)
|
|
self.dire(
|
|
f" /dev/kvm {'oui' if vu['kvm'] else 'NON'},"
|
|
f" nested {'oui' if vu['nested'] else 'NON'},"
|
|
f" {vu['disque_go']} Go libres"
|
|
)
|
|
if not vu["kvm"]:
|
|
self.dire(" ✗ pas de /dev/kvm : l'étage suivant serait ÉMULÉ")
|
|
return False
|
|
if not vu["nested"]:
|
|
self.dire(
|
|
" ✗ virtualisation imbriquée absente :"
|
|
" l'étage suivant serait ÉMULÉ"
|
|
)
|
|
return False
|
|
return True
|
|
|
|
def preparer_parent(self, parent):
|
|
"""Ce qu'il faut du parent : son réseau. C'est tout.
|
|
|
|
Rien à construire, contrairement à Proxmox, où il faut poser un pont
|
|
et un NAT dans /etc/network/interfaces : libvirt fournit déjà
|
|
« default », avec NAT, bail DHCP ET résolveur dnsmasq. Le défaut qui a
|
|
coûté cher là-bas — une VM en adresse fixe qui route mais ne résout
|
|
rien, et une installation qui meurt sur « apt update » sans que rien
|
|
ne l'explique — ne peut pas se produire ici.
|
|
"""
|
|
if self.dry_run:
|
|
print(" réseau default du parent")
|
|
return ("default",)
|
|
code, out = self.executer(
|
|
parent, RESEAU_CMD, self.delai("controle"), "réseau"
|
|
)
|
|
if code:
|
|
self.dire(" ✗ état du réseau illisible : rien conclu")
|
|
return None
|
|
vu = parse_reseau(out)
|
|
if not vu["reseau"]:
|
|
self.dire(
|
|
" ✗ le réseau « default » du parent n'est pas actif"
|
|
)
|
|
return None
|
|
return ("default",)
|
|
|
|
def attendre_adresse(self, parent, nom):
|
|
"""Le bail DHCP de l'enfant, attendu. Rend l'adresse, ou "".
|
|
|
|
ATTENDU, et non lu une fois. Constaté au troisième étage : le domaine
|
|
était créé, en type='kvm', et « domifaddr » ne rendait rien — l'invité
|
|
n'avait pas encore demandé son bail. Plus l'étage est profond, plus il
|
|
démarre lentement, et c'est justement ce qu'on mesure.
|
|
|
|
`deploy_qemu` attend lui-même l'adresse — 90 secondes par défaut — puis
|
|
rend 0 quand il ne l'a pas trouvée. Son code de sortie ne prouve donc
|
|
rien ici non plus.
|
|
"""
|
|
debut = time.time()
|
|
delai = self.delai("ssh")
|
|
while time.time() - debut < delai:
|
|
_c, sortie = self.executer(
|
|
parent,
|
|
f"virsh -c qemu:///system domifaddr {nom} --source lease",
|
|
DELAIS["controle"],
|
|
"domifaddr",
|
|
)
|
|
adresse = parse_domifaddr(sortie)
|
|
if adresse:
|
|
if time.time() - debut > 20:
|
|
self.dire(f" bail après {int(time.time() - debut)} s")
|
|
return adresse
|
|
time.sleep(15)
|
|
return ""
|
|
|
|
def creer_enfant(self, parent, niveau, res, prepare, noter=None):
|
|
"""Une VM dans le parent, par NOTRE deploy_qemu.py.
|
|
|
|
L'ordre compte, et il n'est pas celui de Proxmox. Là-bas l'adresse est
|
|
exigée AVANT la création (« --ipconfig0 ») ; ici libvirt ne la donne
|
|
qu'APRÈS le démarrage. On note donc l'identité — le nom du domaine,
|
|
qui est déterminé — avant la première commande qui peut créer quoi que
|
|
ce soit, faute de quoi une création échouée à mi-chemin laisserait une
|
|
machine que le rapport ne nomme nulle part.
|
|
"""
|
|
(reseau,) = prepare
|
|
nom = self.nom_etage(niveau)
|
|
if noter:
|
|
noter(nom)
|
|
if not self._envoyer_cli(parent):
|
|
return None, None
|
|
pub = cle_publique()
|
|
if pub and not self.dry_run:
|
|
with open(pub, encoding="utf-8") as fh:
|
|
contenu = fh.read().strip()
|
|
self.executer(
|
|
parent,
|
|
f"mkdir -p /root/.ssh && printf '%s\\n'"
|
|
f" {shlex.quote(contenu)} > {CLE_DISTANTE}",
|
|
DELAIS["controle"],
|
|
"clé",
|
|
)
|
|
creation = (
|
|
f"python3 {DISTANT_CLI} --distro {DISTRO} --name {nom}"
|
|
f" --vcpus {res['vcpu']} --memory {res['ram']}"
|
|
f" --disk-size {res['disque']}G --network network={reseau}"
|
|
f" --ssh-key {CLE_DISTANTE} --assume-yes"
|
|
)
|
|
code, _o = self.executer(
|
|
parent,
|
|
creation,
|
|
self.delai("creation"),
|
|
"deploy_qemu",
|
|
montrer=True,
|
|
)
|
|
if code and not self.dry_run:
|
|
return None, None
|
|
if self.dry_run:
|
|
return nom, "10.10.10.150"
|
|
# Le domaine est-il vraiment accéléré ? « deploy_qemu » n'échoue PAS
|
|
# quand KVM manque : il pose --virt-type qemu et continue. Un étage
|
|
# émulé fausserait toute la mesure sans rien dire.
|
|
_c, xml = self.executer(
|
|
parent,
|
|
f"virsh -c qemu:///system dumpxml {nom}",
|
|
self.delai("controle"),
|
|
"dumpxml",
|
|
)
|
|
vu = parse_domaine(xml)
|
|
if vu["type"] != "kvm":
|
|
self.dire(
|
|
f" ✗ domaine type='{vu['type'] or '?'}' : cette VM est"
|
|
" ÉMULÉE, la mesure ne voudrait rien dire"
|
|
)
|
|
return None, None
|
|
self.dire(f" domaine kvm, cpu {vu['cpu'] or '?'}")
|
|
adresse = self.attendre_adresse(parent, nom)
|
|
if not adresse:
|
|
self.dire(" ✗ créée, mais sans adresse : rien à joindre")
|
|
return None, None
|
|
self.dire(f" {nom} : {adresse}")
|
|
return nom, adresse
|
|
|
|
|
|
def detruire_une(parent_alias, identite, nom, journal):
|
|
"""Arrête puis détruit UNE VM chez son parent, par son NOM et son UUID.
|
|
|
|
Par égalité STRICTE du nom : un filtre par sous-chaîne aurait pris une
|
|
« deep-qemu-lab » de production, et « --remove-all-storage » efface un
|
|
disque pour de bon.
|
|
"""
|
|
parent = {"target": parent_alias, "sudo": "sudo ", "jump": ""}
|
|
code, out = pve.run(
|
|
parent, "virsh -c qemu:///system list --all --name", 180
|
|
)
|
|
if code:
|
|
dire(f" ✗ {parent_alias} injoignable : rien touché", journal)
|
|
return False
|
|
presents = [
|
|
ligne.strip()
|
|
for ligne in pve.strip_ssh_noise(out).splitlines()
|
|
if ligne.strip()
|
|
]
|
|
if nom not in presents:
|
|
dire(f" — {nom} : absent de {parent_alias}", journal)
|
|
return True
|
|
pve.run(parent, f"virsh -c qemu:///system destroy {nom}", 300)
|
|
code, _o = pve.run(
|
|
parent,
|
|
f"virsh -c qemu:///system undefine {nom} --nvram --remove-all-storage",
|
|
600,
|
|
)
|
|
if code:
|
|
dire(f" ✗ {nom} sur {parent_alias} : undefine a échoué", journal)
|
|
return False
|
|
dire(f" ✓ {nom} ({identite}) sur {parent_alias}", journal)
|
|
return True
|
|
|
|
|
|
FAMILLE = Famille(OUTIL, NOM_BASE, detruire_une)
|
|
|
|
|
|
def principal(argv=None):
|
|
return mener(
|
|
argv,
|
|
"Jusqu'à quel étage une QEMU dans une QEMU tient-elle ?",
|
|
FAMILLE,
|
|
Descente,
|
|
nesting.COUTS_QEMU,
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(principal())
|