erplibre/test/test_proxmox_uefi.py
Mathieu Benoit 83b9674176 [REF] qemu firmware : une table pour les deux voies, fuseau décodé
Deux tables opposées vivaient dans le même fichier : l'une forçait le
BIOS, l'autre l'UEFI, chacune lue d'un seul côté. Elles pouvaient se
contredire sans que rien ne le dise. FIRMWARE_IMPOSE les remplace, et les
deux suites passent inchangées. « --bios » l'emportait sur un fait
physique : forcé sur une image sans secteur d'amorçage, il donnait une VM
« running » à console muette. Il est ignoré, et l'appelant l'apprend.

Le fuseau était lu en UTF-8 strict sous un « except OSError » : un octet
qui n'en est pas faisait échouer tout le déploiement pour une traduction
de confort.

--- EN ---

Two opposing tables lived in one file: one forced BIOS, the other UEFI,
each read by one side only. They could contradict each other silently.
FIRMWARE_IMPOSE replaces both, and both suites pass unchanged. "--bios"
overrode a physical fact: forced on an image with no boot sector it gave a
"running" VM with a mute console. It is ignored now, and the caller is
told.

The timezone was read as strict UTF-8 under an "except OSError": a byte
that is not one failed the whole deployment for a convenience
translation.

Assisted-by: Claude Opus 5
2026-09-16 21:31:03 -04:00

160 lines
6 KiB
Python

#!/usr/bin/env python3
# © 2026 TechnoLibre (http://www.technolibre.ca)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
"""L'amorçage UEFI des VM créées SUR un hôte Proxmox.
Les deux chemins de déploiement ne partent pas du même firmware : celui de
qemu amorce en UEFI pour tout le monde, celui de Proxmox part en SeaBIOS.
Tant que toutes les images savaient faire les deux, la divergence ne se
voyait pas.
Mesuré sur un Proxmox 9 : une VM NixOS créée en SeaBIOS se déclare
« running » et sa console reste MUETTE ; la même en OVMF démarre — systemd,
cloud-init, réseau, invite de connexion. Debian 13, sur le même hôte, démarre
en SeaBIOS sans rien devoir à l'UEFI.
D'où ce que ces tests gardent : un marqueur PAR DISTRIBUTION, et non un
défaut renversé pour tous. Renverser le défaut aurait ajouté un disque EFI à
chaque VM du parc pour le besoin d'une seule image.
"""
import importlib.util
import sys
import unittest
from pathlib import Path
sys.argv = ["todo.py"]
RACINE = Path(__file__).resolve().parents[1]
from script.proxmox import proxmox_deploy as pve # noqa: E402
def _deploy_qemu():
chemin = RACINE / "script/qemu/deploy_qemu.py"
spec = importlib.util.spec_from_file_location("deploy_qemu", chemin)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
DQ = _deploy_qemu()
SPEC = {
"name": "essai",
"storage": "local",
"bridge": "vmbr0",
"memory": 2048,
"vcpus": 2,
"disk": "40G",
"image": "image.qcow2",
"user": "erplibre",
"start": False,
"ipconfig": "ip=dhcp",
}
class LeCatalogueSaitQuiEnABesoin(unittest.TestCase):
def test_the_image_with_no_bios_sector_is_named(self):
"""NixOS ne publie pas d'image cloud : celle du tiers qui la rebâtit
n'a pas de secteur d'amorçage BIOS."""
self.assertTrue(DQ.requiert_uefi("nixos"))
def test_the_others_are_left_alone(self):
"""Mesuré : Debian 13 démarre en SeaBIOS sur un Proxmox 9. Les
basculer coûterait un disque EFI par VM, pour rien."""
for distro in ("ubuntu", "debian", "fedora", "arch", "opensuse"):
with self.subTest(distro=distro):
self.assertFalse(DQ.requiert_uefi(distro))
def test_an_unknown_distro_does_not_get_uefi(self):
"""Le doute ne vaut pas un firmware : c'est le catalogue qui sait."""
self.assertFalse(DQ.requiert_uefi(""))
self.assertFalse(DQ.requiert_uefi("inconnue"))
class LaSequenceDeCreation(unittest.TestCase):
def _cmds(self, **extra):
return pve.create_cmds(100, dict(SPEC, **extra))
def test_without_the_marker_nothing_changes(self):
"""La VM ordinaire garde exactement la séquence d'avant."""
joint = " ".join(self._cmds())
self.assertNotIn("--bios", joint)
self.assertNotIn("efidisk", joint)
def test_with_the_marker_the_firmware_is_ovmf(self):
creation = self._cmds(uefi=True)[0]
self.assertIn("qm create 100", creation)
self.assertIn("--bios ovmf", creation)
def test_the_efi_disk_is_added(self):
"""« --bios ovmf » sans disque EFI démarre, mais ne retient RIEN :
l'entrée d'amorçage écrite par l'invité est perdue au redémarrage."""
efi = [c for c in self._cmds(uefi=True) if "efidisk0" in c]
self.assertEqual(1, len(efi))
self.assertIn("efitype=4m", efi[0])
def test_the_efi_disk_lives_on_the_same_storage(self):
"""Un stockage inventé fait échouer « qm set » après que la VM
existe : elle reste alors à moitié créée."""
efi = [c for c in self._cmds(uefi=True) if "efidisk0" in c][0]
self.assertIn(f" {SPEC['storage']}:0,", efi)
def test_secure_boot_is_off(self):
"""Un chargeur non signé par les clés Microsoft est refusé, et
l'image ne démarre pas du tout — la même raison que côté qemu."""
efi = [c for c in self._cmds(uefi=True) if "efidisk0" in c][0]
self.assertIn("pre-enrolled-keys=0", efi)
def test_the_efi_disk_comes_before_the_start(self):
"""Ajouter un disque à une VM qui tourne déjà ne l'amorce pas
dessus."""
cmds = self._cmds(uefi=True, start=True)
i_efi = next(i for i, c in enumerate(cmds) if "efidisk0" in c)
i_start = next(
i for i, c in enumerate(cmds) if c.endswith("qm start 100")
)
self.assertLess(i_efi, i_start)
class TousLesConstructeursDeSpecLeDisent(unittest.TestCase):
"""create_cmds lit « uefi » dans le spec, et un spec qui l'omet vaut
SeaBIOS sans rien dire.
Compter les appels dans UN fichier laissait échapper tout constructeur
écrit ailleurs, et il y en avait un. Le garde-fou cherche donc les
appelants plutôt que de les supposer.
"""
def _appelants(self):
trouves = []
for dossier in ("script", "long_test"):
for chemin in sorted((RACINE / dossier).rglob("*.py")):
if chemin.name == "proxmox_deploy.py":
continue
texte = chemin.read_text(encoding="utf-8")
if "create_cmds(" in texte:
trouves.append((chemin, texte))
return trouves
def test_the_callers_are_found_at_all(self):
"""Un test qui ne trouve plus personne passerait en restant vert."""
self.assertTrue(self._appelants())
def test_every_one_of_them_declares_the_firmware(self):
for chemin, texte in self._appelants():
with self.subTest(fichier=chemin.name):
self.assertIn('"uefi"', texte)
def test_none_of_them_decides_it_itself(self):
"""Écrire « nixos » à côté du spec ferait deux autorités sur une même
question, et la seconde vieillirait sans qu'on le sache."""
for chemin, texte in self._appelants():
with self.subTest(fichier=chemin.name):
self.assertNotIn('"uefi": True', texte)
self.assertNotIn('"uefi": "nixos"', texte)
if __name__ == "__main__":
unittest.main()