254 lines
7.6 KiB
Python
254 lines
7.6 KiB
Python
#!/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()
|