From e7df09d72e3e42e20b3d39f880ce7fec4e057b8a Mon Sep 17 00:00:00 2001 From: jeanotx32 Date: Mon, 18 May 2026 21:59:59 -0400 Subject: [PATCH] Add Docker application manager script for Debian VPS --- manage_vps.py | 253 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 253 insertions(+) create mode 100644 manage_vps.py diff --git a/manage_vps.py b/manage_vps.py new file mode 100644 index 0000000..fd9b01e --- /dev/null +++ b/manage_vps.py @@ -0,0 +1,253 @@ +#!/usr/bin/env python3 +""" +Gestionnaire d'applications Docker sur VPS Debian. +Autodétecte les applications sous /home/*/docker-compose.yml +""" + +import os +import subprocess +import sys +from pathlib import Path + +APPS_BASE_DIR = "/home" +COMPOSE_FILES = ["docker-compose.yml", "docker-compose.yaml"] + + +def find_docker_apps() -> list[dict]: + """Détecte les applications Docker sous /home/[nom_app]/.""" + apps = [] + base = Path(APPS_BASE_DIR) + + for entry in sorted(base.iterdir()): + if not entry.is_dir(): + continue + for compose_file in COMPOSE_FILES: + compose_path = entry / compose_file + if compose_path.exists(): + apps.append({ + "name": entry.name, + "path": str(entry), + "compose_file": str(compose_path), + }) + break + + return apps + + +def get_running_containers(app: dict) -> list[str]: + """Retourne les noms des conteneurs en cours d'exécution pour une app.""" + try: + result = subprocess.run( + ["docker", "compose", "-f", app["compose_file"], "ps", "--services", "--filter", "status=running"], + capture_output=True, + text=True, + cwd=app["path"], + ) + services = [s.strip() for s in result.stdout.splitlines() if s.strip()] + return services + except FileNotFoundError: + return [] + + +def get_container_id(app: dict, service: str) -> str | None: + """Retourne l'ID du conteneur Docker pour un service donné.""" + try: + result = subprocess.run( + ["docker", "compose", "-f", app["compose_file"], "ps", "-q", service], + capture_output=True, + text=True, + cwd=app["path"], + ) + container_id = result.stdout.strip() + return container_id if container_id else None + except FileNotFoundError: + return None + + +def print_separator(): + print("-" * 50) + + +def print_header(): + print("=" * 50) + print(" Gestionnaire Docker VPS") + print("=" * 50) + + +def select_from_list(items: list[str], prompt: str) -> int | None: + """Affiche une liste numérotée et retourne l'index choisi, ou None si annulé.""" + for i, item in enumerate(items, 1): + print(f" [{i}] {item}") + print(" [0] Retour") + print() + while True: + try: + choice = input(f"{prompt}: ").strip() + if choice == "0": + return None + idx = int(choice) - 1 + if 0 <= idx < len(items): + return idx + print(f" Choix invalide (1-{len(items)} ou 0 pour retour).") + except (ValueError, EOFError): + print(" Entrée invalide.") + + +def action_bash(app: dict, service: str): + """Ouvre un shell bash interactif dans le conteneur.""" + container_id = get_container_id(app, service) + if not container_id: + print(f" Conteneur '{service}' introuvable ou arrêté.") + input(" Appuyez sur Entrée pour continuer...") + return + + print(f"\n Connexion bash → {app['name']} / {service}") + print(" (tapez 'exit' pour revenir au menu)\n") + subprocess.run(["docker", "exec", "-it", container_id, "bash"]) + + +def action_logs(app: dict, service: str): + """Affiche les logs du conteneur (100 dernières lignes, avec suivi).""" + container_id = get_container_id(app, service) + if not container_id: + print(f" Conteneur '{service}' introuvable ou arrêté.") + input(" Appuyez sur Entrée pour continuer...") + return + + print(f"\n Logs → {app['name']} / {service}") + print(" (Ctrl+C pour arrêter le suivi)\n") + try: + subprocess.run(["docker", "logs", "--tail", "100", "-f", container_id]) + except KeyboardInterrupt: + print("\n Suivi des logs interrompu.") + + +def action_update(app: dict): + """Met à jour l'application avec docker compose pull puis up -d.""" + print(f"\n Mise à jour de '{app['name']}'...") + print_separator() + + print(" → docker compose pull") + result_pull = subprocess.run( + ["docker", "compose", "-f", app["compose_file"], "pull"], + cwd=app["path"], + ) + + if result_pull.returncode != 0: + print(" Erreur lors du pull.") + input(" Appuyez sur Entrée pour continuer...") + return + + print("\n → docker compose up -d") + result_up = subprocess.run( + ["docker", "compose", "-f", app["compose_file"], "up", "-d"], + cwd=app["path"], + ) + + if result_up.returncode == 0: + print("\n Mise à jour terminée avec succès.") + else: + print("\n Erreur lors du redémarrage des conteneurs.") + + input("\n Appuyez sur Entrée pour continuer...") + + +def app_menu(app: dict): + """Menu des actions disponibles pour une application.""" + while True: + os.system("clear") + print_header() + print(f" Application : {app['name']}") + print(f" Répertoire : {app['path']}") + print_separator() + + running = get_running_containers(app) + if running: + print(f" Services actifs : {', '.join(running)}") + else: + print(" Aucun service actif détecté.") + print() + + print(" [1] Ouvrir un shell bash dans un conteneur") + print(" [2] Voir les logs d'un conteneur") + print(" [3] Mettre à jour (docker compose pull + up -d)") + print(" [0] Retour") + print() + + choice = input(" Votre choix : ").strip() + + if choice == "0": + break + elif choice in ("1", "2"): + os.system("clear") + print_header() + label = "shell bash" if choice == "1" else "logs" + print(f" Choisir le service ({label}) :") + print_separator() + + all_services = get_all_services(app) + if not all_services: + print(" Aucun service trouvé dans le compose.") + input(" Appuyez sur Entrée pour continuer...") + continue + + idx = select_from_list(all_services, " Service") + if idx is None: + continue + + service = all_services[idx] + if choice == "1": + action_bash(app, service) + else: + action_logs(app, service) + + elif choice == "3": + action_update(app) + else: + print(" Choix invalide.") + + +def get_all_services(app: dict) -> list[str]: + """Retourne tous les services définis dans le docker-compose (actifs ou non).""" + try: + result = subprocess.run( + ["docker", "compose", "-f", app["compose_file"], "config", "--services"], + capture_output=True, + text=True, + cwd=app["path"], + ) + services = [s.strip() for s in result.stdout.splitlines() if s.strip()] + return services + except FileNotFoundError: + return [] + + +def main(): + if os.geteuid() != 0: + print("Attention : ce script nécessite des droits root pour interagir avec Docker.") + print("Relancez avec : sudo python3 manage_vps.py\n") + + while True: + os.system("clear") + print_header() + + apps = find_docker_apps() + + if not apps: + print(f" Aucune application Docker trouvée sous {APPS_BASE_DIR}/") + print(" (recherche de docker-compose.yml dans chaque sous-dossier)") + sys.exit(0) + + print(f" {len(apps)} application(s) détectée(s) :\n") + app_names = [a["name"] for a in apps] + idx = select_from_list(app_names, " Choisir une application") + + if idx is None: + print("\n Au revoir !") + sys.exit(0) + + app_menu(apps[idx]) + + +if __name__ == "__main__": + main()