[FIX] nixos : pare-feu ouvert, et ce qu'on appelle par son nom déclaré

NixOS active un pare-feu par défaut ; aucune des quatre autres images
cloud n'en active. Le service écoutait bien et répondait en local, mais
l'hôte ne recevait RIEN — pas un refus, un silence jusqu'au délai — et le
suivi déclarait Odoo absent sur une machine où il tournait. Mesuré : 000
après 12 s, puis 303 en 9 ms.

Un système déclaratif n'a que ce qui est écrit : xmlsec1, sans lequel
Odoo refuse d'installer auth_saml ; parallel et shfmt, appelés par leur
nom nu ; growpart, dont l'absence faisait rendre 0 à un agrandissement qui
n'agrandissait rien ; et l'agent invité, qui venait de l'image.

--- EN ---

NixOS enables a firewall by default; none of the four other cloud images
does. The service did listen and answered locally, but the host received
NOTHING — not a refusal, silence until the timeout — and the monitor
declared Odoo absent on a machine where it was running. Measured: 000
after 12 s, then 303 in 9 ms.

A declarative system has only what is written: xmlsec1, without which Odoo
refuses to install auth_saml; parallel and shfmt, called by bare name;
growpart, whose absence made a disk grow return 0 while growing nothing;
and the guest agent, which came from the image.

Assisted-by: Claude Opus 5
This commit is contained in:
Mathieu Benoit 2026-09-16 00:43:40 -04:00
parent e0624d9aaa
commit c1f2f8db7f
2 changed files with 168 additions and 0 deletions

View file

@ -146,6 +146,24 @@
wget wget
unzip unzip
wkhtmltopdf 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 # 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"; 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. # Le service ERPLibre, DÉCLARÉ et non écrit.
# #
# Sur toute autre distribution l'installation dépose l'unité par # Sur toute autre distribution l'installation dépose l'unité par

View file

@ -337,6 +337,111 @@ class LeServiceEstDeclare(unittest.TestCase):
self.assertIn(marqueur, self.script) 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): class LeMenuNEcritPasDansEtcSurNixos(unittest.TestCase):
def _cmd(self, prod=False): def _cmd(self, prod=False):
import sys import sys