erplibre/script/proxmox/nesting.py
Mathieu Benoit 469a5fde9e [FIX] imbrication : dimensionner les étages depuis le bas, vCPU compris
Le plan cédait à l'enfant ce que le parent pouvait céder. Descente réelle à
dix étages : l'étage 4 a reçu 44 Go et 2 vCPU sur un hôte qui en avait 2 —
cent pour cent de surengagement, à chaque étage. Son installation dure 2 h 52
et n'est pas finie, contre 793 s pour l'étage 3. Extrapolé, le dixième
demandait des années.

Le plus profond reçoit désormais ce qu'un Proxmox de test demande, et chaque
parent ajoute son seul surcoût : un vCPU, 2 Gio, 10 Go. Dix étages tiennent
sur 11 vCPU et 22 Go au premier, contre 50 Go avant.

Corrige aussi l'explication du gel à 12 vCPU : cette VM avait douze vCPU sur
un hôte qui en avait deux. C'est le surengagement qui gèle, pas le douze.

--- EN ---

The plan handed the child whatever the parent could spare. Real ten-level
descent: level 4 got 44 GB and 2 vCPU on a host that had 2 — a hundred
percent overcommit, at every level. Its install has run 2h52 and is not done,
against 793 s for level 3. Extrapolated, the tenth wanted years.

The deepest level now gets what a test Proxmox asks for, and each parent adds
its own overhead only: one vCPU, 2 GiB, 10 GB. Ten levels fit in 11 vCPU and
22 GB at the first, against 50 GB before.

Also corrects the account of the 12-vCPU freeze: that VM had twelve vCPU on a
host with two. Overcommit freezes, not the twelve.

Assisted-by: claude-opus-5
(cherry picked from commit 7cda84bf391ee3ed36c5953ca4b86df44bd09e3b)
2026-08-29 01:53:03 -04:00

224 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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. Le nombre n'était pas le fautif : cette
VM avait douze vCPU sur un hôte qui en avait DEUX, six fois plus large que sa
propre machine. C'est le surengagement qui gèle, pas le douze — d'où le
dimensionnement par étage plus bas, qui donne à chaque parent un vCPU de plus
qu'à son enfant ;
* cette VM-là s'arrêtait après avoir lu 33 682 432 octets — 32 Mio, soit
simplement la taille de ses fichiers d'amorçage — et le chiffre ne bougeait
pas quand on lui retirait de la mémoire. La mémoire n'était donc pas le
levier, et c'est pourquoi ce module n'en borne pas.
Une descente complète a ensuite RÉFUTÉ ce qu'on avait conclu de la première :
son quatrième étage, à 2 vCPU, a démarré, s'est installé, et a écrit des
gigaoctets. Le plafond était celui du parallélisme sous imbrication, pas celui
de l'imbrication. La profondeur RÉELLEMENT atteignable se mesure — LongTest la
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. Un PLANCHER, complété par une part —
# quatre gigaoctets sur une machine de soixante, c'est 6 % laissés à l'hôte,
# et le jour où les invités touchent vraiment leur mémoire c'est l'hôte qui
# part en swap. La mesure serait alors celle du swap, pas de l'imbrication.
HOTE_RESERVE_RAM_MO = 4096
HOTE_RESERVE_PART = 8 # un huitième
HOTE_RESERVE_DISQUE_GO = 20
# Ce qu'un étage garde pour LUI, en plus de ce qu'il cède à son enfant. 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
# Ce dont le PLUS PROFOND a besoin — et c'est de là qu'on part.
#
# Le dimensionnement allait d'abord de haut en bas : chaque étage recevait tout
# ce que son parent pouvait céder. Mesuré sur une descente réelle, l'étage 4 se
# retrouvait avec 44 Go — onze millions de pages à cartographier, chaque défaut
# traversant les quatre hyperviseurs empilés. Son installation dépassait deux
# heures et demie là où l'étage 3 mettait treize minutes, et l'extrapolation
# donnait cinq ANS pour le dixième étage.
#
# On part donc du bas : le plus profond reçoit ce qu'un Proxmox de test demande
# vraiment, et chaque parent ajoute seulement son propre surcoût. Pour dix
# étages, le premier a besoin de 4 + 9×2 = 22 Go au lieu de cinquante — et
# chaque étage est PETIT, donc rapide.
PVE_RAM_CIBLE_MO = 4096
PVE_DISQUE_CIBLE_GO = 25
# 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 processeur se dimensionne DEPUIS LE BAS lui aussi, et pour la même
# raison que la mémoire — mais celle-là s'est vue à l'usage.
#
# Avec deux vCPU à chaque étage imbriqué, l'étage 3 avait deux vCPU pour
# héberger un invité qui en demandait deux : cent pour cent de surengagement,
# et l'hyperviseur lui-même à servir par-dessus. À chaque étage. Mesuré sur une
# descente réelle : une seconde VM démarrée au quatrième étage a lu DEUX
# KILO-OCTETS en onze minutes, affamée par l'installation qui tournait à côté.
#
# Chaque étage reçoit donc UN vCPU de plus que son enfant : le plus profond en
# a deux, son parent trois, et ainsi de suite. Le premier étage d'une descente
# à dix en demande onze — sur vingt-huit cœurs réels, cela passe.
VCPU_IMBRIQUE = 2
# Ce qu'on accepte de prendre à la machine physique : la moitié de ses cœurs.
# L'orchestrateur tourne dessus, et la suite de tests aussi.
VCPU_HOTE_PART = 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, dimensionnées DEPUIS LE BAS.
Rend {"demandee", "atteignable", "niveaux": [...], "arret"}. `arret` nomme
ce qui a manqué — « ram » ou « disque » — quand la profondeur demandée
n'est pas atteinte, sinon "".
Depuis le bas, et c'est tout le sujet. De haut en bas, chaque étage
recevait ce que son parent pouvait céder : mesuré, l'étage 4 se retrouvait
avec 44 Go de RAM et son installation dépassait deux heures et demie
contre treize minutes pour l'étage 3. Sous pagination imbriquée, un gros
invité coûte cher à cartographier, et le coût se multiplie par étage.
Le plus profond reçoit donc ce qu'un Proxmox de test demande, et chaque
parent ajoute son surcoût — rien de plus. Une descente à dix étages
demande alors 22 Go au premier au lieu de cinquante, et chaque étage est
petit.
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.
"""
reserve = max(HOTE_RESERVE_RAM_MO, int(ram_dispo_mo) // HOTE_RESERVE_PART)
budget_ram = ((int(ram_dispo_mo) - reserve) // 1024) * 1024
budget_disque = int(disque_libre_go) - HOTE_RESERVE_DISQUE_GO
def besoin(d):
"""Ce que le PREMIER étage doit avoir pour qu'une descente de `d`
étages tienne : la cible du bas, plus un surcoût par étage au-dessus.
Le processeur en fait partie : chaque étage en veut un de plus que son
enfant, donc le premier en veut VCPU_IMBRIQUE + d - 1. Sans cette
condition, on annonçait dix étages sur une machine à quatre cœurs.
"""
return (
PVE_RAM_CIBLE_MO + (d - 1) * PVE_RAM_MO,
PVE_DISQUE_CIBLE_GO + (d - 1) * PVE_DISQUE_GO,
VCPU_IMBRIQUE + d - 1,
)
budget_vcpu = max(VCPU_IMBRIQUE, int(cpu_hote) // VCPU_HOTE_PART)
atteignable, arret = 0, ""
for d in range(max(0, int(profondeur)), 0, -1):
ram1, disque1, vcpu1 = besoin(d)
if (
ram1 <= budget_ram
and disque1 <= budget_disque
and vcpu1 <= budget_vcpu
):
atteignable = d
break
if atteignable < int(profondeur):
# Nommer CE qui a manqué, à la profondeur demandée.
ram1, disque1, vcpu1 = besoin(max(1, int(profondeur)))
if ram1 > budget_ram:
arret = "ram"
elif disque1 > budget_disque:
arret = "disque"
else:
arret = "vcpu"
niveaux = [
{
"niveau": niveau,
# UN de plus que son enfant. Un parent aussi étroit que son
# enfant, c'est cent pour cent de surengagement — et l'hyperviseur
# à servir en plus.
"vcpu": VCPU_IMBRIQUE + (atteignable - niveau),
"ram": PVE_RAM_CIBLE_MO + (atteignable - niveau) * PVE_RAM_MO,
"disque": PVE_DISQUE_CIBLE_GO
+ (atteignable - niveau) * PVE_DISQUE_GO,
}
for niveau in range(1, atteignable + 1)
]
return {
"demandee": int(profondeur),
"atteignable": atteignable,
"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 : sur la VM examinée au
quatrième étage, passer de 9 Go à 2 Go n'a rien déplacé — elle s'arrêtait
après les mêmes 32 Mio, la taille de ses fichiers d'amorçage. Douze vCPU,
en revanche, gelaient là où deux avançaient, et une descente complète a
fini par franchir cet étage à 2 vCPU. La RAM passe donc telle quelle : la
rogner ne gagnerait rien et priverait l'étage suivant, qui en a besoin
pour héberger le sien.
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}",
)