[FIX] proxmox : viser la bonne machine, et dire la vérité sur le disque
« s » ouvrait encore la VM locale homonyme : la vue de progression n'avait que le NOM de la VM, et l'entrée ~/.ssh/config n'existe pas encore à ce moment. Le déploiement lui passe maintenant « ssh -J <hôte> user@<ip> », qui ne dépend de rien. Deux voisines du même défaut, jamais rapportées mais aussi graves : la console ouvrait « virsh console <nom> » — celle de la VM LOCALE — et la pause suspendait la locale. Les deux passent par le VMID sur l'hôte. La colonne Disque annonçait « 6.0G/6.0G » sur une VM qui n'avait écrit que 1,2 Go : « du -sb » rend la taille APPARENTE, et un disque raw creux la donne entière. « du -sB1 » compte les blocs. Enfin l'écran de déploiement dit ce qui l'attend : « Quitter (q) pour lancer l'installation d'ERPLibre » — on attendait devant une fenêtre terminée sans le savoir. --- EN --- "s" still opened the homonymous local VM: the progress view only had the VM's NAME, and the ~/.ssh/config entry does not exist yet at that point. The deployment now hands it "ssh -J <host> user@<ip>", which depends on nothing. Two neighbours of the same defect, never reported but just as serious: the console opened "virsh console <name>" — the LOCAL VM's — and pause suspended the local one. Both now go through the VMID on the host. The Disk column claimed "6.0G/6.0G" on a VM that had written 1.2 GB: "du -sb" returns the APPARENT size, and a sparse raw disk gives it in full. "du -sB1" counts blocks. Finally the deployment screen says what awaits it: "Quit (q) to start the ERPLibre install" — one waited before a finished window without knowing. Assisted-by: Claude Opus 5
This commit is contained in:
parent
07a9f66626
commit
4c2adb7c56
6 changed files with 306 additions and 26 deletions
|
|
@ -591,7 +591,9 @@ def res_row_widgets(index, vm, presets, labels=None, null=None):
|
||||||
return widgets
|
return widgets
|
||||||
|
|
||||||
|
|
||||||
def run_deploy_progress(jobs, parallelism, run_app: bool = True):
|
def run_deploy_progress(
|
||||||
|
jobs, parallelism, run_app: bool = True, ssh_cmds=None, suite=""
|
||||||
|
):
|
||||||
"""Déploie `jobs` = [(id, nom, argv)] en parallèle, un bloc repliable par
|
"""Déploie `jobs` = [(id, nom, argv)] en parallèle, un bloc repliable par
|
||||||
VM. Renvoie [(nom, rc, sortie, durée)]. `run_app=False` renvoie l'app.
|
VM. Renvoie [(nom, rc, sortie, durée)]. `run_app=False` renvoie l'app.
|
||||||
|
|
||||||
|
|
@ -680,9 +682,14 @@ def run_deploy_progress(jobs, parallelism, run_app: bool = True):
|
||||||
self.run_job(jid, name, parts)
|
self.run_job(jid, name, parts)
|
||||||
|
|
||||||
def _refresh_summary(self):
|
def _refresh_summary(self):
|
||||||
|
# CE QUI SUIT, dit ici : l'installation d'ERPLibre ne démarre
|
||||||
|
# qu'au moment où l'on quitte cet écran, et rien ne le disait —
|
||||||
|
# on attendait devant une fenêtre pourtant terminée.
|
||||||
|
fini = self._done >= len(jobs)
|
||||||
|
apres = f"\n → {suite}" if (fini and suite) else ""
|
||||||
self.query_one("#summary", Static).update(
|
self.query_one("#summary", Static).update(
|
||||||
f" {self._done}/{len(jobs)} — "
|
f" {self._done}/{len(jobs)} — "
|
||||||
f"{fmt_dur(time.time() - self._t0)}"
|
f"{fmt_dur(time.time() - self._t0)}{apres}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# `thread=True` : subprocess.run est bloquant ; le faire dans un
|
# `thread=True` : subprocess.run est bloquant ; le faire dans un
|
||||||
|
|
@ -765,10 +772,11 @@ def run_deploy_progress(jobs, parallelism, run_app: bool = True):
|
||||||
def action_ssh(self) -> None:
|
def action_ssh(self) -> None:
|
||||||
"""Entre dans la VM créée, sans quitter l'écran.
|
"""Entre dans la VM créée, sans quitter l'écran.
|
||||||
|
|
||||||
Par son NOM et non par son adresse : c'est l'entrée
|
La commande vient de l'APPELANT quand il sait joindre la VM : sur
|
||||||
~/.ssh/config que le déploiement vient d'écrire qui sait comment
|
Proxmox, l'entrée ~/.ssh/config n'est écrite qu'après le
|
||||||
l'atteindre — et pour une VM posée sur un hôte Proxmox, elle
|
déploiement, et le nom de la VM peut désigner une machine LOCALE
|
||||||
porte le rebond, seul chemin vers son réseau interne.
|
homonyme — « s » ouvrait alors la mauvaise. Le déploiement passe
|
||||||
|
donc « ssh -J <hôte> user@<ip> », qui ne dépend de rien.
|
||||||
|
|
||||||
« suspend() » rend le terminal : ssh a besoin du clavier, et
|
« suspend() » rend le terminal : ssh a besoin du clavier, et
|
||||||
Textual le tient encore.
|
Textual le tient encore.
|
||||||
|
|
@ -780,8 +788,10 @@ def run_deploy_progress(jobs, parallelism, run_app: bool = True):
|
||||||
return
|
return
|
||||||
# La dernière créée : c'est celle qu'on regarde.
|
# La dernière créée : c'est celle qu'on regarde.
|
||||||
nom = self._reussies[-1]
|
nom = self._reussies[-1]
|
||||||
|
cmd = (ssh_cmds or {}).get(nom) or f"ssh {shlex_quote(nom)}"
|
||||||
with self.suspend():
|
with self.suspend():
|
||||||
os.system(f"ssh {shlex_quote(nom)} || true")
|
print(f"\n→ {cmd}\n")
|
||||||
|
os.system(f"{cmd} || true")
|
||||||
|
|
||||||
# -- presse-papiers (OSC 52 : traverse SSH) --------------------- #
|
# -- presse-papiers (OSC 52 : traverse SSH) --------------------- #
|
||||||
def _copy(self, text, what):
|
def _copy(self, text, what):
|
||||||
|
|
|
||||||
|
|
@ -967,7 +967,33 @@ class ProxmoxMenuMixin:
|
||||||
# sortie ; celle-ci les écrit, ce qui vaut mieux qu'un défilement.
|
# sortie ; celle-ci les écrit, ce qui vaut mieux qu'un défilement.
|
||||||
session = self._pve_log_dir()
|
session = self._pve_log_dir()
|
||||||
print(f"\n {t('Log:')} {session}")
|
print(f"\n {t('Log:')} {session}")
|
||||||
resultats = run_deploy_progress(travaux, spec.get("parallelism") or 1)
|
# Comment joindre chaque VM SANS dépendre de ~/.ssh/config, qui n'est
|
||||||
|
# écrit qu'après : par le rebond de l'hôte, explicitement. C'est ce que
|
||||||
|
# « s » utilise dans la vue de progression — sans quoi il partait sur
|
||||||
|
# le nom de la VM, donc sur une locale homonyme (rapporté).
|
||||||
|
cibles_ssh = {}
|
||||||
|
for vm in spec["vms"]:
|
||||||
|
ip = pve.ip_from_ipconfig(vm.get("ipconfig") or "")
|
||||||
|
if ip:
|
||||||
|
compte = (spec.get("user") or "erplibre") + "@" + ip
|
||||||
|
cibles_ssh[vm["name"]] = (
|
||||||
|
f"ssh -J {shlex.quote(host['target'])} "
|
||||||
|
f"{shlex.quote(compte)}"
|
||||||
|
)
|
||||||
|
# Ce qui attend derrière cet écran : sans le dire, on reste devant
|
||||||
|
# une fenêtre « terminée » sans savoir que l'installation d'ERPLibre
|
||||||
|
# démarre en la quittant.
|
||||||
|
suite = ""
|
||||||
|
if spec.get("install"):
|
||||||
|
suite = t("Quit (q) to start the ERPLibre install")
|
||||||
|
elif spec.get("monitor", True):
|
||||||
|
suite = t("Quit (q) to follow the VM starting up")
|
||||||
|
resultats = run_deploy_progress(
|
||||||
|
travaux,
|
||||||
|
spec.get("parallelism") or 1,
|
||||||
|
ssh_cmds=cibles_ssh,
|
||||||
|
suite=suite,
|
||||||
|
)
|
||||||
reussies = [nom for nom, code, _o, _d in resultats if code == 0]
|
reussies = [nom for nom, code, _o, _d in resultats if code == 0]
|
||||||
for nom, code, sortie, duree in resultats:
|
for nom, code, sortie, duree in resultats:
|
||||||
chemin = self._pve_write_log(
|
chemin = self._pve_write_log(
|
||||||
|
|
@ -1215,6 +1241,10 @@ class ProxmoxMenuMixin:
|
||||||
"sudo": host.get("sudo") or "",
|
"sudo": host.get("sudo") or "",
|
||||||
"jump": host.get("jump") or "",
|
"jump": host.get("jump") or "",
|
||||||
"vmid": vm.get("vmid"),
|
"vmid": vm.get("vmid"),
|
||||||
|
# L'adresse INTERNE : elle n'est pas routable d'ici, mais
|
||||||
|
# elle l'est depuis l'hôte. Avec le rebond, le tableau de
|
||||||
|
# bord entre dans la VM sans dépendre de ~/.ssh/config.
|
||||||
|
"addr": vm.get("adresse") or "",
|
||||||
}
|
}
|
||||||
for vm in joignables
|
for vm in joignables
|
||||||
if vm.get("vmid")
|
if vm.get("vmid")
|
||||||
|
|
|
||||||
|
|
@ -1309,10 +1309,14 @@ def read_domstats() -> str:
|
||||||
# disque et le cumul écrit de TOUTES ses VM d'un coup. Le « du » qui suit
|
# disque et le cumul écrit de TOUTES ses VM d'un coup. Le « du » qui suit
|
||||||
# donne la taille RÉELLEMENT occupée : sur un stockage en fichiers, Proxmox
|
# donne la taille RÉELLEMENT occupée : sur un stockage en fichiers, Proxmox
|
||||||
# rapporte « disk: 0 » — il ne la calcule pas.
|
# rapporte « disk: 0 » — il ne la calcule pas.
|
||||||
|
#
|
||||||
|
# « -sB1 » et NON « -sb » : le second rend la taille APPARENTE, et un disque
|
||||||
|
# raw creux de 6 Go la donne entière. La colonne affichait donc « 6.0G/6.0G »,
|
||||||
|
# un disque plein, quand l'invité n'avait écrit que 1,2 Go — rapporté.
|
||||||
PVE_STATS_CMD = (
|
PVE_STATS_CMD = (
|
||||||
"pvesh get /cluster/resources --type vm --output-format json;"
|
"pvesh get /cluster/resources --type vm --output-format json;"
|
||||||
" echo '---ERPLIBRE-DU---';"
|
" echo '---ERPLIBRE-DU---';"
|
||||||
" du -sb /var/lib/vz/images/*/ 2>/dev/null || true"
|
" du -sB1 /var/lib/vz/images/*/ 2>/dev/null || true"
|
||||||
)
|
)
|
||||||
# Une VM distante se relève moins souvent qu'une locale : chaque tour coûte
|
# Une VM distante se relève moins souvent qu'une locale : chaque tour coûte
|
||||||
# une poignée de main ssh (mesuré 1 s), quand « virsh domstats » coûte 0,03 s
|
# une poignée de main ssh (mesuré 1 s), quand « virsh domstats » coûte 0,03 s
|
||||||
|
|
@ -1337,7 +1341,7 @@ def parse_pvestats(text: str) -> dict:
|
||||||
ressources = json.loads(brut.strip() or "[]")
|
ressources = json.loads(brut.strip() or "[]")
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return {}
|
return {}
|
||||||
# {vmid: octets} depuis « du -sb /var/lib/vz/images/<vmid>/ ».
|
# {vmid: octets} depuis « du -sB1 /var/lib/vz/images/<vmid>/ ».
|
||||||
occupe = {}
|
occupe = {}
|
||||||
for ligne in tailles.splitlines():
|
for ligne in tailles.splitlines():
|
||||||
parts = ligne.split()
|
parts = ligne.split()
|
||||||
|
|
@ -1404,6 +1408,46 @@ def read_pvestats(vms, now=None) -> dict:
|
||||||
return dict(stats)
|
return dict(stats)
|
||||||
|
|
||||||
|
|
||||||
|
def vm_ssh_prefix(vm) -> str:
|
||||||
|
"""« ssh … » pour entrer dans CETTE VM, adresse comprise.
|
||||||
|
|
||||||
|
Une VM d'un hôte Proxmox vit derrière lui : son adresse n'est pas
|
||||||
|
routable d'ici, et seul le rebond y mène. On le construit explicitement
|
||||||
|
plutôt que de compter sur un alias ~/.ssh/config, qui peut ne pas exister
|
||||||
|
— ou, pire, désigner une VM LOCALE homonyme. C'est ce qui a fait ouvrir
|
||||||
|
la mauvaise machine avec « s ».
|
||||||
|
"""
|
||||||
|
info = (vm or {}).get("pve") or {}
|
||||||
|
adresse = info.get("addr")
|
||||||
|
if info.get("target") and adresse:
|
||||||
|
saut = f"-J {shlex.quote(info['jump'])} " if info.get("jump") else ""
|
||||||
|
return (
|
||||||
|
f"ssh {SSH_OPTS} {saut}-J {shlex.quote(info['target'])} "
|
||||||
|
f"erplibre@{adresse}"
|
||||||
|
)
|
||||||
|
return f"ssh {SSH_OPTS} erplibre@{(vm or {}).get('ip')}"
|
||||||
|
|
||||||
|
|
||||||
|
def pve_host_cmd(info, remote, tty=False) -> str:
|
||||||
|
"""Commande shell qui exécute `remote` SUR l'hôte Proxmox d'une VM.
|
||||||
|
|
||||||
|
Chaque action du tableau de bord qui parlait à libvirt par le NOM frappait
|
||||||
|
la mauvaise machine dès qu'un domaine local portait le même : la console
|
||||||
|
ouvrait celle de la VM locale, la pause suspendait la locale. L'hôte est
|
||||||
|
la seule autorité pour une VM distante, et le VMID son seul identifiant.
|
||||||
|
"""
|
||||||
|
sudo = (info or {}).get("sudo") or ""
|
||||||
|
cible = (info or {}).get("target") or ""
|
||||||
|
prefixe = f"{sudo}sh -c {shlex.quote(remote)}" if sudo else remote
|
||||||
|
saut = (
|
||||||
|
f"-J {shlex.quote(info['jump'])} " if (info or {}).get("jump") else ""
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
f"ssh {'-t ' if tty else ''}{saut}{shlex.quote(cible)} "
|
||||||
|
f"{shlex.quote(prefixe)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def arm_balloon(names) -> None:
|
def arm_balloon(names) -> None:
|
||||||
"""Arme la période de collecte du ballon (5 s) sur chaque VM.
|
"""Arme la période de collecte du ballon (5 s) sur chaque VM.
|
||||||
|
|
||||||
|
|
@ -2384,8 +2428,10 @@ def run_monitor(manifest_path: str, run_app: bool = True):
|
||||||
vm = self._vm_by_name(self._selected)
|
vm = self._vm_by_name(self._selected)
|
||||||
if not vm:
|
if not vm:
|
||||||
return
|
return
|
||||||
|
cmd = vm_ssh_prefix(vm)
|
||||||
with self.suspend():
|
with self.suspend():
|
||||||
os.system(f"ssh {SSH_OPTS} erplibre@{vm['ip']} || true")
|
print(f"\n→ {cmd}\n")
|
||||||
|
os.system(f"{cmd} || true")
|
||||||
|
|
||||||
def action_console(self) -> None:
|
def action_console(self) -> None:
|
||||||
"""Console série de la VM, sans quitter le suivi.
|
"""Console série de la VM, sans quitter le suivi.
|
||||||
|
|
@ -2402,18 +2448,33 @@ def run_monitor(manifest_path: str, run_app: bool = True):
|
||||||
vm = self._vm_by_name(self._selected)
|
vm = self._vm_by_name(self._selected)
|
||||||
if not vm:
|
if not vm:
|
||||||
return
|
return
|
||||||
name = shlex.quote(vm["name"])
|
info = vm.get("pve")
|
||||||
|
if info:
|
||||||
|
# VM d'un hôte Proxmox : sa console est « qm terminal », sur
|
||||||
|
# l'hôte. « virsh console <nom> » ouvrait celle du domaine
|
||||||
|
# LOCAL homonyme — la mauvaise machine, sans le dire.
|
||||||
|
cmd = pve_host_cmd(
|
||||||
|
info, f"qm terminal {int(info.get('vmid') or 0)}", tty=True
|
||||||
|
)
|
||||||
|
titre = (
|
||||||
|
f"qm terminal {info.get('vmid')} @ {info.get('target')}"
|
||||||
|
)
|
||||||
|
sortie = "Ctrl+O"
|
||||||
|
else:
|
||||||
|
cmd = f"sudo virsh console {shlex.quote(vm['name'])}"
|
||||||
|
titre = f"virsh console {vm['name']}"
|
||||||
|
sortie = "Ctrl+]"
|
||||||
with self.suspend():
|
with self.suspend():
|
||||||
# La console n'affiche que ce qui arrive APRÈS l'attachement :
|
# La console n'affiche que ce qui arrive APRÈS l'attachement :
|
||||||
# sur une VM déjà démarrée l'écran reste noir tant qu'on n'a
|
# sur une VM déjà démarrée l'écran reste noir tant qu'on n'a
|
||||||
# rien envoyé. On le dit, plutôt que de laisser croire à un gel.
|
# rien envoyé. On le dit, plutôt que de laisser croire à un gel.
|
||||||
print(f"\n→ virsh console {vm['name']}")
|
print(f"\n→ {titre}")
|
||||||
print(
|
print(
|
||||||
" Écran vide ? Appuyez sur Entrée : la console ne montre"
|
" Écran vide ? Appuyez sur Entrée : la console ne montre"
|
||||||
" que la sortie qui suit l'attachement."
|
" que la sortie qui suit l'attachement."
|
||||||
)
|
)
|
||||||
print(" Ctrl+] puis Entrée pour revenir au suivi.\n")
|
print(f" {sortie} puis Entrée pour revenir au suivi.\n")
|
||||||
os.system(f"sudo virsh console {name} || true")
|
os.system(f"{cmd} || true")
|
||||||
|
|
||||||
def action_web(self) -> None:
|
def action_web(self) -> None:
|
||||||
"""Ouvre l'UI web de la VM (Odoo :8069) dans un navigateur CLI
|
"""Ouvre l'UI web de la VM (Odoo :8069) dans un navigateur CLI
|
||||||
|
|
@ -2552,15 +2613,30 @@ def run_monitor(manifest_path: str, run_app: bool = True):
|
||||||
|
|
||||||
# -- pause / reprise de tout le parc -------------------------------- #
|
# -- pause / reprise de tout le parc -------------------------------- #
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _virsh_bulk(action, names):
|
def _virsh_bulk(action, cibles):
|
||||||
for n in names:
|
"""Suspend/reprend chaque VM, chacune par SON hyperviseur.
|
||||||
|
|
||||||
|
`cibles` : [(nom, info_pve|None)]. Une VM distante se suspend par
|
||||||
|
son VMID sur son hôte — « virsh suspend <nom> » aurait mis en
|
||||||
|
pause le domaine LOCAL homonyme."""
|
||||||
|
for nom, info in cibles:
|
||||||
try:
|
try:
|
||||||
subprocess.run(
|
if info:
|
||||||
["sudo", "virsh", action, n],
|
vmid = int(info.get("vmid") or 0)
|
||||||
capture_output=True,
|
subprocess.run(
|
||||||
text=True,
|
pve_host_cmd(info, f"qm {action} {vmid}"),
|
||||||
timeout=30,
|
shell=True,
|
||||||
)
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=60,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
subprocess.run(
|
||||||
|
["sudo", "virsh", action, nom],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=30,
|
||||||
|
)
|
||||||
except (OSError, subprocess.SubprocessError):
|
except (OSError, subprocess.SubprocessError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
@ -2701,8 +2777,7 @@ def run_monitor(manifest_path: str, run_app: bool = True):
|
||||||
with self.suspend():
|
with self.suspend():
|
||||||
print(f"\n=== {title} — {vm['name']} ===")
|
print(f"\n=== {title} — {vm['name']} ===")
|
||||||
os.system(
|
os.system(
|
||||||
f"ssh {SSH_OPTS} erplibre@{vm['ip']} "
|
f"{vm_ssh_prefix(vm)} " f"{shlex.quote(cmd)} || true"
|
||||||
f"{shlex.quote(cmd)} || true"
|
|
||||||
)
|
)
|
||||||
input("\nEntrée pour revenir au suivi… ")
|
input("\nEntrée pour revenir au suivi… ")
|
||||||
|
|
||||||
|
|
@ -2751,7 +2826,7 @@ def run_monitor(manifest_path: str, run_app: bool = True):
|
||||||
async def _bulk_worker(self, action):
|
async def _bulk_worker(self, action):
|
||||||
want = "running" if action == "suspend" else "paused"
|
want = "running" if action == "suspend" else "paused"
|
||||||
targets = [
|
targets = [
|
||||||
vm["name"]
|
(vm["name"], vm.get("pve"))
|
||||||
for vm in vms
|
for vm in vms
|
||||||
if self._domstate.get(vm["name"]) == want
|
if self._domstate.get(vm["name"]) == want
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -3986,6 +3986,14 @@ TRANSLATIONS = {
|
||||||
"fr": "Fichiers orphelins :",
|
"fr": "Fichiers orphelins :",
|
||||||
"en": "Orphan files:",
|
"en": "Orphan files:",
|
||||||
},
|
},
|
||||||
|
"Quit (q) to start the ERPLibre install": {
|
||||||
|
"fr": "Quitter (q) pour lancer l'installation d'ERPLibre",
|
||||||
|
"en": "Quit (q) to start the ERPLibre install",
|
||||||
|
},
|
||||||
|
"Quit (q) to follow the VM starting up": {
|
||||||
|
"fr": "Quitter (q) pour suivre la mise en route de la VM",
|
||||||
|
"en": "Quit (q) to follow the VM starting up",
|
||||||
|
},
|
||||||
"A local VM already bears this name:": {
|
"A local VM already bears this name:": {
|
||||||
"fr": "Une VM locale porte déjà ce nom :",
|
"fr": "Une VM locale porte déjà ce nom :",
|
||||||
"en": "A local VM already bears this name:",
|
"en": "A local VM already bears this name:",
|
||||||
|
|
|
||||||
|
|
@ -178,6 +178,88 @@ class TestLaBonneMachine(unittest.TestCase):
|
||||||
self.assertIn("ip=cible", script)
|
self.assertIn("ip=cible", script)
|
||||||
|
|
||||||
|
|
||||||
|
class TestLeDisque(unittest.TestCase):
|
||||||
|
"""La colonne Disque annonçait un disque PLEIN qui ne l'était pas.
|
||||||
|
|
||||||
|
Rapporté : « 6.0G/6.0G » sur une VM dont l'invité disait « 845M utilisés
|
||||||
|
sur 5.8G ». La mesure venait de « du -sb », qui rend la taille APPARENTE :
|
||||||
|
un disque raw creux la donne entière. « du -sB1 » compte les blocs
|
||||||
|
réellement occupés — 1,2 Go, ce qui correspond.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def test_the_command_counts_real_blocks(self):
|
||||||
|
self.assertIn("du -sB1", mon.PVE_STATS_CMD)
|
||||||
|
self.assertNotIn("du -sb", mon.PVE_STATS_CMD)
|
||||||
|
|
||||||
|
def test_the_measure_is_read_per_vmid(self):
|
||||||
|
texte = (
|
||||||
|
'[{"vmid":101,"name":"vm-a","disk":0,"maxdisk":6442450944,'
|
||||||
|
'"mem":1,"maxmem":2,"diskwrite":0,"status":"running","uptime":1}]\n'
|
||||||
|
"---ERPLIBRE-DU---\n"
|
||||||
|
"1268518912\t/var/lib/vz/images/101/\n"
|
||||||
|
"4294967296\t/var/lib/vz/images/999/\n"
|
||||||
|
)
|
||||||
|
rec = mon.parse_pvestats(texte)["vm-a"]
|
||||||
|
self.assertEqual(rec["disk_used"], 1268518912)
|
||||||
|
self.assertEqual(rec["disk_total"], 6442450944)
|
||||||
|
self.assertEqual(
|
||||||
|
mon.fmt_pair(rec["disk_used"], rec["disk_total"]), "1.2G/6.0G"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestOuVaLaCommande(unittest.TestCase):
|
||||||
|
"""Chaque action doit viser la BONNE machine.
|
||||||
|
|
||||||
|
Une VM distante et un domaine local peuvent porter le même nom : « s »
|
||||||
|
ouvrait la locale, la console ouvrait la console de la locale, et la pause
|
||||||
|
suspendait la locale. Le VMID et le rebond sont les seules désignations
|
||||||
|
qui ne trompent pas.
|
||||||
|
"""
|
||||||
|
|
||||||
|
LOCALE = {"name": "vm-a", "ip": "192.168.123.118"}
|
||||||
|
DISTANTE = {
|
||||||
|
"name": "vm-a",
|
||||||
|
"ip": "pve1+vm-a",
|
||||||
|
"pve": {
|
||||||
|
"target": "erplibre@pve1",
|
||||||
|
"sudo": "sudo ",
|
||||||
|
"jump": "",
|
||||||
|
"vmid": 101,
|
||||||
|
"addr": "10.10.10.151",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_ssh_to_a_local_vm_uses_its_address(self):
|
||||||
|
self.assertIn(
|
||||||
|
"erplibre@192.168.123.118", mon.vm_ssh_prefix(self.LOCALE)
|
||||||
|
)
|
||||||
|
self.assertNotIn("-J", mon.vm_ssh_prefix(self.LOCALE))
|
||||||
|
|
||||||
|
def test_ssh_to_a_remote_vm_goes_through_the_jump(self):
|
||||||
|
cmd = mon.vm_ssh_prefix(self.DISTANTE)
|
||||||
|
self.assertIn("-J", cmd)
|
||||||
|
self.assertIn("erplibre@pve1", cmd)
|
||||||
|
self.assertIn("erplibre@10.10.10.151", cmd)
|
||||||
|
|
||||||
|
def test_without_an_address_it_falls_back_to_the_alias(self):
|
||||||
|
vm = {"name": "vm-a", "ip": "pve1+vm-a", "pve": {"target": "pve1"}}
|
||||||
|
self.assertIn("erplibre@pve1+vm-a", mon.vm_ssh_prefix(vm))
|
||||||
|
|
||||||
|
def test_the_console_of_a_remote_vm_is_qm_terminal(self):
|
||||||
|
cmd = mon.pve_host_cmd(
|
||||||
|
self.DISTANTE["pve"], "qm terminal 101", tty=True
|
||||||
|
)
|
||||||
|
self.assertIn("ssh -t", cmd)
|
||||||
|
self.assertIn("qm terminal 101", cmd)
|
||||||
|
self.assertIn("sudo", cmd)
|
||||||
|
# Et surtout PAS virsh, qui viserait le domaine local homonyme.
|
||||||
|
self.assertNotIn("virsh", cmd)
|
||||||
|
|
||||||
|
def test_a_host_without_sudo_is_not_wrapped(self):
|
||||||
|
cmd = mon.pve_host_cmd({"target": "root@pve1", "sudo": ""}, "qm list")
|
||||||
|
self.assertNotIn("sh -c", cmd)
|
||||||
|
|
||||||
|
|
||||||
class TestLEtat(unittest.TestCase):
|
class TestLEtat(unittest.TestCase):
|
||||||
"""Une VM absente de « virsh list » passait pour EFFACÉE."""
|
"""Une VM absente de « virsh list » passait pour EFFACÉE."""
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -142,5 +142,80 @@ class TestLaToucheSsh(unittest.TestCase):
|
||||||
self.assertEqual(vu["reussies"], [])
|
self.assertEqual(vu["reussies"], [])
|
||||||
|
|
||||||
|
|
||||||
|
@unittest.skipUnless(TEXTUAL, "Textual absent")
|
||||||
|
class TestCeQuiSuit(unittest.TestCase):
|
||||||
|
"""Rapporté : on attendait devant une fenêtre « terminée » sans savoir
|
||||||
|
que l'installation d'ERPLibre démarre en la quittant."""
|
||||||
|
|
||||||
|
def _sommaire(self, suite, attendre=3.0):
|
||||||
|
# Un travail qui DURE : sinon il finit avant le premier relevé, et le
|
||||||
|
# test ne prouve rien de l'avant/après.
|
||||||
|
jobs = [("1", "vm-a", ["bash", "-c", "sleep 2; echo ok"])]
|
||||||
|
vu = {}
|
||||||
|
|
||||||
|
async def scenario():
|
||||||
|
from textual.widgets import Static
|
||||||
|
|
||||||
|
app = run_deploy_progress(jobs, 1, run_app=False, suite=suite)
|
||||||
|
async with app.run_test(size=(140, 30)) as pilote:
|
||||||
|
await pilote.pause()
|
||||||
|
vu["pendant"] = str(app.query_one("#summary", Static).render())
|
||||||
|
await asyncio.sleep(attendre)
|
||||||
|
await pilote.pause()
|
||||||
|
vu["apres"] = str(app.query_one("#summary", Static).render())
|
||||||
|
|
||||||
|
asyncio.run(scenario())
|
||||||
|
return vu
|
||||||
|
|
||||||
|
def test_it_says_what_follows_once_everything_is_done(self):
|
||||||
|
vu = self._sommaire("Quitter (q) pour lancer l'installation")
|
||||||
|
self.assertNotIn("Quitter", vu["pendant"])
|
||||||
|
self.assertIn("Quitter", vu["apres"])
|
||||||
|
|
||||||
|
def test_nothing_is_promised_when_nothing_follows(self):
|
||||||
|
vu = self._sommaire("")
|
||||||
|
self.assertNotIn("→", vu["apres"])
|
||||||
|
|
||||||
|
|
||||||
|
@unittest.skipUnless(TEXTUAL, "Textual absent")
|
||||||
|
class TestLaCibleSsh(unittest.TestCase):
|
||||||
|
"""« s » utilisait le NOM de la VM. Sur Proxmox, l'entrée ~/.ssh/config
|
||||||
|
n'existe pas encore à ce moment — et ce nom peut désigner une machine
|
||||||
|
LOCALE homonyme, qui s'ouvrait alors à sa place."""
|
||||||
|
|
||||||
|
def _lance(self, ssh_cmds):
|
||||||
|
import contextlib
|
||||||
|
import os
|
||||||
|
|
||||||
|
jobs = [("1", "erplibre-ubuntu-2604", ["bash", "-c", "echo ok"])]
|
||||||
|
vu = []
|
||||||
|
|
||||||
|
async def scenario():
|
||||||
|
app = run_deploy_progress(
|
||||||
|
jobs, 1, run_app=False, ssh_cmds=ssh_cmds
|
||||||
|
)
|
||||||
|
async with app.run_test(size=(140, 30)) as pilote:
|
||||||
|
await pilote.pause()
|
||||||
|
await asyncio.sleep(1.2)
|
||||||
|
await pilote.pause()
|
||||||
|
vrai = os.system
|
||||||
|
os.system = vu.append
|
||||||
|
app.suspend = lambda: contextlib.nullcontext()
|
||||||
|
try:
|
||||||
|
app.action_ssh()
|
||||||
|
finally:
|
||||||
|
os.system = vrai
|
||||||
|
|
||||||
|
asyncio.run(scenario())
|
||||||
|
return vu[0] if vu else ""
|
||||||
|
|
||||||
|
def test_the_given_command_wins(self):
|
||||||
|
cible = "ssh -J erplibre-proxmox-9 erplibre@10.10.10.151"
|
||||||
|
self.assertIn(cible, self._lance({"erplibre-ubuntu-2604": cible}))
|
||||||
|
|
||||||
|
def test_without_one_it_falls_back_to_the_name(self):
|
||||||
|
self.assertIn("ssh erplibre-ubuntu-2604", self._lance(None))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main(verbosity=2)
|
unittest.main(verbosity=2)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue