#!/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()