La profondeur d'imbrication praticable ne se déduit pas, elle se mesure. Une mesure à la main a trouvé, au quatrième étage, un invité 36 fois plus lent que le temps réel — 583 secondes d'horloge pour 16 secondes de temps invité, chaque ligne d'ACPI prenant une seconde — puis un noyau gelé au MÊME octet quelles que soient les ressources. Un chiffre obtenu une fois, sur une machine, n'est pas un chiffre. D'où trois choses. L'algorithme, en fonctions pures. Deux ressources s'épuisent en descendant : la mémoire, chaque étage gardant de quoi faire tourner ses propres démons, et le disque, celui de l'enfant vivant DANS celui du parent. Une troisième se dégrade, et elle borne le vCPU à deux au-delà du premier étage : douze ont gelé le noyau invité, les mêmes deux avançaient. La mémoire n'est PAS bornée — la même VM gelait au même octet avec 9 Go et avec 2 Go, donc la rogner ne gagnerait rien et priverait l'étage du dessous. Le plan est annoncé avant toute création, et jamais au-delà de ce qui tient. Le garde-fou dans l'écran. Il lisait la capacité de l'HÔTE et l'offrait en entier : sur un troisième étage à 14 cœurs, il a proposé 12 vCPU à une VM qui n'a jamais démarré. Le nombre n'était pas absurde pour la machine ; il l'était pour sa profondeur, que l'écran ignorait. Elle se compte maintenant sur la chaîne de ProxyJump — un rebond par étage, et c'est nous qui écrivons ces entrées. Le test long, dans LongTest/ et non dans test/ : le lanceur unitaire doit rester lançable en quelques secondes, partout, y compris sans virtualisation. La descente est uniforme — créer, attendre le ssh, installer, redémarrer et vérifier le noyau, remettre pmxcfs debout, contrôler le stockage — et s'arrête au premier étage qui échoue en NOMMANT l'étape. Il envoie notre install_proxmox.sh par scp plutôt que de laisser la VM cloner le dépôt : c'est notre code qu'on éprouve, et un correctif absent du distant a fait revenir le même défaut sur trois VM. --- EN --- The practicable nesting depth cannot be deduced, only measured. A manual measurement found, at the fourth level, a guest 36 times slower than real time — 583 seconds of wall clock for 16 seconds of guest time, each ACPI line taking a second — then a kernel frozen at the SAME byte whatever the resources. A number obtained once, on one machine, is not a number. Hence three things. The algorithm, in pure functions. Two resources run out going down: memory, each level keeping what its own daemons need, and disk, the child's living INSIDE the parent's. A third degrades, and it caps the vCPU at two beyond the first level: twelve froze the guest kernel, the same two progressed. Memory is NOT capped — the same VM froze at the same byte with 9 GB and with 2 GB, so trimming it would gain nothing and starve the level below. The plan is announced before anything is created, and never beyond what fits. The guard in the screen. It read the HOST's capacity and offered all of it: on a third level with 14 cores it proposed 12 vCPU to a VM that never booted. The number was not absurd for the machine; it was for its depth, which the screen did not know. It is now counted on the ProxyJump chain — one hop per level, and we are the ones writing those entries. The long test, in LongTest/ and not test/: the unit runner must stay runnable in seconds, anywhere, including without virtualisation. The descent is uniform — create, wait for ssh, install, reboot and check the kernel, bring pmxcfs back, check the storage — and stops at the first level that fails, NAMING the step. It sends our install_proxmox.sh over scp instead of letting the VM clone the repository: it is our code being exercised, and a fix absent from the remote made the same defect return on three VMs. Assisted-by: Claude Opus 5 (cherry picked from commit 4f70c461330cac6f46783a60e0f33052a979fa23)
148 lines
5.8 KiB
Python
148 lines
5.8 KiB
Python
#!/usr/bin/env python3
|
|
# © 2026 TechnoLibre (http://www.technolibre.ca)
|
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
|
"""Combien d'étages de Proxmox tiennent, et avec quelles ressources.
|
|
|
|
Un Proxmox dans un Proxmox dans un Proxmox : chaque étage est un hyperviseur
|
|
qui héberge le suivant. Deux choses s'épuisent en descendant, et une troisième
|
|
se dégrade.
|
|
|
|
Ce qui s'ÉPUISE — et c'est de l'arithmétique :
|
|
|
|
* la mémoire. Chaque étage garde de quoi faire tourner ses propres démons
|
|
(pve-cluster, pvestatd, pvedaemon, pveproxy) avant de céder le reste ;
|
|
* le disque. Le disque de l'enfant vit DANS celui du parent, qui doit aussi
|
|
contenir son propre système.
|
|
|
|
Ce qui se DÉGRADE — et c'est mesuré, pas supposé. Au quatrième étage, sur un
|
|
hôte AMD, une VM tournait 36 fois moins vite que le temps réel : 583 secondes
|
|
d'horloge pour 16 secondes de temps invité, chaque ligne d'ACPI prenant une
|
|
seconde. Chaque sortie de VM traverse tous les hyperviseurs empilés, et AMD ne
|
|
documente l'imbrication qu'à DEUX niveaux.
|
|
|
|
Deux nombres viennent de la même mesure, et méritent d'être dits :
|
|
|
|
* 12 vCPU au quatrième étage ont GELÉ le noyau invité en tout début de
|
|
démarrage — même RIP à trois relevés, deux minutes d'écart, pas un octet lu
|
|
de plus. Les mêmes 2 vCPU avançaient. D'où VCPU_IMBRIQUE = 2 : amener douze
|
|
processeurs en ligne demande autant d'allers-retours à travers la pile ;
|
|
* la même VM s'arrêtait ensuite au MÊME octet — 33 682 432 — quelles que
|
|
soient les ressources, dans la réservation des tables ACPI. Ce mur-là n'est
|
|
pas une question de taille, et aucun réglage ici ne le déplacera. La
|
|
profondeur RÉELLEMENT atteignable se mesure ; ce module ne calcule que ce
|
|
qui est arithmétiquement possible.
|
|
"""
|
|
|
|
# Ce qu'on laisse à la machine physique : elle fait tourner l'orchestrateur,
|
|
# le menu TODO, et le premier QEMU.
|
|
HOTE_RESERVE_RAM_MO = 4096
|
|
HOTE_RESERVE_DISQUE_GO = 20
|
|
|
|
# Ce qu'un étage garde pour lui avant de céder le reste. La RAM vient de
|
|
# l'observation d'un Proxmox imbriqué au repos ; le disque, de la mesure d'un
|
|
# système installé (5,6 Go) plus de la place pour écrire.
|
|
PVE_RAM_MO = 2048
|
|
PVE_DISQUE_GO = 10
|
|
|
|
# En dessous, un Proxmox ne démarre pas ses démons ou n'a plus la place
|
|
# d'importer une image cloud.
|
|
RAM_MIN_MO = 2048
|
|
DISQUE_MIN_GO = 15
|
|
|
|
# Le premier étage tourne sur la machine physique : il peut être large. Les
|
|
# suivants non — voir la mesure dans l'en-tête.
|
|
VCPU_NIVEAU1_MAX = 4
|
|
VCPU_IMBRIQUE = 2
|
|
|
|
# Au-delà, l'imbrication n'est pas un terrain documenté par les fabricants.
|
|
# On ne refuse pas — on le DIT.
|
|
PROFONDEUR_SURE = 2
|
|
|
|
|
|
def nesting_plan(
|
|
profondeur: int,
|
|
cpu_hote: int,
|
|
ram_dispo_mo: int,
|
|
disque_libre_go: int,
|
|
) -> dict:
|
|
"""Les ressources de chaque étage, et jusqu'où l'arithmétique va.
|
|
|
|
Rend {"demandee", "atteignable", "niveaux": [...], "arret"}. `arret`
|
|
nomme ce qui a manqué — « ram » ou « disque » — quand la profondeur
|
|
demandée n'est pas atteinte, sinon "".
|
|
|
|
On ne rend jamais un plan qu'on sait impossible : mieux vaut annoncer six
|
|
étages et en réussir six que d'en promettre dix et mourir au septième
|
|
sans savoir pourquoi.
|
|
"""
|
|
# Arrondi au gibioctet inférieur : « --memory 25203 » marche, mais un
|
|
# nombre rond se relit, se compare d'un étage à l'autre, et évite de
|
|
# traîner les kibioctets du hasard de la mesure jusqu'au dixième étage.
|
|
ram = ((int(ram_dispo_mo) - HOTE_RESERVE_RAM_MO) // 1024) * 1024
|
|
disque = int(disque_libre_go) - HOTE_RESERVE_DISQUE_GO
|
|
niveaux, arret = [], ""
|
|
for niveau in range(1, max(1, int(profondeur)) + 1):
|
|
if niveau > 1:
|
|
ram -= PVE_RAM_MO
|
|
disque -= PVE_DISQUE_GO
|
|
if ram < RAM_MIN_MO:
|
|
arret = "ram"
|
|
break
|
|
if disque < DISQUE_MIN_GO:
|
|
arret = "disque"
|
|
break
|
|
niveaux.append(
|
|
{
|
|
"niveau": niveau,
|
|
"vcpu": (
|
|
max(1, min(VCPU_NIVEAU1_MAX, int(cpu_hote) // 4))
|
|
if niveau == 1
|
|
else VCPU_IMBRIQUE
|
|
),
|
|
"ram": ram,
|
|
"disque": disque,
|
|
}
|
|
)
|
|
return {
|
|
"demandee": int(profondeur),
|
|
"atteignable": len(niveaux),
|
|
"niveaux": niveaux,
|
|
"arret": arret,
|
|
}
|
|
|
|
|
|
def depth_from_jumps(jumps: int) -> int:
|
|
"""Profondeur d'un hôte, comptée depuis sa chaîne de rebonds.
|
|
|
|
Un hôte joint sans rebond est au niveau 1 ; chaque ProxyJump ajoute un
|
|
étage. C'est la seule mesure dont on dispose de l'extérieur, et elle est
|
|
exacte pour les hôtes que nous avons nous-mêmes déployés — c'est nous qui
|
|
écrivons ces entrées.
|
|
"""
|
|
return max(1, int(jumps) + 1)
|
|
|
|
|
|
def capped_for_depth(profondeur: int, vcpu: int, ram_mo: int) -> tuple:
|
|
"""Ressources bornées pour cette profondeur, et pourquoi.
|
|
|
|
Rend (vcpu, ram, raison). `raison` vide quand rien n'a été touché.
|
|
|
|
Seul le vCPU est borné, et la mesure le dit : la même VM au quatrième
|
|
étage gelait au MÊME octet avec 9 Go et avec 2 Go — la mémoire n'est pas
|
|
le levier. Douze vCPU, en revanche, gelaient plus tôt et plus dur que
|
|
deux. La RAM passe donc telle quelle : la rogner ne gagnerait rien et
|
|
priverait l'étage suivant.
|
|
|
|
Pourquoi borner au lieu d'avertir seulement : l'écran lit la capacité de
|
|
l'HÔTE et l'offre en entier. Sur un troisième étage à 14 cœurs et 9 Go, il
|
|
a proposé 12 vCPU — et la VM n'a jamais démarré. Le nombre n'était pas
|
|
absurde pour la machine ; il l'était pour sa profondeur.
|
|
"""
|
|
vcpu, ram_mo = int(vcpu), int(ram_mo)
|
|
if profondeur <= PROFONDEUR_SURE or vcpu <= VCPU_IMBRIQUE:
|
|
return vcpu, ram_mo, ""
|
|
return (
|
|
VCPU_IMBRIQUE,
|
|
ram_mo,
|
|
f"niveau {int(profondeur)} : {vcpu} vCPU -> {VCPU_IMBRIQUE}",
|
|
)
|