[FIX] proxmox : le pont NAT s'écrivait avant de savoir si le NAT existe
« Table does not exist » : six lignes d'iptables et « code de retour 1 », après avoir déjà posé la strophe dans /etc/network/interfaces. Rien dans ce bruit ne dit qu'il faut redémarrer. L'hôte tournait le noyau cloud de Debian, qui est dépouillé de tout netfilter — aucun module NAT, ni legacy ni nft. Et le cas n'a rien d'exotique : c'est notre propre install_proxmox.sh qui le produit. Il pose le noyau Proxmox sans redémarrer, à raison — lancé par ssh, un reboot couperait la session et ferait passer l'installation pour un échec. Une Proxmox imbriquée fraîchement installée est donc TOUJOURS dans cet état. L'avertissement sur le noyau existait déjà, mais à la CONFIRMATION de l'hôte, et l'hôte est ensuite mémorisé : on revient des jours plus tard créer un pont, et plus personne ne rappelle rien. Le garde va donc là où la conséquence tombe, et AVANT toute écriture. Il interroge la table NAT elle-même et non le NOM du noyau — « -pve » est un indice, pas une preuve — puis nomme le noyau en cours, celui qui est posé, et la commande qui règle l'affaire. Le sommaire de déploiement le dit désormais aussi, tant qu'on lit encore l'écran plutôt qu'au bout d'un journal d'une heure. --- EN --- "Table does not exist": six lines of iptables and "exit code 1", after the stanza had already been written into /etc/network/interfaces. Nothing in that noise says a reboot is needed. The host was running Debian's cloud kernel, stripped of all netfilter — no NAT module, legacy or nft. And the case is not exotic: our own install_proxmox.sh produces it. It installs the Proxmox kernel without rebooting, rightly — run over ssh, a reboot would cut the session and make the install look failed. A freshly installed nested Proxmox is therefore ALWAYS in this state. The kernel warning already existed, but at host CONFIRMATION, and the host is then remembered: you come back days later to create a bridge and nothing reminds you. So the guard moves to where the consequence lands, and BEFORE any write. It asks the NAT table itself rather than the kernel's NAME — "-pve" is a hint, not a proof — then names the running kernel, the installed one, and the command that settles it. The deployment summary now says it too, while the screen is still being read rather than at the end of an hour-long log. Assisted-by: Claude Opus 5
This commit is contained in:
parent
30ffb262b6
commit
eb5e607e1e
5 changed files with 317 additions and 0 deletions
|
|
@ -218,6 +218,38 @@ def parse_kernel(text: str) -> str:
|
|||
return trouve.group(1) if trouve else ""
|
||||
|
||||
|
||||
# Ce qu'il faut savoir AVANT d'écrire un pont NAT, en un aller-retour.
|
||||
#
|
||||
# Le noyau seul ne suffit pas à juger : « -pve » dans son nom est un indice,
|
||||
# pas une preuve, et l'inverse non plus — c'est la table NAT elle-même qu'on
|
||||
# interroge. « iptables -t nat -S » échoue avec « Table does not exist » quand
|
||||
# aucun module netfilter n'est chargeable, et réussit sinon.
|
||||
NAT_CHECK_CMD = (
|
||||
"uname -r; echo '---ERPLIBRE-NAT---'; "
|
||||
"iptables -t nat -S >/dev/null 2>&1 && echo NAT-OK || echo NAT-KO; "
|
||||
"echo '---ERPLIBRE-PVE-KERNEL---'; "
|
||||
"ls -1 /lib/modules 2>/dev/null | grep -- -pve | sort -V | tail -1"
|
||||
)
|
||||
|
||||
|
||||
def parse_nat_check(text: str) -> dict:
|
||||
"""{"kernel": …, "nat": bool, "pve_kernel": …} depuis NAT_CHECK_CMD.
|
||||
|
||||
`nat` à False sans `pve_kernel` veut dire que l'installation Proxmox n'est
|
||||
pas allée au bout ; avec, qu'elle attend un redémarrage."""
|
||||
brut = strip_ssh_noise(text or "")
|
||||
parts = brut.split("---ERPLIBRE-NAT---")
|
||||
kernel = parts[0].strip().splitlines()
|
||||
reste = parts[1] if len(parts) > 1 else ""
|
||||
suite = reste.split("---ERPLIBRE-PVE-KERNEL---")
|
||||
pve_kernel = suite[1].strip().splitlines() if len(suite) > 1 else []
|
||||
return {
|
||||
"kernel": kernel[-1].strip() if kernel else "",
|
||||
"nat": "NAT-OK" in (suite[0] if suite else ""),
|
||||
"pve_kernel": pve_kernel[-1].strip() if pve_kernel else "",
|
||||
}
|
||||
|
||||
|
||||
def parse_qm_list(text: str) -> list:
|
||||
"""Sortie de « qm list » -> [{vmid, name, status, mem, disk}].
|
||||
|
||||
|
|
|
|||
|
|
@ -648,6 +648,56 @@ class ProxmoxMenuMixin:
|
|||
parts = (sortie or "").split()
|
||||
return parts[parts.index("dev") + 1] if "dev" in parts else ""
|
||||
|
||||
def _pve_nat_ready(self, host):
|
||||
"""(prêt ?, lignes à dire). La table NAT existe-t-elle sur cet hôte ?
|
||||
|
||||
Posée ICI, au moment d'écrire un pont NAT, et non à la connexion : le
|
||||
noyau est vérifié quand on confirme l'hôte, mais l'hôte est ensuite
|
||||
MÉMORISÉ — on revient des jours plus tard créer un pont, et plus
|
||||
personne ne rappelle rien. Le garde doit être là où la conséquence
|
||||
tombe.
|
||||
|
||||
Sans cette question, ifupdown2 rendait six lignes d'iptables et « code
|
||||
de retour 1 », après avoir déjà écrit la strophe dans
|
||||
/etc/network/interfaces. Rien dans ce bruit ne dit qu'il faut
|
||||
redémarrer.
|
||||
|
||||
Le cas n'a rien d'exotique : notre propre install_proxmox.sh pose le
|
||||
noyau Proxmox sans redémarrer — lancé par ssh, un reboot couperait la
|
||||
session. Une Proxmox imbriquée fraîchement installée est donc TOUJOURS
|
||||
dans cet état, sur le noyau cloud de Debian, qui est dépouillé de tout
|
||||
netfilter."""
|
||||
from script.proxmox import proxmox_deploy as pve
|
||||
|
||||
_c, out = pve.run(host, pve.NAT_CHECK_CMD, 40)
|
||||
etat = pve.parse_nat_check(out)
|
||||
if etat["nat"]:
|
||||
return True, []
|
||||
lignes = [
|
||||
f"✗ {t('No NAT table on this host: the bridge would lead nowhere.')}",
|
||||
f" {t('Running kernel:')} {etat['kernel'] or '?'}",
|
||||
]
|
||||
if etat["pve_kernel"]:
|
||||
lignes += [
|
||||
f" {t('The distribution kernel carries no netfilter module.')}",
|
||||
f" {t('Proxmox kernel installed:')} {etat['pve_kernel']}"
|
||||
f" — {t('taken at next boot')}",
|
||||
f"→ ssh {host.get('target', '')} sudo reboot,"
|
||||
f" {t('then come back here.')}",
|
||||
]
|
||||
else:
|
||||
lignes.append(
|
||||
f" {t('No Proxmox kernel installed: finish the install first.')}"
|
||||
)
|
||||
return False, lignes
|
||||
|
||||
def _pve_nat_reason(self, host):
|
||||
"""La même chose en UNE ligne, pour l'écran Textual."""
|
||||
ok, lignes = self._pve_nat_ready(host)
|
||||
if ok:
|
||||
return ""
|
||||
return " ".join(ligne.strip("✗→ ") for ligne in lignes[:2])
|
||||
|
||||
def _pve_make_internal_bridge(self):
|
||||
"""Crée le pont INTERNE et le rend, ou ('', raison). SANS rien demander.
|
||||
|
||||
|
|
@ -662,6 +712,11 @@ class ProxmoxMenuMixin:
|
|||
host = self._pve_host(ask=False)
|
||||
if not host:
|
||||
return "", t("No Proxmox host.")
|
||||
# AVANT d'écrire quoi que ce soit : une strophe posée puis un
|
||||
# « ifup » qui échoue laisse le fichier modifié et le pont absent.
|
||||
raison = self._pve_nat_reason(host)
|
||||
if raison:
|
||||
return "", raison
|
||||
uplink = self._pve_uplink()
|
||||
for cmd in pve.bridge_setup_cmds(uplink=uplink):
|
||||
code, sortie = pve.run(host, cmd, 180)
|
||||
|
|
@ -704,6 +759,13 @@ class ProxmoxMenuMixin:
|
|||
f" ⚠ {t('This moves the host address: do it from a console.')}"
|
||||
)
|
||||
return ""
|
||||
host = self._pve_host(ask=False)
|
||||
ok, lignes = self._pve_nat_ready(host) if host else (True, [])
|
||||
if not ok:
|
||||
print()
|
||||
for ligne in lignes:
|
||||
print(f" {ligne}")
|
||||
return ""
|
||||
uplink = self._pve_uplink()
|
||||
print(f" {t('uplink for NAT')} : {uplink or t('none')}")
|
||||
for cmd in pve.bridge_setup_cmds(uplink=uplink):
|
||||
|
|
@ -1292,6 +1354,19 @@ class ProxmoxMenuMixin:
|
|||
)
|
||||
if vm.get("alias"):
|
||||
print(f" ssh {vm['alias']}")
|
||||
# Une VM qui vient de recevoir Proxmox tourne encore le noyau de
|
||||
# son image cloud : celui-ci n'a AUCUN module netfilter, donc ni
|
||||
# pont NAT ni VM à l'intérieur. install_proxmox.sh ne redémarre
|
||||
# pas de lui-même — lancé par ssh, un reboot couperait la session
|
||||
# et ferait passer l'installation pour un échec. Le dire ICI, où
|
||||
# on lit encore l'écran, plutôt qu'au bout d'un journal d'une
|
||||
# heure : sans cela on le redécouvre en créant un pont, devant six
|
||||
# lignes d'iptables qui ne parlent pas de redémarrage.
|
||||
if self._pve_installs_proxmox(vm, spec):
|
||||
print(
|
||||
f" ⚠ {t('reboot it to boot the Proxmox kernel:')}"
|
||||
f" ssh {vm.get('alias') or vm['name']} sudo reboot"
|
||||
)
|
||||
if spec.get("install"):
|
||||
print(
|
||||
f" {t('Install:')} {spec['install'].get('label') or ''}"
|
||||
|
|
@ -1299,6 +1374,16 @@ class ProxmoxMenuMixin:
|
|||
)
|
||||
print(f" {t('Log:')} {session}")
|
||||
|
||||
@staticmethod
|
||||
def _pve_installs_proxmox(vm, spec) -> bool:
|
||||
"""Cette VM reçoit-elle l'hyperviseur Proxmox VE ?
|
||||
|
||||
Jugé sur la commande EFFECTIVE de la VM — celle que son système lui
|
||||
impose, sinon le choix commun — et non sur son nom ni sur sa
|
||||
distribution : un parc mixte est le cas normal ici."""
|
||||
cmd = vm.get("install_cmd") or (spec.get("install") or {}).get("cmd")
|
||||
return "install_proxmox.sh" in (cmd or "")
|
||||
|
||||
def _pve_confirm_spec(self, host, spec):
|
||||
"""Récapitulatif puis confirmation, dans le TERMINAL.
|
||||
|
||||
|
|
|
|||
|
|
@ -3362,6 +3362,38 @@ TRANSLATIONS = {
|
|||
"fr": "Aucun stockage capable d'héberger un disque de VM.",
|
||||
"en": "No storage able to hold a VM disk.",
|
||||
},
|
||||
"No NAT table on this host: the bridge would lead nowhere.": {
|
||||
"fr": "Pas de table NAT sur cet hôte : le pont ne mènerait nulle part.",
|
||||
"en": "No NAT table on this host: the bridge would lead nowhere.",
|
||||
},
|
||||
"reboot it to boot the Proxmox kernel:": {
|
||||
"fr": "à redémarrer pour amorcer le noyau Proxmox :",
|
||||
"en": "reboot it to boot the Proxmox kernel:",
|
||||
},
|
||||
"Running kernel:": {
|
||||
"fr": "Noyau en cours :",
|
||||
"en": "Running kernel:",
|
||||
},
|
||||
"The distribution kernel carries no netfilter module.": {
|
||||
"fr": "Le noyau de la distribution ne porte aucun module netfilter.",
|
||||
"en": "The distribution kernel carries no netfilter module.",
|
||||
},
|
||||
"Proxmox kernel installed:": {
|
||||
"fr": "Noyau Proxmox installé :",
|
||||
"en": "Proxmox kernel installed:",
|
||||
},
|
||||
"taken at next boot": {
|
||||
"fr": "pris au prochain démarrage",
|
||||
"en": "taken at next boot",
|
||||
},
|
||||
"then come back here.": {
|
||||
"fr": "puis revenir ici.",
|
||||
"en": "then come back here.",
|
||||
},
|
||||
"No Proxmox kernel installed: finish the install first.": {
|
||||
"fr": "Aucun noyau Proxmox installé : terminer l'installation d'abord.",
|
||||
"en": "No Proxmox kernel installed: finish the install first.",
|
||||
},
|
||||
"No network bridge on this host.": {
|
||||
"fr": "Aucun pont réseau sur cet hôte.",
|
||||
"en": "No network bridge on this host.",
|
||||
|
|
|
|||
|
|
@ -619,5 +619,59 @@ class TestLeMenu(unittest.TestCase):
|
|||
self.assertEqual(0, res.returncode, res.stderr)
|
||||
|
||||
|
||||
class TestLaTableNat(unittest.TestCase):
|
||||
"""« Table does not exist » : six lignes d'iptables et « code de retour 1 »,
|
||||
après avoir déjà écrit la strophe dans /etc/network/interfaces.
|
||||
|
||||
Rien dans ce bruit ne dit qu'il faut redémarrer. Et le cas n'a rien
|
||||
d'exotique : notre propre install_proxmox.sh pose le noyau Proxmox sans
|
||||
redémarrer — lancé par ssh, un reboot couperait la session. Une Proxmox
|
||||
imbriquée fraîchement installée est donc TOUJOURS sur le noyau cloud de
|
||||
Debian, qui est dépouillé de tout netfilter.
|
||||
|
||||
On demande donc à la table NAT elle-même, et non au NOM du noyau : « -pve »
|
||||
est un indice, pas une preuve."""
|
||||
|
||||
def _sortie(self, kernel, nat, pve_kernel=""):
|
||||
return (
|
||||
f"{kernel}\n---ERPLIBRE-NAT---\n"
|
||||
f"{'NAT-OK' if nat else 'NAT-KO'}\n"
|
||||
f"---ERPLIBRE-PVE-KERNEL---\n{pve_kernel}\n"
|
||||
)
|
||||
|
||||
def test_a_working_host(self):
|
||||
lu = pve.parse_nat_check(
|
||||
self._sortie("7.0.14-14-pve", True, "7.0.14-14-pve")
|
||||
)
|
||||
self.assertTrue(lu["nat"])
|
||||
self.assertEqual(lu["kernel"], "7.0.14-14-pve")
|
||||
|
||||
def test_the_cloud_kernel_waiting_for_a_reboot(self):
|
||||
# L'état exact rapporté : le noyau Proxmox est POSÉ, pas amorcé.
|
||||
lu = pve.parse_nat_check(
|
||||
self._sortie("6.12.101+deb13-cloud-amd64", False, "7.0.14-14-pve")
|
||||
)
|
||||
self.assertFalse(lu["nat"])
|
||||
self.assertEqual(lu["pve_kernel"], "7.0.14-14-pve")
|
||||
|
||||
def test_an_unfinished_install_has_no_pve_kernel(self):
|
||||
lu = pve.parse_nat_check(
|
||||
self._sortie("6.12.101+deb13-cloud-amd64", False)
|
||||
)
|
||||
self.assertFalse(lu["nat"])
|
||||
self.assertEqual(lu["pve_kernel"], "")
|
||||
|
||||
def test_ssh_noise_does_not_become_a_kernel(self):
|
||||
brut = (
|
||||
"Warning: Permanently added 'x' (ED25519) to the list of known"
|
||||
" hosts.\n" + self._sortie("7.0.14-14-pve", True, "7.0.14-14-pve")
|
||||
)
|
||||
self.assertEqual(pve.parse_nat_check(brut)["kernel"], "7.0.14-14-pve")
|
||||
|
||||
def test_the_probe_asks_the_table_not_the_name(self):
|
||||
self.assertIn("iptables -t nat", pve.NAT_CHECK_CMD)
|
||||
self.assertIn("uname -r", pve.NAT_CHECK_CMD)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=1)
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ aucun hôte Proxmox n'est joint.
|
|||
import asyncio
|
||||
import sys
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
sys.argv = ["todo.py"]
|
||||
from script.todo.proxmox_deploy_form import ( # noqa: E402
|
||||
|
|
@ -685,6 +686,119 @@ class TestUnParcMixte(unittest.TestCase):
|
|||
self.assertEqual(vu["kw"]["desktop"], "")
|
||||
|
||||
|
||||
class TestLePontQuiNeMeneraitNullePart(unittest.TestCase):
|
||||
"""Le pont NAT était écrit AVANT qu'on sache si le NAT existe.
|
||||
|
||||
Résultat rapporté : la strophe posée dans /etc/network/interfaces, le
|
||||
pont absent, et six lignes d'iptables qui ne parlent pas de redémarrage.
|
||||
L'avertissement sur le noyau existait — mais à la CONFIRMATION de l'hôte,
|
||||
et l'hôte est ensuite mémorisé : on revient des jours plus tard créer un
|
||||
pont, et plus personne ne rappelle rien."""
|
||||
|
||||
def _todo(self, sortie):
|
||||
import sys
|
||||
|
||||
sys.argv = ["todo.py"]
|
||||
from script.todo.todo import TODO
|
||||
|
||||
todo = TODO.__new__(TODO)
|
||||
vu = []
|
||||
todo._pve_host = lambda ask=True: {"target": "pve9", "sudo": ""}
|
||||
todo._pve_uplink = lambda: "eth0"
|
||||
|
||||
def faux_run(host, cmd, timeout=120):
|
||||
vu.append(cmd)
|
||||
from script.proxmox import proxmox_deploy as pve
|
||||
|
||||
if cmd == pve.NAT_CHECK_CMD:
|
||||
return 0, sortie
|
||||
return 0, ""
|
||||
|
||||
return todo, vu, faux_run
|
||||
|
||||
def _sortie(self, nat, pve_kernel="7.0.14-14-pve"):
|
||||
return (
|
||||
f"{'7.0.14-14-pve' if nat else '6.12.101+deb13-cloud-amd64'}\n"
|
||||
f"---ERPLIBRE-NAT---\n{'NAT-OK' if nat else 'NAT-KO'}\n"
|
||||
f"---ERPLIBRE-PVE-KERNEL---\n{pve_kernel}\n"
|
||||
)
|
||||
|
||||
def test_nothing_is_written_when_there_is_no_nat(self):
|
||||
todo, vu, faux = self._todo(self._sortie(nat=False))
|
||||
with mock.patch("script.proxmox.proxmox_deploy.run", faux):
|
||||
nom, raison = todo._pve_make_internal_bridge()
|
||||
self.assertEqual(nom, "")
|
||||
self.assertTrue(raison)
|
||||
# Une seule commande : la sonde. Rien n'a touché au fichier.
|
||||
self.assertEqual(len(vu), 1, vu)
|
||||
self.assertNotIn(
|
||||
"interfaces", " ".join(vu), "la strophe ne doit pas être écrite"
|
||||
)
|
||||
|
||||
def test_the_reason_names_the_kernel_to_boot(self):
|
||||
todo, _vu, faux = self._todo(self._sortie(nat=False))
|
||||
with mock.patch("script.proxmox.proxmox_deploy.run", faux):
|
||||
ok, lignes = todo._pve_nat_ready({"target": "pve9", "sudo": ""})
|
||||
self.assertFalse(ok)
|
||||
texte = " ".join(lignes)
|
||||
self.assertIn("6.12.101+deb13-cloud-amd64", texte)
|
||||
self.assertIn("7.0.14-14-pve", texte)
|
||||
self.assertIn("reboot", texte)
|
||||
|
||||
def test_an_unfinished_install_says_so_instead(self):
|
||||
todo, _vu, faux = self._todo(self._sortie(nat=False, pve_kernel=""))
|
||||
with mock.patch("script.proxmox.proxmox_deploy.run", faux):
|
||||
_ok, lignes = todo._pve_nat_ready({"target": "pve9", "sudo": ""})
|
||||
texte = " ".join(lignes)
|
||||
self.assertNotIn("reboot", texte, "rien à redémarrer, rien de posé")
|
||||
|
||||
def test_a_working_host_goes_through(self):
|
||||
todo, vu, faux = self._todo(self._sortie(nat=True))
|
||||
with mock.patch("script.proxmox.proxmox_deploy.run", faux):
|
||||
todo._pve_make_internal_bridge()
|
||||
self.assertGreater(len(vu), 1, "la création doit suivre la sonde")
|
||||
|
||||
|
||||
class TestUneProxmoxImbriqueeDoitRedemarrer(unittest.TestCase):
|
||||
"""Le sommaire ne disait pas qu'une VM qui vient de recevoir Proxmox
|
||||
tourne encore le noyau de son image cloud.
|
||||
|
||||
On le redécouvrait des jours plus tard, en créant un pont, devant six
|
||||
lignes d'iptables."""
|
||||
|
||||
def _juge(self, vm, commun=""):
|
||||
import sys
|
||||
|
||||
sys.argv = ["todo.py"]
|
||||
from script.todo.todo import TODO
|
||||
|
||||
return TODO._pve_installs_proxmox(
|
||||
vm, {"install": {"cmd": commun}} if commun else {}
|
||||
)
|
||||
|
||||
def test_a_vm_that_gets_the_hypervisor(self):
|
||||
self.assertTrue(
|
||||
self._juge({"install_cmd": "./script/proxmox/install_proxmox.sh"})
|
||||
)
|
||||
|
||||
def test_through_the_common_choice_too(self):
|
||||
self.assertTrue(self._juge({}, "./script/proxmox/install_proxmox.sh"))
|
||||
|
||||
def test_an_erplibre_vm_is_left_alone(self):
|
||||
self.assertFalse(
|
||||
self._juge({}, "make install_os && make install_odoo_18")
|
||||
)
|
||||
|
||||
def test_a_vm_of_its_own_overrides_the_common_choice(self):
|
||||
# Parc mixte : la commande de la VM l'emporte sur celle du parc.
|
||||
self.assertFalse(
|
||||
self._juge(
|
||||
{"install_cmd": "make install_odoo_18"},
|
||||
"./script/proxmox/install_proxmox.sh",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class TestLEcranDUneVmProxmox(unittest.TestCase):
|
||||
"""« Console de l'hyperviseur » conseillait des commandes virsh sur une
|
||||
machine qui n'a pas libvirt.
|
||||
|
|
|
|||
Loading…
Reference in a new issue