diff --git a/Makefile b/Makefile index fc9920f..465190a 100644 --- a/Makefile +++ b/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 diff --git a/conf/make.ssh.Makefile b/conf/make.ssh.Makefile new file mode 100644 index 0000000..c899fe9 --- /dev/null +++ b/conf/make.ssh.Makefile @@ -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),)" diff --git a/script/todo/todo.py b/script/todo/todo.py index 771b513..1866f13 100755 --- a/script/todo/todo.py +++ b/script/todo/todo.py @@ -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 : diff --git a/script/todo/todo_i18n.py b/script/todo/todo_i18n.py index 6867282..a09e17c 100644 --- a/script/todo/todo_i18n.py +++ b/script/todo/todo_i18n.py @@ -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",