Compare commits
No commits in common. "dcb1af48802b58c7ba27ead14b4a5f6311d59923" and "188dad9960743a35b54e2f958c48847572f1913e" have entirely different histories.
dcb1af4880
...
188dad9960
3 changed files with 10 additions and 235 deletions
|
|
@ -1168,38 +1168,12 @@ class TODO:
|
|||
print(f"{t('Will execute:')} {cmd}")
|
||||
self.execute.exec_command_live(cmd, source_erplibre=False)
|
||||
|
||||
def _qemu_ensure_tools(self):
|
||||
"""virsh absent : proposer l'installation plutôt que de laisser
|
||||
chaque commande échouer sur « sudo: virsh: command not found ».
|
||||
|
||||
deploy_qemu.py --setup-host connaît les paquets de chaque
|
||||
distribution ; on ne devine donc rien ici, on le délègue."""
|
||||
if shutil.which("virsh"):
|
||||
return True
|
||||
print(f"\n⚠ {t('virsh is missing: libvirt is not installed here.')}")
|
||||
print(f" {t('Every VM command will fail until it is.')}")
|
||||
if not self._is_yes_default_yes(
|
||||
input(t("Install the QEMU/libvirt tools now? (Y/n): "))
|
||||
):
|
||||
return False
|
||||
cmd = f"sudo {self._QEMU_QEMU_PKGS}"
|
||||
print(f"{t('Will execute:')} {cmd}")
|
||||
self.execute.exec_command_live(cmd, source_erplibre=False)
|
||||
if shutil.which("virsh"):
|
||||
print(f"✅ {t('libvirt is available.')}")
|
||||
return True
|
||||
# Sur une distribution à noyau roulant, --setup-host peut demander un
|
||||
# redémarrage avant que les modules soient chargeables.
|
||||
print(f"⚠ {t('virsh still missing; a reboot may be required.')}")
|
||||
return False
|
||||
|
||||
def prompt_execute_qemu(self):
|
||||
print(f"🤖 {t('Deploy a QEMU/KVM virtual machine (libvirt)!')}")
|
||||
script_path = self._qemu_script_path()
|
||||
if not os.path.isfile(script_path):
|
||||
print(f"{t('QEMU deploy script not found: ')}{script_path}")
|
||||
return False
|
||||
self._qemu_ensure_tools()
|
||||
choices = [
|
||||
{"section": t("Deployment")},
|
||||
{"prompt_description": t("Deploy VM(s) (one or many)")},
|
||||
|
|
@ -5061,16 +5035,8 @@ class TODO:
|
|||
"""Invites en ligne : clé SSH, installation ERPLibre, ~/.ssh/config,
|
||||
parallélisme, puis récapitulatif et confirmation.
|
||||
Renvoie la spec complète, ou None si l'utilisateur renonce."""
|
||||
# Clé SSH (partagée par tout le parc). Sans clé, cloud-init n'en
|
||||
# injecte aucune : la VM démarre sans accès SSH, donc sans
|
||||
# installation ni vérification possibles. On propose donc d'en créer
|
||||
# une plutôt que de laisser passer un déploiement inutilisable.
|
||||
# Clé SSH (partagée par tout le parc).
|
||||
default_key = self._qemu_default_ssh_key()
|
||||
if not default_key:
|
||||
print(f"\n⚠ {t('No SSH public key found in ~/.ssh.')}")
|
||||
print(f" {t('Without one the VMs start with no SSH access.')}")
|
||||
if self._is_yes_default_yes(input(t("Generate one now? (Y/n): "))):
|
||||
default_key = self._ssh_ensure_key()
|
||||
key_hint = default_key or t("none")
|
||||
ssh_key = input(f"{t('SSH public key path')} ({key_hint}): ").strip()
|
||||
if not ssh_key:
|
||||
|
|
@ -5406,43 +5372,6 @@ class TODO:
|
|||
except OSError:
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _remote_port_open(host, port):
|
||||
"""Quelqu'un écoute-t-il sur ce port DEPUIS l'hôte distant ?
|
||||
|
||||
True / False / None quand on n'a pas pu conclure (hôte injoignable,
|
||||
pas de bash). On teste une vraie connexion TCP vers « localhost » et
|
||||
non la table d'écoute : c'est exactement ce que fera le tunnel, y
|
||||
compris le choix IPv4/IPv6 de la résolution.
|
||||
"""
|
||||
probe = (
|
||||
f"exec 3<>/dev/tcp/localhost/{int(port)} && echo OPEN"
|
||||
" || echo CLOSED"
|
||||
)
|
||||
try:
|
||||
res = subprocess.run(
|
||||
[
|
||||
"ssh",
|
||||
"-o",
|
||||
"BatchMode=yes",
|
||||
"-o",
|
||||
"ConnectTimeout=10",
|
||||
host,
|
||||
f"bash -c {shlex.quote(probe)} 2>/dev/null",
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=45,
|
||||
)
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
return None
|
||||
out = res.stdout.strip()
|
||||
if "OPEN" in out:
|
||||
return True
|
||||
if "CLOSED" in out:
|
||||
return False
|
||||
return None
|
||||
|
||||
def _deploy_port_forward(self):
|
||||
"""Ouvre un tunnel SSH pour joindre un service distant depuis le
|
||||
navigateur local.
|
||||
|
|
@ -5468,22 +5397,6 @@ class TODO:
|
|||
raw = input(f"{t('Local port (default:')} {remote}): ").strip()
|
||||
local = raw if raw.isdigit() else remote
|
||||
|
||||
# Sonde AVANT d'ouvrir : sans elle, un service arrêté à l'autre bout
|
||||
# ne se manifeste que par un mur de « channel N: open failed » à
|
||||
# chaque requête du navigateur, qui ne dit pas d'où vient le refus.
|
||||
print(f" {t('Checking the remote port...')}")
|
||||
listening = self._remote_port_open(host, remote)
|
||||
if listening is False:
|
||||
print(
|
||||
f" ⚠ {t('Nothing is listening on port')} {remote}"
|
||||
f" {t('of')} {host}"
|
||||
)
|
||||
print(f" {t('Start the service there, or continue anyway.')}")
|
||||
if not self._is_yes(input(t("Continue anyway? (y/N): "))):
|
||||
return
|
||||
elif listening is None:
|
||||
print(f" ℹ️ {t('Could not probe the remote port; going on.')}")
|
||||
|
||||
if not self._port_is_free(local):
|
||||
print(f" ⚠ {t('Local port already in use:')} {local}")
|
||||
if not self._is_yes(input(t("Try anyway? (y/N): "))):
|
||||
|
|
|
|||
|
|
@ -1419,63 +1419,6 @@ TRANSLATIONS = {
|
|||
"fr": "Port local (défaut :",
|
||||
"en": "Local port (default:",
|
||||
},
|
||||
"virsh is missing: libvirt is not installed here.": {
|
||||
"fr": "virsh est absent : libvirt n'est pas installé ici.",
|
||||
"en": "virsh is missing: libvirt is not installed here.",
|
||||
},
|
||||
"Every VM command will fail until it is.": {
|
||||
"fr": "Toutes les commandes VM échoueront tant que ce sera le cas.",
|
||||
"en": "Every VM command will fail until it is.",
|
||||
},
|
||||
"Install the QEMU/libvirt tools now? (Y/n): ": {
|
||||
"fr": "Installer les outils QEMU/libvirt maintenant ? "
|
||||
"(O/n, défaut : oui) : ",
|
||||
"en": "Install the QEMU/libvirt tools now? (Y/n, default: yes): ",
|
||||
},
|
||||
"libvirt is available.": {
|
||||
"fr": "libvirt est disponible.",
|
||||
"en": "libvirt is available.",
|
||||
},
|
||||
"virsh still missing; a reboot may be required.": {
|
||||
"fr": "virsh toujours absent ; un redémarrage est peut-être requis.",
|
||||
"en": "virsh still missing; a reboot may be required.",
|
||||
},
|
||||
"No SSH public key found in ~/.ssh.": {
|
||||
"fr": "Aucune clé publique SSH trouvée dans ~/.ssh.",
|
||||
"en": "No SSH public key found in ~/.ssh.",
|
||||
},
|
||||
"Without one the VMs start with no SSH access.": {
|
||||
"fr": "Sans elle, les VM démarrent sans accès SSH.",
|
||||
"en": "Without one the VMs start with no SSH access.",
|
||||
},
|
||||
"Generate one now? (Y/n): ": {
|
||||
"fr": "En générer une maintenant ? (O/n, défaut : oui) : ",
|
||||
"en": "Generate one now? (Y/n, default: yes): ",
|
||||
},
|
||||
"Checking the remote port...": {
|
||||
"fr": "Vérification du port distant...",
|
||||
"en": "Checking the remote port...",
|
||||
},
|
||||
"Nothing is listening on port": {
|
||||
"fr": "Rien n'écoute sur le port",
|
||||
"en": "Nothing is listening on port",
|
||||
},
|
||||
"of": {
|
||||
"fr": "de",
|
||||
"en": "of",
|
||||
},
|
||||
"Start the service there, or continue anyway.": {
|
||||
"fr": "Démarrer le service là-bas, ou continuer quand même.",
|
||||
"en": "Start the service there, or continue anyway.",
|
||||
},
|
||||
"Continue anyway? (y/N): ": {
|
||||
"fr": "Continuer quand même ? (o/N, défaut : non) : ",
|
||||
"en": "Continue anyway? (y/N, default: no): ",
|
||||
},
|
||||
"Could not probe the remote port; going on.": {
|
||||
"fr": "Impossible de sonder le port distant ; on continue.",
|
||||
"en": "Could not probe the remote port; going on.",
|
||||
},
|
||||
"Local port already in use:": {
|
||||
"fr": "Port local déjà occupé :",
|
||||
"en": "Local port already in use:",
|
||||
|
|
@ -1666,23 +1609,6 @@ TRANSLATIONS = {
|
|||
"fr": "Rien de désinstallé.",
|
||||
"en": "Nothing uninstalled.",
|
||||
},
|
||||
"Check the COW views that drifted": {
|
||||
"fr": "Vérifier les vues COW en retard sur leur vue module",
|
||||
"en": "Check the COW views that drifted",
|
||||
},
|
||||
"Tool not found": {
|
||||
"fr": "Outil introuvable",
|
||||
"en": "Tool not found",
|
||||
},
|
||||
"To reset one of them onto its module view:": {
|
||||
"fr": "Pour réinitialiser l'une d'elles sur sa vue module :",
|
||||
"en": "To reset one of them onto its module view:",
|
||||
},
|
||||
"Read the diff first: a copy can hold a customisation.": {
|
||||
"fr": "Lire le diff d'abord : une copie peut porter une"
|
||||
" personnalisation.",
|
||||
"en": "Read the diff first: a copy can hold a customisation.",
|
||||
},
|
||||
"Migration statistics (read-only)": {
|
||||
"fr": "📊 Statistiques de la migration (lecture seule)",
|
||||
"en": "📊 Migration statistics (read-only)",
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import datetime
|
|||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
import zipfile
|
||||
|
|
@ -313,52 +312,6 @@ class TodoUpgrade:
|
|||
answer = input(t("Choice (0-3, default 1): ")).strip()
|
||||
return {"0": None, "2": "cli", "3": "stats"}.get(answer, "tui")
|
||||
|
||||
@staticmethod
|
||||
def database_from_command(cmd):
|
||||
"""Nom de base visé par une commande, ou "" si indécelable.
|
||||
|
||||
Sert à proposer le bon outil au bon moment quand une commande échoue.
|
||||
On reconnaît les trois formes du dépôt : « -d <base> », « --database
|
||||
<base> », et l'argument positionnel des scripts addons
|
||||
(`update_addons_all.sh <base>`, `install_addons*.sh <base> <modules>`).
|
||||
"""
|
||||
if not cmd:
|
||||
return ""
|
||||
match = re.search(r"(?:^|\s)(?:-d|--database)[=\s]+([\w.-]+)", cmd)
|
||||
if match:
|
||||
return match.group(1)
|
||||
match = re.search(
|
||||
r"\./script/addons/\w+\.sh\s+([\w.-]+)",
|
||||
cmd,
|
||||
)
|
||||
return match.group(1) if match else ""
|
||||
|
||||
def check_stale_cow_views(self, database_name):
|
||||
"""Lance le détecteur de copies COW en retard sur leur vue module.
|
||||
|
||||
Purement consultatif : il n'écrit rien sans « --reset … --apply », que
|
||||
l'on propose seulement après avoir montré le diff — réinitialiser une
|
||||
copie peut effacer une personnalisation réelle."""
|
||||
script_path = os.path.join(
|
||||
PATH_MIGRATION_GLOBAL, "reset_stale_cow_views.py"
|
||||
)
|
||||
if not os.path.exists(script_path):
|
||||
print(f"⚠️ {t('Tool not found')}: {script_path}")
|
||||
return
|
||||
status, _cmd = self.todo_upgrade_execute(
|
||||
f"{PYTHON_BIN} ./{script_path} -d {database_name}",
|
||||
wait_at_error=False,
|
||||
)
|
||||
if status:
|
||||
# Sortie 1 = des écarts ont été trouvés (le script les a listés).
|
||||
warn = t("Read the diff first: a copy can hold a customisation.")
|
||||
print(f"\n💡 {t('To reset one of them onto its module view:')}")
|
||||
print(
|
||||
f" {PYTHON_BIN} ./{script_path} -d {database_name}"
|
||||
f" --reset <key> --apply"
|
||||
)
|
||||
print(f" {warn}")
|
||||
|
||||
def show_stats(self):
|
||||
"""Écran de statistiques, en lecture seule : rien n'est écrit, ni
|
||||
dans la base ni dans le journal de migration."""
|
||||
|
|
@ -2697,34 +2650,17 @@ class TodoUpgrade:
|
|||
# failure, never as a success (defence in depth: exec_command_live now
|
||||
# always sets one, but a silent None must not skip this prompt).
|
||||
if (status is None or status) and wait_at_error:
|
||||
database_name = self.database_from_command(cmd)
|
||||
while True:
|
||||
print("[1] to redo the command")
|
||||
if database_name:
|
||||
print(
|
||||
f"[2] {t('Check the COW views that drifted')}"
|
||||
f" ({database_name})"
|
||||
)
|
||||
wait_status = (
|
||||
input(
|
||||
"💬 Error detected, press to continue or ctrl+c to"
|
||||
" stop : "
|
||||
)
|
||||
.strip()
|
||||
.lower()
|
||||
print("[1] to redo the command")
|
||||
wait_status = (
|
||||
input(
|
||||
"💬 Error detected, press to continue or ctrl+c to stop : "
|
||||
)
|
||||
.strip()
|
||||
.lower()
|
||||
)
|
||||
|
||||
# psycopg2.errors.UndefinedTable: relation "discuss_channel" does not exist
|
||||
# LIGNE 1 : SELECT "discuss_channel"."id" FROM "discuss_channel" WHERE (...
|
||||
|
||||
if wait_status == "2" and database_name:
|
||||
# Le motif d'échec le plus fréquent ici est « Element
|
||||
# <xpath …> cannot be located in parent view » : une copie
|
||||
# COW en retard sur sa vue module. On propose l'outil sur
|
||||
# place, puis on repose le choix pour rejouer.
|
||||
self.check_stale_cow_views(database_name)
|
||||
continue
|
||||
break
|
||||
# psycopg2.errors.UndefinedTable: relation "discuss_channel" does not exist
|
||||
# LIGNE 1 : SELECT "discuss_channel"."id" FROM "discuss_channel" WHERE (...
|
||||
|
||||
if wait_status == "1":
|
||||
return self.todo_upgrade_execute(
|
||||
|
|
|
|||
Loading…
Reference in a new issue