[FIX] proxmox : pmxcfs sans adresse routable, et le stockage vide

Rapporté sur un Proxmox imbriqué. « pvesm » ne parle qu'à travers /etc/pve,
monté par pmxcfs ; pmxcfs à terre, la commande répond « Connection refused »,
la liste est vide, et l'écran s'arrête sur « aucun stockage » — trois étages
au-dessus du défaut.

pmxcfs ne démarrait pas parce que le nom d'hôte ne résolvait que vers
127.0.1.1, et il cherche une adresse ROUTABLE. L'installation corrige bien
/etc/hosts, mais l'image cloud règle « manage_etc_hosts: True » : cloud-init
le réécrit à CHAQUE démarrage. C'est le redémarrage désormais automatique qui
l'a révélé — l'installation corrigeait, le reboot amorçait le bon noyau, et
cloud-init défaisait la correction dans le même mouvement. Un fichier de
surcharge le gèle.

Deuxième geste manquant : systemd marque pve-cluster « failed » après cinq
essais rapprochés et n'y revient JAMAIS seul. Corriger /etc/hosts ne suffisait
donc pas ; l'installation relance l'unité et CONSTATE le montage plutôt que de
le supposer.

Et l'écran nomme maintenant la cause quand il n'a pas de stockage, plutôt que
de laisser chercher.

Un détail qui aurait fait un faux diagnostic : la sonde de montage
n'interroge pas storage.cfg. Ce fichier N'EXISTE PAS sur une installation
neuve — Proxmox se contente alors de ses stockages par défaut, et « local »
répond parfaitement. Vérifié sur l'hôte : /etc/pve monté, storage.cfg absent,
« pvesm status » rendant local avec 25 Go libres. C'est « .version », fichier
virtuel de pmxcfs, qui fait foi.

--- EN ---

Reported on a nested Proxmox. "pvesm" only speaks through /etc/pve, mounted by
pmxcfs; with pmxcfs down the command answers "Connection refused", the list is
empty, and the screen stops at "no storage" — three floors above the defect.

pmxcfs would not start because the hostname resolved only to 127.0.1.1, and it
needs a ROUTABLE address. The installer does fix /etc/hosts, but the cloud
image sets "manage_etc_hosts: True": cloud-init rewrites it at EVERY boot. The
now-automatic reboot is what revealed it — the install fixed it, the reboot
booted the right kernel, and cloud-init undid the fix in the same motion. An
override file freezes it.

Second missing step: systemd marks pve-cluster "failed" after five rapid
attempts and never returns to it on its own. Fixing /etc/hosts was therefore
not enough; the installer restarts the unit and VERIFIES the mount rather than
assuming it.

And the screen now names the cause when it has no storage, instead of leaving
you to hunt.

One detail that would have made a false diagnosis: the mount probe does not
ask for storage.cfg. That file DOES NOT EXIST on a fresh install — Proxmox
then uses its default storages, and "local" answers perfectly. Verified on the
host: /etc/pve mounted, storage.cfg absent, "pvesm status" returning local with
25 GB free. It is ".version", a pmxcfs virtual file, that tells the truth.

Assisted-by: Claude Opus 5
(cherry picked from commit 6212853048ba8154f2833744e45076d70bd33c72)
This commit is contained in:
Mathieu Benoit 2026-08-25 04:30:48 -04:00
parent 7dc5df67c3
commit 855acd8e61
5 changed files with 281 additions and 0 deletions

View file

