[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:
parent
7dc5df67c3
commit
855acd8e61
5 changed files with 281 additions and 0 deletions
|
|
@ -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é --------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -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 = []
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue