[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)
This commit is contained in:
Mathieu Benoit 2026-08-27 05:14:43 -04:00
parent 667842b832
commit 469a5fde9e
5 changed files with 294 additions and 197 deletions

View file

@ -48,27 +48,42 @@ the repository: it is our code we want to exercise, and the remote is often
behind the checkout — a fix absent from the remote made the same defect "come
back" on three VMs in a row.
### The resource algorithm
### The resource algorithm — sized from the bottom up
Two things run out going down, and a third degrades. What runs out is
arithmetic, and `script/proxmox/nesting.py` computes it:
The first version handed down whatever the parent could spare, and a real
descent showed what that costs. Level 4 ended up with 44 GB of memory and
2 vCPU **on a host that had 2** — a hundred percent overcommit, at every
level, with the hypervisor itself to serve on top. Its install ran past two
and a half hours against thirteen minutes for level 3, and extrapolating that
ratio gave five years for the tenth.
* **memory** — each level keeps what its own daemons need (`pve-cluster`,
`pvestatd`, `pvedaemon`, `pveproxy`) before handing the rest down;
So the direction is reversed. The deepest level gets what a test Proxmox
actually asks for — 4 GB of memory, 25 GB of disk, 2 vCPU — and every parent
above it adds its own overhead and nothing else: one vCPU, 2 GiB, 10 GB. A
ten-level descent therefore asks its first level for 11 vCPU, 22 GB and
115 GB, where handing resources down wanted 50 GB of memory for the same
depth.
Three budgets can bound the depth, and `script/proxmox/nesting.py` names the
one that ran out:
* **memory** — every level must run its own daemons (`pve-cluster`,
`pvestatd`, `pvedaemon`, `pveproxy`) *and* hold its child;
* **disk** — the child's disk lives *inside* the parent's, which must also
hold its own system.
hold its own system;
* **processor** — each level wants one vCPU more than its child, so ten levels
ask eleven of the first. Half the physical cores is the ceiling: the
orchestrator runs on that machine too.
What degrades is measured, not assumed: past the second level, vendors
document nothing. Hence one capped number — **2 vCPU** for every nested
level. Twelve vCPU at the fourth level froze the guest kernel in early boot;
the same two progressed. Bringing twelve processors online costs as many
round trips through the whole stack.
That third budget is measured, not assumed. Twelve vCPU at the fourth level
froze the guest kernel in early boot — same instruction pointer at three
readings two minutes apart — while two progressed. The number was not the
culprit: that VM had twelve vCPU on a host with two, six times wider than its
own machine. Overcommit freezes, not the twelve.
Memory is **not** capped. On that one manual VM, dropping it from 9 GB to
2 GB moved nothing — it stopped after reading the same 32 MiB, which is simply
the size of the boot files. Memory was not the lever; the vCPU count was. And
trimming memory would starve the level below, which needs it to host the
next.
Memory is not the lever. On that same manual VM, dropping it from 9 GB to 2 GB
moved nothing — it stopped after reading the same 32 MiB, which is simply the
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
@ -123,28 +138,44 @@ cloner le dépôt : c'est notre code qu'on veut éprouver, et le dépôt distant
est souvent en retard sur le checkout — un correctif absent du distant a fait
« revenir » le même défaut sur trois VM de suite.
### L'algorithme de ressources
### L'algorithme de ressources — dimensionné depuis le bas
Deux choses s'épuisent en descendant, et une troisième se dégrade. Ce qui
s'épuise est de l'arithmétique, et `script/proxmox/nesting.py` la calcule :
La première version cédait à l'enfant ce que le parent pouvait céder, et une
descente réelle a montré ce que cela coûte. L'étage 4 se retrouvait avec 44 Go
de mémoire et 2 vCPU **sur un hôte qui en avait 2** — cent pour cent de
surengagement, à chaque étage, avec l'hyperviseur lui-même à servir par-dessus.
Son installation dépassait deux heures et demie contre treize minutes pour
l'étage 3, et l'extrapolation de ce rapport donnait cinq ANS pour le dixième.
* **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 sens est donc inversé. Le plus profond reçoit ce qu'un Proxmox de test
demande vraiment — 4 Go de mémoire, 25 Go de disque, 2 vCPU — et chaque parent
au-dessus ajoute son propre surcoût, rien d'autre : un vCPU, 2 Gio, 10 Go. Une
descente à dix étages demande ainsi 11 vCPU, 22 Go et 115 Go à son premier
étage, là où la cession de haut en bas voulait 50 Go de mémoire pour la même
profondeur.
Trois budgets peuvent borner la profondeur, et `script/proxmox/nesting.py`
nomme celui qui a manqué :
* **la mémoire** — chaque étage doit faire tourner ses propres démons
(`pve-cluster`, `pvestatd`, `pvedaemon`, `pveproxy`) *et* héberger son
enfant ;
* **le disque** — le disque de l'enfant vit *dans* celui du parent, qui doit
aussi contenir son propre système.
aussi contenir son propre système ;
* **le processeur** — chaque étage en veut un de plus que son enfant, donc dix
étages en demandent onze au premier. La moitié des cœurs physiques est le
plafond : l'orchestrateur tourne sur cette machine lui aussi.
Ce qui se dégrade est mesuré, pas supposé : au-delà du deuxième étage, les
fabricants ne documentent rien. D'où un seul nombre borné — **2 vCPU** pour
tout étage imbriqué. Douze vCPU au quatrième étage ont gelé le noyau invité en
tout début de démarrage ; les mêmes deux avançaient. Amener douze processeurs
en ligne coûte autant d'allers-retours à travers toute la pile.
Ce troisième budget est mesuré, pas supposé. Douze vCPU au quatrième étage ont
gelé le noyau invité en tout début de démarrage — même pointeur d'instruction à
trois relevés, deux minutes d'écart — quand deux 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.
La mémoire n'est **pas** bornée. Sur cette unique VM examinée à la main, la
faire passer de 9 Go à 2 Go n'a rien déplacé : elle s'arrêtait après avoir lu
les mêmes 32 Mio, c'est-à-dire simplement la taille des fichiers d'amorçage.
La mémoire n'était pas le levier ; le nombre de vCPU l'était. Et la rogner
priverait l'étage du dessous, qui en a besoin pour héberger le suivant.
La mémoire n'est pas le levier. Sur cette même VM examinée à la main, la faire
passer de 9 Go à 2 Go n'a rien déplacé : elle s'arrêtait après avoir lu les
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