@ -127,9 +127,48 @@ host_ip() {
return 1
}
# Sans ceci, tout ce que fait fix_hosts est ANNULÉ au prochain démarrage.
# L'image cloud Debian règle « manage_etc_hosts: True » : cloud-init réécrit
# alors /etc/hosts depuis son gabarit à chaque boot, et y remet
# « 127.0.1.1 <nom> ». pmxcfs, qui cherche une adresse non-bouclage pour le
# nom d'hôte, ne démarre plus — /etc/pve n'est pas monté, « pvesm » répond
# « Connection refused », et l'écran de déploiement conclut « il manque le
# stockage ». Le vrai défaut est trois étages plus bas.
#
# Vécu, et révélé par le redémarrage désormais automatique : l'installation
# corrigeait /etc/hosts, le reboot amorçait le noyau Proxmox, et cloud-init
# défaisait la correction dans le même mouvement.
#
# Un fichier de surcharge plutôt qu'une édition de cloud.cfg : c'est la voie
# que cloud-init documente, et une mise à jour du paquet ne l'écrase pas.
freeze_cloud_hosts() {
local dossier=/etc/cloud/cloud.cfg.d
local fichier="${dossier}/99-erplibre-hosts.cfg"
[ -d /etc/cloud ] || return 0
if [ -f "${fichier}" ]; then
say " cloud-init ne touche déjà plus à /etc/hosts"
return 0
fi
say " cloud-init : gel de /etc/hosts (${fichier})"
if [ "${DRY}" = "1" ]; then
say " ${Yellow}[dry-run]${Color_Off} manage_etc_hosts: false" \
"> ${fichier}"
return 0
fi
sudo mkdir -p "${dossier}"
printf '%s\n' \
"# Posé par ERPLibre : Proxmox exige que le nom d'hôte résolve vers" \
"# une adresse ROUTABLE. cloud-init y remettait 127.0.1.1 à chaque" \
"# démarrage, et pmxcfs ne démarrait plus." \
"manage_etc_hosts: false" \
| sudo tee "${fichier}" >/dev/null
CHANGED=1
}
fix_hosts() {
local ip fqdn short
ip="$(host_ip)" || die "aucune adresse IPv4 routable : réseau absent ?"
freeze_cloud_hosts
short="$(hostname -s)"
fqdn="$(hostname -f 2>/dev/null || echo "${short}")"
[ "${fqdn}" = "${short}" ] && fqdn="${short}.local"
@ -159,6 +198,42 @@ fix_hosts() {
"« hostname --ip-address » rend « ${vu:-rien} » : le nom d'hôte ne" \
"résout toujours pas vers une adresse routable."
say " hostname --ip-address : $(printf '%s ' ${routables})"
revive_pmxcfs
}
# pmxcfs abandonne après cinq essais rapprochés : systemd marque l'unité
# « failed » et n'y revient JAMAIS de lui-même — « Start request repeated too
# quickly ». Corriger /etc/hosts ne suffit donc pas ; sans ce coup de pouce,
# l'hôte reste sans /etc/pve, donc sans stockage, et l'écran de déploiement
# s'arrête sur « il manque le stockage ».
#
# « reset-failed » d'abord, sinon le démarrage est refusé sans même être tenté.
revive_pmxcfs() {
command -v systemctl >/dev/null 2>&1 || return 0
[ -e /etc/pve/.version ] && return 0
say " pve-cluster : /etc/pve n'est pas monté, relance"
if [ "${DRY}" = "1" ]; then
say " ${Yellow}[dry-run]${Color_Off} systemctl reset-failed" \
"pve-cluster && systemctl start pve-cluster"
return 0
fi
sudo systemctl reset-failed pve-cluster 2>/dev/null || true
if sudo systemctl start pve-cluster 2>&1; then
CHANGED=1
fi
# Le montage n'est pas instantané : on le CONSTATE plutôt que de le
# supposer, et on le dit quand il n'arrive pas.
local i
for i in 1 2 3 4 5 6 7 8 9 10; do
[ -e /etc/pve/.version ] && break
sleep 1
done
if [ -e /etc/pve/.version ]; then
say " ${Green}✓${Color_Off} /etc/pve monté"
else
say " ${Yellow}⚠${Color_Off} /etc/pve toujours absent :" \
"journalctl -u pve-cluster -n 30"
fi
}
# --- 4. Dépôt et clé --------------------------------------------------------

View file

@ -282,6 +282,52 @@ def parse_qm_list(text: str) -> list:
return out
# De quoi savoir POURQUOI il n'y a aucun stockage, en un aller-retour.
#
# « pvesm » ne parle qu'à travers /etc/pve, un système de fichiers monté par
# pmxcfs. pmxcfs à terre, la commande répond « Connection refused » et la liste
# est vide — l'écran conclut « il manque le stockage » alors que le défaut est
# trois étages plus bas.
CLUSTER_CHECK_CMD = (
"systemctl is-active pve-cluster 2>/dev/null || true; "
"echo '---ERPLIBRE-PVE-FS---'; "
# « .version » et non « storage.cfg » : ce dernier N'EXISTE PAS sur une
# installation neuve — Proxmox se contente alors de ses stockages par
# défaut, et « local » répond parfaitement. Le tester revenait à déclarer
# /etc/pve absent sur un hôte sain. « .version » est un fichier virtuel de
# pmxcfs : il est là si et seulement si le montage est là.
"test -e /etc/pve/.version && echo MONTE || echo ABSENT; "
"echo '---ERPLIBRE-HOSTNAME-IP---'; "
"hostname --ip-address 2>/dev/null || true"
)
def parse_cluster_check(text: str) -> dict:
"""{"actif": bool, "monte": bool, "adresses": [...]} depuis
CLUSTER_CHECK_CMD.
`adresses` sans aucune adresse routable est la cause la plus fréquente :
pmxcfs parcourt les adresses du nom d'hôte jusqu'à en trouver une qui ne
soit pas de bouclage, et l'entrée « 127.0.1.1 <nom> » de l'image cloud le
mène dans le mur."""
brut = strip_ssh_noise(text or "")
tete, _, reste = brut.partition("---ERPLIBRE-PVE-FS---")
milieu, _, queue = reste.partition("---ERPLIBRE-HOSTNAME-IP---")
adresses = [
a
for a in queue.split()
if re.match(r"^\d{1,3}(\.\d{1,3}){3}$", a) or ":" in a
]
return {
"actif": "active" in tete and "inactive" not in tete,
"monte": "MONTE" in milieu,
"adresses": adresses,
"routables": [
a for a in adresses if not a.startswith("127.") and a != "::1"
],
}
def parse_storages(text: str) -> list:
"""Sortie de « pvesm status --content images » -> [{name, type, avail}]."""
out = []

View file

@ -648,6 +648,43 @@ class ProxmoxMenuMixin:
parts = (sortie or "").split()
return parts[parts.index("dev") + 1] if "dev" in parts else ""
def _pve_cluster_reason(self, host):
"""Pourquoi il n'y a AUCUN stockage. Liste vide si tout va bien.
« Il manque le stockage » est un symptôme, pas une cause : « pvesm »
ne parle qu'à travers /etc/pve, monté par pmxcfs. pmxcfs à terre, la
liste est vide et l'écran s'arrête sur le symptôme — le défaut est
trois étages plus bas, et il a fallu lire un journal pour le trouver.
La cause la plus fréquente sur une image cloud : le nom d'hôte ne
résout que vers 127.0.1.1. pmxcfs cherche une adresse NON-bouclage et
n'en trouve pas. Notre installeur corrige /etc/hosts, mais cloud-init
le réécrit à chaque démarrage — donc la correction ne survivait pas au
redémarrage que nous faisons maintenant nous-mêmes."""
from script.proxmox import proxmox_deploy as pve
_c, out = pve.run(host, pve.CLUSTER_CHECK_CMD, 40)
etat = pve.parse_cluster_check(out)
if etat["monte"]:
return []
lignes = [
f"✗ {t('pve-cluster is down: /etc/pve is not mounted.')}",
f" {t('Without it pvesm answers nothing, hence no storage.')}",
]
if not etat["routables"]:
lignes += [
f" {t('The hostname only resolves to')}"
f" {' '.join(etat['adresses']) or '?'}"
f" — {t('pmxcfs needs a routable address.')}",
f" {t('cloud-init rewrites /etc/hosts at every boot.')}",
]
conseil = t(
"replay install_proxmox.sh on the host: it fixes /etc/hosts"
" and stops cloud-init undoing it."
)
lignes.append(f"→ {conseil}")
return lignes
def _pve_internal_cidr(self, host):
"""Réseau du futur pont interne, CHOISI d'après l'hôte.
@ -863,6 +900,12 @@ class ProxmoxMenuMixin:
vms = self._pve_vms()
_c, out = self._pve_show("pvesm status --content images", quiet=True)
stockages = pve.parse_storages(out)
if not stockages:
# AVANT d'ouvrir l'écran : une fois Textual à l'affiche, ces
# lignes n'ont plus d'endroit où aller, et l'écran ne dirait que
# « aucun stockage ».
for ligne in self._pve_cluster_reason(host):
print(f" {ligne}")
_c, out = self._pve_show("ip -o link show type bridge", quiet=True)
ponts = pve.parse_bridges(out)
if not ponts:
@ -1685,6 +1728,9 @@ class ProxmoxMenuMixin:
pont = pve.pick_bridge(ponts)
if not stockage:
print(f"\n ✗ {t('No storage able to hold a VM disk.')}")
# Le symptôme ne suffit pas : dire la CAUSE quand on la connaît.
for ligne in self._pve_cluster_reason(host):
print(f" {ligne}")
return
if not pont and not dry_run:
pont = self._pve_offer_bridge()

View file

@ -3358,6 +3358,30 @@ TRANSLATIONS = {
"fr": "Taille (+10G pour ajouter, 40G pour une cible) : ",
"en": "Size (+10G to add, 40G for a target): ",
},
"pve-cluster is down: /etc/pve is not mounted.": {
"fr": "pve-cluster est à terre : /etc/pve n'est pas monté.",
"en": "pve-cluster is down: /etc/pve is not mounted.",
},
"Without it pvesm answers nothing, hence no storage.": {
"fr": "Sans lui, pvesm ne répond rien — d'où l'absence de stockage.",
"en": "Without it pvesm answers nothing, hence no storage.",
},
"The hostname only resolves to": {
"fr": "Le nom d'hôte ne résout que vers",
"en": "The hostname only resolves to",
},
"pmxcfs needs a routable address.": {
"fr": "pmxcfs exige une adresse routable.",
"en": "pmxcfs needs a routable address.",
},
"cloud-init rewrites /etc/hosts at every boot.": {
"fr": "cloud-init réécrit /etc/hosts à chaque démarrage.",
"en": "cloud-init rewrites /etc/hosts at every boot.",
},
"replay install_proxmox.sh on the host: it fixes /etc/hosts and stops cloud-init undoing it.": {
"fr": "rejouer install_proxmox.sh sur l'hôte : il corrige /etc/hosts et empêche cloud-init de le défaire.",
"en": "replay install_proxmox.sh on the host: it fixes /etc/hosts and stops cloud-init undoing it.",
},
"No storage able to hold a VM disk.": {
"fr": "Aucun stockage capable d'héberger un disque de VM.",
"en": "No storage able to hold a VM disk.",

View file

@ -786,5 +786,95 @@ class TestLeRepliQuiNeCoupePasLaLigne(unittest.TestCase):
self.assertEqual(res.stderr, "", res.stderr)
class TestPourquoiAucunStockage(unittest.TestCase):
"""« Il manque le stockage » est un symptôme, pas une cause.
« pvesm » ne parle qu'à travers /etc/pve, monté par pmxcfs. pmxcfs à
terre, la commande répond « Connection refused », la liste est vide, et
l'écran s'arrête sur le symptôme — le défaut est trois étages plus bas.
Vécu sur un Proxmox imbriqué : le nom d'hôte ne résolvait que vers
127.0.1.1, parce que cloud-init réécrit /etc/hosts à CHAQUE démarrage. Le
redémarrage désormais automatique défaisait donc la correction que
l'installation venait de poser."""
def _sortie(self, actif, monte, adresses):
return (
f"{'active' if actif else 'inactive'}\n"
"---ERPLIBRE-PVE-FS---\n"
f"{'MONTE' if monte else 'ABSENT'}\n"
"---ERPLIBRE-HOSTNAME-IP---\n"
f"{' '.join(adresses)}\n"
)
def test_a_healthy_host(self):
lu = pve.parse_cluster_check(
self._sortie(True, True, ["10.10.10.152"])
)
self.assertTrue(lu["monte"])
self.assertEqual(lu["routables"], ["10.10.10.152"])
def test_the_loopback_only_case(self):
lu = pve.parse_cluster_check(self._sortie(False, False, ["127.0.1.1"]))
self.assertFalse(lu["monte"])
self.assertEqual(lu["routables"], [])
self.assertEqual(lu["adresses"], ["127.0.1.1"])
def test_the_probe_does_not_ask_for_storage_cfg(self):
"""storage.cfg N'EXISTE PAS sur une installation neuve.
Proxmox se contente alors de ses stockages par défaut, et « local »
répond parfaitement — mesuré sur l'hôte imbriqué, où /etc/pve était
monté sans ce fichier. Le tester revenait à déclarer /etc/pve absent
sur un hôte sain."""
self.assertNotIn("storage.cfg", pve.CLUSTER_CHECK_CMD)
self.assertIn("/etc/pve/.version", pve.CLUSTER_CHECK_CMD)
def test_inactive_is_not_read_as_active(self):
# « inactive » contient « active » : la naïveté coûterait un
# diagnostic inversé.
lu = pve.parse_cluster_check(self._sortie(False, False, []))
self.assertFalse(lu["actif"])
class TestLInstalleurRendPmxcfsAuMonde(unittest.TestCase):
"""Deux gestes que l'installation ne faisait pas, et sans lesquels elle
laissait un hôte inutilisable."""
@classmethod
def setUpClass(cls):
from pathlib import Path as P
cls.src = P("script/proxmox/install_proxmox.sh").read_text(
encoding="utf-8"
)
def test_cloud_init_stops_rewriting_etc_hosts(self):
# Sans ce gel, tout ce que fait fix_hosts est ANNULÉ au prochain
# démarrage — celui que nous déclenchons nous-mêmes désormais.
self.assertIn("manage_etc_hosts: false", self.src)
self.assertIn("/etc/cloud/cloud.cfg.d", self.src)
self.assertIn("freeze_cloud_hosts", self.src)
def test_a_failed_pmxcfs_is_revived(self):
# systemd marque l'unité « failed » après cinq essais rapprochés et
# n'y revient jamais seul : corriger /etc/hosts ne suffit pas.
self.assertIn("reset-failed pve-cluster", self.src)
self.assertIn("start pve-cluster", self.src)
def test_the_mount_is_verified_not_assumed(self):
self.assertIn("/etc/pve/.version", self.src)
def test_the_script_is_valid_shell(self):
import subprocess
res = subprocess.run(
["bash", "-n", "script/proxmox/install_proxmox.sh"],
capture_output=True,
text=True,
)
self.assertEqual(res.returncode, 0, res.stderr)
if __name__ == "__main__":
unittest.main(verbosity=1)