diff --git a/conf/nixos/erplibre.nix b/conf/nixos/erplibre.nix index bbcd12d..c4e4c53 100644 --- a/conf/nixos/erplibre.nix +++ b/conf/nixos/erplibre.nix @@ -146,6 +146,24 @@ wget unzip wkhtmltopdf + # « xmlsec » pose bin/xmlsec1, que les CINQ autres plateformes posent + # aussi. Le manifeste d'auth_saml (OCA server-auth, présent dans + # l'addons_path) déclare « bin: ["xmlsec1"] », et Odoo REFUSE d'installer + # ou de mettre à niveau le module tant que le binaire n'est pas dans le + # PATH : « Unable to find 'xmlsec1' in path », en boîte de dialogue. + xmlsec + # « parallel » et « shfmt » sont appelés par leur NOM NU, l'un par + # script/database/db_drop_all.py, l'autre par script/maintenance/ + # format_bash.sh. Sans parallel, « make db_drop_all » annonce des bases + # détruites qui ne l'ont pas été — une opération destructrice qui rend un + # succès qu'elle n'a pas obtenu. + parallel + shfmt + # « cloud-utils » porte growpart, qu'aucune autre voie ne fournit ici. + # L'agrandissement du disque s'écrit « sudo growpart … || true » : sans le + # binaire, il rend 0 sans rien agrandir, et la VM garde la taille de son + # image pendant que le déploiement annonce la taille demandée. + cloud-utils ]; # Le profil du système ne porte PAS « /include » : la liste par défaut de @@ -178,6 +196,51 @@ PKG_CONFIG_PATH = "/run/current-system/sw/lib/pkgconfig"; }; + # L'agent invité, DÉCLARÉ ici plutôt que reçu de l'image. + # + # L'image épinglée l'active déjà (son configuration.nix porte la ligne), et + # c'est précisément le problème : la garantie appartient alors au tiers qui + # rebâtit l'image, pas au dépôt. Les quatre autres distributions reçoivent + # l'agent par le runcmd du déploiement, qui appelle leur gestionnaire de + # paquets — geste impossible sur un système déclaratif. Ici, c'est cette + # ligne ou rien. + # + # Sans agent, la voie Proxmox perd sa source PRIMAIRE d'adresse et retombe + # sur « ip neigh ». Déclarer l'option deux fois est sans effet : une option + # booléenne ne rompt l'évaluation que sur des valeurs DIFFÉRENTES. Hors + # QEMU, l'unité ne démarre pas — elle n'a pas de section [Install] et + # attend une règle udev sur le port virtio. + services.qemuGuest.enable = true; + + # Ce que l'agent doit trouver dans son PATH. + # + # Une unité systemd n'hérite pas du profil du système : celle-ci ne porte + # que coreutils, findutils, grep, sed et systemd. Or « guest-exec » exécute + # les commandes du produit DANS ce PATH, et l'agrandissement du disque + # commence par « findmnt -no SOURCE / » — absent, donc code 127 dès la + # première ligne. + systemd.services.qemu-guest-agent.path = with pkgs; [ + util-linux + e2fsprogs + cloud-utils + ]; + + # Le port d'Odoo, OUVERT. + # + # NixOS active un pare-feu par défaut ; aucune des images cloud des quatre + # autres distributions n'en active un. Le service écoute bien sur + # 0.0.0.0:8069 et répond en local, mais l'extérieur ne reçoit RIEN — pas un + # refus, un silence, donc une attente jusqu'au délai. Ce qui sonde depuis + # l'hôte conclut « Odoo absent » sur une machine où il tourne, et le journal + # de l'installation ne porte aucune trace de la cause : elle est dans le + # pare-feu, pas dans l'application. + # + # 8069 SEUL. Le port websocket est configuré à 8072, mais Odoo ne le lie + # qu'en mode multi-processus, que cette configuration n'emploie pas : rien + # n'y écoute, et l'ouvrir donnerait un port béant sans service derrière. + # PostgreSQL n'écoute déjà que sur la boucle locale et n'a rien à ouvrir. + networking.firewall.allowedTCPPorts = [ 8069 ]; + # Le service ERPLibre, DÉCLARÉ et non écrit. # # Sur toute autre distribution l'installation dépose l'unité par diff --git a/test/test_install_nixos.py b/test/test_install_nixos.py index b9785c8..f191c3b 100644 --- a/test/test_install_nixos.py +++ b/test/test_install_nixos.py @@ -337,6 +337,111 @@ class LeServiceEstDeclare(unittest.TestCase): self.assertIn(marqueur, self.script) +class CeQueLeDepotAPPELLE_ParSonNomNu(unittest.TestCase): + """Un système déclaratif n'a que ce qui est écrit. + + Les quatre autres distributions posent ces outils par leur gestionnaire + de paquets ; ici, l'absence ne se voit qu'à l'usage, et tard. + """ + + def setUp(self): + self.src = MODULE.read_text(encoding="utf-8") + + def test_xmlsec_is_declared(self): + """Le manifeste d'auth_saml (OCA server-auth, dans l'addons_path) + déclare « bin: ["xmlsec1"] » : Odoo REFUSE d'installer et de mettre à + niveau le module tant que le binaire n'est pas dans le PATH.""" + self.assertIn("\n xmlsec\n", self.src) + + def test_the_tools_called_by_bare_name_are_declared(self): + """« parallel » et « shfmt » sont invoqués sans chemin. Sans + parallel, « make db_drop_all » annonce des bases détruites qui ne + l'ont pas été.""" + for outil in ("parallel", "shfmt"): + with self.subTest(outil=outil): + self.assertIn(f"\n {outil}\n", self.src) + + def test_growpart_has_a_provider(self): + """L'agrandissement s'écrit « sudo growpart … || true » : sans le + binaire il rend 0 sans rien agrandir, et la VM garde la taille de son + image pendant que le déploiement annonce celle demandée.""" + self.assertIn("\n cloud-utils\n", self.src) + + +class LAgentInviteVientDuDepot(unittest.TestCase): + """L'image épinglée active déjà l'agent — et c'est le problème : la + garantie appartient alors au tiers qui la rebâtit. + + Les quatre autres distributions le reçoivent du runcmd, qui appelle leur + gestionnaire de paquets. Sur un système déclaratif, c'est cette ligne ou + rien. + """ + + def setUp(self): + self.src = MODULE.read_text(encoding="utf-8") + + def test_the_agent_is_declared(self): + self.assertIn("services.qemuGuest.enable = true;", self.src) + + def test_its_path_carries_what_guest_exec_runs(self): + """Une unité systemd n'hérite pas du profil du système. « guest-exec » + exécute les commandes du produit DANS ce PATH, et l'agrandissement + commence par « findmnt -no SOURCE / » : absent, code 127 dès la + première ligne.""" + i = self.src.index("systemd.services.qemu-guest-agent.path") + bloc = self.src[i : self.src.index("];", i)] + for paquet in ("util-linux", "e2fsprogs", "cloud-utils"): + with self.subTest(paquet=paquet): + self.assertIn(paquet, bloc) + + +class LePortDOdooTraverseLePareFeu(unittest.TestCase): + """NixOS active un pare-feu par défaut ; aucune des images cloud des + quatre autres distributions n'en active un. + + Le service écoute bien sur 0.0.0.0:8069 et répond en local, mais + l'extérieur ne reçoit RIEN — pas un refus, un silence, donc une attente + jusqu'au délai. Ce qui sonde depuis l'hôte conclut « Odoo absent » sur une + machine où il tourne, et le journal de l'installation ne porte aucune + trace de la cause. + """ + + def setUp(self): + self.src = MODULE.read_text(encoding="utf-8") + + def _port_sonde(self): + """Le port que l'hôte sonde VRAIMENT, lu de sa signature : une + constante recopiée ici vieillirait sans que rien ne le dise.""" + import inspect + import sys + + sys.path.insert(0, str(RACINE)) + from script.todo.qemu_install_monitor import _port_open + + return inspect.signature(_port_open).parameters["port"].default + + def test_the_port_probed_from_the_host_is_the_one_opened(self): + port = self._port_sonde() + self.assertIn( + f"networking.firewall.allowedTCPPorts = [ {port} ];", self.src + ) + + def test_the_firewall_is_not_simply_turned_off(self): + """Le couper ouvrirait TOUT. PostgreSQL n'écoute aujourd'hui que sur + la boucle locale, mais un module qui désactive le pare-feu ne le + protégerait plus le jour où cela changerait.""" + self.assertNotIn("networking.firewall.enable = false", self.src) + + def test_only_what_serves_is_opened(self): + """Odoo ne lie le port websocket qu'en mode multi-processus, que cette + configuration n'emploie pas : rien n'y écoute, et l'ouvrir donnerait + un port béant sans service derrière.""" + i = self.src.index("allowedTCPPorts") + ligne = self.src[i : self.src.index("\n", i)] + self.assertNotIn("8072", ligne) + self.assertNotIn("5432", ligne) + + class LeMenuNEcritPasDansEtcSurNixos(unittest.TestCase): def _cmd(self, prod=False): import sys