[ADD] make: add SSH remote deployment targets and todo.py integration
Enable deploying and managing ERPLibre on remote servers via SSH directly from make and the interactive todo.py CLI, since only local deployment was previously supported. - New conf/make.ssh.Makefile with 11 targets: ssh_check, ssh_push, ssh_install, ssh_run, ssh_stop, ssh_restart, ssh_status, ssh_logs, ssh_make, ssh_install_systemd, ssh_install_nginx - Variables: SSH_HOST (required), SSH_USER, SSH_PORT, SSH_KEY, SSH_PATH, SSH_TARGET, SSH_DOMAIN, SSH_ADMIN_EMAIL - Execute > Deploy menu extended with 11 SSH options in todo.py - All strings translated fr/en in todo_i18n.py Generated by Claude Code 2.1.101 model claude-sonnet-4-6 Co-Authored-By: Mathieu Benoit <mathben@technolibre.ca>
This commit is contained in:
parent
cd14e128ff
commit
e03befb28e
4 changed files with 418 additions and 0 deletions
1
Makefile
1
Makefile
|
|
@ -47,6 +47,7 @@ endif
|
|||
-include ./conf/make.installation.Makefile
|
||||
-include ./conf/make.installation.poetry.Makefile
|
||||
-include ./conf/make.test.Makefile
|
||||
-include ./conf/make.ssh.Makefile
|
||||
-include ./conf/make.todo.Makefile
|
||||
|
||||
# Include private Makefile
|
||||
|
|
|
|||
130
conf/make.ssh.Makefile
Normal file
130
conf/make.ssh.Makefile
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
#######
|
||||
# SSH #
|
||||
#######
|
||||
|
||||
# Connection variables (override on command line or via environment)
|
||||
SSH_HOST ?=
|
||||
SSH_USER ?= erplibre
|
||||
SSH_PORT ?= 22
|
||||
SSH_KEY ?=
|
||||
SSH_PATH ?= ~/erplibre_deploy_2
|
||||
|
||||
# Target to run remotely via ssh_make
|
||||
SSH_TARGET ?= run
|
||||
|
||||
# Build SSH/rsync options from variables
|
||||
_SSH_KEY_OPT = $(if $(SSH_KEY),-i $(SSH_KEY),)
|
||||
_SSH_CMD = ssh -p $(SSH_PORT) -o StrictHostKeyChecking=accept-new $(_SSH_KEY_OPT)
|
||||
_RSYNC_SSH = ssh -p $(SSH_PORT) -o StrictHostKeyChecking=accept-new $(_SSH_KEY_OPT)
|
||||
|
||||
define _require_host
|
||||
@test -n "$(SSH_HOST)" || \
|
||||
(echo "Error: SSH_HOST is required. Usage: make $@ SSH_HOST=hostname [SSH_USER=erplibre] [SSH_PORT=22] [SSH_KEY=~/.ssh/id_rsa]" && exit 1)
|
||||
endef
|
||||
|
||||
# Test SSH connectivity
|
||||
.PHONY: ssh_check
|
||||
ssh_check:
|
||||
$(call _require_host)
|
||||
$(_SSH_CMD) $(SSH_USER)@$(SSH_HOST) "echo 'SSH connection to $(SSH_HOST) OK && uname -a'"
|
||||
|
||||
# Sync project files to remote (excludes venvs, addons, odoo sources, git)
|
||||
.PHONY: ssh_push
|
||||
ssh_push:
|
||||
$(call _require_host)
|
||||
rsync -avz --delete \
|
||||
--exclude='.venv.*/' \
|
||||
--exclude='addons/' \
|
||||
--exclude='odoo12.0/' \
|
||||
--exclude='odoo13.0/' \
|
||||
--exclude='odoo14.0/' \
|
||||
--exclude='odoo15.0/' \
|
||||
--exclude='odoo16.0/' \
|
||||
--exclude='odoo17.0/' \
|
||||
--exclude='odoo18.0/' \
|
||||
--exclude='.git/' \
|
||||
--exclude='private/' \
|
||||
--exclude='*.pyc' \
|
||||
--exclude='__pycache__/' \
|
||||
-e "$(_RSYNC_SSH)" \
|
||||
./ $(SSH_USER)@$(SSH_HOST):$(SSH_PATH)/
|
||||
|
||||
# Install ERPLibre on remote server (Odoo 18 default)
|
||||
.PHONY: ssh_install
|
||||
ssh_install:
|
||||
$(call _require_host)
|
||||
$(_SSH_CMD) $(SSH_USER)@$(SSH_HOST) "cd $(SSH_PATH) && make install_odoo_18"
|
||||
|
||||
# Start Odoo on remote server
|
||||
.PHONY: ssh_run
|
||||
ssh_run:
|
||||
$(call _require_host)
|
||||
$(_SSH_CMD) $(SSH_USER)@$(SSH_HOST) "cd $(SSH_PATH) && make run"
|
||||
|
||||
# Stop Odoo on remote server (via systemd if available, else kill process)
|
||||
.PHONY: ssh_stop
|
||||
ssh_stop:
|
||||
$(call _require_host)
|
||||
$(_SSH_CMD) $(SSH_USER)@$(SSH_HOST) \
|
||||
"systemctl --user stop erplibre 2>/dev/null || \
|
||||
sudo systemctl stop erplibre 2>/dev/null || \
|
||||
cd $(SSH_PATH) && make process_kill_odoo"
|
||||
|
||||
# Restart Odoo on remote server
|
||||
.PHONY: ssh_restart
|
||||
ssh_restart:
|
||||
$(call _require_host)
|
||||
$(_SSH_CMD) $(SSH_USER)@$(SSH_HOST) \
|
||||
"systemctl --user restart erplibre 2>/dev/null || \
|
||||
sudo systemctl restart erplibre 2>/dev/null || \
|
||||
(cd $(SSH_PATH) && make process_kill_odoo && make run)"
|
||||
|
||||
# Show service status on remote server
|
||||
.PHONY: ssh_status
|
||||
ssh_status:
|
||||
$(call _require_host)
|
||||
$(_SSH_CMD) $(SSH_USER)@$(SSH_HOST) \
|
||||
"systemctl --user status erplibre 2>/dev/null || \
|
||||
sudo systemctl status erplibre 2>/dev/null || \
|
||||
ps aux | grep odoo | grep -v grep"
|
||||
|
||||
# Stream logs from remote server
|
||||
.PHONY: ssh_logs
|
||||
ssh_logs:
|
||||
$(call _require_host)
|
||||
$(_SSH_CMD) $(SSH_USER)@$(SSH_HOST) \
|
||||
"journalctl --user -u erplibre -f 2>/dev/null || \
|
||||
sudo journalctl -u erplibre -f"
|
||||
|
||||
# Execute any make target on remote server (e.g.: make ssh_make SSH_HOST=host SSH_TARGET=db_create_db_test)
|
||||
.PHONY: ssh_make
|
||||
ssh_make:
|
||||
$(call _require_host)
|
||||
@test -n "$(SSH_TARGET)" || \
|
||||
(echo "Error: SSH_TARGET is required. Usage: make ssh_make SSH_HOST=hostname SSH_TARGET=make_target" && exit 1)
|
||||
$(_SSH_CMD) $(SSH_USER)@$(SSH_HOST) "cd $(SSH_PATH) && make $(SSH_TARGET)"
|
||||
|
||||
# Install systemd service on remote server
|
||||
.PHONY: ssh_install_systemd
|
||||
ssh_install_systemd:
|
||||
$(call _require_host)
|
||||
$(_SSH_CMD) $(SSH_USER)@$(SSH_HOST) \
|
||||
"cd $(SSH_PATH) && python3 script/systemd/install_daemon.py \
|
||||
--user $(SSH_USER) \
|
||||
--home-erplibre $(SSH_PATH) \
|
||||
--port 8069"
|
||||
|
||||
# Configure nginx + SSL on remote server (requires SSH_DOMAIN)
|
||||
SSH_DOMAIN ?=
|
||||
SSH_ADMIN_EMAIL ?=
|
||||
.PHONY: ssh_install_nginx
|
||||
ssh_install_nginx:
|
||||
$(call _require_host)
|
||||
@test -n "$(SSH_DOMAIN)" || \
|
||||
(echo "Error: SSH_DOMAIN is required. Usage: make ssh_install_nginx SSH_HOST=hostname SSH_DOMAIN=example.com [SSH_ADMIN_EMAIL=admin@example.com]" && exit 1)
|
||||
$(_SSH_CMD) $(SSH_USER)@$(SSH_HOST) \
|
||||
"cd $(SSH_PATH) && sudo python3 script/nginx/deploy_nginx_and_certbot.py \
|
||||
--generate_nginx \
|
||||
--run_certbot \
|
||||
--domain $(SSH_DOMAIN) \
|
||||
$(if $(SSH_ADMIN_EMAIL),--admin_email $(SSH_ADMIN_EMAIL),)"
|
||||
|
|
@ -651,6 +651,17 @@ class TODO:
|
|||
choices = [
|
||||
{"prompt_description": t("Clone ERPLibre locally (git clone)")},
|
||||
{"prompt_description": t("Configure sshfs")},
|
||||
{"prompt_description": t("SSH - Check connection")},
|
||||
{"prompt_description": t("SSH - Sync files (rsync)")},
|
||||
{"prompt_description": t("SSH - Install ERPLibre")},
|
||||
{"prompt_description": t("SSH - Start Odoo")},
|
||||
{"prompt_description": t("SSH - Stop Odoo")},
|
||||
{"prompt_description": t("SSH - Restart Odoo")},
|
||||
{"prompt_description": t("SSH - Service status")},
|
||||
{"prompt_description": t("SSH - View logs")},
|
||||
{"prompt_description": t("SSH - Run make target")},
|
||||
{"prompt_description": t("SSH - Install systemd service")},
|
||||
{"prompt_description": t("SSH - Configure nginx + SSL")},
|
||||
]
|
||||
help_info = self.fill_help_info(choices)
|
||||
|
||||
|
|
@ -663,6 +674,28 @@ class TODO:
|
|||
self._deploy_clone_erplibre()
|
||||
elif status == "2":
|
||||
self._configure_sshfs()
|
||||
elif status == "3":
|
||||
self._deploy_ssh_check()
|
||||
elif status == "4":
|
||||
self._deploy_ssh_push()
|
||||
elif status == "5":
|
||||
self._deploy_ssh_install()
|
||||
elif status == "6":
|
||||
self._deploy_ssh_run()
|
||||
elif status == "7":
|
||||
self._deploy_ssh_stop()
|
||||
elif status == "8":
|
||||
self._deploy_ssh_restart()
|
||||
elif status == "9":
|
||||
self._deploy_ssh_status()
|
||||
elif status == "10":
|
||||
self._deploy_ssh_logs()
|
||||
elif status == "11":
|
||||
self._deploy_ssh_make()
|
||||
elif status == "12":
|
||||
self._deploy_ssh_install_systemd()
|
||||
elif status == "13":
|
||||
self._deploy_ssh_install_nginx()
|
||||
else:
|
||||
print(t("Command not found !"))
|
||||
|
||||
|
|
@ -793,6 +826,171 @@ class TODO:
|
|||
except Exception as e:
|
||||
print(f"{t('Error mounting sshfs: ')}{e}")
|
||||
|
||||
def _get_ssh_params(self):
|
||||
"""Prompt for SSH connection parameters. Returns dict or None on cancel."""
|
||||
host = click.prompt(t("Remote host (user@hostname or hostname): ")).strip()
|
||||
if not host:
|
||||
print(t("SSH host is required!"))
|
||||
return None
|
||||
user = (
|
||||
click.prompt(t("SSH user (default: erplibre): ")).strip()
|
||||
or "erplibre"
|
||||
)
|
||||
port = click.prompt(t("SSH port (default: 22): ")).strip() or "22"
|
||||
key = (
|
||||
click.prompt(
|
||||
t("SSH key path (default: ~/.ssh/id_rsa, empty for none): ")
|
||||
).strip()
|
||||
)
|
||||
path = (
|
||||
click.prompt(t("Remote path (default: ~/erplibre_deploy_2): ")).strip()
|
||||
or "~/erplibre_deploy_2"
|
||||
)
|
||||
return {
|
||||
"SSH_HOST": host,
|
||||
"SSH_USER": user,
|
||||
"SSH_PORT": port,
|
||||
"SSH_KEY": key,
|
||||
"SSH_PATH": path,
|
||||
}
|
||||
|
||||
def _build_ssh_make_cmd(self, target, params, extra=None):
|
||||
"""Build a make SSH command string from params dict."""
|
||||
parts = [f"make {target}"]
|
||||
for k, v in params.items():
|
||||
if v:
|
||||
parts.append(f'{k}="{v}"')
|
||||
if extra:
|
||||
for k, v in extra.items():
|
||||
if v:
|
||||
parts.append(f'{k}="{v}"')
|
||||
return " ".join(parts)
|
||||
|
||||
def _deploy_ssh_check(self):
|
||||
params = self._get_ssh_params()
|
||||
if not params:
|
||||
return
|
||||
cmd = self._build_ssh_make_cmd("ssh_check", params)
|
||||
print(f"{t('Will execute:')} {cmd}")
|
||||
self.execute.exec_command_live(
|
||||
cmd, source_erplibre=False, single_source_erplibre=True
|
||||
)
|
||||
|
||||
def _deploy_ssh_push(self):
|
||||
params = self._get_ssh_params()
|
||||
if not params:
|
||||
return
|
||||
cmd = self._build_ssh_make_cmd("ssh_push", params)
|
||||
print(f"{t('Will execute:')} {cmd}")
|
||||
self.execute.exec_command_live(
|
||||
cmd, source_erplibre=False, single_source_erplibre=True
|
||||
)
|
||||
|
||||
def _deploy_ssh_install(self):
|
||||
params = self._get_ssh_params()
|
||||
if not params:
|
||||
return
|
||||
cmd = self._build_ssh_make_cmd("ssh_install", params)
|
||||
print(f"{t('Will execute:')} {cmd}")
|
||||
self.execute.exec_command_live(
|
||||
cmd, source_erplibre=False, single_source_erplibre=True
|
||||
)
|
||||
|
||||
def _deploy_ssh_run(self):
|
||||
params = self._get_ssh_params()
|
||||
if not params:
|
||||
return
|
||||
cmd = self._build_ssh_make_cmd("ssh_run", params)
|
||||
print(f"{t('Will execute:')} {cmd}")
|
||||
self.execute.exec_command_live(
|
||||
cmd, source_erplibre=False, single_source_erplibre=True
|
||||
)
|
||||
|
||||
def _deploy_ssh_stop(self):
|
||||
params = self._get_ssh_params()
|
||||
if not params:
|
||||
return
|
||||
cmd = self._build_ssh_make_cmd("ssh_stop", params)
|
||||
print(f"{t('Will execute:')} {cmd}")
|
||||
self.execute.exec_command_live(
|
||||
cmd, source_erplibre=False, single_source_erplibre=True
|
||||
)
|
||||
|
||||
def _deploy_ssh_restart(self):
|
||||
params = self._get_ssh_params()
|
||||
if not params:
|
||||
return
|
||||
cmd = self._build_ssh_make_cmd("ssh_restart", params)
|
||||
print(f"{t('Will execute:')} {cmd}")
|
||||
self.execute.exec_command_live(
|
||||
cmd, source_erplibre=False, single_source_erplibre=True
|
||||
)
|
||||
|
||||
def _deploy_ssh_status(self):
|
||||
params = self._get_ssh_params()
|
||||
if not params:
|
||||
return
|
||||
cmd = self._build_ssh_make_cmd("ssh_status", params)
|
||||
print(f"{t('Will execute:')} {cmd}")
|
||||
self.execute.exec_command_live(
|
||||
cmd, source_erplibre=False, single_source_erplibre=True
|
||||
)
|
||||
|
||||
def _deploy_ssh_logs(self):
|
||||
params = self._get_ssh_params()
|
||||
if not params:
|
||||
return
|
||||
cmd = self._build_ssh_make_cmd("ssh_logs", params)
|
||||
print(f"{t('Will execute:')} {cmd}")
|
||||
self.execute.exec_command_live(
|
||||
cmd, source_erplibre=False, single_source_erplibre=True
|
||||
)
|
||||
|
||||
def _deploy_ssh_make(self):
|
||||
params = self._get_ssh_params()
|
||||
if not params:
|
||||
return
|
||||
target = click.prompt(t("Make target to run remotely: ")).strip()
|
||||
if not target:
|
||||
print(t("SSH host is required!"))
|
||||
return
|
||||
cmd = self._build_ssh_make_cmd(
|
||||
"ssh_make", params, extra={"SSH_TARGET": target}
|
||||
)
|
||||
print(f"{t('Will execute:')} {cmd}")
|
||||
self.execute.exec_command_live(
|
||||
cmd, source_erplibre=False, single_source_erplibre=True
|
||||
)
|
||||
|
||||
def _deploy_ssh_install_systemd(self):
|
||||
params = self._get_ssh_params()
|
||||
if not params:
|
||||
return
|
||||
cmd = self._build_ssh_make_cmd("ssh_install_systemd", params)
|
||||
print(f"{t('Will execute:')} {cmd}")
|
||||
self.execute.exec_command_live(
|
||||
cmd, source_erplibre=False, single_source_erplibre=True
|
||||
)
|
||||
|
||||
def _deploy_ssh_install_nginx(self):
|
||||
params = self._get_ssh_params()
|
||||
if not params:
|
||||
return
|
||||
domain = click.prompt(t("Domain name (e.g.: example.com): ")).strip()
|
||||
if not domain:
|
||||
print(t("SSH host is required!"))
|
||||
return
|
||||
email = click.prompt(t("Admin email for SSL certificate: ")).strip()
|
||||
cmd = self._build_ssh_make_cmd(
|
||||
"ssh_install_nginx",
|
||||
params,
|
||||
extra={"SSH_DOMAIN": domain, "SSH_ADMIN_EMAIL": email},
|
||||
)
|
||||
print(f"{t('Will execute:')} {cmd}")
|
||||
self.execute.exec_command_live(
|
||||
cmd, source_erplibre=False, single_source_erplibre=True
|
||||
)
|
||||
|
||||
def prompt_execute_code(self):
|
||||
print(f"🤖 {t('What do you need for development?')}")
|
||||
# help_info = """Commande :
|
||||
|
|
|
|||
|
|
@ -722,6 +722,95 @@ TRANSLATIONS = {
|
|||
"fr": "Erreur lors de l'ajout de l'automatisation : ",
|
||||
"en": "Error adding automation: ",
|
||||
},
|
||||
# SSH Deploy section
|
||||
"Deploy - Deploy ERPLibre via SSH": {
|
||||
"fr": "Déploiement - Déployer ERPLibre via SSH",
|
||||
"en": "Deploy - Deploy ERPLibre via SSH",
|
||||
},
|
||||
"SSH deployment tools!": {
|
||||
"fr": "Outils de déploiement SSH!",
|
||||
"en": "SSH deployment tools!",
|
||||
},
|
||||
"SSH - Check connection": {
|
||||
"fr": "SSH - Vérifier la connexion",
|
||||
"en": "SSH - Check connection",
|
||||
},
|
||||
"SSH - Sync files (rsync)": {
|
||||
"fr": "SSH - Synchroniser les fichiers (rsync)",
|
||||
"en": "SSH - Sync files (rsync)",
|
||||
},
|
||||
"SSH - Install ERPLibre": {
|
||||
"fr": "SSH - Installer ERPLibre",
|
||||
"en": "SSH - Install ERPLibre",
|
||||
},
|
||||
"SSH - Start Odoo": {
|
||||
"fr": "SSH - Démarrer Odoo",
|
||||
"en": "SSH - Start Odoo",
|
||||
},
|
||||
"SSH - Stop Odoo": {
|
||||
"fr": "SSH - Arrêter Odoo",
|
||||
"en": "SSH - Stop Odoo",
|
||||
},
|
||||
"SSH - Restart Odoo": {
|
||||
"fr": "SSH - Redémarrer Odoo",
|
||||
"en": "SSH - Restart Odoo",
|
||||
},
|
||||
"SSH - Service status": {
|
||||
"fr": "SSH - Statut du service",
|
||||
"en": "SSH - Service status",
|
||||
},
|
||||
"SSH - View logs": {
|
||||
"fr": "SSH - Voir les logs",
|
||||
"en": "SSH - View logs",
|
||||
},
|
||||
"SSH - Run make target": {
|
||||
"fr": "SSH - Exécuter une cible make",
|
||||
"en": "SSH - Run make target",
|
||||
},
|
||||
"SSH - Install systemd service": {
|
||||
"fr": "SSH - Installer le service systemd",
|
||||
"en": "SSH - Install systemd service",
|
||||
},
|
||||
"SSH - Configure nginx + SSL": {
|
||||
"fr": "SSH - Configurer nginx + SSL",
|
||||
"en": "SSH - Configure nginx + SSL",
|
||||
},
|
||||
"Remote host (user@hostname or hostname): ": {
|
||||
"fr": "Hôte distant (user@hostname ou hostname) : ",
|
||||
"en": "Remote host (user@hostname or hostname): ",
|
||||
},
|
||||
"SSH user (default: erplibre): ": {
|
||||
"fr": "Utilisateur SSH (défaut: erplibre) : ",
|
||||
"en": "SSH user (default: erplibre): ",
|
||||
},
|
||||
"SSH port (default: 22): ": {
|
||||
"fr": "Port SSH (défaut: 22) : ",
|
||||
"en": "SSH port (default: 22): ",
|
||||
},
|
||||
"SSH key path (default: ~/.ssh/id_rsa, empty for none): ": {
|
||||
"fr": "Chemin de la clé SSH (défaut: ~/.ssh/id_rsa, vide pour aucune) : ",
|
||||
"en": "SSH key path (default: ~/.ssh/id_rsa, empty for none): ",
|
||||
},
|
||||
"Remote path (default: ~/erplibre_deploy_2): ": {
|
||||
"fr": "Chemin distant (défaut: ~/erplibre_deploy_2) : ",
|
||||
"en": "Remote path (default: ~/erplibre_deploy_2): ",
|
||||
},
|
||||
"Make target to run remotely: ": {
|
||||
"fr": "Cible make à exécuter à distance : ",
|
||||
"en": "Make target to run remotely: ",
|
||||
},
|
||||
"Domain name (e.g.: example.com): ": {
|
||||
"fr": "Nom de domaine (ex: example.com) : ",
|
||||
"en": "Domain name (e.g.: example.com): ",
|
||||
},
|
||||
"Admin email for SSL certificate: ": {
|
||||
"fr": "Email administrateur pour le certificat SSL : ",
|
||||
"en": "Admin email for SSL certificate: ",
|
||||
},
|
||||
"SSH host is required!": {
|
||||
"fr": "L'hôte SSH est requis!",
|
||||
"en": "SSH host is required!",
|
||||
},
|
||||
# Language selection
|
||||
"Choose language / Choisir la langue": {
|
||||
"fr": "Choisir la langue / Choose language",
|
||||
|
|
|
|||
Loading…
Reference in a new issue