[FIX] proxmox : quatre écrans qui parlaient d'une machine locale
La confirmation de suppression promettait à TOUTE VM « son disque qcow2 EFFACÉ », puis nommait /var/lib/libvirt/images/<nom>.qcow2. Sur une VM Proxmox ce fichier n'existe pas — au mieux, au pire c'est celui d'une autre VM du même nom. C'est la peur exacte qui avait fait remonter le nettoyage. Elle nomme désormais l'hôte, le VMID et « qm destroy ». « Console de l'hyperviseur » lisait le port par « virsh vncdisplay ». Un Proxmox n'a pas de libvirt : l'échec se lisait « écran fermé » et on conseillait « sudo virsh edit » sur une machine sans ce binaire. Ce n'est pas un écran fermé, c'est la mauvaise question — Proxmox sert le sien par un ticket. Les deux vrais chemins sont nommés : la console série, l'interface web par tunnel. La colonne Odoo était un 🟢 acquis pour toujours : « Odoo ne redescend pas en cours d'install » est faux — le service redémarre au moins une fois, et il lui arrive de mourir. Elle est relue, gratuitement sur Proxmox, toutes les trente secondes ailleurs. Un hôte muet reste distinct d'un Odoo tombé. Enfin « Versions principales » (F7) manquait à l'écran Proxmox, qui affiche pourtant le même catalogue. Les trois gestes du catalogue vivent maintenant dans le socle du plan, où ils ne peuvent plus diverger. --- EN --- The delete confirmation promised EVERY VM "its qcow2 disk ERASED", then named /var/lib/libvirt/images/<name>.qcow2. On a Proxmox VM that file does not exist — at best; at worst it is another VM's, of the same name. That is the very fear that surfaced the cleanup report. It now names the host, the VMID and "qm destroy". "Hypervisor console" read the port through "virsh vncdisplay". Proxmox has no libvirt: the failure read as "screen closed" and we advised "sudo virsh edit" on a machine without that binary. It is not a closed screen, it is the wrong question — Proxmox serves its own by ticket. Both real paths are named: the serial console, the web interface through a tunnel. The Odoo column was a 🟢 acquired forever: "Odoo does not go back down during the install" is false — the service restarts at least once, and it does die. It is re-read, free on Proxmox, every thirty seconds elsewhere. A silent host stays distinct from a dead Odoo. Finally "Main versions" (F7) was missing from the Proxmox screen, which shows the same catalog. The catalog's three gestures now live in the plan foundation, where they can no longer diverge. Assisted-by: Claude Opus 5
This commit is contained in:
parent
5e75f3da31
commit
dcbba5295c
9 changed files with 394 additions and 46 deletions
|
|
@ -344,6 +344,32 @@ class PlanMixin:
|
|||
rename_screen()(self.rows[index]["vm"]["name"], auto), done
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------- #
|
||||
# Le catalogue — les trois raccourcis de sélection
|
||||
# ---------------------------------------------------------------- #
|
||||
def _catalog(self):
|
||||
from textual.widgets import SelectionList
|
||||
|
||||
return self.query_one("#f_catalog", SelectionList)
|
||||
|
||||
def action_select_all(self) -> None:
|
||||
self._catalog().select_all()
|
||||
|
||||
def action_select_none(self) -> None:
|
||||
self._catalog().deselect_all()
|
||||
|
||||
def action_select_main(self) -> None:
|
||||
"""Une VM par distribution : la version marquée par défaut.
|
||||
|
||||
Ici et non dans un formulaire : les trois gestes sont les mêmes des
|
||||
deux côtés, et celui-ci manquait à l'écran Proxmox — qui affiche
|
||||
pourtant le même catalogue, drapeau « default » compris."""
|
||||
widget = self._catalog()
|
||||
widget.deselect_all()
|
||||
for i, e in enumerate(self._entries()):
|
||||
if e.get("default"):
|
||||
widget.select(i)
|
||||
|
||||
def _auto_name(self, index):
|
||||
"""Nom que la VM reprendrait si on effaçait le sien."""
|
||||
return self._plan_entries()[index]["name"]
|
||||
|
|
|
|||
|
|
@ -161,7 +161,13 @@ def run_proxmox_form(ctx, run_app: bool = True):
|
|||
}
|
||||
|
||||
def entry_label(e):
|
||||
return f"{e['distro']} {e['version']} [{e['arch']}] {e['name']}"
|
||||
# L'étoile marque la version par défaut d'une distribution : c'est
|
||||
# elle que « F7 » retient, et sans repère le raccourci choisissait
|
||||
# sans qu'on sache quoi.
|
||||
star = " *" if e.get("default") else ""
|
||||
return (
|
||||
f"{e['distro']} {e['version']}{star} [{e['arch']}] {e['name']}"
|
||||
)
|
||||
|
||||
class ProxmoxForm(ExtrasMixin, PlanMixin, App):
|
||||
TITLE = t("Deploy one or more ERPLibre VMs on Proxmox VE!")
|
||||
|
|
@ -170,6 +176,7 @@ def run_proxmox_form(ctx, run_app: bool = True):
|
|||
("f4", "clear_vm", t("Reset VM")),
|
||||
("f3", "preview", t("Preview")),
|
||||
("f6", "select_all", t("All")),
|
||||
("f7", "select_main", t("Main versions")),
|
||||
("f8", "select_none", t("None")),
|
||||
("escape", "cancel", t("Cancel")),
|
||||
]
|
||||
|
|
@ -871,12 +878,6 @@ def run_proxmox_form(ctx, run_app: bool = True):
|
|||
self.overrides.pop(cle, None)
|
||||
self._refresh_after(remonter=True)
|
||||
|
||||
def action_select_all(self) -> None:
|
||||
self.query_one("#f_catalog", SelectionList).select_all()
|
||||
|
||||
def action_select_none(self) -> None:
|
||||
self.query_one("#f_catalog", SelectionList).deselect_all()
|
||||
|
||||
def action_cancel(self) -> None:
|
||||
self.result = None
|
||||
self.exit()
|
||||
|
|
|
|||
|
|
@ -735,6 +735,9 @@ class QemuAccessMixin:
|
|||
# Les commandes de réparation se lancent SUR l'hyperviseur : le préfixe
|
||||
# évite de les copier sur la mauvaise machine, l'erreur naturelle ici.
|
||||
pre = f"ssh {jump} " if jump else ""
|
||||
if not port and self._hypervisor_is_proxmox(jump):
|
||||
self._pve_console_hint(jump, domain)
|
||||
return
|
||||
if not port:
|
||||
print(f"\n ⚠ {t('This VM exposes no VNC port.')}")
|
||||
print(f" {t('Its display is likely spice with listen=none:')}")
|
||||
|
|
@ -772,6 +775,55 @@ class QemuAccessMixin:
|
|||
print(f" {t('then point your VNC client at')} localhost:{port}")
|
||||
print(f" {t('The tunnel stays open as long as that ssh runs.')}")
|
||||
|
||||
@staticmethod
|
||||
def _hypervisor_is_proxmox(jump) -> bool:
|
||||
"""Cet hyperviseur est-il un Proxmox VE ?
|
||||
|
||||
La question se pose quand « virsh vncdisplay » n'a rien rendu : un
|
||||
Proxmox n'a PAS de libvirt, donc l'absence de port n'y veut pas dire
|
||||
« écran fermé », elle veut dire « mauvaise question ». Sans cette
|
||||
distinction, on conseillait « virsh edit » sur une machine où la
|
||||
commande n'existe pas.
|
||||
|
||||
« qm » et non « pveversion » : c'est le binaire dont on parle ensuite.
|
||||
"""
|
||||
if not jump:
|
||||
# Un Proxmox n'est jamais l'hôte local ici : ce menu tourne sur le
|
||||
# poste de travail, et un Proxmox se joint par ssh.
|
||||
return False
|
||||
try:
|
||||
res = subprocess.run(
|
||||
["ssh", "-o", "BatchMode=yes", jump, "command -v qm"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=25,
|
||||
)
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
return False
|
||||
return res.returncode == 0 and bool(res.stdout.strip())
|
||||
|
||||
def _pve_console_hint(self, jump, domain) -> None:
|
||||
"""Les deux vraies façons de voir l'écran d'une VM Proxmox.
|
||||
|
||||
Proxmox ne sert pas son écran par un port VNC qu'on tunnelise : il le
|
||||
sert par un TICKET, sur son interface web (« qm vncproxy » ouvre un
|
||||
websocket authentifié, pas un socket qu'on relaie). Un « ssh -L » vers
|
||||
un port VNC n'y trouve rien, quel que soit le port."""
|
||||
print(
|
||||
f"\n ⚠ {t('This hypervisor is Proxmox VE: it has no libvirt.')}"
|
||||
)
|
||||
print(f" {t('Its screen is served by a ticket, not by a VNC port.')}")
|
||||
print(f"\n {t('Two ways in:')}")
|
||||
print(f" • {t('the serial console, VMID from the Proxmox menu:')}")
|
||||
print(f" ssh {jump} sudo qm list # {domain}")
|
||||
print(f" ssh -t {jump} sudo qm terminal <VMID>")
|
||||
print(f" • {t('the web interface, through a tunnel:')}")
|
||||
print(f" ssh -N -L 8006:127.0.0.1:8006 {jump}")
|
||||
print(f" https://localhost:8006 → {t('VM')} → Console")
|
||||
print(
|
||||
f"\n {t('TODO > Execute > Deploy > Proxmox VE does the first.')}"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _qemu_vnc_port(domain, jump=""):
|
||||
"""Port VNC réel d'un domaine, localement ou sur un hyperviseur distant.
|
||||
|
|
|
|||
|
|
@ -1001,20 +1001,6 @@ def run_deploy_form(ctx, run_app: bool = True):
|
|||
self._recompute()
|
||||
|
||||
# -- actions ---------------------------------------------------- #
|
||||
def action_select_all(self) -> None:
|
||||
self.query_one("#f_catalog", SelectionList).select_all()
|
||||
|
||||
def action_select_none(self) -> None:
|
||||
self.query_one("#f_catalog", SelectionList).deselect_all()
|
||||
|
||||
def action_select_main(self) -> None:
|
||||
"""Une VM par distro : la version marquée par défaut."""
|
||||
widget = self.query_one("#f_catalog", SelectionList)
|
||||
widget.deselect_all()
|
||||
for i, e in enumerate(self._entries()):
|
||||
if e.get("default"):
|
||||
widget.select(i)
|
||||
|
||||
def action_clear_vm(self) -> None:
|
||||
"""Rend au profil commun la VM dont un widget a le focus. Sans
|
||||
cette sortie, un réglage posé par erreur ne se défaisait qu'en
|
||||
|
|
|
|||
|
|
@ -1322,6 +1322,40 @@ PVE_STATS_CMD = (
|
|||
# une poignée de main ssh (mesuré 1 s), quand « virsh domstats » coûte 0,03 s
|
||||
# pour tout le parc. Cinq secondes suffisent à voir une installation avancer.
|
||||
PVE_STATS_INTERVAL = 5.0
|
||||
|
||||
# Une VM locale DÉJÀ verte est resondée à cette cadence, pas à chaque tour.
|
||||
# La sonde est un connect() TCP par VM : à chaque tour (2 s) c'est cher pour
|
||||
# une réponse qui ne bouge presque jamais, jamais c'est un mensonge — Odoo
|
||||
# redémarre au moins une fois pendant l'installation, et il lui arrive de
|
||||
# mourir. Trente secondes bornent le mensonge à un demi-écran de journal.
|
||||
ODOO_RECHECK = 30.0
|
||||
|
||||
|
||||
def odoo_reading(vm, releve, deja_vert, dernier, maintenant, sonde):
|
||||
"""Odoo répond-il sur cette VM ? (état ou None, sondé ?)
|
||||
|
||||
None veut dire « pas de réponse ce tour-ci », et NON « Odoo est tombé » :
|
||||
l'appelant garde alors le dernier état connu. C'est toute la différence
|
||||
entre un hôte muet et une VM qui ne sert plus rien.
|
||||
|
||||
Deux voies, parce que la sonde n'a pas le même prix. Sur une VM Proxmox
|
||||
le port a été testé DEPUIS L'HÔTE, dans l'appel des statistiques — d'ici,
|
||||
une adresse de pont interne ne répond jamais — donc c'est gratuit et relu
|
||||
à chaque tour. Sur une VM locale c'est un connect() TCP par VM : on la
|
||||
refait tant qu'elle est rouge, puis seulement toutes les ODOO_RECHECK
|
||||
secondes.
|
||||
|
||||
Ce qu'on ne fait plus, c'est ne jamais la refaire. « Odoo ne redescend pas
|
||||
en cours d'install » était faux : le service redémarre au moins une fois,
|
||||
et il lui arrive de mourir. Le 🟢 restait alors acquis pour toujours.
|
||||
"""
|
||||
if vm.get("pve"):
|
||||
return (bool(releve.get("odoo")) if releve else None), False
|
||||
if deja_vert and maintenant - dernier < ODOO_RECHECK:
|
||||
return None, False
|
||||
return bool(sonde(vm.get("ip"), 8069)), True
|
||||
|
||||
|
||||
# Proxmox dit « running » / « stopped » ; le suivi raisonne en états libvirt.
|
||||
# Une VM absente de la réponse de l'hôte a vraiment disparu.
|
||||
PVE_ETATS = {"running": "running", "stopped": "shut off", "paused": "paused"}
|
||||
|
|
@ -1722,6 +1756,39 @@ def delete_vm_cmd_pve(info, purge: bool = True) -> str:
|
|||
return pve_host_cmd(info, suite)
|
||||
|
||||
|
||||
def delete_lines(vm) -> list:
|
||||
"""Ce qui va RÉELLEMENT disparaître, dit selon l'endroit où la VM vit.
|
||||
|
||||
L'écran annonçait à toute VM « son disque qcow2 EFFACÉ », puis nommait
|
||||
/var/lib/libvirt/images/<nom>.qcow2. Sur une VM Proxmox ce fichier
|
||||
n'existe pas : son disque vit dans un stockage que seul l'hôte connaît, et
|
||||
la ligne désignait donc un fichier local — au mieux inexistant, au pire
|
||||
celui d'une autre VM du même nom. C'est exactement la peur qui a fait
|
||||
remonter le nettoyage : « le nettoyage risque d'effacer des VM en
|
||||
production ».
|
||||
|
||||
Une confirmation doit nommer ce qu'elle détruit, sur la machine où elle
|
||||
le détruit."""
|
||||
info = vm.get("pve")
|
||||
if not info:
|
||||
return [
|
||||
"La VM est arrêtée, sa définition retirée,",
|
||||
"et son disque qcow2 EFFACÉ. Rien n'est récupérable.",
|
||||
"",
|
||||
f" /var/lib/libvirt/images/{vm['name']}.qcow2",
|
||||
]
|
||||
hote = info.get("target") or "?"
|
||||
return [
|
||||
f"Sur l'hôte Proxmox {hote}, la VM {info.get('vmid')} est arrêtée",
|
||||
"puis DÉTRUITE avec ses disques. Rien n'est récupérable.",
|
||||
"",
|
||||
f" qm destroy {info.get('vmid')} --purge",
|
||||
"",
|
||||
"Aucun fichier n'est touché ici : le disque vit dans le",
|
||||
"stockage de l'hôte.",
|
||||
]
|
||||
|
||||
|
||||
def delete_vm_cmd(name: str, with_disks: bool) -> str:
|
||||
"""Efface la VM sur l'HÔTE. Même séquence que « TODO._qemu_delete_vm » :
|
||||
arrêt, retrait de la définition (nvram si UEFI, repli sinon), puis les
|
||||
|
|
@ -2016,9 +2083,18 @@ def run_monitor(manifest_path: str, run_app: bool = True):
|
|||
self._errcount = {}
|
||||
# Sommaire de stats déplié (clic) ou non.
|
||||
self._stats_open = False
|
||||
# VM dont l'UI Odoo (:8069) répond déjà : une fois détectée « up »,
|
||||
# on ne re-teste plus (Odoo ne redescend pas en cours d'install).
|
||||
# VM dont l'UI Odoo (:8069) répond. RELUE, et non accumulée :
|
||||
# « Odoo ne redescend pas en cours d'install » est faux — il
|
||||
# redémarre au moins une fois (service systemd), et il lui arrive
|
||||
# de mourir. Un 🟢 acquis pour toujours affirmait alors qu'une VM
|
||||
# servait Odoo alors qu'elle ne servait plus rien.
|
||||
self._odoo_up = set()
|
||||
# Dernier tour où le port d'une VM DÉJÀ verte a été retesté. Sur
|
||||
# une VM locale la sonde est un connect() TCP par VM : la refaire
|
||||
# à chaque tour pour rien serait cher, la refaire jamais serait
|
||||
# faux. Sur Proxmox la question ne se pose pas — la réponse vient
|
||||
# avec les statistiques, gratuitement.
|
||||
self._odoo_revu = {}
|
||||
# Relevés SUCCESSIFS sans la VM, par nom : « effacée » est un état
|
||||
# terminal, il se mérite.
|
||||
self._pve_absences = {}
|
||||
|
|
@ -2229,7 +2305,7 @@ def run_monitor(manifest_path: str, run_app: bool = True):
|
|||
# colonnes restaient vides. Même forme de relevé, donc la suite ne
|
||||
# change pas d'un iota.
|
||||
stats.update(read_pvestats(vms))
|
||||
now_s = time.time()
|
||||
now_s = maintenant = time.time()
|
||||
for name, rec in stats.items():
|
||||
self._wrate.add(name, rec["wr_bytes"], now_s)
|
||||
self._vmstats = stats
|
||||
|
|
@ -2257,21 +2333,22 @@ def run_monitor(manifest_path: str, run_app: bool = True):
|
|||
# peut contenir des erreurs passées inaperçues.
|
||||
if st[0] in ("done", "failed") and name not in errors:
|
||||
errors[name] = scan_log_errors(vm["log"])
|
||||
# Odoo up ? On ne teste que celles pas encore confirmées up
|
||||
# et non effacées (test TCP court sur :8069).
|
||||
if (
|
||||
name not in self._odoo_up
|
||||
and self._domstate.get(name) != "gone"
|
||||
):
|
||||
releve = (self._vmstats or {}).get(name) or {}
|
||||
if vm.get("pve"):
|
||||
# Le port a été testé DEPUIS L'HÔTE, dans l'appel des
|
||||
# statistiques : d'ici, une adresse de pont interne ne
|
||||
# répond jamais.
|
||||
if releve.get("odoo"):
|
||||
odoo[name] = True
|
||||
elif _port_open(vm.get("ip"), 8069):
|
||||
odoo[name] = True
|
||||
# Odoo up ? La réponse est RENDUE À CHAQUE TOUR pour les
|
||||
# VM sondées, pas accumulée : la colonne doit pouvoir
|
||||
# redescendre à « — ».
|
||||
if self._domstate.get(name) != "gone":
|
||||
etat, sonde = odoo_reading(
|
||||
vm,
|
||||
(self._vmstats or {}).get(name) or {},
|
||||
name in self._odoo_up,
|
||||
self._odoo_revu.get(name, 0.0),
|
||||
maintenant,
|
||||
_port_open,
|
||||
)
|
||||
if sonde:
|
||||
self._odoo_revu[name] = maintenant
|
||||
if etat is not None:
|
||||
odoo[name] = etat
|
||||
return disks, status, self._collect_tele(), errors, odoo, wr, ram
|
||||
|
||||
async def _tick_table(self):
|
||||
|
|
@ -2291,7 +2368,14 @@ def run_monitor(manifest_path: str, run_app: bool = True):
|
|||
except Exception:
|
||||
return
|
||||
self._errcount.update(errors)
|
||||
self._odoo_up.update(odoo)
|
||||
# Remplacement et non union : une VM sondée qui ne répond
|
||||
# plus doit repasser à « — ». Les VM absentes du relevé (hôte
|
||||
# muet, VM effacée) gardent leur dernier état connu.
|
||||
for nom_vm, vivant in odoo.items():
|
||||
if vivant:
|
||||
self._odoo_up.add(nom_vm)
|
||||
else:
|
||||
self._odoo_up.discard(nom_vm)
|
||||
try:
|
||||
table = self.query_one("#vms", DataTable)
|
||||
now = time.time()
|
||||
|
|
@ -3011,12 +3095,7 @@ def run_monitor(manifest_path: str, run_app: bool = True):
|
|||
self.push_screen(
|
||||
ConfirmScreen(
|
||||
f"Supprimer {vm['name']} ?",
|
||||
[
|
||||
"La VM est arrêtée, sa définition retirée,",
|
||||
"et son disque qcow2 EFFACÉ. Rien n'est récupérable.",
|
||||
"",
|
||||
f" /var/lib/libvirt/images/{vm['name']}.qcow2",
|
||||
],
|
||||
delete_lines(vm),
|
||||
"Supprimer définitivement",
|
||||
),
|
||||
confirmed,
|
||||
|
|
|
|||
|
|
@ -2346,6 +2346,30 @@ TRANSLATIONS = {
|
|||
"fr": "et non la VM : le socket est du côté de QEMU.",
|
||||
"en": "not the VM: the socket is QEMU-side.",
|
||||
},
|
||||
"This hypervisor is Proxmox VE: it has no libvirt.": {
|
||||
"fr": "Cet hyperviseur est un Proxmox VE : il n'a pas de libvirt.",
|
||||
"en": "This hypervisor is Proxmox VE: it has no libvirt.",
|
||||
},
|
||||
"Its screen is served by a ticket, not by a VNC port.": {
|
||||
"fr": "Son écran est servi par un ticket, pas par un port VNC.",
|
||||
"en": "Its screen is served by a ticket, not by a VNC port.",
|
||||
},
|
||||
"Two ways in:": {
|
||||
"fr": "Deux chemins :",
|
||||
"en": "Two ways in:",
|
||||
},
|
||||
"the serial console, VMID from the Proxmox menu:": {
|
||||
"fr": "la console série, le VMID venant du menu Proxmox :",
|
||||
"en": "the serial console, VMID from the Proxmox menu:",
|
||||
},
|
||||
"the web interface, through a tunnel:": {
|
||||
"fr": "l'interface web, par un tunnel :",
|
||||
"en": "the web interface, through a tunnel:",
|
||||
},
|
||||
"TODO > Execute > Deploy > Proxmox VE does the first.": {
|
||||
"fr": "TODO > Execute > Deploy > Proxmox VE fait le premier.",
|
||||
"en": "TODO > Execute > Deploy > Proxmox VE does the first.",
|
||||
},
|
||||
"This VM exposes no VNC port.": {
|
||||
"fr": "Cette VM n'expose aucun port VNC.",
|
||||
"en": "This VM exposes no VNC port.",
|
||||
|
|
|
|||
|
|
@ -598,6 +598,54 @@ class TestDeuxVmDuMemeNom(unittest.TestCase):
|
|||
)
|
||||
|
||||
|
||||
class TestLEcranDUneVmProxmox(unittest.TestCase):
|
||||
"""« Console de l'hyperviseur » conseillait des commandes virsh sur une
|
||||
machine qui n'a pas libvirt.
|
||||
|
||||
Le tunnel lit le port VNC par « virsh vncdisplay » sur l'hyperviseur. Un
|
||||
Proxmox VE n'a pas de libvirt : la commande échoue, et l'absence de port
|
||||
était lue « écran fermé ». On imprimait alors « sudo virsh edit » — sur un
|
||||
hôte où le binaire n'existe pas. Ce n'est pas un écran fermé, c'est la
|
||||
mauvaise question : Proxmox sert son écran par un ticket, sur son
|
||||
interface web."""
|
||||
|
||||
def _sortie(self, qm_present, port=0):
|
||||
import contextlib
|
||||
import io
|
||||
import sys
|
||||
|
||||
sys.argv = ["todo.py"]
|
||||
from script.todo.todo import TODO
|
||||
|
||||
todo = TODO.__new__(TODO)
|
||||
todo._ssh_proxyjump = lambda nom: "pve9"
|
||||
todo._qemu_vnc_port = staticmethod(lambda d, j="": port)
|
||||
todo._hypervisor_is_proxmox = lambda jump: qm_present
|
||||
tampon = io.StringIO()
|
||||
with contextlib.redirect_stdout(tampon):
|
||||
todo._qemu_console_tunnel("pve9+vm-a", "ssh_config")
|
||||
return tampon.getvalue()
|
||||
|
||||
def test_a_proxmox_host_is_never_told_to_run_virsh(self):
|
||||
sortie = self._sortie(qm_present=True)
|
||||
self.assertNotIn("virsh", sortie)
|
||||
self.assertIn("qm terminal", sortie)
|
||||
self.assertIn("8006", sortie, "l'interface web est le second chemin")
|
||||
|
||||
def test_a_libvirt_host_keeps_its_repair_commands(self):
|
||||
# La voie libvirt ne régresse pas : sans port, ses commandes de
|
||||
# réparation restent la bonne réponse.
|
||||
sortie = self._sortie(qm_present=False)
|
||||
self.assertIn("virsh edit", sortie)
|
||||
|
||||
def test_a_working_vnc_port_still_wins(self):
|
||||
# La sonde ne doit pas s'exécuter quand il y a un port : ce serait un
|
||||
# aller-retour ssh pour rien.
|
||||
sortie = self._sortie(qm_present=True, port=5901)
|
||||
self.assertIn("-L 5901:127.0.0.1:5901", sortie)
|
||||
self.assertNotIn("qm terminal", sortie)
|
||||
|
||||
|
||||
class TestUnSeulNomDansSshConfig(unittest.TestCase):
|
||||
"""L'entrée portait DEUX noms sur sa ligne « Host » : le nom chaîné
|
||||
« hôte+vm » et le nom court.
|
||||
|
|
|
|||
|
|
@ -230,6 +230,119 @@ class TestLesAutresCheminsVersLaPoubelle(unittest.TestCase):
|
|||
self.assertGreaterEqual(mon.PVE_ABSENCES_AVANT_EFFACEE, 2)
|
||||
|
||||
|
||||
class TestCeQueLaConfirmationPromet(unittest.TestCase):
|
||||
"""La confirmation de suppression annonçait un fichier qcow2 local à
|
||||
TOUTE VM, Proxmox comprise.
|
||||
|
||||
Sur une VM Proxmox ce fichier n'existe pas : son disque vit dans un
|
||||
stockage que seul l'hôte connaît. La ligne désignait donc un chemin local
|
||||
— au mieux inexistant, au pire celui d'une autre VM du même nom. C'est
|
||||
exactement la peur qui avait fait remonter le nettoyage : « le nettoyage
|
||||
risque d'effacer des VM en production »."""
|
||||
|
||||
def test_a_proxmox_vm_never_shows_a_local_path(self):
|
||||
lignes = mon.delete_lines(
|
||||
{"name": "vm-a", "pve": {"target": "pve9", "vmid": 101}}
|
||||
)
|
||||
texte = " ".join(lignes)
|
||||
self.assertNotIn("/var/lib/libvirt", texte)
|
||||
self.assertNotIn("qcow2", texte)
|
||||
|
||||
def test_it_names_the_host_and_the_vmid(self):
|
||||
# Le nom ne suffit pas : deux VM peuvent le porter, seul le VMID est
|
||||
# unique — et il faut savoir SUR QUELLE machine ça se passe.
|
||||
texte = " ".join(
|
||||
mon.delete_lines(
|
||||
{"name": "vm-a", "pve": {"target": "pve9", "vmid": 101}}
|
||||
)
|
||||
)
|
||||
self.assertIn("pve9", texte)
|
||||
self.assertIn("101", texte)
|
||||
self.assertIn("qm destroy 101", texte)
|
||||
|
||||
def test_a_local_vm_still_names_its_file(self):
|
||||
# La voie libvirt ne régresse pas : là, le fichier EST ce qu'on efface.
|
||||
texte = " ".join(mon.delete_lines({"name": "vm-a"}))
|
||||
self.assertIn("/var/lib/libvirt/images/vm-a.qcow2", texte)
|
||||
|
||||
|
||||
class TestQuandLeVertRedescend(unittest.TestCase):
|
||||
"""Le 🟢 était acquis pour toujours.
|
||||
|
||||
« Odoo ne redescend pas en cours d'install » : c'était faux. Le service
|
||||
redémarre au moins une fois — systemd l'active à la fin du make — et il
|
||||
lui arrive de mourir. La VM restait verte en servant plus rien, et c'est
|
||||
précisément le moment où on veut le savoir."""
|
||||
|
||||
def _sonde(self, reponse):
|
||||
return lambda _ip, _port: reponse
|
||||
|
||||
def test_a_local_vm_that_stopped_answering_goes_back(self):
|
||||
etat, sonde = mon.odoo_reading(
|
||||
{"ip": "10.0.0.1"},
|
||||
{},
|
||||
deja_vert=True,
|
||||
dernier=0.0,
|
||||
maintenant=mon.ODOO_RECHECK + 1,
|
||||
sonde=self._sonde(False),
|
||||
)
|
||||
self.assertIs(etat, False)
|
||||
self.assertTrue(sonde)
|
||||
|
||||
def test_a_green_local_vm_is_not_probed_every_tick(self):
|
||||
# La sonde est un connect() TCP par VM : à chaque tour (2 s) c'est
|
||||
# cher pour une réponse qui ne bouge presque jamais.
|
||||
etat, sonde = mon.odoo_reading(
|
||||
{"ip": "10.0.0.1"},
|
||||
{},
|
||||
deja_vert=True,
|
||||
dernier=100.0,
|
||||
maintenant=101.0,
|
||||
sonde=self._sonde(False),
|
||||
)
|
||||
self.assertIsNone(etat, "rien de neuf ne doit être affirmé")
|
||||
self.assertFalse(sonde)
|
||||
|
||||
def test_a_red_local_vm_is_probed_every_tick(self):
|
||||
_etat, sonde = mon.odoo_reading(
|
||||
{"ip": "10.0.0.1"},
|
||||
{},
|
||||
deja_vert=False,
|
||||
dernier=100.0,
|
||||
maintenant=101.0,
|
||||
sonde=self._sonde(True),
|
||||
)
|
||||
self.assertTrue(sonde)
|
||||
|
||||
def test_a_proxmox_vm_is_read_every_tick_for_free(self):
|
||||
# Le port est testé DEPUIS L'HÔTE, avec les statistiques : d'ici, une
|
||||
# adresse de pont interne ne répond jamais.
|
||||
def lu(odoo):
|
||||
return mon.odoo_reading(
|
||||
{"pve": {"vmid": 1}},
|
||||
{"odoo": odoo},
|
||||
deja_vert=True,
|
||||
dernier=0.0,
|
||||
maintenant=1.0,
|
||||
sonde=self._sonde(True),
|
||||
)[0]
|
||||
|
||||
self.assertIs(lu(True), True)
|
||||
self.assertIs(lu(False), False)
|
||||
|
||||
def test_a_silent_host_is_not_a_dead_odoo(self):
|
||||
# Sans relevé, on ne sait RIEN : l'appelant garde le dernier état.
|
||||
etat, _s = mon.odoo_reading(
|
||||
{"pve": {"vmid": 1}},
|
||||
{},
|
||||
deja_vert=True,
|
||||
dernier=0.0,
|
||||
maintenant=1.0,
|
||||
sonde=self._sonde(True),
|
||||
)
|
||||
self.assertIsNone(etat)
|
||||
|
||||
|
||||
class TestLaBonneMachine(unittest.TestCase):
|
||||
"""Le pire défaut de la série : l'installation partie AILLEURS.
|
||||
|
||||
|
|
|
|||
|
|
@ -163,6 +163,25 @@ class TestLesDeuxEcrans(unittest.TestCase):
|
|||
vide.extras_init({})
|
||||
return vide
|
||||
|
||||
def test_both_screens_bind_the_same_catalog_shortcuts(self):
|
||||
# « Versions principales » (F7) manquait à l'écran Proxmox, qui
|
||||
# affiche pourtant le même catalogue, drapeau « default » compris.
|
||||
from script.todo.proxmox_deploy_form import run_proxmox_form
|
||||
from script.todo.qemu_deploy_form import run_deploy_form
|
||||
|
||||
todo = todo_muet()
|
||||
mod = todo._qemu_import_module()
|
||||
touches = {}
|
||||
for nom, app in (
|
||||
("QEMU/KVM", run_deploy_form(todo._qemu_form_context(mod), False)),
|
||||
("Proxmox", run_proxmox_form(contexte_proxmox(todo), False)),
|
||||
):
|
||||
touches[nom] = {
|
||||
b[0]: b[1] for b in app.BINDINGS if b[1].startswith("select_")
|
||||
}
|
||||
self.assertEqual(touches["QEMU/KVM"], touches["Proxmox"])
|
||||
self.assertIn("select_main", touches["Proxmox"].values())
|
||||
|
||||
def test_parallelism_follows_the_host_on_both_screens(self):
|
||||
# L'écran Proxmox plafonnait à quatre choix et en proposait UN, quel
|
||||
# que soit le nombre de cœurs de l'hôte.
|
||||
|
|
|
|||
Loading…
Reference in a new issue