Add Docker application manager script for Debian VPS
This commit is contained in:
253
manage_vps.py
Normal file
253
manage_vps.py
Normal file
@@ -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()
|
||||
Reference in New Issue
Block a user