View file

@ -47,28 +47,44 @@ cloner le dépôt : c'est notre code qu'on veut éprouver, et le dépôt distant
est souvent en retard sur le checkout — un correctif absent du distant a fait
« revenir » le même défaut sur trois VM de suite.
### L'algorithme de ressources
### L'algorithme de ressources — dimensionné depuis le bas
Deux choses s'épuisent en descendant, et une troisième se dégrade. Ce qui
s'épuise est de l'arithmétique, et `script/proxmox/nesting.py` la calcule :
La première version cédait à l'enfant ce que le parent pouvait céder, et une
descente réelle a montré ce que cela coûte. L'étage 4 se retrouvait avec 44 Go
de mémoire et 2 vCPU **sur un hôte qui en avait 2** — cent pour cent de
surengagement, à chaque étage, avec l'hyperviseur lui-même à servir par-dessus.
Son installation dépassait deux heures et demie contre treize minutes pour
l'étage 3, et l'extrapolation de ce rapport donnait cinq ANS pour le dixième.
* **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 sens est donc inversé. Le plus profond reçoit ce qu'un Proxmox de test
demande vraiment — 4 Go de mémoire, 25 Go de disque, 2 vCPU — et chaque parent
au-dessus ajoute son propre surcoût, rien d'autre : un vCPU, 2 Gio, 10 Go. Une
descente à dix étages demande ainsi 11 vCPU, 22 Go et 115 Go à son premier
étage, là où la cession de haut en bas voulait 50 Go de mémoire pour la même
profondeur.
Trois budgets peuvent borner la profondeur, et `script/proxmox/nesting.py`
nomme celui qui a manqué :
* **la mémoire** — chaque étage doit faire tourner ses propres démons
(`pve-cluster`, `pvestatd`, `pvedaemon`, `pveproxy`) *et* héberger son
enfant ;
* **le disque** — le disque de l'enfant vit *dans* celui du parent, qui doit
aussi contenir son propre système.
aussi contenir son propre système ;
* **le processeur** — chaque étage en veut un de plus que son enfant, donc dix
étages en demandent onze au premier. La moitié des cœurs physiques est le
plafond : l'orchestrateur tourne sur cette machine lui aussi.
Ce qui se dégrade est mesuré, pas supposé : au-delà du deuxième étage, les
fabricants ne documentent rien. D'où un seul nombre borné — **2 vCPU** pour
tout étage imbriqué. Douze vCPU au quatrième étage ont gelé le noyau invité en
tout début de démarrage ; les mêmes deux avançaient. Amener douze processeurs
en ligne coûte autant d'allers-retours à travers toute la pile.
Ce troisième budget est mesuré, pas supposé. Douze vCPU au quatrième étage ont
gelé le noyau invité en tout début de démarrage — même pointeur d'instruction à
trois relevés, deux minutes d'écart — quand deux 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.
La mémoire n'est **pas** bornée. Sur cette unique VM examinée à la main, la
faire passer de 9 Go à 2 Go n'a rien déplacé : elle s'arrêtait après avoir lu
les mêmes 32 Mio, c'est-à-dire simplement la taille des fichiers d'amorçage.
La mémoire n'était pas le levier ; le nombre de vCPU l'était. Et la rogner
priverait l'étage du dessous, qui en a besoin pour héberger le suivant.
La mémoire n'est pas le levier. Sur cette même VM examinée à la main, la faire
passer de 9 Go à 2 Go n'a rien déplacé : elle s'arrêtait après avoir lu les
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

