erplibre/test/test_qemu_nixos.py
Mathieu Benoit fce1d5b00b [ADD] qemu : NixOS déployable, de son image à ses dépendances
Neuvième système du catalogue, et le seul dont aucune distribution ne
publie d'image cloud : celle d'un tiers est épinglée et sa somme vérifiée
à chaque téléchargement. Son seed diffère aussi — networkd prend la clé
d'un bloc pour un NOM d'interface là où netplan honore un « match: », et
sshd refuse un compte dont le shell n'existe pas, d'où /bin/sh.

Ses dépendances se DÉCLARENT dans conf/nixos/erplibre.nix : envfs répond
à /usr/bin/env, nix-ld donne aux roues manylinux leur chargeur. Vérifié
sur une VM : le verrou de 362 paquets s'installe et Odoo répond.

--- EN ---

Ninth system of the catalogue, and the only one no distribution publishes
a cloud image for: a third party's is pinned and its sum verified at every
download. Its seed differs too — networkd reads a block's key as an
interface NAME where netplan honours a "match:", and sshd refuses an
account whose shell does not exist, hence /bin/sh.

Its dependencies are DECLARED in conf/nixos/erplibre.nix: envfs answers
/usr/bin/env, nix-ld gives manylinux wheels their loader. Checked on a VM:
the 362-package lock installs and Odoo answers.

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

230 lines
9.4 KiB
Python

