[FIX] suivi : relevé Proxmox squelettique, index par nom, état terminal
Une cause, deux symptômes. « /cluster/resources » est bâti par pvestatd ; celui-ci arrêté, l'hôte rend quand même une entrée par VM, mais SQUELETTIQUE — ni nom, ni mémoire, ni disque, et « status: unknown ». Le relevé était indexé par NOM : l'entrée disparaissait donc, la VM passait pour absente alors que l'hôte venait de la nommer, et trois tours plus tard 🗑. Comme « effacée » est un état TERMINAL, la ligne comptait pour finie — d'où « 1/1 terminées · 00:09 » sur une installation qui tournait. Le relevé est maintenant indexé par VMID, seul identifiant unique d'un hôte Proxmox, et la correspondance vers les noms se fait là où le manifeste est sous les yeux. Une entrée squelettique reste donc une VM présente, avec ce que l'hôte sait d'elle — sa taille occupée, que « du » donne par VMID. Reste à savoir pourquoi pvestatd était mort. Son journal le dit mot pour mot : « ipcc_send_rec failed: Connection refused » — pve-cluster absent, c'est-à-dire la panne /etc/hosts d'hier. Tous les services de Proxmox avaient échoué ensemble, et systemd n'y revient jamais seul. L'installation relançait le seul pve-cluster ; elle relance désormais l'ensemble, pve-cluster d'abord puisqu'il monte /etc/pve. Vérifié sur l'hôte : pvestatd relancé, et les colonnes passent de « - - - » à « 3.4G/4.0G, 2.3G/25G, 2.2G écrit ». --- EN --- One cause, two symptoms. "/cluster/resources" is built by pvestatd; with it stopped the host still returns one entry per VM, but SKELETAL — no name, no memory, no disk, and "status: unknown". Readings were indexed by NAME, so that entry vanished, the VM looked absent although the host had just named it, and three rounds later 🗑. Since "deleted" is a TERMINAL state the row counted as finished — hence "1/1 done · 00:09" on a running install. Readings are now indexed by VMID, a Proxmox host's only unique identifier, and the mapping to names happens where the manifest is at hand. A skeletal entry therefore stays a present VM, with whatever the host does know about it — its used size, which "du" reports per VMID. Why was pvestatd dead? Its journal says it verbatim: "ipcc_send_rec failed: Connection refused" — no pve-cluster, that is yesterday's /etc/hosts fault. All of Proxmox's services had failed together, and systemd never returns to them on its own. The installer restarted pve-cluster alone; it now restarts the whole set, pve-cluster first since it mounts /etc/pve. Verified on the host: pvestatd restarted, and the columns go from "- - -" to "3.4G/4.0G, 2.3G/25G, 2.2G written". Assisted-by: Claude Opus 5 (cherry picked from commit 3fb85f66842d4b0e9d6ae6446691229b31ef725a)
This commit is contained in:
parent
855acd8e61
commit
98c2355ca0
4 changed files with 188 additions and 43 deletions
|
|
@ -198,29 +198,46 @@ 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
|
||||
revive_pve_services
|
||||
}
|
||||
|
||||
# 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 ».
|
||||
# Les services de Proxmox abandonnent 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 ». Or ils ont TOUS échoué pendant que /etc/hosts était
|
||||
# faux. Corriger le fichier ne suffit donc pas.
|
||||
#
|
||||
# « reset-failed » d'abord, sinon le démarrage est refusé sans même être tenté.
|
||||
revive_pmxcfs() {
|
||||
# L'ordre compte : pve-cluster d'abord, il monte /etc/pve dont les autres
|
||||
# dépendent.
|
||||
#
|
||||
# pvestatd n'est pas un luxe. C'est lui qui remplit « /cluster/resources » ;
|
||||
# arrêté, l'hôte rend une entrée SQUELETTIQUE par VM — ni nom, ni mémoire, ni
|
||||
# disque, et « status: unknown ». Le tableau de bord n'a alors aucune colonne
|
||||
# vivante, et il a même pris cette entrée pour une VM disparue.
|
||||
PVE_SERVICES="pve-cluster pvestatd pvedaemon pveproxy pve-firewall"
|
||||
|
||||
revive_pve_services() {
|
||||
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"
|
||||
local unite etat casse=""
|
||||
for unite in ${PVE_SERVICES}; do
|
||||
systemctl list-unit-files "${unite}.service" >/dev/null 2>&1 || continue
|
||||
etat="$(systemctl is-active "${unite}" 2>/dev/null || true)"
|
||||
[ "${etat}" = "active" ] && continue
|
||||
casse="${casse} ${unite}"
|
||||
done
|
||||
[ -n "${casse}" ] || return 0
|
||||
say " services à relancer :${casse}"
|
||||
if [ "${DRY}" = "1" ]; then
|
||||
say " ${Yellow}[dry-run]${Color_Off} systemctl reset-failed" \
|
||||
"pve-cluster && systemctl start pve-cluster"
|
||||
say " ${Yellow}[dry-run]${Color_Off} systemctl reset-failed puis" \
|
||||
"start :${casse}"
|
||||
return 0
|
||||
fi
|
||||
sudo systemctl reset-failed pve-cluster 2>/dev/null || true
|
||||
if sudo systemctl start pve-cluster 2>&1; then
|
||||
for unite in ${casse}; do
|
||||
sudo systemctl reset-failed "${unite}" 2>/dev/null || true
|
||||
sudo systemctl start "${unite}" 2>&1 || \
|
||||
say " ${Yellow}⚠${Color_Off} ${unite} :" \
|
||||
"journalctl -u ${unite} -n 30"
|
||||
CHANGED=1
|
||||
fi
|
||||
done
|
||||
# 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
|
||||
|
|
|
|||
|
|
@ -1524,12 +1524,24 @@ def parse_odoo_probe(text: str) -> set:
|
|||
|
||||
|
||||
def parse_pvestats(text: str) -> dict:
|
||||
"""Sortie de PVE_STATS_CMD -> {nom: relevé}, même forme que domstats.
|
||||
"""Sortie de PVE_STATS_CMD -> {VMID: relevé}, même forme que domstats.
|
||||
|
||||
Même forme exprès : les colonnes, le débit d'écriture et la RAM se
|
||||
calculent alors sans savoir d'où vient la mesure. Une VM sur un hôte
|
||||
Proxmox distant n'avait aucune de ces colonnes — elles viennent de virsh,
|
||||
qui ne sait rien de cet hôte.
|
||||
Par VMID et non par NOM, et c'est tout le sujet. « /cluster/resources »
|
||||
est bâti par pvestatd ; celui-ci arrêté, l'hôte rend quand même une entrée
|
||||
par VM, mais SQUELETTIQUE :
|
||||
|
||||
{"id":"qemu/100","node":"…","status":"unknown","type":"qemu",
|
||||
"vmid":100}
|
||||
|
||||
Ni nom, ni mémoire, ni disque. Indexée par nom, cette entrée disparaissait
|
||||
— la VM était donc « absente du relevé » alors que l'hôte venait de la
|
||||
nommer. Trois tours plus tard : 🗑, état TERMINAL, et le suivi annonçait
|
||||
« 1/1 terminées » au bout de neuf secondes sur une installation qui
|
||||
tournait. Le VMID, lui, est toujours là ; c'est d'ailleurs le seul
|
||||
identifiant unique d'un hôte Proxmox.
|
||||
|
||||
Même forme que domstats exprès : les colonnes, le débit d'écriture et la
|
||||
RAM se calculent alors sans savoir d'où vient la mesure.
|
||||
"""
|
||||
brut, _, tailles = (text or "").partition("---ERPLIBRE-DU---")
|
||||
try:
|
||||
|
|
@ -1547,14 +1559,15 @@ def parse_pvestats(text: str) -> dict:
|
|||
out = {}
|
||||
maintenant = time.time()
|
||||
for r in ressources if isinstance(ressources, list) else ():
|
||||
nom = r.get("name")
|
||||
if not nom:
|
||||
vmid = int(r.get("vmid") or 0)
|
||||
if not vmid:
|
||||
continue
|
||||
total = int(r.get("maxdisk") or 0)
|
||||
utilise = int(r.get("disk") or 0) or occupe.get(
|
||||
int(r.get("vmid") or 0), 0
|
||||
)
|
||||
out[nom] = {
|
||||
utilise = int(r.get("disk") or 0) or occupe.get(vmid, 0)
|
||||
out[vmid] = {
|
||||
# Le nom reste DANS le relevé : il ne sert plus de clé, mais il
|
||||
# aide à lire un journal quand les deux divergent.
|
||||
"name": r.get("name") or "",
|
||||
"ram_used": int(r.get("mem") or 0),
|
||||
"ram_total": int(r.get("maxmem") or 0),
|
||||
# Le relevé vient d'être fait : il n'est pas périmé, et c'est ce
|
||||
|
|
@ -1678,11 +1691,21 @@ def _read_pvestats(vms, now=None):
|
|||
# analysable. Rien d'autre, et surtout pas le code.
|
||||
if _resources_parsable(sortie):
|
||||
ok = True
|
||||
# {VMID: relevé} -> {nom du manifeste: relevé}. La correspondance
|
||||
# se fait ICI, où le manifeste est sous les yeux : lui seul dit
|
||||
# quel VMID porte quel nom, et l'hôte peut très bien ne pas
|
||||
# nommer ses VM (pvestatd arrêté).
|
||||
releves = parse_pvestats(sortie)
|
||||
ouverts = parse_odoo_probe(sortie)
|
||||
for nom, rec in releves.items():
|
||||
rec["odoo"] = adresses.get(nom) in ouverts
|
||||
stats.update(releves)
|
||||
for vm in vms or ():
|
||||
pve_info = vm.get("pve") or {}
|
||||
if pve_info.get("target") != target:
|
||||
continue
|
||||
rec = releves.get(int(pve_info.get("vmid") or 0))
|
||||
if not rec:
|
||||
continue
|
||||
rec["odoo"] = adresses.get(vm["name"]) in ouverts
|
||||
stats[vm["name"]] = rec
|
||||
_PVE_CACHE.update({"at": maintenant, "stats": stats, "ok": ok})
|
||||
return dict(stats), ok
|
||||
|
||||
|
|
|
|||
|
|
@ -856,11 +856,26 @@ class TestLInstalleurRendPmxcfsAuMonde(unittest.TestCase):
|
|||
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_every_failed_pve_service_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.
|
||||
|
||||
Et ils ont TOUS échoué pendant que le fichier était faux — le journal
|
||||
de pvestatd le dit mot pour mot : « ipcc_send_rec failed: Connection
|
||||
refused », c'est-à-dire pve-cluster absent. Relancer le seul
|
||||
pve-cluster laissait pvestatd mort, donc un hôte qui ne nomme même pas
|
||||
ses VM."""
|
||||
self.assertIn("reset-failed", self.src)
|
||||
for unite in ("pve-cluster", "pvestatd", "pvedaemon", "pveproxy"):
|
||||
self.assertIn(unite, self.src, unite)
|
||||
|
||||
def test_pve_cluster_comes_first(self):
|
||||
# Il monte /etc/pve, dont les autres dépendent.
|
||||
import re
|
||||
|
||||
m = re.search(r'PVE_SERVICES="([^"]+)"', self.src)
|
||||
self.assertIsNotNone(m)
|
||||
self.assertEqual(m.group(1).split()[0], "pve-cluster")
|
||||
|
||||
def test_the_mount_is_verified_not_assumed(self):
|
||||
self.assertIn("/etc/pve/.version", self.src)
|
||||
|
|
|
|||
|
|
@ -37,9 +37,18 @@ class TestLaLecture(unittest.TestCase):
|
|||
def setUp(self):
|
||||
self.releves = mon.parse_pvestats(REPONSE)
|
||||
|
||||
def test_the_vm_is_keyed_by_its_name(self):
|
||||
# Le suivi raisonne en NOMS : c'est ce que porte le manifeste.
|
||||
self.assertEqual(list(self.releves), ["pve-suivi"])
|
||||
def test_the_vm_is_keyed_by_its_vmid(self):
|
||||
"""Par VMID, et c'est tout le sujet.
|
||||
|
||||
« /cluster/resources » est bâti par pvestatd ; celui-ci arrêté, l'hôte
|
||||
rend une entrée SQUELETTIQUE — ni nom, ni mémoire, ni disque, et
|
||||
« status: unknown ». Indexée par nom, elle disparaissait : la VM
|
||||
passait pour absente du relevé alors que l'hôte venait de la nommer.
|
||||
Trois tours plus tard, 🗑 — état TERMINAL — et le suivi annonçait
|
||||
« 1/1 terminées » au bout de neuf secondes. Vécu sur une VM Arch dans
|
||||
un Proxmox imbriqué."""
|
||||
self.assertEqual(list(self.releves), [100])
|
||||
self.assertEqual(self.releves[100]["name"], "pve-suivi")
|
||||
|
||||
def test_the_shape_matches_the_virsh_one(self):
|
||||
# Même forme exprès : `ram_pair`, `WriteWindow` et les colonnes
|
||||
|
|
@ -52,10 +61,10 @@ class TestLaLecture(unittest.TestCase):
|
|||
"disk_used",
|
||||
"disk_total",
|
||||
}
|
||||
self.assertTrue(attendus <= set(self.releves["pve-suivi"]))
|
||||
self.assertTrue(attendus <= set(self.releves[100]))
|
||||
|
||||
def test_the_measures_are_the_ones_the_host_gave(self):
|
||||
rec = self.releves["pve-suivi"]
|
||||
rec = self.releves[100]
|
||||
self.assertEqual(rec["ram_used"], 385351680)
|
||||
self.assertEqual(rec["ram_total"], 536870912)
|
||||
self.assertEqual(rec["wr_bytes"], 328233472)
|
||||
|
|
@ -65,13 +74,13 @@ class TestLaLecture(unittest.TestCase):
|
|||
def test_a_zero_disk_falls_back_to_the_real_size(self):
|
||||
# Sur un stockage en fichiers, Proxmox NE CALCULE PAS la taille
|
||||
# occupée et rapporte 0 : la colonne aurait affiché « 0/4G ».
|
||||
self.assertEqual(self.releves["pve-suivi"]["disk_used"], 4294971392)
|
||||
self.assertEqual(self.releves["pve-suivi"]["disk_total"], 4294967296)
|
||||
self.assertEqual(self.releves[100]["disk_used"], 4294971392)
|
||||
self.assertEqual(self.releves[100]["disk_total"], 4294967296)
|
||||
|
||||
def test_the_reading_is_fresh_so_the_ram_is_shown(self):
|
||||
# `ram_pair` refuse un relevé périmé : sans horodatage, la RAM d'une
|
||||
# VM distante ne s'afficherait jamais.
|
||||
rec = self.releves["pve-suivi"]
|
||||
rec = self.releves[100]
|
||||
self.assertNotEqual(mon.ram_pair(rec, rec["ram_at"]), "-")
|
||||
|
||||
def test_garbage_yields_nothing_rather_than_raising(self):
|
||||
|
|
@ -336,6 +345,87 @@ class TestLeRedemarrageQuiFaitPartieDeLInstallation(unittest.TestCase):
|
|||
self.assertIn("_reboot_steps(log_q, reboot) if reboot else", src)
|
||||
|
||||
|
||||
class TestUnHoteQuiNeNommePasSesVm(unittest.TestCase):
|
||||
"""pvestatd arrêté, l'hôte rend une entrée SQUELETTIQUE par VM.
|
||||
|
||||
Vécu sur une VM Arch dans un Proxmox imbriqué :
|
||||
|
||||
{"id":"qemu/100","node":"…","status":"unknown","type":"qemu",
|
||||
"vmid":100}
|
||||
|
||||
Le nom manque, donc la VM passait pour absente du relevé — alors que
|
||||
l'hôte venait de la nommer. Trois tours plus tard : 🗑, état TERMINAL, et
|
||||
le suivi annonçait « 1/1 terminées » au bout de neuf secondes sur une
|
||||
installation qui tournait. Une cause, deux symptômes."""
|
||||
|
||||
SQUELETTE = (
|
||||
'[{"id":"qemu/100","node":"n","status":"unknown","type":"qemu",'
|
||||
'"vmid":100}]\n'
|
||||
"---ERPLIBRE-DU---\n"
|
||||
"2248339456\t/var/lib/vz/images/100/\n"
|
||||
"---ERPLIBRE-ODOO---\n"
|
||||
)
|
||||
|
||||
def _lit(self, sortie, nom="vm-arch", vmid=100):
|
||||
mon._PVE_CACHE.update({"at": 0.0, "stats": {}, "ok": False})
|
||||
vm = {
|
||||
"name": nom,
|
||||
"pve": {
|
||||
"target": "h",
|
||||
"sudo": "",
|
||||
"vmid": vmid,
|
||||
"addr": "1.2.3.4",
|
||||
},
|
||||
}
|
||||
with mock.patch(
|
||||
"script.proxmox.proxmox_deploy.run", return_value=(1, sortie)
|
||||
):
|
||||
return mon.read_pvestats_detail([vm], now=50.0)
|
||||
|
||||
def test_the_vm_is_still_found(self):
|
||||
stats, ok = self._lit(self.SQUELETTE)
|
||||
self.assertTrue(ok)
|
||||
self.assertIn("vm-arch", stats, "trouvée par son VMID, pas par un nom")
|
||||
|
||||
def test_an_unknown_status_is_not_a_deletion(self):
|
||||
stats, _ok = self._lit(self.SQUELETTE)
|
||||
etat = stats["vm-arch"]["state"]
|
||||
self.assertEqual(mon.PVE_ETATS.get(etat, "running"), "running")
|
||||
|
||||
def test_what_the_host_does_know_is_kept(self):
|
||||
# Le « du » est indexé par VMID : la taille occupée survit même quand
|
||||
# tout le reste manque.
|
||||
stats, _ok = self._lit(self.SQUELETTE)
|
||||
self.assertEqual(stats["vm-arch"]["disk_used"], 2248339456)
|
||||
|
||||
def test_a_vmid_of_another_host_is_not_borrowed(self):
|
||||
# Deux hôtes peuvent avoir un VMID 100. La correspondance ne vaut que
|
||||
# pour les VM de CET hôte.
|
||||
mon._PVE_CACHE.update({"at": 0.0, "stats": {}, "ok": False})
|
||||
vms = [
|
||||
{
|
||||
"name": "ici",
|
||||
"pve": {"target": "h", "sudo": "", "vmid": 100},
|
||||
},
|
||||
{
|
||||
"name": "ailleurs",
|
||||
"pve": {"target": "autre", "sudo": "", "vmid": 100},
|
||||
},
|
||||
]
|
||||
|
||||
# Le bouchon répond PAR HÔTE : sans cela, la même sortie servirait
|
||||
# aux deux et le test ne prouverait rien.
|
||||
def par_hote(info, _cmd, _timeout=40):
|
||||
if info.get("target") == "h":
|
||||
return 1, self.SQUELETTE
|
||||
return 1, "[]\n---ERPLIBRE-DU---\n---ERPLIBRE-ODOO---\n"
|
||||
|
||||
with mock.patch("script.proxmox.proxmox_deploy.run", par_hote):
|
||||
stats, _ok = mon.read_pvestats_detail(vms, now=60.0)
|
||||
self.assertIn("ici", stats)
|
||||
self.assertNotIn("ailleurs", stats)
|
||||
|
||||
|
||||
class TestTroisVmSurUnProxmox(unittest.TestCase):
|
||||
"""Rapporté à l'usage : sur trois VM d'un même Proxmox, une seule avait
|
||||
ses colonnes vides — et les deux autres montraient les chiffres d'une
|
||||
|
|
@ -701,7 +791,7 @@ class TestLeDisque(unittest.TestCase):
|
|||
"1268518912\t/var/lib/vz/images/101/\n"
|
||||
"4294967296\t/var/lib/vz/images/999/\n"
|
||||
)
|
||||
rec = mon.parse_pvestats(texte)["vm-a"]
|
||||
rec = mon.parse_pvestats(texte)[101]
|
||||
self.assertEqual(rec["disk_used"], 1268518912)
|
||||
self.assertEqual(rec["disk_total"], 6442450944)
|
||||
self.assertEqual(
|
||||
|
|
|
|||
Loading…
Reference in a new issue