View file

@ -43,27 +43,42 @@ the repository: it is our code we want to exercise, and the remote is often
behind the checkout — a fix absent from the remote made the same defect "come
back" on three VMs in a row.
### The resource algorithm
### The resource algorithm — sized from the bottom up
Two things run out going down, and a third degrades. What runs out is
arithmetic, and `script/proxmox/nesting.py` computes it:
The first version handed down whatever the parent could spare, and a real
descent showed what that costs. Level 4 ended up with 44 GB of memory and
2 vCPU **on a host that had 2** — a hundred percent overcommit, at every
level, with the hypervisor itself to serve on top. Its install ran past two
and a half hours against thirteen minutes for level 3, and extrapolating that
ratio gave five years for the tenth.
* **memory** — each level keeps what its own daemons need (`pve-cluster`,
`pvestatd`, `pvedaemon`, `pveproxy`) before handing the rest down;
So the direction is reversed. The deepest level gets what a test Proxmox
actually asks for — 4 GB of memory, 25 GB of disk, 2 vCPU — and every parent
above it adds its own overhead and nothing else: one vCPU, 2 GiB, 10 GB. A
ten-level descent therefore asks its first level for 11 vCPU, 22 GB and
115 GB, where handing resources down wanted 50 GB of memory for the same
depth.
Three budgets can bound the depth, and `script/proxmox/nesting.py` names the
one that ran out:
* **memory** — every level must run its own daemons (`pve-cluster`,
`pvestatd`, `pvedaemon`, `pveproxy`) *and* hold its child;
* **disk** — the child's disk lives *inside* the parent's, which must also
hold its own system.
hold its own system;
* **processor** — each level wants one vCPU more than its child, so ten levels
ask eleven of the first. Half the physical cores is the ceiling: the
orchestrator runs on that machine too.
What degrades is measured, not assumed: past the second level, vendors
document nothing. Hence one capped number — **2 vCPU** for every nested
level. Twelve vCPU at the fourth level froze the guest kernel in early boot;
the same two progressed. Bringing twelve processors online costs as many
round trips through the whole stack.
That third budget is measured, not assumed. Twelve vCPU at the fourth level
froze the guest kernel in early boot — same instruction pointer at three
readings two minutes apart — while two progressed. The number was not the
culprit: that VM had twelve vCPU on a host with two, six times wider than its
own machine. Overcommit freezes, not the twelve.
Memory is **not** capped. On that one manual VM, dropping it from 9 GB to
2 GB moved nothing — it stopped after reading the same 32 MiB, which is simply
the size of the boot files. Memory was not the lever; the vCPU count was. And
trimming memory would starve the level below, which needs it to host the
next.
Memory is not the lever. On that same manual VM, dropping it from 9 GB to 2 GB
moved nothing — it stopped after reading the same 32 MiB, which is simply the
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

