[IMP] script todo: déploiement — choix dev/prod (/opt + SELinux confiné en prod)

Au déploiement (« Déployer une ou plusieurs VM »), après le choix de la
branche, on demande l'ENVIRONNEMENT cible (dev par défaut / prod).

- DEV : ERPLibre dans ~/git/erplibre ; service systemd unconfined si
  SELinux actif (comportement inchangé).
- PROD : ERPLibre dans /opt/erplibre (sudo git clone + chown à
  l'utilisateur) ; service systemd CONFINÉ par SELinux (pas d'unconfined)
  + restorecon des contextes -> hors user_home_t, un service peut exécuter
  le contenu sans lever le confinement.

Le drapeau `prod` est propagé : _qemu_deploy -> _qemu_install_erplibre_
monitored/_vm -> _qemu_erplibre_remote_cmd -> _qemu_odoo_service_cmd.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mathieu Benoit 2026-07-30 02:09:29 -04:00
parent 43d268e448
commit 808e37cd59
2 changed files with 100 additions and 35 deletions

View file

@ -3017,6 +3017,16 @@ class TODO:
"libvirt virt-install libguestfs 2>/dev/null || true)" "libvirt virt-install libguestfs 2>/dev/null || true)"
) )
def _qemu_ask_prod(self):
"""Environnement cible : dev (défaut) ou prod. En PROD : ERPLibre est
installé dans /opt/erplibre (au lieu de ~/git/erplibre) et le service
systemd reste CONFINÉ par SELinux (pas d'unconfined)."""
print(f"\n{t('Target environment?')}")
print(f" [1] {t('Development (~/git/erplibre, SELinux relaxed)')} *")
print(f" [2] {t('Production (/opt/erplibre, SELinux enforced)')}")
sel = input(t("Choice (1-2, default 1): ")).strip()
return sel == "2"
def _qemu_pick_install_profile(self): def _qemu_pick_install_profile(self):
"""Choix de CE QU'ON installe sur la VM. Renvoie (label, commande """Choix de CE QU'ON installe sur la VM. Renvoie (label, commande
finale exécutée dans ~/git/erplibre).""" finale exécutée dans ~/git/erplibre)."""
@ -3059,24 +3069,43 @@ class TODO:
return profiles[0] # défaut : ERPLibre + Odoo 18 return profiles[0] # défaut : ERPLibre + Odoo 18
@staticmethod @staticmethod
def _qemu_odoo_service_cmd(): @staticmethod
def _qemu_install_dir(prod):
"""Répertoire d'installation ERPLibre dans la VM : /opt/erplibre en
PROD (hors /home -> service SELinux confiné possible), sinon
~/git/erplibre (dev)."""
return "/opt/erplibre" if prod else "$HOME/git/erplibre"
def _qemu_odoo_service_cmd(self, prod=False):
"""Snippet shell (exécuté dans la VM) qui installe ERPLibre/Odoo comme """Snippet shell (exécuté dans la VM) qui installe ERPLibre/Odoo comme
service systemd (inspiré de script/systemd/install_daemon.sh) puis service systemd puis l'active. N'est ajouté QUE pour les profils Odoo.
l'active + démarre. N'est ajouté QUE pour les profils avec Odoo."""
DEV : ERPLibre sous ~/home ; si SELinux est actif (Fedora), on lève le
confinement du service (unconfined) un service système ne peut pas
exécuter du user_home_t. Acceptable pour une VM de dev.
PROD : ERPLibre sous /opt/erplibre (hors user_home_t) -> le service
reste CONFINÉ par SELinux ; on restaure les contextes (restorecon)."""
svc_dir = self._qemu_install_dir(prod)
if prod:
pre = (
"command -v restorecon >/dev/null 2>&1 && "
"sudo restorecon -R /opt/erplibre >/dev/null 2>&1 || true; "
)
selinux_shell = 'SELINUX_LINE=""; ' # confiné : pas d'unconfined
else:
pre = ""
selinux_shell = (
'SELINUX_LINE=""; '
"if command -v getenforce >/dev/null 2>&1 && "
'[ "$(getenforce)" != "Disabled" ]; then '
'SELINUX_LINE="SELinuxContext=unconfined_u:unconfined_r:'
'unconfined_t:s0"; fi; '
)
return ( return (
'SVC_USER=$(whoami); SVC_GROUP=$(id -gn); ' f'SVC_USER=$(whoami); SVC_GROUP=$(id -gn); SVC_DIR="{svc_dir}"; '
'SVC_DIR="$HOME/git/erplibre"; ' + pre
# SELinux (Fedora) INTERDIT à un service système d'exécuter des + selinux_shell
# binaires sous /home (run.sh ET le python du venv) -> le service + "sudo tee /etc/systemd/system/erplibre.service >/dev/null <<UNIT\n"
# échoue en « status=203/EXEC ». On fait tourner l'ExecStart dans
# le domaine unconfined (autorisé à exécuter /home) UNIQUEMENT si
# SELinux est actif (ligne vide sinon, inoffensive).
'SELINUX_LINE=""; '
"if command -v getenforce >/dev/null 2>&1 && "
'[ "$(getenforce)" != "Disabled" ]; then '
'SELINUX_LINE="SELinuxContext=unconfined_u:unconfined_r:'
'unconfined_t:s0"; fi; '
"sudo tee /etc/systemd/system/erplibre.service >/dev/null <<UNIT\n"
"[Unit]\n" "[Unit]\n"
"Description=ERPLibre\n" "Description=ERPLibre\n"
"Requires=postgresql.service\n" "Requires=postgresql.service\n"
@ -3100,17 +3129,18 @@ class TODO:
"sudo systemctl enable --now erplibre.service" "sudo systemctl enable --now erplibre.service"
) )
def _qemu_erplibre_remote_cmd(self, branch, final_cmd=None): def _qemu_erplibre_remote_cmd(self, branch, final_cmd=None, prod=False):
"""Script d'installation ERPLibre exécuté DANS la VM (curl/git/make """Script d'installation ERPLibre exécuté DANS la VM (curl/git/make
puis clone + `final_cmd` dans ~/git/erplibre). `final_cmd` par défaut : puis clone + `final_cmd`). `final_cmd` par défaut : install_os +
install_os + install_odoo_18.""" install_odoo_18. `prod` : installe dans /opt/erplibre (au lieu de
~/git/erplibre) + service SELinux confiné."""
if not final_cmd: if not final_cmd:
final_cmd = f"make install_os && make {self.ERPLIBRE_ODOO_TARGET}" final_cmd = f"make install_os && make {self.ERPLIBRE_ODOO_TARGET}"
# Profils AVEC Odoo (install_odoo*) uniquement : après l'install, on # Profils AVEC Odoo (install_odoo*) uniquement : après l'install, on
# enregistre Odoo comme service systemd (enable + start). Pas pour # enregistre Odoo comme service systemd (enable + start). Pas pour
# « ERPLibre seul », « mobile » ni « Déploiement ». # « ERPLibre seul », « mobile » ni « Déploiement ».
if "install_odoo" in final_cmd: if "install_odoo" in final_cmd:
final_cmd = f"{final_cmd} && {self._qemu_odoo_service_cmd()}" final_cmd = f"{final_cmd} && {self._qemu_odoo_service_cmd(prod)}"
return ( return (
"set -e; " "set -e; "
# Attendre la FIN de cloud-init : pendant sa phase « paquets » il # Attendre la FIN de cloud-init : pendant sa phase « paquets » il
@ -3166,27 +3196,42 @@ class TODO:
"for t in curl git make; do command -v $t >/dev/null 2>&1 || " "for t in curl git make; do command -v $t >/dev/null 2>&1 || "
'{ echo "Outil manquant apres installation: $t ' '{ echo "Outil manquant apres installation: $t '
'(reseau de la VM ?)"; exit 1; }; done; ' '(reseau de la VM ?)"; exit 1; }; done; '
"mkdir -p ~/git; " # Clone : /opt/erplibre en PROD (racine, puis chown à l'utilisateur
"if [ ! -d ~/git/erplibre/.git ]; then " # pour que make/venv s'exécutent sans sudo), ~/git/erplibre en dev.
f"git clone --branch {shlex.quote(branch)} " + (
f"{self.ERPLIBRE_GIT_URL} ~/git/erplibre; fi; " (
# Installation : commande finale selon le profil choisi. "sudo mkdir -p /opt; "
f"cd ~/git/erplibre && {final_cmd}" "if [ ! -d /opt/erplibre/.git ]; then "
f"sudo git clone --branch {shlex.quote(branch)} "
f"{self.ERPLIBRE_GIT_URL} /opt/erplibre; "
"sudo chown -R $(id -un):$(id -gn) /opt/erplibre; fi; "
f"cd /opt/erplibre && {final_cmd}"
)
if prod
else (
"mkdir -p ~/git; "
"if [ ! -d ~/git/erplibre/.git ]; then "
f"git clone --branch {shlex.quote(branch)} "
f"{self.ERPLIBRE_GIT_URL} ~/git/erplibre; fi; "
f"cd ~/git/erplibre && {final_cmd}"
)
)
) )
def _qemu_install_erplibre_monitored( def _qemu_install_erplibre_monitored(
self, names, branch, ip_map=None, final_cmd=None self, names, branch, ip_map=None, final_cmd=None, prod=False
): ):
"""Lance l'install ERPLibre en parallèle DÉTACHÉE sur les VM et ouvre """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. le dashboard Textual. Quitter le dashboard n'arrête pas les installs.
`ip_map` : IP déjà résolues (sinon on résout ici, EN PARALLÈLE). `ip_map` : IP déjà résolues (sinon on résout ici, EN PARALLÈLE).
`final_cmd` : commande d'install selon le profil choisi.""" `final_cmd` : commande d'install selon le profil choisi.
`prod` : install /opt/erplibre + service SELinux confiné."""
from script.todo.qemu_install_monitor import ( from script.todo.qemu_install_monitor import (
launch_installs, launch_installs,
run_monitor, run_monitor,
) )
remote = self._qemu_erplibre_remote_cmd(branch, final_cmd) remote = self._qemu_erplibre_remote_cmd(branch, final_cmd, prod)
try: try:
mod = self._qemu_import_module() mod = self._qemu_import_module()
except Exception: except Exception:
@ -3239,11 +3284,11 @@ class TODO:
print(f" {t('Read the logs:')} tail -n +1 {logdir}/*.log") print(f" {t('Read the logs:')} tail -n +1 {logdir}/*.log")
def _qemu_install_erplibre_vm( def _qemu_install_erplibre_vm(
self, name, ssh_key, branch, ip=None, final_cmd=None self, name, ssh_key, branch, ip=None, final_cmd=None, prod=False
): ):
"""Clone ERPLibre (branche donnée) dans ~/git/erplibre de la VM puis """Clone ERPLibre (branche donnée) dans la VM puis exécute la commande
exécute la commande d'install du profil choisi (streamé). `ip` : IP d'install du profil choisi (streamé). `ip` : IP déjà résolue ;
déjà résolue ; `final_cmd` : commande d'install.""" `final_cmd` : commande d'install ; `prod` : /opt + SELinux confiné."""
if ip is None: if ip is None:
ip = self._qemu_vm_ip(name) ip = self._qemu_vm_ip(name)
if not ip: if not ip:
@ -3260,7 +3305,7 @@ class TODO:
f"{t('SSH not reachable, ERPLibre install skipped.')}" f"{t('SSH not reachable, ERPLibre install skipped.')}"
) )
return return
remote = self._qemu_erplibre_remote_cmd(branch, final_cmd) remote = self._qemu_erplibre_remote_cmd(branch, final_cmd, prod)
ssh_opts = ( ssh_opts = (
"-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null " "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "
"-o ConnectTimeout=15" "-o ConnectTimeout=15"
@ -3574,12 +3619,14 @@ class TODO:
# 4) Option : installer ERPLibre dans ~/git/erplibre de chaque VM. # 4) Option : installer ERPLibre dans ~/git/erplibre de chaque VM.
install_branch = None install_branch = None
install_monitor = False install_monitor = False
install_prod = False # dev (~/git, SELinux dev) vs prod (/opt, confiné)
install_cmd = None # commande finale selon le profil choisi install_cmd = None # commande finale selon le profil choisi
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(ans): if self._is_yes(ans):
install_branch = self._qemu_pick_branch() install_branch = self._qemu_pick_branch()
install_prod = self._qemu_ask_prod()
_label, install_cmd = self._qemu_pick_install_profile() _label, install_cmd = self._qemu_pick_install_profile()
install_monitor = self._is_yes_default_yes( install_monitor = self._is_yes_default_yes(
input(t("Interactive monitoring dashboard? (y/N): ")) input(t("Interactive monitoring dashboard? (y/N): "))
@ -3698,7 +3745,8 @@ class TODO:
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, install_branch, ip_map, install_cmd deployed, install_branch, ip_map, install_cmd,
install_prod,
) )
else: else:
print( print(
@ -3712,6 +3760,7 @@ class TODO:
install_branch, install_branch,
ip_map.get(name), ip_map.get(name),
install_cmd, install_cmd,
install_prod,
) )
# Sommaire TOTAL (déploiement + résolution IP + ssh_config + install # Sommaire TOTAL (déploiement + résolution IP + ssh_config + install

View file

@ -1794,6 +1794,22 @@ TRANSLATIONS = {
"fr": "État cible :", "fr": "État cible :",
"en": "Target state:", "en": "Target state:",
}, },
"Target environment?": {
"fr": "Environnement cible ?",
"en": "Target environment?",
},
"Development (~/git/erplibre, SELinux relaxed)": {
"fr": "Développement (~/git/erplibre, SELinux relâché)",
"en": "Development (~/git/erplibre, SELinux relaxed)",
},
"Production (/opt/erplibre, SELinux enforced)": {
"fr": "Production (/opt/erplibre, SELinux confiné)",
"en": "Production (/opt/erplibre, SELinux enforced)",
},
"Choice (1-2, default 1): ": {
"fr": "Choix (1-2, défaut 1) : ",
"en": "Choice (1-2, default 1): ",
},
"Open (start)": { "Open (start)": {
"fr": "Ouvrir (démarrer)", "fr": "Ouvrir (démarrer)",
"en": "Open (start)", "en": "Open (start)",