From 1726cb5dabe8fd454c8b82b93702033670355413 Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Wed, 12 Aug 2026 03:24:19 -0400 Subject: [PATCH] [ADD] qemu: choose the application store of a graphical VM MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit snap was not a choice but a fate: we disabled snapd, then gnome-core pulled a Firefox snap that froze the install for thirty minutes. The previous fix imposed "deb". Three answers are now offered. deb nothing but .deb, epiphany-browser as the browser. Default, and lightest: nothing extra to download. flatpak Flatpak tooling as well, WITHOUT the Flathub remote or any install. The machine is ready, the admin picks its remotes. snap Ubuntu's default, snapd left running and Firefox as a snap. It is the only mode where snapd is not disabled -- disabling it was precisely the cause of the freeze. The question is only asked when it means something: at least one graphical VM on a distro shipping snapd, Ubuntu alone here. A server pulls no snap, and Debian ships none. The TUI greys the choice out and says why; the CLI does not ask. Verified on a 26.04 VM: snapd is indeed preinstalled (2.75.2), flatpak and its GNOME Software plugin are packaged, and firefox-esr does not exist on Ubuntu. --- FR --- snap n'était plus un choix mais une fatalité : on coupait snapd, puis gnome-core tirait un Firefox-snap qui figeait l'installation trente minutes. Le correctif précédent imposait « deb ». Trois réponses sont maintenant offertes. deb rien que des .deb, epiphany-browser comme navigateur. Défaut, et le plus léger : rien de plus à télécharger. flatpak l'outillage Flatpak en plus, SANS dépôt Flathub ni installation. La machine est prête, l'administrateur choisit ses dépôts. snap le défaut d'Ubuntu, snapd laissé actif et Firefox en snap. C'est le seul mode où snapd n'est pas coupé — l'y couper était précisément la cause du blocage. La question n'est posée que lorsqu'elle a un sens : au moins une VM graphique sur une distribution qui livre snapd, Ubuntu seule ici. Un serveur ne tire aucun snap, et Debian n'en livre pas. La TUI grise le choix et dit pourquoi ; la CLI ne le demande pas. Vérifié sur une VM 26.04 : snapd y est bien préinstallé (2.75.2), flatpak et son greffon GNOME Software sont empaquetés, et firefox-esr n'existe pas sur Ubuntu. Assisted-by: Claude Opus 5 --- script/todo/qemu_deploy_form.py | 45 ++++++++++++++++++ script/todo/todo.py | 82 +++++++++++++++++++++++++++------ 2 files changed, 112 insertions(+), 15 deletions(-) diff --git a/script/todo/qemu_deploy_form.py b/script/todo/qemu_deploy_form.py index 81a6f10..101aca3 100644 --- a/script/todo/qemu_deploy_form.py +++ b/script/todo/qemu_deploy_form.py @@ -212,6 +212,7 @@ def build_spec(vms, domains, form): "timezone": form.get("timezone", ""), "desktop": form.get("desktop", ""), "python_provider": form.get("python_provider", ""), + "app_store": form.get("app_store", "deb"), "install": form["install"], "add_ssh_config": form["add_ssh_config"], "parallelism": form["parallelism"], @@ -278,6 +279,10 @@ def run_deploy_form(ctx, run_app: bool = True): desktops = list(ctx.get("desktops") or []) # Architectures pour lesquelles mise publie un binaire. mise_arches = set(ctx.get("mise_arches") or ()) + # [(clé, libellé)] des magasins d'applications, et les distributions qui + # livrent snapd — la question n'a de sens que pour celles-là, graphiques. + app_stores = list(ctx.get("app_stores") or []) + snap_distros = set(ctx.get("snap_distros") or ()) defaults = ctx.get("defaults") or {} result = {"spec": None} @@ -500,6 +505,14 @@ def run_deploy_form(ctx, run_app: bool = True): f"{t('Graphical (server + desktop):')} {label}", value=defaults.get("desktop", "") == key, ) + if app_stores: + yield Static( + t("Application store:"), classes="grouptitle" + ) + with RadioSet(id="f_store"): + for i, (_k, label) in enumerate(app_stores): + yield RadioButton(label, value=i == 0) + yield Static("", id="storewarn") yield Static("ERPLibre", classes="grouptitle") yield Checkbox( t("Install ERPLibre"), @@ -655,6 +668,7 @@ def run_deploy_form(ctx, run_app: bool = True): row["custom"] = bool(self.overrides.get(entry_key(entry))) self._render_plan() self._render_mise() + self._render_store() def _render_mise(self): """Grise le choix quand aucune VM retenue n'est servie par mise, @@ -690,6 +704,36 @@ def run_deploy_form(ctx, run_app: bool = True): index = self.query_one("#f_python", RadioSet).pressed_index return "pyenv" if index == 1 else "mise" + def _app_store(self): + """Magasin retenu. Sans VM concernée, la réponse est « deb » : + elle ne change rien, et laisser passer « snap » réactiverait snapd + pour rien.""" + if not app_stores or not self._app_store_needed(): + return "deb" + index = self.query_one("#f_store", RadioSet).pressed_index + if index is None or not (0 <= index < len(app_stores)): + return app_stores[0][0] + return app_stores[index][0] + + def _app_store_needed(self): + """Au moins une VM graphique sur une distribution qui livre snapd.""" + return any( + vm.get("desktop") and vm["distro"] in snap_distros + for vm in self.vms + ) + + def _render_store(self): + """Grise le choix quand aucune VM ne le concerne, et dit pourquoi.""" + if not app_stores: + return + needed = self._app_store_needed() + self.query_one("#f_store", RadioSet).disabled = not needed + self.query_one("#storewarn", Static).update( + "" + if needed + else f" {t('No graphical VM on a snap-based distro.')}" + ) + def _mise_usable(self): return any(vm["arch"] in mise_arches for vm in self.vms) @@ -1157,6 +1201,7 @@ def run_deploy_form(ctx, run_app: bool = True): or "", "desktop": self._default_desktop(), "python_provider": self._python_provider(), + "app_store": self._app_store(), "install": install, "add_ssh_config": self.query_one("#f_sshcfg", Checkbox).value, # Une exécution par installation : le nombre de VM retenues diff --git a/script/todo/todo.py b/script/todo/todo.py index 9741d7a..ad78f7e 100755 --- a/script/todo/todo.py +++ b/script/todo/todo.py @@ -4457,7 +4457,7 @@ class TODO: ) @staticmethod - def _qemu_no_auto_upgrade(prod): + def _qemu_no_auto_upgrade(prod, app_store="deb"): """Coupe les mises à jour automatiques sur une VM de DÉVELOPPEMENT. Vécu sur erplibre-ubuntu-2404 : unattended-upgrades s'est déclenché en @@ -4485,13 +4485,20 @@ class TODO: "fi; " # snapd : 57 s sur le CHEMIN CRITIQUE du démarrage, mesurés par # « systemd-analyze critical-chain » sur une VM s390x — - # multi-user.target attend snapd.seeded. Aucune VM ERPLibre - # n'installe de snap : c'est du temps payé pour rien, à chaque - # démarrage. On désactive plutôt que désinstaller, pour rester - # réversible d'un « systemctl enable ». - "sudo systemctl disable --now snapd.seeded.service " - "snapd.service snapd.socket snapd.apparmor.service " - ">/dev/null 2>&1 || true; " + # multi-user.target attend snapd.seeded. C'est du temps payé pour + # rien quand aucun snap n'est voulu. On désactive plutôt que + # désinstaller, pour rester réversible d'un « systemctl enable ». + # + # Sauf si le magasin RETENU est snap : le couper puis laisser un + # postinst appeler « snap install » est exactement ce qui figeait + # une VM graphique trente minutes durant. + + ( + "" + if app_store == "snap" + else "sudo systemctl disable --now snapd.seeded.service " + "snapd.service snapd.socket snapd.apparmor.service " + ">/dev/null 2>&1 || true; " + ) ) # Miroirs openSUSE préférés, du plus proche au dernier recours. Le @@ -4602,7 +4609,7 @@ class TODO: f'echo " {t("openSUSE mirror:")} $zm"; break; fi; done; ' ) - def _qemu_desktop_remote_cmd(self, flavour="gnome"): + def _qemu_desktop_remote_cmd(self, flavour="gnome", app_store="deb"): """Bloc shell installant le bureau choisi + son accès distant, quelle que soit la distribution. Même aiguillage que l'installation ERPLibre, et même traitement du verrou apt : cette étape passe par la commande @@ -4619,7 +4626,7 @@ class TODO: "sudo DEBIAN_FRONTEND=noninteractive " "apt-get -o DPkg::Lock::Timeout=600 install -y " f"{de['apt']} {rem['apt']['packages']} " - f"{self._QEMU_APT_NO_SNAP}; " + f"{self._qemu_apt_store_pkgs(app_store)}; " "elif command -v dnf >/dev/null 2>&1; then " # Cascade d'environnements : le premier qui existe gagne. Un # environnement absent fait rendre 1 à dnf sans rien installer, @@ -4704,6 +4711,7 @@ class TODO: prod=False, desktop=False, python_provider="", + app_store="deb", ): """Script exécuté DANS la VM. `branch` à None n'installe QUE le bureau — le choix graphique ne dépend pas d'ERPLibre, et une VM peut être @@ -4723,8 +4731,8 @@ class TODO: return ( "set -e; " + self._qemu_cloud_init_wait() - + self._qemu_no_auto_upgrade(prod) - + self._qemu_desktop_remote_cmd(desktop) + + self._qemu_no_auto_upgrade(prod, app_store) + + self._qemu_desktop_remote_cmd(desktop, app_store) ) if not final_cmd: final_cmd = f"make install_os && make {self.ERPLIBRE_ODOO_TARGET}" @@ -4749,7 +4757,7 @@ class TODO: # secondaire bienvenu : les timers apt-daily ne tiennent plus le verrou # apt pendant l'installation. En PROD on ne touche à rien : les # correctifs de sécurité automatiques doivent rester actifs. - no_auto_upgrade = self._qemu_no_auto_upgrade(prod) + no_auto_upgrade = self._qemu_no_auto_upgrade(prod, app_store) return ( "set -e; " + self._qemu_cloud_init_wait() # Coupé AVANT les apt-get ci-dessous : sinon apt-daily peut reprendre @@ -4758,7 +4766,12 @@ class TODO: # Le bureau d'abord : il repose sur les dépôts de la distribution, # là où l'installation ERPLibre compile longuement. Un échec ici se # voit donc tôt plutôt qu'après une heure. - + (self._qemu_desktop_remote_cmd(desktop) if desktop else "") + + + ( + self._qemu_desktop_remote_cmd(desktop, app_store) + if desktop + else "" + ) + + # Outils d'amorçage (absents des images cloud minimales) : curl, # git, make. Chaque branche RAFRAÎCHIT d'abord les dépôts pour que # la VM soit la plus rapide possible (miroirs à jour / les plus @@ -4859,6 +4872,7 @@ class TODO: prod=False, desktop=False, python_provider="", + app_store="deb", ): """Lance l'install ERPLibre en parallèle DÉTACHÉE sur les VM et ouvre le dashboard Textual. Quitter le dashboard n'arrête pas les installs. @@ -4881,6 +4895,7 @@ class TODO: prod, "" if desk_map else desktop, python_provider, + app_store, ) try: mod = self._qemu_import_module() @@ -4911,6 +4926,7 @@ class TODO: prod, desk_map.get(name, ""), python_provider, + app_store, ) vms.append(entry) else: @@ -4953,6 +4969,7 @@ class TODO: prod=False, desktop=False, python_provider="", + app_store="deb", ): """Clone ERPLibre (branche donnée) dans la VM puis exécute la commande d'install du profil choisi (streamé). `ip` : IP déjà résolue ; @@ -4974,7 +4991,7 @@ class TODO: ) return remote = self._qemu_erplibre_remote_cmd( - branch, final_cmd, prod, desktop, python_provider + branch, final_cmd, prod, desktop, python_provider, app_store ) ssh_opts = ( "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null " @@ -5689,6 +5706,8 @@ class TODO: "extra_disk_gb": self.ERPLIBRE_EXTRA_DISK_GB, "desktop_disk_gb": self.QEMU_DESKTOP_EXTRA_DISK_GB, "mise_arches": self.QEMU_MISE_ARCHES, + "app_stores": [(k, t(lbl)) for k, lbl in self.QEMU_APP_STORES], + "snap_distros": self.QEMU_SNAP_DISTROS, "desktops": [ (k, v["label"]) for k, v in self._QEMU_DESKTOP.items() ], @@ -5979,6 +5998,30 @@ class TODO: return "" return flavours[index] if 0 <= index < len(flavours) else "" + @classmethod + def _qemu_app_store_needed(cls, vms): + """Vrai si au moins une VM du parc est à la fois graphique et d'une + distribution qui livre snapd. Ailleurs la question n'a pas d'objet : + un serveur ne tire aucun snap, et Debian ou Fedora n'en livrent pas.""" + return any( + vm.get("desktop") and vm.get("distro") in cls.QEMU_SNAP_DISTROS + for vm in vms + ) + + def _qemu_ask_app_store(self, vms): + """Magasin d'applications des VM graphiques Ubuntu.""" + if not self._qemu_app_store_needed(vms): + return "deb" + print(f"\n{t('Application store (graphical Ubuntu VMs):')}") + for i, (_key, label) in enumerate(self.QEMU_APP_STORES, 1): + star = " *" if i == 1 else "" + print(f" [{i}] {t(label)}{star}") + print(f" ⚠ {t('snap needs the store; slow under emulation.')}") + answer = input(f"{t('Choice')} [1]: ").strip() or "1" + if answer.isdigit() and 1 <= int(answer) <= len(self.QEMU_APP_STORES): + return self.QEMU_APP_STORES[int(answer) - 1][0] + return "deb" + def _qemu_ask_python_provider(self, arches): """mise (CPython précompilé) ou pyenv (compilation). @@ -6064,6 +6107,11 @@ class TODO: timezone = self._qemu_ask_timezone() locale = self._qemu_ask_locale() desktop = self._qemu_ask_desktop() + # La CLI ne pose qu'un type pour tout le parc : on le recopie sur chaque + # VM avant de décider du magasin, qui ne concerne que les graphiques. + for _vm in vms: + _vm.setdefault("desktop", desktop) + app_store = self._qemu_ask_app_store(vms) python_provider = self._qemu_ask_python_provider( [vm["arch"] for vm in vms] ) @@ -6145,6 +6193,7 @@ class TODO: "locale": locale, "desktop": desktop, "python_provider": python_provider, + "app_store": app_store, "install": install, "add_ssh_config": add_ssh_config, "parallelism": parallelism, @@ -6225,6 +6274,7 @@ class TODO: desktop_map.setdefault(_name, desktop_default) desktop = next((d for d in desktop_map.values() if d), "") python_provider = spec.get("python_provider") or "" + app_store = spec.get("app_store") or "deb" ssh_key = spec.get("ssh_key") add_ssh_config = spec["add_ssh_config"] parallelism = spec["parallelism"] @@ -6300,6 +6350,7 @@ class TODO: install["prod"] if install else False, desktop=desktop_map, python_provider=python_provider, + app_store=app_store, ) else: print( @@ -6316,6 +6367,7 @@ class TODO: install["prod"], desktop=desktop_map.get(name, ""), python_provider=python_provider, + app_store=app_store, ) # Sommaire TOTAL (déploiement + résolution IP + ssh_config + install