erplibre/test/test_proxmox_uefi.py
Mathieu Benoit f3edd806bc [ADD] proxmox : NixOS s'y déploie, ce qui a pris trois correctifs
Aucune lecture de code ne les aurait trouvés. Son image n'a pas de secteur
d'amorçage BIOS : une VM créée en SeaBIOS se déclare « running » avec une
console MUETTE, d'où un marqueur par distribution — Debian 13 démarre en
SeaBIOS sur le même hôte, et renverser le défaut coûterait un disque EFI
à chaque VM du parc.

« --ciuser » s'en remet au compte par DÉFAUT de l'image, que NixOS nomme
autrement : le cloud-config du dépôt part en extrait pour toutes les
distributions. Et le lecteur cloud-init passe sur le bus SCSI, une image
bâtie pour virtio seul ne voyant jamais un lecteur IDE.

--- EN ---

No code reading would have found them. Its image has no BIOS boot sector:
a VM created in SeaBIOS reports "running" with a SILENT console, hence a
per-distribution marker — Debian 13 boots in SeaBIOS on the same host, and
flipping the default would cost an EFI disk on every VM.

"--ciuser" defers to the image's DEFAULT account, which NixOS names
otherwise: the repository's cloud-config now travels as a snippet for
every distribution. And the cloud-init drive moves to the SCSI bus, an
image built for virtio alone never seeing an IDE drive.

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

137 lines
5.1 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 LeMenuTransmetLeMarqueur(unittest.TestCase):
"""Un catalogue qui sait et un menu qui ne dit pas laisse la VM en
SeaBIOS : les deux points d'appel sont tenus."""
SRC = (RACINE / "script/todo/proxmox_menu.py").read_text(encoding="utf-8")
def test_both_call_sites_pass_it(self):
self.assertEqual(2, self.SRC.count('"uefi": mod.requiert_uefi('))
def test_it_is_read_from_the_catalogue_not_hardcoded(self):
"""Écrire « nixos » ici ferait deux autorités sur une même question."""
self.assertNotIn('"uefi": True', self.SRC)
self.assertNotIn('"uefi": "nixos"', self.SRC)
if __name__ == "__main__":
unittest.main()