[ADD] qemu: statistics screen, with reset — and record failures too
The install history was only surfaced as a « ~5m avg (3) » suffix next to a distro. Entry [12] now shows what that history actually contains: totals and success rate, period covered, median/min/max and cumulated duration, then a breakdown by distribution, version and architecture, plus the current VMs and the disk they occupy. « [r] » erases the history after confirmation. record_duration() was only called on success, so no success rate could ever be computed. Failures are now recorded with ok=False. They are counted separately and EXCLUDED from the averages and the ETA: how long a failed install ran says nothing about how long a successful one takes. Entries written before the flag existed have no « ok » key and are read as successes, which they were. Aggregation lives in qemu_install_monitor.py as pure functions (stats_summary, stats_by, all_runs, reset_stats), display in todo.py — the split the file already follows. Disk presets extended to 400G, 600G, 800G, 1T, 1.5T and 2T. The parser only understood G, so « 1T » would have been rejected: sizes are now normalised through _qemu_parse_disk (1 T = 1024 G, decimal comma accepted), since the rest of the chain reasons in gigabytes. Verified with a synthetic history of 9 runs including 2 failures: the rate, the per-group failure counts and the reset all behave; a group with no success shows « — » rather than a misleading « ~0s ». Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6d3a4c38cb
commit
c1afa2f4d2
3 changed files with 352 additions and 45 deletions
|
|
@ -260,7 +260,8 @@ def scan_log_errors(log_path: str) -> tuple[int, int]:
|
|||
"""(nb_erreurs, nb_avertissements) dans un log d'installation, en
|
||||
réutilisant la détection de la suite de tests ERPLibre : sous-chaîne
|
||||
« error »/« warning » (insensible à la casse) moins les listes d'ignore.
|
||||
Lit le fichier COMPLET (appelé une seule fois, à la complétion d'une VM)."""
|
||||
Lit le fichier COMPLET (appelé une seule fois, à la complétion d'une VM).
|
||||
"""
|
||||
try:
|
||||
text = Path(log_path).read_text(errors="replace")
|
||||
except OSError:
|
||||
|
|
@ -270,9 +271,7 @@ def scan_log_errors(log_path: str) -> tuple[int, int]:
|
|||
low = line.lower()
|
||||
if EXIT_MARKER in line:
|
||||
continue
|
||||
if "error" in low and not any(
|
||||
ig in line for ig in _LST_IGNORE_ERROR
|
||||
):
|
||||
if "error" in low and not any(ig in line for ig in _LST_IGNORE_ERROR):
|
||||
nerr += 1
|
||||
if "warning" in low and not any(
|
||||
ig in line for ig in _LST_IGNORE_WARNING
|
||||
|
|
@ -311,7 +310,8 @@ def _read_tail(
|
|||
`max_lines` lignes) et renvoie (texte, TAILLE TOTALE du fichier). Utilisé
|
||||
au CHANGEMENT de VM : lire+réafficher le fichier ENTIER (offset 0) gelait
|
||||
l'UI sur les gros logs (250 Ko / milliers de lignes). L'offset renvoyé =
|
||||
taille totale -> le suivi incrémental (_tick_log) continue depuis la fin."""
|
||||
taille totale -> le suivi incrémental (_tick_log) continue depuis la fin.
|
||||
"""
|
||||
try:
|
||||
size = os.path.getsize(path)
|
||||
with open(path, "rb") as fh:
|
||||
|
|
@ -351,10 +351,14 @@ def load_stats() -> dict:
|
|||
return {}
|
||||
|
||||
|
||||
def record_duration(distro, version, arch, secs) -> None:
|
||||
def record_duration(distro, version, arch, secs, ok=True) -> None:
|
||||
"""Enregistre une install (distro + version + archi + durée + horodatage)
|
||||
dans l'historique, pour l'ETA et les moyennes par archi/distro. Garde les
|
||||
500 derniers runs."""
|
||||
500 derniers runs.
|
||||
|
||||
`ok=False` conserve la trace d'un ÉCHEC : sans elle aucun taux de réussite
|
||||
n'est calculable. Les échecs sont exclus des moyennes et de l'ETA (leur
|
||||
durée ne dit rien du temps d'une install qui aboutit)."""
|
||||
data = load_stats()
|
||||
runs = data.setdefault("runs", [])
|
||||
runs.append(
|
||||
|
|
@ -364,6 +368,7 @@ def record_duration(distro, version, arch, secs) -> None:
|
|||
"arch": arch or "?",
|
||||
"seconds": int(secs),
|
||||
"ts": int(time.time()),
|
||||
"ok": bool(ok),
|
||||
}
|
||||
)
|
||||
data["runs"] = runs[-500:]
|
||||
|
|
@ -373,10 +378,82 @@ def record_duration(distro, version, arch, secs) -> None:
|
|||
pass
|
||||
|
||||
|
||||
def _runs(stats=None):
|
||||
def reset_stats() -> int:
|
||||
"""Vide l'historique. Renvoie le nombre de runs effacés."""
|
||||
count = len(all_runs())
|
||||
try:
|
||||
_stats_path().write_text(json.dumps({"runs": []}))
|
||||
except OSError:
|
||||
pass
|
||||
return count
|
||||
|
||||
|
||||
def all_runs(stats=None):
|
||||
"""Tous les runs, succès ET échecs."""
|
||||
return (stats or load_stats()).get("runs", []) or []
|
||||
|
||||
|
||||
def _runs(stats=None):
|
||||
"""Runs RÉUSSIS seulement : base des moyennes et de l'ETA.
|
||||
|
||||
Les entrées écrites avant l'ajout du champ « ok » n'en ont pas ; elles
|
||||
étaient forcément des succès (seuls ceux-là étaient enregistrés).
|
||||
"""
|
||||
return [r for r in all_runs(stats) if r.get("ok", True)]
|
||||
|
||||
|
||||
def stats_summary(stats=None):
|
||||
"""Chiffres globaux de l'historique d'installation.
|
||||
|
||||
Renvoie un dict vide quand rien n'a encore été enregistré, pour que
|
||||
l'appelant distingue « aucune donnée » de « zéro seconde »."""
|
||||
runs = all_runs(stats)
|
||||
if not runs:
|
||||
return {}
|
||||
ok = [r for r in runs if r.get("ok", True)]
|
||||
secs = sorted(r["seconds"] for r in ok)
|
||||
lst_ts = [r.get("ts", 0) for r in runs if r.get("ts")]
|
||||
return {
|
||||
"total": len(runs),
|
||||
"ok": len(ok),
|
||||
"failed": len(runs) - len(ok),
|
||||
"first_ts": min(lst_ts) if lst_ts else 0,
|
||||
"last_ts": max(lst_ts) if lst_ts else 0,
|
||||
"median": secs[len(secs) // 2] if secs else 0,
|
||||
"min": secs[0] if secs else 0,
|
||||
"max": secs[-1] if secs else 0,
|
||||
"total_secs": sum(secs),
|
||||
}
|
||||
|
||||
|
||||
def stats_by(field, stats=None):
|
||||
"""Agrège par « distro », « arch » ou « distro version ».
|
||||
|
||||
Renvoie [(clé, nb_réussis, moyenne_secondes, nb_échecs)] trié du plus
|
||||
utilisé au moins utilisé."""
|
||||
dct = {}
|
||||
for run in all_runs(stats):
|
||||
if field == "version":
|
||||
key = f"{run.get('distro', '?')} {run.get('version', '?')}"
|
||||
else:
|
||||
key = run.get(field, "?")
|
||||
entry = dct.setdefault(key, {"secs": [], "failed": 0})
|
||||
if run.get("ok", True):
|
||||
entry["secs"].append(run["seconds"])
|
||||
else:
|
||||
entry["failed"] += 1
|
||||
lst = [
|
||||
(
|
||||
key,
|
||||
len(e["secs"]),
|
||||
int(sum(e["secs"]) / len(e["secs"])) if e["secs"] else 0,
|
||||
e["failed"],
|
||||
)
|
||||
for key, e in dct.items()
|
||||
]
|
||||
return sorted(lst, key=lambda row: (-(row[1] + row[3]), row[0]))
|
||||
|
||||
|
||||
def eta_reference(stats, arch):
|
||||
"""Durée d'install de RÉFÉRENCE (médiane) pour cette archi ; repli toutes
|
||||
archis confondues. None si aucun historique."""
|
||||
|
|
@ -650,7 +727,9 @@ def run_monitor(manifest_path: str, run_app: bool = True):
|
|||
self._cells = {}
|
||||
# Historique de durées (ETA) + dossier disque à surveiller.
|
||||
self._stats = load_stats()
|
||||
self._disk_dir = os.path.dirname(vm_disk_path(vms[0])) if vms else "/"
|
||||
self._disk_dir = (
|
||||
os.path.dirname(vm_disk_path(vms[0])) if vms else "/"
|
||||
)
|
||||
# État libvirt (running/paused/gone), rafraîchi à intervalle LENT.
|
||||
self._domstate = {}
|
||||
# Erreurs détectées dans le log à la complétion : {nom: (err, warn)}.
|
||||
|
|
@ -698,7 +777,13 @@ def run_monitor(manifest_path: str, run_app: bool = True):
|
|||
table.add_column("Disque", key="disk", width=8)
|
||||
for i, vm in enumerate(vms, 1):
|
||||
table.add_row(
|
||||
str(i), vm["name"], "", "⏳", "—", "--:--", "-",
|
||||
str(i),
|
||||
vm["name"],
|
||||
"",
|
||||
"⏳",
|
||||
"—",
|
||||
"--:--",
|
||||
"-",
|
||||
key=vm["name"],
|
||||
)
|
||||
# max_lines borne la mémoire/rendu (un install verbeux × 30 VM).
|
||||
|
|
@ -719,7 +804,9 @@ def run_monitor(manifest_path: str, run_app: bool = True):
|
|||
def on_mount(self) -> None:
|
||||
# Le NOMBRE de VM figure dans le titre ; le sous-titre suit la
|
||||
# progression (terminées / total + durée globale).
|
||||
self.title = f"ERPLibre — {t('install monitoring')} ({len(vms)} VM)"
|
||||
self.title = (
|
||||
f"ERPLibre — {t('install monitoring')} ({len(vms)} VM)"
|
||||
)
|
||||
self.sub_title = f"0/{len(vms)} {t('completed')}"
|
||||
self._refresh_ssh()
|
||||
self._load_selected_log(reset=True)
|
||||
|
|
@ -808,9 +895,10 @@ def run_monitor(manifest_path: str, run_app: bool = True):
|
|||
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":
|
||||
if (
|
||||
name not in self._odoo_up
|
||||
and self._domstate.get(name) != "gone"
|
||||
):
|
||||
if _port_open(vm.get("ip"), 8069):
|
||||
odoo[name] = True
|
||||
return disks, status, self._collect_tele(), errors, odoo
|
||||
|
|
@ -836,7 +924,9 @@ def run_monitor(manifest_path: str, run_app: bool = True):
|
|||
self._set_cell(table, name, "disk", disks.get(name, "-"))
|
||||
# Colonne Odoo : 🟢 dès que :8069 répond, sinon « — ».
|
||||
self._set_cell(
|
||||
table, name, "odoo",
|
||||
table,
|
||||
name,
|
||||
"odoo",
|
||||
"🟢" if name in self._odoo_up else "—",
|
||||
)
|
||||
if name in self._final:
|
||||
|
|
@ -855,14 +945,16 @@ def run_monitor(manifest_path: str, run_app: bool = True):
|
|||
if state in ("done", "failed"):
|
||||
elapsed = now - started
|
||||
self._final[name] = (state, code, elapsed)
|
||||
if state == "done":
|
||||
record_duration(
|
||||
vm.get("distro"),
|
||||
vm.get("version"),
|
||||
vm.get("arch"),
|
||||
elapsed,
|
||||
)
|
||||
self._stats = load_stats()
|
||||
# Les échecs sont enregistrés eux aussi (ok=False) :
|
||||
# sans eux, aucun taux de réussite n'est calculable.
|
||||
record_duration(
|
||||
vm.get("distro"),
|
||||
vm.get("version"),
|
||||
vm.get("arch"),
|
||||
elapsed,
|
||||
ok=state == "done",
|
||||
)
|
||||
self._stats = load_stats()
|
||||
lbl = "✅" if state == "done" else f"❌ ({code})"
|
||||
self._set_cell(table, name, "state", lbl)
|
||||
self._set_cell(
|
||||
|
|
@ -871,7 +963,9 @@ def run_monitor(manifest_path: str, run_app: bool = True):
|
|||
# Colonne ⚠ (à gauche d'État) : erreurs détectées dans
|
||||
# le log, y compris pour un « succès ».
|
||||
self._set_cell(
|
||||
table, name, "err",
|
||||
table,
|
||||
name,
|
||||
"err",
|
||||
self._err_label(self._errcount.get(name)),
|
||||
)
|
||||
elif ds == "paused":
|
||||
|
|
@ -940,9 +1034,14 @@ def run_monitor(manifest_path: str, run_app: bool = True):
|
|||
else:
|
||||
running += 1
|
||||
return {
|
||||
"total": len(vms), "done": done, "fail": fail,
|
||||
"deleted": deleted, "running": running, "paused": paused,
|
||||
"err_vms": err_vms, "warn_vms": warn_vms,
|
||||
"total": len(vms),
|
||||
"done": done,
|
||||
"fail": fail,
|
||||
"deleted": deleted,
|
||||
"running": running,
|
||||
"paused": paused,
|
||||
"err_vms": err_vms,
|
||||
"warn_vms": warn_vms,
|
||||
}
|
||||
|
||||
def _update_stats(self):
|
||||
|
|
@ -1082,9 +1181,11 @@ def run_monitor(manifest_path: str, run_app: bool = True):
|
|||
for i, b in enumerate(available, 1):
|
||||
print(f" [{i}] {b}{' *' if i == 1 else ''}")
|
||||
print(" [i] Installer un autre navigateur")
|
||||
sel = input(
|
||||
f"Choix (numéro, vide = {available[0]}) : "
|
||||
).strip().lower()
|
||||
sel = (
|
||||
input(f"Choix (numéro, vide = {available[0]}) : ")
|
||||
.strip()
|
||||
.lower()
|
||||
)
|
||||
if sel == "i":
|
||||
return self._install_cli_browser()
|
||||
if not sel:
|
||||
|
|
@ -1123,9 +1224,7 @@ def run_monitor(manifest_path: str, run_app: bool = True):
|
|||
return None
|
||||
printable = " ".join(cmd)
|
||||
print(f"Commande : {printable}")
|
||||
ans = input(
|
||||
"Installer maintenant ? (o/N) : "
|
||||
).strip().lower()
|
||||
ans = input("Installer maintenant ? (o/N) : ").strip().lower()
|
||||
if ans not in ("o", "oui", "y", "yes"):
|
||||
return None
|
||||
rc = os.system(printable)
|
||||
|
|
|
|||
|
|
@ -1085,6 +1085,7 @@ class TODO:
|
|||
"Reopen install monitoring (last run / history)"
|
||||
)
|
||||
},
|
||||
{"prompt_description": t("Statistics (installs, durations, VMs)")},
|
||||
{"section": t("Catalog")},
|
||||
{"prompt_description": t("List available images and specs")},
|
||||
]
|
||||
|
|
@ -1121,6 +1122,8 @@ class TODO:
|
|||
elif status == "11":
|
||||
self._qemu_reopen_monitor()
|
||||
elif status == "12":
|
||||
self._qemu_stats()
|
||||
elif status == "13":
|
||||
self._qemu_list_images()
|
||||
else:
|
||||
cmd_no_found = True
|
||||
|
|
@ -1137,6 +1140,134 @@ class TODO:
|
|||
if cmd_no_found:
|
||||
print(t("Command not found !"))
|
||||
|
||||
def _qemu_stats(self):
|
||||
"""Statistiques d'utilisation de QEMU, et remise à zéro.
|
||||
|
||||
Tout vient de l'historique tenu par le moniteur d'installation
|
||||
(.venv.erplibre/qemu_install_stats.json) et de l'état libvirt courant.
|
||||
"""
|
||||
try:
|
||||
from script.todo import qemu_install_monitor as mon
|
||||
except ImportError:
|
||||
print(t("Install textual for the dashboard (pip)."))
|
||||
return
|
||||
|
||||
while True:
|
||||
summary = mon.stats_summary()
|
||||
print(f"\n📊 {t('QEMU statistics')}")
|
||||
if not summary:
|
||||
print(f" {t('No installation recorded yet.')}")
|
||||
else:
|
||||
rate = 100 * summary["ok"] // max(summary["total"], 1)
|
||||
print(f"\n── {t('Installations')} ──")
|
||||
print(
|
||||
f" {t('Total'):<18}: {summary['total']}"
|
||||
f" ({summary['ok']} {t('succeeded')},"
|
||||
f" {summary['failed']} {t('failed')} — {rate} %)"
|
||||
)
|
||||
if summary["first_ts"]:
|
||||
days = max(
|
||||
1,
|
||||
(summary["last_ts"] - summary["first_ts"]) // 86400,
|
||||
)
|
||||
print(
|
||||
f" {t('Period'):<18}:"
|
||||
f" {self._qemu_stamp(summary['first_ts'])}"
|
||||
f" → {self._qemu_stamp(summary['last_ts'])}"
|
||||
f" ({days} {t('days')})"
|
||||
)
|
||||
print(
|
||||
f" {t('Median duration'):<18}:"
|
||||
f" {mon._fmt_secs(summary['median'])}"
|
||||
f" ({t('min')} {mon._fmt_secs(summary['min'])} ·"
|
||||
f" {t('max')} {mon._fmt_secs(summary['max'])})"
|
||||
)
|
||||
print(
|
||||
f" {t('Cumulated time'):<18}:"
|
||||
f" {mon._fmt_secs(summary['total_secs'])}"
|
||||
)
|
||||
for field, title in (
|
||||
("distro", t("By distribution")),
|
||||
("version", t("By version")),
|
||||
("arch", t("By architecture")),
|
||||
):
|
||||
rows = mon.stats_by(field)
|
||||
if not rows:
|
||||
continue
|
||||
print(f"\n── {title} ──")
|
||||
for key, count, avg, failed in rows[:8]:
|
||||
# Un groupe sans aucun succès n'a pas de moyenne : « — »
|
||||
# plutôt qu'un « ~0s » trompeur.
|
||||
moy = f"~{mon._fmt_secs(avg)}" if count else "—"
|
||||
fail = (
|
||||
f" ⚠ {failed} {self._plural(t('failure'), failed)}"
|
||||
if failed
|
||||
else ""
|
||||
)
|
||||
print(f" {key:<22} {count:>3} × {moy:<8}{fail}")
|
||||
|
||||
self._qemu_stats_vms(mon)
|
||||
print(f"\n [r] {t('Reset the statistics')}")
|
||||
print(f" [0] {t('Back')}")
|
||||
answer = input(f"💬 {t('Your choice')} : ").strip().lower()
|
||||
if answer in ("", "0"):
|
||||
return
|
||||
if answer == "r":
|
||||
if not summary:
|
||||
print(f" {t('Nothing to reset.')}")
|
||||
continue
|
||||
confirm = input(
|
||||
f" {t('Erase')} {summary['total']}"
|
||||
f" {t('recorded runs')}? (y/N): "
|
||||
).strip()
|
||||
if self._is_yes(confirm):
|
||||
count = mon.reset_stats()
|
||||
print(f" ✅ {count} {t('runs erased')}.")
|
||||
else:
|
||||
print(f" {t('Cancelled.')}")
|
||||
|
||||
@staticmethod
|
||||
def _qemu_stamp(ts):
|
||||
"""Horodatage court « 2026-08-01 »."""
|
||||
try:
|
||||
return datetime.datetime.fromtimestamp(ts).strftime("%Y-%m-%d")
|
||||
except (OSError, OverflowError, ValueError):
|
||||
return "?"
|
||||
|
||||
def _qemu_stats_vms(self, mon):
|
||||
"""Machines virtuelles actuelles : nombre, états, place disque."""
|
||||
try:
|
||||
states = mon.virsh_domstates()
|
||||
except Exception:
|
||||
return
|
||||
if not states:
|
||||
return
|
||||
running = sum(1 for s in states.values() if s == "running")
|
||||
total_bytes = 0
|
||||
counted = 0
|
||||
for name in states:
|
||||
try:
|
||||
# vm_disk_path attend un dict ; le chemin par défaut de libvirt
|
||||
# se déduit du seul nom.
|
||||
size = mon.disk_actual_size(mon.vm_disk_path({"name": name}))
|
||||
except Exception:
|
||||
size = None
|
||||
if size:
|
||||
total_bytes += size
|
||||
counted += 1
|
||||
print(f"\n── {t('Virtual machines')} ──")
|
||||
print(
|
||||
f" {t('Defined'):<18}: {len(states)}"
|
||||
f" ({running} {t('running')},"
|
||||
f" {len(states) - running} {t('stopped')})"
|
||||
)
|
||||
if counted:
|
||||
print(
|
||||
f" {t('Disk used'):<18}:"
|
||||
f" {mon._fmt_size(total_bytes)}"
|
||||
f" ({counted} {self._plural(t('image'), counted)})"
|
||||
)
|
||||
|
||||
def _qemu_download_image(self):
|
||||
script_path = self._qemu_script_path()
|
||||
distro = self._qemu_prompt_distro()
|
||||
|
|
@ -3421,7 +3552,20 @@ class TODO:
|
|||
# Suggestions proposées aux invites de taille. Les lettres démarrent à « a »
|
||||
# pour ne JAMAIS entrer en conflit avec une valeur tapée directement : toute
|
||||
# saisie commençant par un chiffre est lue comme la valeur elle-même.
|
||||
_QEMU_DISK_PRESETS = ("20G", "40G", "60G", "80G", "120G", "200G")
|
||||
_QEMU_DISK_PRESETS = (
|
||||
"20G",
|
||||
"40G",
|
||||
"60G",
|
||||
"80G",
|
||||
"120G",
|
||||
"200G",
|
||||
"400G",
|
||||
"600G",
|
||||
"800G",
|
||||
"1T",
|
||||
"1.5T",
|
||||
"2T",
|
||||
)
|
||||
# Jusqu'à 256 Go : les hôtes de virtualisation récents dépassent largement
|
||||
# 32 Go, et l'invite est en Mo — l'équivalent en Go est donc affiché.
|
||||
_QEMU_RAM_PRESETS = (
|
||||
|
|
@ -3436,6 +3580,31 @@ class TODO:
|
|||
262144,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _plural(word, count):
|
||||
"""Accord simple : « échec » / « échecs ». Vaut pour fr et en."""
|
||||
return word if abs(count) <= 1 else f"{word}s"
|
||||
|
||||
@staticmethod
|
||||
def _qemu_parse_disk(value):
|
||||
"""Normalise une taille de disque en « <n>G », ou None si invalide.
|
||||
|
||||
Accepte « 60 », « 60G », « 1T », « 1,5T ». Le suffixe T est converti
|
||||
(1 T = 1024 G) : tout le reste de la chaîne — nom de fichier qcow2,
|
||||
argument --disk-size — raisonne en gigaoctets.
|
||||
"""
|
||||
txt = value.strip().upper().replace(",", ".")
|
||||
factor = 1
|
||||
if txt.endswith("T"):
|
||||
factor, txt = 1024, txt[:-1]
|
||||
elif txt.endswith("G"):
|
||||
txt = txt[:-1]
|
||||
try:
|
||||
gigs = int(float(txt) * factor)
|
||||
except ValueError:
|
||||
return None
|
||||
return f"{gigs}G" if gigs > 0 else None
|
||||
|
||||
@staticmethod
|
||||
def _qemu_ram_label(mb):
|
||||
"""« 65536 (64G) » : l'invite est en Mo, on raisonne en Go."""
|
||||
|
|
@ -3542,19 +3711,16 @@ class TODO:
|
|||
).strip()
|
||||
if new:
|
||||
names[i] = new
|
||||
dk = (
|
||||
self._qemu_ask_value(
|
||||
t("New disk size in G, blank = keep"),
|
||||
sel[i][3],
|
||||
self._QEMU_DISK_PRESETS,
|
||||
)
|
||||
.upper()
|
||||
.rstrip("G")
|
||||
dk = self._qemu_ask_value(
|
||||
t("New disk size in G, blank = keep"),
|
||||
sel[i][3],
|
||||
self._QEMU_DISK_PRESETS,
|
||||
)
|
||||
if dk:
|
||||
try:
|
||||
sel[i][3] = f"{int(float(dk))}G"
|
||||
except ValueError:
|
||||
parsed = self._qemu_parse_disk(dk)
|
||||
if parsed:
|
||||
sel[i][3] = parsed
|
||||
else:
|
||||
print(f" ⚠ {t('Invalid size.')}")
|
||||
rm = self._qemu_ask_value(
|
||||
t("New RAM in MB, blank = keep"),
|
||||
|
|
|
|||
|
|
@ -2457,6 +2457,48 @@ TRANSLATIONS = {
|
|||
"fr": "QEMU - Exemple dry-run (demo-vm, Ubuntu 24.04)",
|
||||
"en": "QEMU - Sample dry-run (demo-vm, Ubuntu 24.04)",
|
||||
},
|
||||
# QEMU - statistics screen
|
||||
"Statistics (installs, durations, VMs)": {
|
||||
"fr": "📊 Statistiques (installations, durées, VM)",
|
||||
"en": "📊 Statistics (installs, durations, VMs)",
|
||||
},
|
||||
"QEMU statistics": {"fr": "Statistiques QEMU", "en": "QEMU statistics"},
|
||||
"No installation recorded yet.": {
|
||||
"fr": "Aucune installation enregistrée pour l'instant.",
|
||||
"en": "No installation recorded yet.",
|
||||
},
|
||||
"Installations": {"fr": "Installations", "en": "Installations"},
|
||||
"Total": {"fr": "Total", "en": "Total"},
|
||||
"succeeded": {"fr": "réussies", "en": "succeeded"},
|
||||
"failed": {"fr": "échouées", "en": "failed"},
|
||||
"Period": {"fr": "Période", "en": "Period"},
|
||||
"days": {"fr": "jours", "en": "days"},
|
||||
"Median duration": {"fr": "Durée médiane", "en": "Median duration"},
|
||||
"min": {"fr": "min", "en": "min"},
|
||||
"max": {"fr": "max", "en": "max"},
|
||||
"Cumulated time": {"fr": "Temps cumulé", "en": "Cumulated time"},
|
||||
"By distribution": {"fr": "Par distribution", "en": "By distribution"},
|
||||
"By version": {"fr": "Par version", "en": "By version"},
|
||||
"By architecture": {"fr": "Par architecture", "en": "By architecture"},
|
||||
"Virtual machines": {
|
||||
"fr": "Machines virtuelles",
|
||||
"en": "Virtual machines",
|
||||
},
|
||||
"Defined": {"fr": "Définies", "en": "Defined"},
|
||||
"running": {"fr": "en cours", "en": "running"},
|
||||
"stopped": {"fr": "arrêtées", "en": "stopped"},
|
||||
"Disk used": {"fr": "Disque utilisé", "en": "Disk used"},
|
||||
"image": {"fr": "image", "en": "image"},
|
||||
"failure": {"fr": "échec", "en": "failure"},
|
||||
"Reset the statistics": {
|
||||
"fr": "Réinitialiser les statistiques",
|
||||
"en": "Reset the statistics",
|
||||
},
|
||||
"Nothing to reset.": {"fr": "Rien à effacer.", "en": "Nothing to reset."},
|
||||
"Erase": {"fr": "Effacer", "en": "Erase"},
|
||||
"recorded runs": {"fr": "runs enregistrés", "en": "recorded runs"},
|
||||
"runs erased": {"fr": "runs effacés", "en": "runs erased"},
|
||||
"Cancelled.": {"fr": "Annulé.", "en": "Cancelled."},
|
||||
# Database migration - resume menu
|
||||
"Migration in progress": {
|
||||
"fr": "Migration en cours",
|
||||
|
|
|
|||
Loading…
Reference in a new issue