#!/usr/bin/env python3
# © 2026 TechnoLibre (http://www.technolibre.ca)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
"""NixOS au catalogue des systèmes déployables.
NixOS ne publie AUCUNE image cloud : sa page de téléchargement offre des ISO,
des AMI Amazon et Docker, rien d'autre. L'image vient donc d'un tiers qui la
reconstruit depuis nixpkgs — ce qui change deux choses par rapport aux sept
autres distributions du catalogue, et ces tests gardent les deux :
- la release est ÉPINGLÉE et sa somme sha256 FIGÉE dans le dépôt. L'amont
n'en publie pas ; celle-ci a été relevée une fois à la revue, et c'est la
seule chose qui distingue l'image revue de n'importe quel fichier servi
sous la même URL. Elle se vérifie donc sans drapeau à passer ;
- l'origine se DIT à l'écran avant de déployer : le système de base d'une VM
n'est pas un détail d'implémentation.
Le reste est le lot commun d'une distribution qu'on ajoute : deux catalogues
à tenir d'accord, un nom de fichier de cache qui ne doit pas retomber sur
celui de Fedora, une famille de paquets, un groupe d'administration.
S'y ajoute ce qu'un système DÉCLARATIF ne peut pas recevoir : les outils de
VM qui posent un binaire par « curl | sh » ou écrivent dans /usr/local n'y
survivraient pas à la reconstruction suivante.
"""
import importlib.util
import sys
import unittest
from pathlib import Path
from unittest import mock
sys.argv = ["todo.py"]
from script.todo.todo import TODO # noqa: E402
RACINE = Path(__file__).resolve().parents[1]
try:
import yaml
YAML = True
except Exception: # pragma: no cover - dépend de l'environnement
YAML = False
def _deploy_qemu():
path = RACINE / "script/qemu/deploy_qemu.py"
spec = importlib.util.spec_from_file_location("deploy_qemu", path)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
DQ = _deploy_qemu()
class LeCatalogue(unittest.TestCase):
def test_it_is_in_both_catalogues_with_the_same_versions(self):
"""Le sélecteur en ligne tient sa propre copie : deux tables à la
main, dont une qu'on oublie."""
self.assertIn("nixos", DQ.DISTROS)
self.assertIn("nixos", TODO._QEMU_DISTROS)
self.assertEqual(
sorted(DQ.DISTROS["nixos"][0]),
sorted(TODO._QEMU_DISTROS["nixos"][0]),
)
self.assertEqual(
DQ.DISTROS["nixos"][1], TODO._QEMU_DISTROS["nixos"][1]
)
def test_the_version_is_the_one_inside_the_image(self):
"""Annoncer une version que l'image ne porte pas mentirait sur ce
qu'on livre : la release épinglée embarque 25.11."""
self.assertIn("25.11", DQ.DISTROS["nixos"][0])
code, osinfo, ram, disque = DQ.DISTROS["nixos"][0]["25.11"]
self.assertEqual("nixos-25.11", osinfo)
self.assertEqual(2048, ram)
self.assertEqual("40G", disque)
def test_the_disk_floor_is_above_the_rolling_neighbour(self):
"""Le store Nix garde chaque génération et ne se purge qu'à la main :
le plancher est plus haut que celui d'Arch, à dessein."""
_c, _o, ram_arch, _d = DQ.DISTROS["arch"][0]["latest"]
_c, _o, ram_nix, _d = DQ.DISTROS["nixos"][0]["25.11"]
self.assertGreater(ram_nix, ram_arch)
def test_it_is_x86_only(self):
"""Le tiers ne publie qu'un asset, sans architecture dans son nom."""
self.assertNotIn("nixos", DQ.ARM64_DISTROS)
self.assertNotIn("nixos", DQ.S390X_DISTROS)
class LImage(unittest.TestCase):
def test_the_release_is_pinned(self):
"""« latest » changerait le système de base d'un déploiement à
l'autre, sans que rien ne le dise."""
url = DQ.image_candidates("nixos", "25.11", "amd64", "25.11", True)[0]
self.assertIn(DQ.NIXOS_IMAGE_TAG, url)
self.assertNotIn("latest", url)
def test_the_cache_name_carries_the_tag(self):
"""Changer le tag doit RETÉLÉCHARGER, et non réutiliser une image qui
ne répond plus à la somme figée."""
nom = DQ.default_image_name("nixos", "25.11", "amd64", "25.11")
self.assertIn(DQ.NIXOS_IMAGE_TAG, nom)
def test_the_cache_name_never_falls_back_to_fedora(self):
"""Le repli de fin de default_image_name() nomme « fedora-cloud-* » :
une distribution oubliée là met son image en cache sous le nom d'une
autre, silencieusement."""
nom = DQ.default_image_name("nixos", "25.11", "amd64", "25.11")
self.assertNotIn("fedora", nom)
def test_the_pinned_sum_looks_like_a_sha256(self):
self.assertEqual(64, len(DQ.NIXOS_IMAGE_SHA256))
int(DQ.NIXOS_IMAGE_SHA256, 16)
def test_a_wrong_sum_stops_the_deployment(self):
"""Continuer reviendrait à installer un système que personne n'a
regardé."""
with mock.patch.object(DQ, "open", mock.mock_open(read_data=b"x")):
with self.assertRaises(SystemExit):
DQ.verify_pinned_sha256("nixos", Path("/x"), False)
def test_other_distros_are_not_checked_against_it(self):
"""Leur somme, quand elle existe, vient de leur propre SHA256SUMS."""
DQ.verify_pinned_sha256("ubuntu", Path("/inexistant"), False)
class CeQuiSeDitALEcran(unittest.TestCase):
def test_the_third_party_origin_is_named(self):
note = DQ.image_source_note("nixos")
self.assertIsNotNone(note)
url, tag = note
self.assertIn(tag, url)
self.assertTrue(url.startswith("https://"))
def test_an_official_image_says_nothing(self):
"""Commenter chaque image noierait la seule qui le mérite."""
for distro in ("ubuntu", "debian", "fedora", "arch", "opensuse"):
with self.subTest(distro=distro):
self.assertIsNone(DQ.image_source_note(distro))
def test_the_recap_shows_the_link_and_the_warning(self):
todo = TODO.__new__(TODO)
spec = {
"vms": [{"name": "vm", "distro": "nixos"}],
"install": {"branch": "develop"},
}
with mock.patch.object(TODO, "_qemu_import_module", return_value=DQ):
lignes = todo._qemu_image_lines(spec)
texte = " ".join(lignes)
self.assertIn(DQ.NIXOS_IMAGE_TAG, texte)
self.assertIn("https://", texte)
self.assertIn("ERPLibre", texte)
def test_without_an_install_the_warning_is_not_shown(self):
"""Une VM NixOS sans installation ERPLibre n'a rien qui échoue : le
dire serait du bruit."""
todo = TODO.__new__(TODO)
spec = {"vms": [{"name": "vm", "distro": "nixos"}], "install": None}
with mock.patch.object(TODO, "_qemu_import_module", return_value=DQ):
lignes = todo._qemu_image_lines(spec)
self.assertNotIn("ERPLibre", " ".join(lignes))
def test_a_deployment_without_nixos_says_nothing(self):
todo = TODO.__new__(TODO)
spec = {"vms": [{"name": "vm", "distro": "debian"}], "install": None}
with mock.patch.object(TODO, "_qemu_import_module", return_value=DQ):
self.assertEqual([], todo._qemu_image_lines(spec))
class LeSystemeInvite(unittest.TestCase):
def test_the_package_family_is_its_own(self):
"""Aucune des quatre familles impératives : ce qui doit rester se
déclare, et le guide de connexion le dit dans cet ordre."""
self.assertEqual("nix", DQ.DISTRO_PKG["nixos"])
self.assertEqual("nix", TODO._QEMU_DISTRO_FAMILY["nixos"])
self.assertIn("nix", DQ.PKG_GUIDE)
def test_the_guide_never_suggests_an_imperative_install(self):
commandes = [c for c, _fr, _en in DQ.PKG_GUIDE["nix"]]
self.assertTrue(any("nixos-rebuild" in c for c in commandes))
self.assertTrue(
any("configuration.nix" in c for c in commandes), commandes
)
for interdit in ("apt ", "dnf ", "pacman ", "zypper "):
self.assertFalse(any(interdit in c for c in commandes), interdit)
def test_the_admin_group_is_wheel(self):
self.assertIn("wheel", DQ.user_groups("nixos").split(", "))
def test_the_label_names_the_distribution(self):
self.assertEqual("NixOS 25.11", DQ.distro_label("nixos", "25.11"))
@unittest.skipUnless(YAML, "PyYAML absent")
def test_the_cloud_config_stays_parsable(self):
args = DQ.build_parser().parse_args(
["--distro", "fedora", "--hostname", "vm"]
)
args.distro = "nixos"
vu = yaml.safe_load(
DQ.build_cloud_config(args, None, ["ssh-ed25519 A"])
)
(compte,) = vu["users"]
self.assertEqual("/bin/sh", compte["shell"])
class CeQuUnSystemeDeclaratifNeRecoitPas(unittest.TestCase):
def test_no_vm_tool_is_offered(self):
"""Chacun pose un binaire par « curl | sh », écrit dans /usr/local ou
installe par le gestionnaire du système. Aucun de ces gestes ne
survit à la reconstruction suivante."""
tous = list(TODO._QEMU_VM_TOOLS)
self.assertEqual(
[], TODO._qemu_tools_for(tous, "amd64", "gnome", "nixos")
)
def test_the_other_distros_keep_theirs(self):
"""Le bornage ne doit rien retirer à qui sait s'en servir."""
tous = list(TODO._QEMU_VM_TOOLS)
for distro in ("ubuntu", "debian", "arch", "fedora"):
with self.subTest(distro=distro):
self.assertTrue(
TODO._qemu_tools_for(tous, "amd64", "gnome", distro)
)
if __name__ == "__main__":
unittest.main()