View file

@ -24,8 +24,11 @@ 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 ;
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
@ -47,21 +50,49 @@ HOTE_RESERVE_RAM_MO = 4096
HOTE_RESERVE_PART = 8 # un huitième
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.
# 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 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
# 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.
@ -74,59 +105,81 @@ def nesting_plan(
ram_dispo_mo: int,
disque_libre_go: int,
) -> dict:
"""Les ressources de chaque étage, et jusqu'où l'arithmétique va.
"""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 "".
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.
é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.
reserve = max(HOTE_RESERVE_RAM_MO, int(ram_dispo_mo) // HOTE_RESERVE_PART)
ram = ((int(ram_dispo_mo) - reserve) // 1024) * 1024
disque = int(disque_libre_go) - HOTE_RESERVE_DISQUE_GO
niveaux, arret = [], ""
# « max(1, …) » forçait un tour : profondeur 0 rendait un plan d'UN
# étage, et « --depth 0 » créait donc une VM. range(1, 1) est déjà vide,
# et un plan vide est la bonne réponse à une demande vide.
for niveau in range(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,
# Le plancher est VCPU_IMBRIQUE et non 1 : sur un hôte de
# quatre cœurs, « // 4 » donnait UN vCPU au premier étage —
# l'hyperviseur parent — alors que son invité en recevait
# deux. Un parent plus étroit que son enfant est absurde, et
# c'est tout l'inverse de ce que ce module raconte.
"vcpu": (
max(
VCPU_IMBRIQUE,
min(VCPU_NIVEAU1_MAX, int(cpu_hote) // 4),
)
if niveau == 1
else VCPU_IMBRIQUE
),
"ram": ram,
"disque": disque,
}
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": len(niveaux),
"atteignable": atteignable,
"niveaux": niveaux,
"arret": arret,
}

View file

@ -18,97 +18,96 @@ from script.proxmox import nesting # noqa: E402
class TestLePlanDesEtages(unittest.TestCase):
"""Deux ressources s'épuisent en descendant, et le plan doit le dire
AVANT de créer quoi que ce soit."""
"""Le plan se dimensionne DEPUIS LE BAS, et c'est une correction.
De haut en bas, chaque étage recevait ce que son parent pouvait céder.
Mesuré sur une descente réelle : l'étage 4 se retrouvait avec 44 Go de RAM
et deux vCPU sur un hôte qui en avait deux — cent pour cent de
surengagement, à chaque étage. Son installation dépassait deux heures et
demie contre treize minutes pour l'étage 3, et l'extrapolation donnait cinq
ANS pour le dixième.
Le plus profond reçoit donc ce qu'un Proxmox de test demande, et chaque
parent ajoute son propre surcoût — un vCPU, deux gibioctets, dix
gigaoctets. Rien de plus."""
# La machine réelle sur laquelle l'algorithme a été réglé.
HOTE = dict(cpu_hote=28, ram_dispo_mo=29549, disque_libre_go=139)
HOTE = dict(cpu_hote=28, ram_dispo_mo=58000, disque_libre_go=165)
def test_ten_levels_fit_on_this_machine(self):
plan = nesting.nesting_plan(10, **self.HOTE)
self.assertEqual(plan["atteignable"], 10)
self.assertEqual(plan["arret"], "")
def test_every_level_shrinks(self):
# Le disque de l'enfant vit DANS celui du parent, qui doit aussi
# contenir son propre système : rien ne peut rester constant.
niveaux = nesting.nesting_plan(6, **self.HOTE)["niveaux"]
for precedent, suivant in zip(niveaux, niveaux[1:]):
self.assertLess(suivant["ram"], precedent["ram"])
self.assertLess(suivant["disque"], precedent["disque"])
def test_the_deepest_level_gets_exactly_the_target(self):
"""C'est de là qu'on part : ce qu'un Proxmox de test demande, pas ce
qui reste."""
plan = nesting.nesting_plan(10, **self.HOTE)
fond = plan["niveaux"][-1]
self.assertEqual(fond["ram"], nesting.PVE_RAM_CIBLE_MO)
self.assertEqual(fond["disque"], nesting.PVE_DISQUE_CIBLE_GO)
self.assertEqual(fond["vcpu"], nesting.VCPU_IMBRIQUE)
def test_the_first_level_may_be_wide_the_others_not(self):
"""12 vCPU au quatrième étage ont GELÉ le noyau invité en tout début
de démarrage ; les mêmes 2 vCPU avançaient. Amener douze processeurs
en ligne demande autant d'allers-retours à travers la pile."""
niveaux = nesting.nesting_plan(4, **self.HOTE)["niveaux"]
# STRICTEMENT plus grand. « > VCPU_IMBRIQUE - 1 » était satisfait par
# la valeur imbriquée elle-même : remplacer tout le calcul du premier
# étage par VCPU_IMBRIQUE laissait les tests verts, donc cpu_hote
# n'était couvert par rien.
self.assertGreater(niveaux[0]["vcpu"], nesting.VCPU_IMBRIQUE)
for n in niveaux[1:]:
self.assertEqual(n["vcpu"], nesting.VCPU_IMBRIQUE)
def test_each_parent_adds_exactly_its_own_overhead(self):
# Ni plus ni moins : un parent plus large que nécessaire ralentit tout
# ce qu'il héberge, un parent trop juste ne le fait pas tourner.
niveaux = nesting.nesting_plan(8, **self.HOTE)["niveaux"]
for parent, enfant in zip(niveaux, niveaux[1:]):
self.assertEqual(parent["ram"] - enfant["ram"], nesting.PVE_RAM_MO)
self.assertEqual(
parent["disque"] - enfant["disque"], nesting.PVE_DISQUE_GO
)
self.assertEqual(parent["vcpu"] - enfant["vcpu"], 1)
def test_a_parent_is_never_narrower_than_its_child(self):
"""Sur un hôte de quatre cœurs, « // 4 » donnait UN vCPU au premier
étage — l'hyperviseur — alors que son invité en recevait deux."""
for coeurs in (2, 4, 8, 12, 28):
"""Deux vCPU hébergeant deux vCPU, c'est cent pour cent de
surengagement — et l'hyperviseur à servir en plus. Mesuré : une 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é."""
for coeurs in (4, 8, 12, 28):
with self.subTest(coeurs=coeurs):
niveaux = nesting.nesting_plan(
3,
6,
cpu_hote=coeurs,
ram_dispo_mo=32768,
disque_libre_go=300,
ram_dispo_mo=64000,
disque_libre_go=400,
)["niveaux"]
self.assertGreaterEqual(
niveaux[0]["vcpu"], niveaux[1]["vcpu"], f"{coeurs} cœurs"
)
for parent, enfant in zip(niveaux, niveaux[1:]):
self.assertGreater(parent["vcpu"], enfant["vcpu"])
self.assertGreater(parent["ram"], enfant["ram"])
self.assertGreater(parent["disque"], enfant["disque"])
def test_the_cpu_budget_can_bound_the_depth(self):
"""Sur une petite machine, c'est le PROCESSEUR qui borne, pas la
mémoire : chaque étage en veut un de plus que son enfant, donc dix
étages demandent onze vCPU au premier."""
plan = nesting.nesting_plan(
10, cpu_hote=8, ram_dispo_mo=64000, disque_libre_go=400
)
self.assertEqual(plan["arret"], "vcpu")
self.assertLess(plan["atteignable"], 10)
# Et le premier étage ne dépasse pas la part concédée à l'hôte.
self.assertLessEqual(
plan["niveaux"][0]["vcpu"], 8 // nesting.VCPU_HOTE_PART
)
def test_running_out_of_ram_is_named(self):
plan = nesting.nesting_plan(
10, cpu_hote=8, ram_dispo_mo=12288, disque_libre_go=500
10, cpu_hote=28, ram_dispo_mo=12288, disque_libre_go=500
)
self.assertEqual(plan["arret"], "ram")
self.assertLess(plan["atteignable"], 10)
# Aucun étage sous le plancher : un Proxmox sous 2 Go ne démarre pas
# ses démons.
for n in plan["niveaux"]:
self.assertGreaterEqual(n["ram"], nesting.RAM_MIN_MO)
def test_running_out_of_disk_is_named(self):
plan = nesting.nesting_plan(
10, cpu_hote=8, ram_dispo_mo=200000, disque_libre_go=60
10, cpu_hote=28, ram_dispo_mo=200000, disque_libre_go=60
)
self.assertEqual(plan["arret"], "disque")
for n in plan["niveaux"]:
self.assertGreaterEqual(n["disque"], nesting.DISQUE_MIN_GO)
def test_the_host_keeps_a_share_not_just_a_floor(self):
"""Quatre gigaoctets sur une machine de soixante, c'est 6 % laissés à
l'hôte : le jour où les invités touchent vraiment leur mémoire, c'est
lui qui part en swap — et la mesure serait celle du swap, pas de
l'imbrication."""
for dispo in (60000, 260000):
with self.subTest(dispo=dispo):
plan = nesting.nesting_plan(
1, cpu_hote=28, ram_dispo_mo=dispo, disque_libre_go=500
)
reserve = dispo - plan["niveaux"][0]["ram"]
self.assertGreater(reserve, nesting.HOTE_RESERVE_RAM_MO)
self.assertGreaterEqual(
reserve, dispo // nesting.HOTE_RESERVE_PART
)
def test_a_small_host_keeps_the_floor(self):
# Sur une petite machine, la part serait dérisoire : le plancher tient.
plan = nesting.nesting_plan(
1, cpu_hote=4, ram_dispo_mo=16384, disque_libre_go=200
)
self.assertEqual(
16384 - plan["niveaux"][0]["ram"], nesting.HOTE_RESERVE_RAM_MO
)
def test_a_depth_of_zero_asks_for_nothing(self):
for profondeur in (0, -1, -7):
with self.subTest(profondeur=profondeur):
@ -122,34 +121,17 @@ class TestLePlanDesEtages(unittest.TestCase):
)
self.assertEqual(plan["atteignable"], 0)
self.assertEqual(plan["niveaux"], [])
self.assertEqual(plan["arret"], "ram")
self.assertTrue(plan["arret"])
def test_a_plan_is_never_promised_beyond_what_fits(self):
# Mieux vaut annoncer six étages et en réussir six que d'en promettre
# dix et mourir au septième sans savoir pourquoi.
# Depuis 0 et depuis les négatifs : « max(1, …) » forçait un tour,
# donc « --depth 0 » rendait un plan d'UN étage et créait une VM.
for profondeur in range(-2, 13):
plan = nesting.nesting_plan(profondeur, **self.HOTE)
self.assertEqual(len(plan["niveaux"]), plan["atteignable"])
# « max(0, …) » : une demande négative ne peut pas donner un
# nombre d'étages négatif, elle donne zéro.
self.assertLessEqual(plan["atteignable"], max(0, profondeur))
class TestLaProfondeurDUnHote(unittest.TestCase):
"""Comptée depuis la chaîne de rebonds : 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."""
def test_no_jump_is_the_first_level(self):
self.assertEqual(nesting.depth_from_jumps(0), 1)
def test_each_jump_adds_a_level(self):
for sauts, attendu in ((1, 2), (2, 3), (3, 4), (9, 10)):
self.assertEqual(nesting.depth_from_jumps(sauts), attendu)
class TestCompterLesRebonds(unittest.TestCase):
"""La profondeur se lit dans ~/.ssh/config : un ProxyJump par étage.