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