[REF] qemu deploy: split collecting the choices from executing them
_qemu_deploy was ~400 lines mixing a dozen prompts with the parallel deployment, IP resolution, ~/.ssh/config and the ERPLibre install. A second interface could not be added to it without duplicating all of that — and the repository already shows where duplication leads: _qemu_choose_cli_browser (todo.py) and Monitor._choose_browser (qemu_install_monitor.py) are the same function twice, and they have already drifted apart. The function now has three parts around a plain dict, the SPEC: _qemu_collect_vms_cli arch, catalog, resources, names -> the VM list _qemu_collect_options_cli SSH key, install, parallelism -> the spec _qemu_run_spec consumes a spec, asks nothing _qemu_deploy_parts_for is the single point every command goes through, so two interfaces producing the same spec necessarily produce the same command — which makes their divergence testable rather than a matter of discipline. Pure, I/O-free helpers come out of the body: _qemu_catalog_entries (the flat distro × version × arch list), _qemu_arches_for, _qemu_make_vm, _qemu_split_existing and _qemu_orphan_disks. The last two matter beyond tidiness — the form must recompute collisions on every keystroke, and every virsh call in this file goes through sudo. Existence is now resolved with ONE virsh list --all --name (_qemu_list_domains, already present) instead of one sudo per VM, and the orphan-disk scan needs no privileges at all. VMs travel as dicts rather than 6-tuples plus a parallel names list. The tuple-based resource prompts are left untouched and converted at the boundary. No behaviour change intended, and verified as such: replaying identical scripted answers against a worktree pinned at the previous commit produces byte-identical output — including the granular selection spanning three architectures — once the repository path and the instantaneous free-RAM reading are normalised. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b8582f5bc6
commit
ce5fb38634
1 changed files with 247 additions and 141 deletions
|
|
@ -3928,7 +3928,22 @@ class TODO:
|
||||||
):
|
):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _qemu_confirm_collisions(self, deployed, pending):
|
@staticmethod
|
||||||
|
def _qemu_orphan_disks(names):
|
||||||
|
"""qcow2 présents sans VM définie, parmi `names` : [(nom, chemin)].
|
||||||
|
|
||||||
|
Pur : ni sudo ni virsh — l'appelant fournit déjà les noms qui n'ont
|
||||||
|
PAS de domaine. C'est ce qui permet au formulaire TUI de recalculer
|
||||||
|
les collisions à chaque frappe sans déclencher d'invite de mot de
|
||||||
|
passe."""
|
||||||
|
orphans = []
|
||||||
|
for name in names:
|
||||||
|
path = f"/var/lib/libvirt/images/{name}.qcow2"
|
||||||
|
if os.path.exists(path):
|
||||||
|
orphans.append((name, path))
|
||||||
|
return orphans
|
||||||
|
|
||||||
|
def _qemu_confirm_collisions(self, existing, pending_names):
|
||||||
"""Signale les noms qui heurtent l'existant, et demande confirmation.
|
"""Signale les noms qui heurtent l'existant, et demande confirmation.
|
||||||
|
|
||||||
Deux cas, de gravité différente : une VM déjà définie est simplement
|
Deux cas, de gravité différente : une VM déjà définie est simplement
|
||||||
|
|
@ -3936,16 +3951,12 @@ class TODO:
|
||||||
supprimée sans son disque) fait échouer deploy_qemu, qui refuse
|
supprimée sans son disque) fait échouer deploy_qemu, qui refuse
|
||||||
d'écraser sans --force. Défaut NON : on ne poursuit que sur un « oui »
|
d'écraser sans --force. Défaut NON : on ne poursuit que sur un « oui »
|
||||||
explicite."""
|
explicite."""
|
||||||
orphans = []
|
orphans = self._qemu_orphan_disks(pending_names)
|
||||||
for name, *_rest in pending:
|
if not existing and not orphans:
|
||||||
path = f"/var/lib/libvirt/images/{name}.qcow2"
|
|
||||||
if os.path.exists(path):
|
|
||||||
orphans.append((name, path))
|
|
||||||
if not deployed and not orphans:
|
|
||||||
return True
|
return True
|
||||||
print(f"\n⚠ {t('Name collisions detected')} :")
|
print(f"\n⚠ {t('Name collisions detected')} :")
|
||||||
skipped = t("VM already defined - SKIPPED, nothing overwritten")
|
skipped = t("VM already defined - SKIPPED, nothing overwritten")
|
||||||
for name in deployed:
|
for name in existing:
|
||||||
print(f" {name:<28.28} {skipped}")
|
print(f" {name:<28.28} {skipped}")
|
||||||
for name, path in orphans:
|
for name, path in orphans:
|
||||||
print(
|
print(
|
||||||
|
|
@ -3958,49 +3969,44 @@ class TODO:
|
||||||
input(f"{t('Continue despite these collisions? (y/N): ')}")
|
input(f"{t('Continue despite these collisions? (y/N): ')}")
|
||||||
)
|
)
|
||||||
|
|
||||||
def _qemu_print_recap(
|
def _qemu_print_recap(self, spec, existing):
|
||||||
self,
|
|
||||||
pending,
|
|
||||||
deployed,
|
|
||||||
branch,
|
|
||||||
install_label,
|
|
||||||
install_prod,
|
|
||||||
ssh_key,
|
|
||||||
add_ssh_config,
|
|
||||||
parallelism,
|
|
||||||
):
|
|
||||||
"""État final soumis à approbation : tout ce qui va changer sur
|
"""État final soumis à approbation : tout ce qui va changer sur
|
||||||
l'hôte, y compris ce qui ne changera PAS (VM existantes)."""
|
l'hôte, y compris ce qui ne changera PAS (VM existantes)."""
|
||||||
|
install = spec.get("install")
|
||||||
|
branch = install["branch"] if install else None
|
||||||
print(f"\n── {t('Final review before deployment')} ──")
|
print(f"\n── {t('Final review before deployment')} ──")
|
||||||
print(f" {t('VMs to create:')} {len(pending)}")
|
print(f" {t('VMs to create:')} {len(spec['vms'])}")
|
||||||
for name, d, v, ram, disk, a, vcpus in pending:
|
for vm in spec["vms"]:
|
||||||
# Le disque annoncé est celui qui sera réellement créé : ERPLibre
|
# Le disque annoncé est celui qui sera réellement créé : ERPLibre
|
||||||
# ajoute ERPLIBRE_EXTRA_DISK_GB à la demande initiale.
|
# ajoute ERPLIBRE_EXTRA_DISK_GB à la demande initiale.
|
||||||
gigs = self._parse_disk_gb(disk) + (
|
gigs = self._parse_disk_gb(vm["disk"]) + (
|
||||||
self.ERPLIBRE_EXTRA_DISK_GB if branch else 0
|
self.ERPLIBRE_EXTRA_DISK_GB if branch else 0
|
||||||
)
|
)
|
||||||
print(
|
print(
|
||||||
f" {name:<30} {d} {v:<7} [{a:<5}] "
|
f" {vm['name']:<30} {vm['distro']} {vm['version']:<7} "
|
||||||
f"{vcpus} vCPU RAM {ram}Mo {t('disk')} {gigs}G"
|
f"[{vm['arch']:<5}] {vm['vcpus']} vCPU RAM {vm['ram']}Mo "
|
||||||
|
f"{t('disk')} {gigs}G"
|
||||||
)
|
)
|
||||||
if deployed:
|
if existing:
|
||||||
print(f" {t('Existing, left untouched:')} {', '.join(deployed)}")
|
print(f" {t('Existing, left untouched:')} {', '.join(existing)}")
|
||||||
if branch:
|
if install:
|
||||||
env = (
|
env = (
|
||||||
t("production (/opt, confined)")
|
t("production (/opt, confined)")
|
||||||
if install_prod
|
if install["prod"]
|
||||||
else t("development (~/git)")
|
else t("development (~/git)")
|
||||||
)
|
)
|
||||||
print(
|
print(
|
||||||
f" {t('ERPLibre install:')} {t('branch')} {branch}, "
|
f" {t('ERPLibre install:')} {t('branch')} {branch}, "
|
||||||
f"{t('profile')} {install_label}, {env}"
|
f"{t('profile')} {install['label']}, {env}"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
print(f" {t('ERPLibre install:')} {t('no')}")
|
print(f" {t('ERPLibre install:')} {t('no')}")
|
||||||
print(f" {t('SSH key:')} {ssh_key or t('none')}")
|
print(f" {t('SSH key:')} {spec.get('ssh_key') or t('none')}")
|
||||||
cfg = t("one entry per VM") if add_ssh_config else t("untouched")
|
cfg = (
|
||||||
|
t("one entry per VM") if spec["add_ssh_config"] else t("untouched")
|
||||||
|
)
|
||||||
print(f" {t('~/.ssh/config:')} {cfg}")
|
print(f" {t('~/.ssh/config:')} {cfg}")
|
||||||
print(f" {t('Parallelism:')} {parallelism} {t('at a time')}")
|
print(f" {t('Parallelism:')} {spec['parallelism']} {t('at a time')}")
|
||||||
|
|
||||||
def _qemu_build_deploy_parts(
|
def _qemu_build_deploy_parts(
|
||||||
self, d, v, arch, name, eram, evcpus, disk, ssh_key, branch, dry_run
|
self, d, v, arch, name, eram, evcpus, disk, ssh_key, branch, dry_run
|
||||||
|
|
@ -4037,19 +4043,131 @@ class TODO:
|
||||||
parts.append("--dry-run" if dry_run else "-y")
|
parts.append("--dry-run" if dry_run else "-y")
|
||||||
return parts
|
return parts
|
||||||
|
|
||||||
|
def _qemu_deploy_parts_for(self, vm, spec, dry_run=False):
|
||||||
|
"""Commande deploy_qemu.py d'une VM de la spec.
|
||||||
|
|
||||||
|
POINT DE PASSAGE UNIQUE des deux interfaces : le formulaire TUI et les
|
||||||
|
invites en ligne produisent la même spec, donc forcément la même
|
||||||
|
commande. C'est ce qui rend leur divergence vérifiable par un test."""
|
||||||
|
install = spec.get("install")
|
||||||
|
return self._qemu_build_deploy_parts(
|
||||||
|
vm["distro"],
|
||||||
|
vm["version"],
|
||||||
|
vm["arch"],
|
||||||
|
vm["name"],
|
||||||
|
vm["ram"],
|
||||||
|
vm["vcpus"],
|
||||||
|
vm["disk"],
|
||||||
|
spec.get("ssh_key"),
|
||||||
|
install["branch"] if install else None,
|
||||||
|
dry_run=dry_run,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- #
|
||||||
|
# Déploiement : catalogue (pur) -> collecte (CLI ou TUI) -> exécution
|
||||||
|
# ---------------------------------------------------------------- #
|
||||||
|
|
||||||
|
def _qemu_arches_for(self, distro, arch):
|
||||||
|
"""Architectures à déployer pour cette distro selon le choix global.
|
||||||
|
« all » = uniquement celles que la distro publie réellement."""
|
||||||
|
if arch != "all":
|
||||||
|
return [arch]
|
||||||
|
out = ["amd64"]
|
||||||
|
if distro in self._QEMU_ARM64_DISTROS:
|
||||||
|
out.append("arm64")
|
||||||
|
if distro in self._QEMU_S390X_DISTROS:
|
||||||
|
out.append("s390x")
|
||||||
|
return out
|
||||||
|
|
||||||
|
def _qemu_catalog_entries(self, mod, distros, arch):
|
||||||
|
"""Catalogue APLATI : une entrée par (distro, version, architecture).
|
||||||
|
|
||||||
|
Fonction pure, sans I/O : c'est la source unique de ce qui est
|
||||||
|
déployable, aussi bien pour la liste granulaire de la CLI que pour la
|
||||||
|
liste à cocher du formulaire TUI."""
|
||||||
|
flat = []
|
||||||
|
for d in distros:
|
||||||
|
versions_map, default_v = mod.DISTROS[d]
|
||||||
|
for v, (_c, _o, ram, disk) in versions_map.items():
|
||||||
|
for a in self._qemu_arches_for(d, arch):
|
||||||
|
flat.append(
|
||||||
|
{
|
||||||
|
"distro": d,
|
||||||
|
"version": v,
|
||||||
|
"arch": a,
|
||||||
|
"ram": ram,
|
||||||
|
"disk": disk,
|
||||||
|
"default": v == default_v,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return flat
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _qemu_make_vm(distro, version, arch, ram, disk, vcpus, name):
|
||||||
|
"""Une VM de la spec. Un seul endroit décrit sa forme."""
|
||||||
|
return {
|
||||||
|
"name": name,
|
||||||
|
"distro": distro,
|
||||||
|
"version": version,
|
||||||
|
"arch": arch,
|
||||||
|
"ram": ram,
|
||||||
|
"disk": disk,
|
||||||
|
"vcpus": vcpus,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _qemu_split_existing(self, vms, domains):
|
||||||
|
"""Sépare les VM à créer de celles dont le domaine existe déjà.
|
||||||
|
`domains` est la liste des noms libvirt, obtenue UNE fois (un seul
|
||||||
|
sudo) et non par VM. Renvoie (à_créer, noms_existants)."""
|
||||||
|
known = set(domains)
|
||||||
|
pending = [vm for vm in vms if vm["name"] not in known]
|
||||||
|
existing = [vm["name"] for vm in vms if vm["name"] in known]
|
||||||
|
return pending, existing
|
||||||
|
|
||||||
def _qemu_deploy(self, dry_run=False):
|
def _qemu_deploy(self, dry_run=False):
|
||||||
|
"""Déploiement d'un parc de VM, en trois temps : collecte des choix
|
||||||
|
(invites en ligne), aperçu ou exécution. La collecte produit une SPEC
|
||||||
|
que le formulaire TUI sait produire à l'identique."""
|
||||||
print(f"🚀 {t('Deploy ERPLibre VM(s)!')}")
|
print(f"🚀 {t('Deploy ERPLibre VM(s)!')}")
|
||||||
try:
|
try:
|
||||||
mod = self._qemu_import_module()
|
mod = self._qemu_import_module()
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
print(f"{t('Cannot load QEMU catalog: ')}{exc}")
|
print(f"{t('Cannot load QEMU catalog: ')}{exc}")
|
||||||
return
|
return
|
||||||
distros = list(mod.DISTROS)
|
|
||||||
# Rappel de la dernière installation enregistrée (si historique).
|
# Rappel de la dernière installation enregistrée (si historique).
|
||||||
last = self._qemu_last_run_line()
|
last = self._qemu_last_run_line()
|
||||||
if last:
|
if last:
|
||||||
print(last)
|
print(last)
|
||||||
|
|
||||||
|
got = self._qemu_collect_vms_cli(mod)
|
||||||
|
if not got:
|
||||||
|
return
|
||||||
|
res_label, vms = got
|
||||||
|
|
||||||
|
if dry_run:
|
||||||
|
self._qemu_print_dry_run(vms)
|
||||||
|
return
|
||||||
|
|
||||||
|
spec = self._qemu_collect_options_cli(vms, res_label)
|
||||||
|
if not spec:
|
||||||
|
return
|
||||||
|
self._qemu_run_spec(spec)
|
||||||
|
|
||||||
|
def _qemu_print_dry_run(self, vms):
|
||||||
|
"""Aperçu : les commandes deploy_qemu, sans rien créer (ni sudo, ni
|
||||||
|
installation). Passe par le point de passage unique, donc montre
|
||||||
|
exactement ce qui serait lancé."""
|
||||||
|
spec = {"vms": vms, "ssh_key": self._qemu_default_ssh_key()}
|
||||||
|
print(f"\n{t('Preview (dry-run):')}")
|
||||||
|
for vm in vms:
|
||||||
|
parts = self._qemu_deploy_parts_for(vm, spec, dry_run=True)
|
||||||
|
print(" " + " ".join(shlex.quote(p) for p in parts))
|
||||||
|
|
||||||
|
def _qemu_collect_vms_cli(self, mod):
|
||||||
|
"""Invites en ligne : architecture, catalogue, ressources, noms.
|
||||||
|
Renvoie (étiquette_de_profil, vms) ou None si rien à faire."""
|
||||||
|
distros = list(mod.DISTROS)
|
||||||
|
|
||||||
# 0) Architecture du parc (défaut : native ; [all] = TOUTES les archis
|
# 0) Architecture du parc (défaut : native ; [all] = TOUTES les archis
|
||||||
# supportées). Pour une arch précise non-amd64, on restreint le
|
# supportées). Pour une arch précise non-amd64, on restreint le
|
||||||
# catalogue aux distros qui la publient ; pour [all], chaque distro
|
# catalogue aux distros qui la publient ; pour [all], chaque distro
|
||||||
|
|
@ -4069,18 +4187,10 @@ class TODO:
|
||||||
distros = keep
|
distros = keep
|
||||||
if not distros:
|
if not distros:
|
||||||
print(t("Nothing selected."))
|
print(t("Nothing selected."))
|
||||||
return
|
return None
|
||||||
|
|
||||||
def arches_for(distro):
|
def arches_for(distro):
|
||||||
"""Architectures à déployer pour cette distro selon le choix."""
|
return self._qemu_arches_for(distro, arch)
|
||||||
if arch != "all":
|
|
||||||
return [arch]
|
|
||||||
out = ["amd64"]
|
|
||||||
if distro in self._QEMU_ARM64_DISTROS:
|
|
||||||
out.append("arm64")
|
|
||||||
if distro in self._QEMU_S390X_DISTROS:
|
|
||||||
out.append("s390x")
|
|
||||||
return out
|
|
||||||
|
|
||||||
# 1) Distributions : multi-sélection, catalogue complet, principal (la
|
# 1) Distributions : multi-sélection, catalogue complet, principal (la
|
||||||
# version par défaut de chaque distro, marquée d'un *), ou granulaire
|
# version par défaut de chaque distro, marquée d'un *), ou granulaire
|
||||||
|
|
@ -4117,26 +4227,25 @@ class TODO:
|
||||||
selected = [] # (distro, version, ram_mb, disk_str, arch)
|
selected = [] # (distro, version, ram_mb, disk_str, arch)
|
||||||
if granular:
|
if granular:
|
||||||
# Liste APLATIE distro + version + ARCHITECTURE : on choisit des
|
# Liste APLATIE distro + version + ARCHITECTURE : on choisit des
|
||||||
# combinaisons précises par numéros séparés de virgules.
|
# combinaisons précises par numéros séparés de virgules. La liste
|
||||||
flat = [] # (distro, version, ram, disk, is_default, arch)
|
# vient du catalogue partagé avec le formulaire TUI.
|
||||||
for d in distros:
|
flat = self._qemu_catalog_entries(mod, distros, arch)
|
||||||
versions_map, default_v = mod.DISTROS[d]
|
|
||||||
for v, (_c, _o, ram, disk) in versions_map.items():
|
|
||||||
for a in arches_for(d):
|
|
||||||
flat.append((d, v, ram, disk, v == default_v, a))
|
|
||||||
print(f"\n{t('All versions:')}")
|
print(f"\n{t('All versions:')}")
|
||||||
for i, (d, v, ram, disk, isdef, a) in enumerate(flat, 1):
|
for i, e in enumerate(flat, 1):
|
||||||
star = " *" if isdef else ""
|
star = " *" if e["default"] else ""
|
||||||
print(f" [{i}] {d} {v}{star} [{a}] (RAM≥{ram}Mo, {disk})")
|
print(
|
||||||
|
f" [{i}] {e['distro']} {e['version']}{star} "
|
||||||
|
f"[{e['arch']}] (RAM≥{e['ram']}Mo, {e['disk']})"
|
||||||
|
)
|
||||||
r = (
|
r = (
|
||||||
input(t("Selection (comma-separated numbers): "))
|
input(t("Selection (comma-separated numbers): "))
|
||||||
.strip()
|
.strip()
|
||||||
.lower()
|
.lower()
|
||||||
)
|
)
|
||||||
for d, v, ram, disk, _isdef, a in self._parse_index_selection(
|
for e in self._parse_index_selection(r, flat):
|
||||||
r, flat
|
selected.append(
|
||||||
):
|
(e["distro"], e["version"], e["ram"], e["disk"], e["arch"])
|
||||||
selected.append((d, v, ram, disk, a))
|
)
|
||||||
elif principal:
|
elif principal:
|
||||||
# Une VM par distro (version par défaut) × chaque archi supportée.
|
# Une VM par distro (version par défaut) × chaque archi supportée.
|
||||||
for d in distros:
|
for d in distros:
|
||||||
|
|
@ -4152,7 +4261,7 @@ class TODO:
|
||||||
)
|
)
|
||||||
if not sel_distros:
|
if not sel_distros:
|
||||||
print(t("Nothing selected."))
|
print(t("Nothing selected."))
|
||||||
return
|
return None
|
||||||
# 2) Versions par distro (multi-sélection) ; « all » si catalogue.
|
# 2) Versions par distro (multi-sélection) ; « all » si catalogue.
|
||||||
for d in sel_distros:
|
for d in sel_distros:
|
||||||
versions_map = mod.DISTROS[d][0]
|
versions_map = mod.DISTROS[d][0]
|
||||||
|
|
@ -4180,7 +4289,7 @@ class TODO:
|
||||||
selected.append((d, v, ram, disk, a))
|
selected.append((d, v, ram, disk, a))
|
||||||
if not selected:
|
if not selected:
|
||||||
print(t("Nothing selected."))
|
print(t("Nothing selected."))
|
||||||
return
|
return None
|
||||||
|
|
||||||
# 2b) Ressources par VM : multiplicateur x1..x4 ou « Personnalisé ».
|
# 2b) Ressources par VM : multiplicateur x1..x4 ou « Personnalisé ».
|
||||||
# Le profil est CUIT dans `selected`, qui porte dès lors les valeurs
|
# Le profil est CUIT dans `selected`, qui porte dès lors les valeurs
|
||||||
|
|
@ -4194,15 +4303,24 @@ class TODO:
|
||||||
# 2c) Personnalisation par VM : nom, disque, RAM, vCPU (à la demande).
|
# 2c) Personnalisation par VM : nom, disque, RAM, vCPU (à la demande).
|
||||||
names, selected = self._qemu_customize_vms(selected, host_cpu)
|
names, selected = self._qemu_customize_vms(selected, host_cpu)
|
||||||
|
|
||||||
# 3) Plan + estimation des ressources.
|
vms = [
|
||||||
total_ram = sum(s[2] for s in selected)
|
self._qemu_make_vm(d, v, a, ram, disk, vcpus, names[i])
|
||||||
total_disk = sum(self._parse_disk_gb(s[3]) for s in selected)
|
for i, (d, v, ram, disk, a, vcpus) in enumerate(selected)
|
||||||
total_cpu = sum(s[5] for s in selected)
|
]
|
||||||
print(f"\n{t('Deployment plan')} ({len(selected)} VM, {res_label}) :")
|
self._qemu_print_plan(vms, res_label, host_cpu, free_ram)
|
||||||
for i, (d, v, ram, disk, a, vcpus) in enumerate(selected):
|
return res_label, vms
|
||||||
|
|
||||||
|
def _qemu_print_plan(self, vms, res_label, host_cpu, free_ram):
|
||||||
|
"""Plan + estimation des ressources de l'hôte."""
|
||||||
|
total_ram = sum(vm["ram"] for vm in vms)
|
||||||
|
total_disk = sum(self._parse_disk_gb(vm["disk"]) for vm in vms)
|
||||||
|
total_cpu = sum(vm["vcpus"] for vm in vms)
|
||||||
|
print(f"\n{t('Deployment plan')} ({len(vms)} VM, {res_label}) :")
|
||||||
|
for vm in vms:
|
||||||
print(
|
print(
|
||||||
f" - {names[i]:<30} {d} {v:<7} [{a:<5}] "
|
f" - {vm['name']:<30} {vm['distro']} {vm['version']:<7} "
|
||||||
f"{vcpus} vCPU RAM {ram}Mo {t('disk')} {disk}"
|
f"[{vm['arch']:<5}] {vm['vcpus']} vCPU RAM {vm['ram']}Mo "
|
||||||
|
f"{t('disk')} {vm['disk']}"
|
||||||
)
|
)
|
||||||
cpu_warn = (
|
cpu_warn = (
|
||||||
f" ⚠ {t('> host cores')} ({host_cpu})"
|
f" ⚠ {t('> host cores')} ({host_cpu})"
|
||||||
|
|
@ -4221,27 +4339,10 @@ class TODO:
|
||||||
)
|
)
|
||||||
print(f" ⚠ {warn}")
|
print(f" ⚠ {warn}")
|
||||||
|
|
||||||
# Aperçu (dry-run) : montre les commandes deploy_qemu sans rien créer
|
def _qemu_collect_options_cli(self, vms, res_label):
|
||||||
# (ni sudo, ni installation), puis on s'arrête.
|
"""Invites en ligne : clé SSH, installation ERPLibre, ~/.ssh/config,
|
||||||
if dry_run:
|
parallélisme, puis récapitulatif et confirmation.
|
||||||
default_key = self._qemu_default_ssh_key()
|
Renvoie la spec complète, ou None si l'utilisateur renonce."""
|
||||||
print(f"\n{t('Preview (dry-run):')}")
|
|
||||||
for i, (d, v, ram, disk, a, vcpus) in enumerate(selected):
|
|
||||||
parts = self._qemu_build_deploy_parts(
|
|
||||||
d,
|
|
||||||
v,
|
|
||||||
a,
|
|
||||||
names[i],
|
|
||||||
ram,
|
|
||||||
vcpus,
|
|
||||||
disk,
|
|
||||||
default_key,
|
|
||||||
None,
|
|
||||||
dry_run=True,
|
|
||||||
)
|
|
||||||
print(" " + " ".join(shlex.quote(p) for p in parts))
|
|
||||||
return
|
|
||||||
|
|
||||||
# Clé SSH (partagée par tout le parc).
|
# Clé SSH (partagée par tout le parc).
|
||||||
default_key = self._qemu_default_ssh_key()
|
default_key = self._qemu_default_ssh_key()
|
||||||
key_hint = default_key or t("none")
|
key_hint = default_key or t("none")
|
||||||
|
|
@ -4252,23 +4353,25 @@ class TODO:
|
||||||
ssh_key = os.path.expanduser(ssh_key)
|
ssh_key = os.path.expanduser(ssh_key)
|
||||||
|
|
||||||
# 4) Option : installer ERPLibre dans ~/git/erplibre de chaque VM.
|
# 4) Option : installer ERPLibre dans ~/git/erplibre de chaque VM.
|
||||||
install_branch = None
|
install = None
|
||||||
install_monitor = False
|
|
||||||
install_prod = (
|
|
||||||
False # dev (~/git, SELinux dev) vs prod (/opt, confiné)
|
|
||||||
)
|
|
||||||
install_cmd = None # commande finale selon le profil choisi
|
|
||||||
install_label = None
|
|
||||||
ans = input(
|
ans = input(
|
||||||
t("Install ERPLibre into ~/git/erplibre on each VM? (Y/n): ")
|
t("Install ERPLibre into ~/git/erplibre on each VM? (Y/n): ")
|
||||||
)
|
)
|
||||||
if self._is_yes_default_yes(ans):
|
if self._is_yes_default_yes(ans):
|
||||||
install_branch = self._qemu_pick_branch()
|
branch = self._qemu_pick_branch()
|
||||||
install_prod = self._qemu_ask_prod()
|
# dev (~/git, SELinux relâché) vs prod (/opt, confiné)
|
||||||
install_label, install_cmd = self._qemu_pick_install_profile()
|
prod = self._qemu_ask_prod()
|
||||||
install_monitor = self._is_yes_default_yes(
|
label, cmd = self._qemu_pick_install_profile()
|
||||||
|
monitor = self._is_yes_default_yes(
|
||||||
input(t("Interactive monitoring dashboard? (y/N): "))
|
input(t("Interactive monitoring dashboard? (y/N): "))
|
||||||
)
|
)
|
||||||
|
install = {
|
||||||
|
"branch": branch,
|
||||||
|
"prod": prod,
|
||||||
|
"label": label,
|
||||||
|
"cmd": cmd,
|
||||||
|
"monitor": monitor,
|
||||||
|
}
|
||||||
|
|
||||||
add_ssh_config = self._is_yes_default_yes(
|
add_ssh_config = self._is_yes_default_yes(
|
||||||
input(t("Add each VM to ~/.ssh/config? (Y/n): "))
|
input(t("Add each VM to ~/.ssh/config? (Y/n): "))
|
||||||
|
|
@ -4277,25 +4380,22 @@ class TODO:
|
||||||
# 5) Sépare les VM à CRÉER des déjà existantes AVANT de proposer le
|
# 5) Sépare les VM à CRÉER des déjà existantes AVANT de proposer le
|
||||||
# parallélisme : on connaît alors le vrai nombre à déployer (affiché
|
# parallélisme : on connaît alors le vrai nombre à déployer (affiché
|
||||||
# dans le prompt) et on peut numéroter chaque tâche.
|
# dans le prompt) et on peut numéroter chaque tâche.
|
||||||
pending = [] # (name, d, v, ram, disk, a, vcpus)
|
pending, existing = self._qemu_split_existing(
|
||||||
deployed = []
|
vms, self._qemu_list_domains()
|
||||||
for i, (d, v, ram, disk, a, vcpus) in enumerate(selected):
|
)
|
||||||
name = names[i]
|
|
||||||
if self._qemu_domain_exists(name):
|
|
||||||
deployed.append(name)
|
|
||||||
else:
|
|
||||||
pending.append((name, d, v, ram, disk, a, vcpus))
|
|
||||||
n_jobs = len(pending)
|
n_jobs = len(pending)
|
||||||
|
|
||||||
# Collisions de noms : une VM déjà définie est ignorée (rien n'est
|
# Collisions de noms : une VM déjà définie est ignorée (rien n'est
|
||||||
# écrasé), mais un qcow2 orphelin fera ÉCHOUER deploy_qemu, qui refuse
|
# écrasé), mais un qcow2 orphelin fera ÉCHOUER deploy_qemu, qui refuse
|
||||||
# d'écraser sans --force. On le dit avant, pas après l'attente.
|
# d'écraser sans --force. On le dit avant, pas après l'attente.
|
||||||
if not self._qemu_confirm_collisions(deployed, pending):
|
if not self._qemu_confirm_collisions(
|
||||||
|
existing, [vm["name"] for vm in pending]
|
||||||
|
):
|
||||||
print(t("Cancelled."))
|
print(t("Cancelled."))
|
||||||
return
|
return None
|
||||||
if not pending:
|
if not pending:
|
||||||
print(t("Nothing to create - every VM already exists."))
|
print(t("Nothing to create - every VM already exists."))
|
||||||
return
|
return None
|
||||||
|
|
||||||
# Nombre de déploiements en parallèle. Défaut = nombre de CPU de l'hôte
|
# Nombre de déploiements en parallèle. Défaut = nombre de CPU de l'hôte
|
||||||
# (borné par le nombre de VM). On affiche aussi le NB DE VM à déployer.
|
# (borné par le nombre de VM). On affiche aussi le NB DE VM à déployer.
|
||||||
|
|
@ -4309,40 +4409,46 @@ class TODO:
|
||||||
except ValueError:
|
except ValueError:
|
||||||
parallelism = default_par
|
parallelism = default_par
|
||||||
|
|
||||||
|
spec = {
|
||||||
|
"res_label": res_label,
|
||||||
|
"vms": pending,
|
||||||
|
"existing": existing,
|
||||||
|
"ssh_key": ssh_key,
|
||||||
|
"install": install,
|
||||||
|
"add_ssh_config": add_ssh_config,
|
||||||
|
"parallelism": parallelism,
|
||||||
|
}
|
||||||
|
|
||||||
# 6) Récapitulatif final, puis confirmation. Toutes les réponses
|
# 6) Récapitulatif final, puis confirmation. Toutes les réponses
|
||||||
# données jusqu'ici sont rassemblées ici : c'est le dernier point où
|
# données jusqu'ici sont rassemblées ici : c'est le dernier point où
|
||||||
# une erreur de saisie se rattrape sans avoir rien créé.
|
# une erreur de saisie se rattrape sans avoir rien créé.
|
||||||
self._qemu_print_recap(
|
self._qemu_print_recap(spec, existing)
|
||||||
pending,
|
|
||||||
deployed,
|
|
||||||
install_branch,
|
|
||||||
install_label,
|
|
||||||
install_prod,
|
|
||||||
ssh_key,
|
|
||||||
add_ssh_config,
|
|
||||||
parallelism,
|
|
||||||
)
|
|
||||||
if not self._confirm_or_discard(t("Deploy these VMs now? (Y/n): ")):
|
if not self._confirm_or_discard(t("Deploy these VMs now? (Y/n): ")):
|
||||||
print(t("Cancelled."))
|
print(t("Cancelled."))
|
||||||
return
|
return None
|
||||||
|
return spec
|
||||||
|
|
||||||
|
def _qemu_run_spec(self, spec):
|
||||||
|
"""Exécute une spec de déploiement : création des VM en parallèle,
|
||||||
|
résolution des IP, ~/.ssh/config, installation ERPLibre.
|
||||||
|
|
||||||
|
Ne pose AUCUNE question — tous les choix sont dans la spec, d'où
|
||||||
|
qu'elle vienne (invites en ligne ou formulaire TUI)."""
|
||||||
|
pending = spec["vms"]
|
||||||
|
deployed = list(spec.get("existing") or [])
|
||||||
|
install = spec.get("install")
|
||||||
|
install_branch = install["branch"] if install else None
|
||||||
|
ssh_key = spec.get("ssh_key")
|
||||||
|
add_ssh_config = spec["add_ssh_config"]
|
||||||
|
parallelism = spec["parallelism"]
|
||||||
|
n_jobs = len(pending)
|
||||||
|
|
||||||
# Jobs numérotés (k/N) : l'ID suit l'ORDRE de préparation, stable même
|
# Jobs numérotés (k/N) : l'ID suit l'ORDRE de préparation, stable même
|
||||||
# si les résultats reviennent dans le désordre (exécution parallèle).
|
# si les résultats reviennent dans le désordre (exécution parallèle).
|
||||||
jobs = [] # (id, name, parts)
|
jobs = [] # (id, name, parts)
|
||||||
for k, (name, d, v, ram, disk, a, vcpus) in enumerate(pending, 1):
|
for k, vm in enumerate(pending, 1):
|
||||||
parts = self._qemu_build_deploy_parts(
|
parts = self._qemu_deploy_parts_for(vm, spec, dry_run=False)
|
||||||
d,
|
jobs.append((f"{k}/{n_jobs}", vm["name"], parts))
|
||||||
v,
|
|
||||||
a,
|
|
||||||
name,
|
|
||||||
ram,
|
|
||||||
vcpus,
|
|
||||||
disk,
|
|
||||||
ssh_key,
|
|
||||||
install_branch,
|
|
||||||
dry_run=False,
|
|
||||||
)
|
|
||||||
jobs.append((f"{k}/{n_jobs}", name, parts))
|
|
||||||
|
|
||||||
deploy_start = time.time()
|
deploy_start = time.time()
|
||||||
n_ok = 0
|
n_ok = 0
|
||||||
|
|
@ -4405,15 +4511,15 @@ class TODO:
|
||||||
self._write_ssh_config_entry(name, "erplibre", ip)
|
self._write_ssh_config_entry(name, "erplibre", ip)
|
||||||
|
|
||||||
# 7) Installation ERPLibre (clone + make) si demandée.
|
# 7) Installation ERPLibre (clone + make) si demandée.
|
||||||
if install_branch:
|
if install:
|
||||||
if install_monitor:
|
if install["monitor"]:
|
||||||
# Installs détachées en parallèle + dashboard Textual.
|
# Installs détachées en parallèle + dashboard Textual.
|
||||||
self._qemu_install_erplibre_monitored(
|
self._qemu_install_erplibre_monitored(
|
||||||
deployed,
|
deployed,
|
||||||
install_branch,
|
install_branch,
|
||||||
ip_map,
|
ip_map,
|
||||||
install_cmd,
|
install["cmd"],
|
||||||
install_prod,
|
install["prod"],
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
print(
|
print(
|
||||||
|
|
@ -4426,8 +4532,8 @@ class TODO:
|
||||||
ssh_key,
|
ssh_key,
|
||||||
install_branch,
|
install_branch,
|
||||||
ip_map.get(name),
|
ip_map.get(name),
|
||||||
install_cmd,
|
install["cmd"],
|
||||||
install_prod,
|
install["prod"],
|
||||||
)
|
)
|
||||||
|
|
||||||
# Sommaire TOTAL (déploiement + résolution IP + ssh_config + install
|
# Sommaire TOTAL (déploiement + résolution IP + ssh_config + install
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue