Some checks failed
Build and Push Docker Images / docker (push) Failing after 5s
- Create systemd service for VPS Monitor agent. - Add FastAPI backend with endpoints for managing VPS configurations and statuses. - Implement Dockerfile for backend service with required dependencies. - Create frontend using React with Vite and Tailwind CSS for styling. - Add API client for communicating with the backend. - Implement components for displaying VPS information and logs. - Set up Docker Compose for orchestrating backend and frontend services. - Add environment configuration files for backend and agent. - Implement CORS support in the backend for frontend communication.
739 lines
24 KiB
Python
739 lines
24 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
Gestionnaire d'applications Docker sur VPS Debian.
|
||
Navigation au clavier (↑↓ + Entrée), sans dépendance externe.
|
||
"""
|
||
|
||
import os
|
||
import subprocess
|
||
import sys
|
||
import termios
|
||
import tty
|
||
from pathlib import Path
|
||
|
||
# ─── Configuration ────────────────────────────────────────────────────────────
|
||
|
||
APPS_BASE_DIR = "/home"
|
||
COMPOSE_FILES = ["compose.yml", "compose.yaml", "docker-compose.yml", "docker-compose.yaml"]
|
||
|
||
# ─── ANSI ─────────────────────────────────────────────────────────────────────
|
||
|
||
R = "\033[0m"
|
||
BOLD = "\033[1m"
|
||
DIM = "\033[2m"
|
||
CYN = "\033[36m"
|
||
YEL = "\033[33m"
|
||
GRN = "\033[32m"
|
||
RED = "\033[31m"
|
||
SEL = "\033[44m\033[97m" # fond bleu + texte blanc
|
||
|
||
W = 56 # largeur de la boîte
|
||
|
||
# ─── Gestion du clavier ───────────────────────────────────────────────────────
|
||
|
||
KEY_UP = "\x1b[A"
|
||
KEY_DOWN = "\x1b[B"
|
||
KEY_ENTER = ("\r", "\n")
|
||
KEY_QUIT = ("q", "Q", "\x1b", "\x03") # q, ESC, Ctrl+C
|
||
|
||
|
||
def getch() -> str:
|
||
"""Lit un seul appui clavier sans attendre Entrée."""
|
||
fd = sys.stdin.fileno()
|
||
old = termios.tcgetattr(fd)
|
||
try:
|
||
tty.setraw(fd)
|
||
ch = sys.stdin.read(1)
|
||
if ch == "\x1b":
|
||
nxt = sys.stdin.read(1)
|
||
if nxt == "[":
|
||
return "\x1b[" + sys.stdin.read(1)
|
||
return "\x1b"
|
||
return ch
|
||
finally:
|
||
termios.tcsetattr(fd, termios.TCSADRAIN, old)
|
||
|
||
|
||
def hide_cursor() -> None:
|
||
print("\033[?25l", end="", flush=True)
|
||
|
||
|
||
def show_cursor() -> None:
|
||
print("\033[?25h", end="", flush=True)
|
||
|
||
|
||
# ─── Rendu ────────────────────────────────────────────────────────────────────
|
||
|
||
def draw_header(title: str, subtitle: str = "") -> None:
|
||
print(f"{CYN}╔{'═' * (W - 2)}╗{R}")
|
||
print(f"{CYN}║{R}{BOLD}{title.center(W - 2)}{R}{CYN}║{R}")
|
||
print(f"{CYN}╠{'═' * (W - 2)}╣{R}")
|
||
if subtitle:
|
||
pad = W - 2 - len(subtitle)
|
||
print(f"{CYN}║{R} {YEL}{subtitle}{R}{' ' * max(0, pad - 1)}{CYN}║{R}")
|
||
print(f"{CYN}╠{'─' * (W - 2)}╣{R}")
|
||
|
||
|
||
def draw_footer() -> None:
|
||
print(f"{CYN}╠{'─' * (W - 2)}╣{R}")
|
||
hint = f" {YEL}↑↓{R} naviguer {YEL}↵{R} sélectionner {YEL}q{R} retour"
|
||
print(f"{CYN}║{R}{hint}")
|
||
print(f"{CYN}╚{'═' * (W - 2)}╝{R}")
|
||
|
||
|
||
def draw_item(index: int, label: str, selected: bool, badge: str = "") -> None:
|
||
num = f"[{index + 1}]"
|
||
badge_str = f" {GRN}({badge}){R}" if badge else ""
|
||
text = f" {num} {label}"
|
||
visible_len = len(text) + (len(badge) + 3 if badge else 0)
|
||
padding = " " * max(0, W - 4 - visible_len)
|
||
if selected:
|
||
print(f"{CYN}║{R}{SEL} › {text}{R}{GRN if badge else ''}{badge_str}{R}{SEL}{padding} {R}{CYN}║{R}")
|
||
else:
|
||
print(f"{CYN}║{R} {text}{badge_str}{padding} {CYN}║{R}")
|
||
|
||
|
||
# ─── Menu générique ───────────────────────────────────────────────────────────
|
||
|
||
def navigate_menu(
|
||
title: str,
|
||
items: list[str],
|
||
subtitle: str = "",
|
||
badges: list[str] | None = None,
|
||
) -> int | None:
|
||
"""
|
||
Menu interactif à navigation clavier (↑↓ + Entrée).
|
||
Retourne l'index sélectionné, ou None si l'utilisateur quitte.
|
||
"""
|
||
current = 0
|
||
badges = badges or [""] * len(items)
|
||
|
||
while True:
|
||
os.system("clear")
|
||
hide_cursor()
|
||
draw_header(title, subtitle)
|
||
for i, item in enumerate(items):
|
||
draw_item(i, item, i == current, badges[i])
|
||
draw_footer()
|
||
|
||
key = getch()
|
||
|
||
if key == KEY_UP:
|
||
current = (current - 1) % len(items)
|
||
elif key == KEY_DOWN:
|
||
current = (current + 1) % len(items)
|
||
elif key in KEY_ENTER:
|
||
show_cursor()
|
||
return current
|
||
elif key in KEY_QUIT:
|
||
show_cursor()
|
||
return None
|
||
elif key.isdigit():
|
||
idx = int(key) - 1
|
||
if 0 <= idx < len(items):
|
||
show_cursor()
|
||
return idx
|
||
|
||
|
||
def pause(message: str = "Appuyez sur une touche pour continuer…") -> None:
|
||
show_cursor()
|
||
print(f"\n {DIM}{message}{R}", end="", flush=True)
|
||
getch()
|
||
|
||
|
||
# ─── Détection Docker ─────────────────────────────────────────────────────────
|
||
|
||
def _apps_from_docker_ps() -> list[dict]:
|
||
"""Détecte les projets Compose via les labels des conteneurs docker ps -a."""
|
||
apps = []
|
||
seen_dirs: set[str] = set()
|
||
try:
|
||
result = subprocess.run(
|
||
[
|
||
"docker", "ps", "-a",
|
||
"--format",
|
||
"{{.Label \"com.docker.compose.project\"}}\t"
|
||
"{{.Label \"com.docker.compose.project.working_dir\"}}\t"
|
||
"{{.Label \"com.docker.compose.project.config_files\"}}",
|
||
],
|
||
capture_output=True,
|
||
text=True,
|
||
)
|
||
except FileNotFoundError:
|
||
return []
|
||
|
||
for line in result.stdout.splitlines():
|
||
parts = line.split("\t")
|
||
if len(parts) != 3:
|
||
continue
|
||
project, working_dir, config_files = (p.strip() for p in parts)
|
||
if not project or not working_dir or working_dir in seen_dirs:
|
||
continue
|
||
compose_file = config_files.split(",")[0].strip() if config_files else ""
|
||
if not compose_file or not Path(compose_file).exists():
|
||
compose_file = ""
|
||
for name in COMPOSE_FILES:
|
||
candidate = Path(working_dir) / name
|
||
if candidate.exists():
|
||
compose_file = str(candidate)
|
||
break
|
||
if not compose_file:
|
||
continue
|
||
seen_dirs.add(working_dir)
|
||
apps.append({"name": project, "path": working_dir, "compose_file": compose_file})
|
||
|
||
return apps
|
||
|
||
|
||
def _apps_from_filesystem(exclude_dirs: set) -> list[dict]:
|
||
"""Fallback : scan récursif /home/*/ (profondeur 3) pour trouver des fichiers compose."""
|
||
apps = []
|
||
base = Path(APPS_BASE_DIR)
|
||
|
||
def search(directory: Path, app_name: str, depth: int) -> None:
|
||
if depth == 0:
|
||
return
|
||
try:
|
||
for compose_name in COMPOSE_FILES:
|
||
compose_path = directory / compose_name
|
||
if compose_path.exists() and str(directory) not in exclude_dirs:
|
||
rel = directory.relative_to(base / app_name)
|
||
label = app_name if rel == Path(".") else f"{app_name}/{rel}"
|
||
apps.append({"name": label, "path": str(directory), "compose_file": str(compose_path)})
|
||
exclude_dirs.add(str(directory))
|
||
return
|
||
for sub in sorted(directory.iterdir()):
|
||
if sub.is_dir():
|
||
search(sub, app_name, depth - 1)
|
||
except PermissionError:
|
||
pass
|
||
|
||
try:
|
||
for entry in sorted(base.iterdir()):
|
||
if entry.is_dir():
|
||
search(entry, entry.name, depth=3)
|
||
except PermissionError:
|
||
show_cursor()
|
||
print(f"Erreur : permission refusée pour lire {APPS_BASE_DIR}")
|
||
sys.exit(1)
|
||
|
||
return apps
|
||
|
||
|
||
def find_docker_apps() -> list[dict]:
|
||
"""Détecte les apps Compose via docker ps (labels) puis complète avec le filesystem."""
|
||
apps = _apps_from_docker_ps()
|
||
seen = {a["path"] for a in apps}
|
||
apps += _apps_from_filesystem(seen)
|
||
return sorted(apps, key=lambda a: a["name"].lower())
|
||
|
||
|
||
# ─── Actions Docker ───────────────────────────────────────────────────────────
|
||
|
||
def get_running_containers(app: dict) -> list[str]:
|
||
try:
|
||
result = subprocess.run(
|
||
["docker", "compose", "-f", app["compose_file"], "ps", "--services", "--filter", "status=running"],
|
||
capture_output=True, text=True, cwd=app["path"],
|
||
)
|
||
return [s.strip() for s in result.stdout.splitlines() if s.strip()]
|
||
except FileNotFoundError:
|
||
return []
|
||
|
||
|
||
def get_all_services(app: dict) -> list[str]:
|
||
try:
|
||
result = subprocess.run(
|
||
["docker", "compose", "-f", app["compose_file"], "config", "--services"],
|
||
capture_output=True, text=True, cwd=app["path"],
|
||
)
|
||
return [s.strip() for s in result.stdout.splitlines() if s.strip()]
|
||
except FileNotFoundError:
|
||
return []
|
||
|
||
|
||
def get_container_id(app: dict, service: str) -> str | None:
|
||
try:
|
||
result = subprocess.run(
|
||
["docker", "compose", "-f", app["compose_file"], "ps", "-q", service],
|
||
capture_output=True, text=True, cwd=app["path"],
|
||
)
|
||
cid = result.stdout.strip()
|
||
return cid if cid else None
|
||
except FileNotFoundError:
|
||
return None
|
||
|
||
|
||
def action_bash(app: dict, service: str) -> None:
|
||
container_id = get_container_id(app, service)
|
||
if not container_id:
|
||
print(f"\n {RED}Conteneur '{service}' introuvable ou arrêté.{R}")
|
||
pause()
|
||
return
|
||
show_cursor()
|
||
os.system("clear")
|
||
print(f"{CYN}{'─' * W}{R}")
|
||
print(f" Shell bash → {BOLD}{app['name']}{R} / {service}")
|
||
print(f" {DIM}(tapez 'exit' pour revenir au menu){R}")
|
||
print(f"{CYN}{'─' * W}{R}\n")
|
||
subprocess.run(["docker", "exec", "-it", container_id, "bash"])
|
||
|
||
|
||
def action_logs(app: dict, service: str) -> None:
|
||
container_id = get_container_id(app, service)
|
||
if not container_id:
|
||
print(f"\n {RED}Conteneur '{service}' introuvable ou arrêté.{R}")
|
||
pause()
|
||
return
|
||
show_cursor()
|
||
os.system("clear")
|
||
print(f"{CYN}{'─' * W}{R}")
|
||
print(f" Logs → {BOLD}{app['name']}{R} / {service}")
|
||
print(f" {DIM}(Ctrl+C pour arrêter){R}")
|
||
print(f"{CYN}{'─' * W}{R}\n")
|
||
try:
|
||
subprocess.run(["docker", "logs", "--tail", "100", "-f", container_id])
|
||
except KeyboardInterrupt:
|
||
print(f"\n {YEL}Suivi interrompu.{R}")
|
||
pause()
|
||
|
||
|
||
def action_update(app: dict) -> None:
|
||
show_cursor()
|
||
os.system("clear")
|
||
print(f"{CYN}{'─' * W}{R}")
|
||
print(f" Mise à jour → {BOLD}{app['name']}{R}")
|
||
print(f"{CYN}{'─' * W}{R}\n")
|
||
|
||
print(f" {YEL}→ docker compose pull{R}\n")
|
||
result_pull = subprocess.run(
|
||
["docker", "compose", "-f", app["compose_file"], "pull"],
|
||
cwd=app["path"],
|
||
)
|
||
if result_pull.returncode != 0:
|
||
print(f"\n {RED}Erreur lors du pull.{R}")
|
||
pause()
|
||
return
|
||
|
||
print(f"\n {YEL}→ docker compose up -d{R}\n")
|
||
result_up = subprocess.run(
|
||
["docker", "compose", "-f", app["compose_file"], "up", "-d"],
|
||
cwd=app["path"],
|
||
)
|
||
if result_up.returncode == 0:
|
||
print(f"\n {GRN}Mise à jour terminée avec succès.{R}")
|
||
else:
|
||
print(f"\n {RED}Erreur lors du redémarrage.{R}")
|
||
pause()
|
||
|
||
|
||
# ─── Menus ────────────────────────────────────────────────────────────────────
|
||
|
||
def service_menu(app: dict, action: str) -> None:
|
||
"""Sélection d'un service puis exécution de l'action (bash ou logs)."""
|
||
services = get_all_services(app)
|
||
if not services:
|
||
print(f"\n {RED}Aucun service trouvé dans le compose.{R}")
|
||
pause()
|
||
return
|
||
|
||
running = set(get_running_containers(app))
|
||
badges = ["actif" if s in running else "" for s in services]
|
||
label = "Shell bash" if action == "bash" else "Logs"
|
||
|
||
idx = navigate_menu(
|
||
title=" Gestionnaire Docker VPS ",
|
||
subtitle=f"{app['name']} › {label}",
|
||
items=services,
|
||
badges=badges,
|
||
)
|
||
if idx is None:
|
||
return
|
||
|
||
if action == "bash":
|
||
action_bash(app, services[idx])
|
||
else:
|
||
action_logs(app, services[idx])
|
||
|
||
|
||
def app_menu(app: dict) -> None:
|
||
"""Menu des actions pour une application."""
|
||
actions = [
|
||
"Shell bash dans un conteneur",
|
||
"Voir les logs d'un conteneur",
|
||
"Mettre à jour (pull + up -d)",
|
||
]
|
||
|
||
while True:
|
||
running = get_running_containers(app)
|
||
if running:
|
||
status = f"{len(running)} service(s) actif(s)"
|
||
else:
|
||
status = "aucun service actif"
|
||
|
||
idx = navigate_menu(
|
||
title=" Gestionnaire Docker VPS ",
|
||
subtitle=f"{app['name']} · {status}",
|
||
items=actions,
|
||
)
|
||
|
||
if idx is None:
|
||
return
|
||
elif idx == 0:
|
||
service_menu(app, "bash")
|
||
elif idx == 1:
|
||
service_menu(app, "logs")
|
||
elif idx == 2:
|
||
action_update(app)
|
||
|
||
|
||
def main() -> None:
|
||
if os.geteuid() != 0:
|
||
print(f"{YEL}Attention : droits root recommandés (sudo python3 manage_vps.py){R}\n")
|
||
|
||
try:
|
||
while True:
|
||
apps = find_docker_apps()
|
||
|
||
if not apps:
|
||
os.system("clear")
|
||
print(f"\n {RED}Aucune application Docker trouvée sous {APPS_BASE_DIR}/{R}")
|
||
print(f" {DIM}(vérifiez que des fichiers compose.yml existent){R}\n")
|
||
sys.exit(0)
|
||
|
||
idx = navigate_menu(
|
||
title=" Gestionnaire Docker VPS ",
|
||
subtitle=f"{len(apps)} application(s) détectée(s)",
|
||
items=[a["name"] for a in apps],
|
||
)
|
||
|
||
if idx is None:
|
||
os.system("clear")
|
||
print(f"\n Au revoir !\n")
|
||
sys.exit(0)
|
||
|
||
app_menu(apps[idx])
|
||
|
||
except KeyboardInterrupt:
|
||
show_cursor()
|
||
os.system("clear")
|
||
print("\n Au revoir !\n")
|
||
sys.exit(0)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|
||
|
||
|
||
|
||
def _apps_from_docker_ps() -> list[dict]:
|
||
"""Détecte les projets Compose via les labels des conteneurs docker ps -a."""
|
||
apps = []
|
||
seen_dirs = set()
|
||
try:
|
||
result = subprocess.run(
|
||
[
|
||
"docker", "ps", "-a",
|
||
"--format",
|
||
"{{.Label \"com.docker.compose.project\"}}\t"
|
||
"{{.Label \"com.docker.compose.project.working_dir\"}}\t"
|
||
"{{.Label \"com.docker.compose.project.config_files\"}}",
|
||
],
|
||
capture_output=True,
|
||
text=True,
|
||
)
|
||
except FileNotFoundError:
|
||
return [] # docker non installé
|
||
|
||
for line in result.stdout.splitlines():
|
||
parts = line.split("\t")
|
||
if len(parts) != 3:
|
||
continue
|
||
project, working_dir, config_files = (p.strip() for p in parts)
|
||
if not project or not working_dir or working_dir in seen_dirs:
|
||
continue
|
||
# Prend le premier fichier de config listé
|
||
compose_file = config_files.split(",")[0].strip() if config_files else ""
|
||
if not compose_file or not Path(compose_file).exists():
|
||
# Cherche un fichier compose connu dans le working_dir
|
||
compose_file = ""
|
||
for name in COMPOSE_FILES:
|
||
candidate = Path(working_dir) / name
|
||
if candidate.exists():
|
||
compose_file = str(candidate)
|
||
break
|
||
if not compose_file:
|
||
continue
|
||
seen_dirs.add(working_dir)
|
||
apps.append({
|
||
"name": project,
|
||
"path": working_dir,
|
||
"compose_file": compose_file,
|
||
})
|
||
return apps
|
||
|
||
|
||
def _apps_from_filesystem(exclude_dirs: set) -> list[dict]:
|
||
"""Fallback : scanne /home/*/ récursivement (profondeur 3) pour trouver des fichiers compose."""
|
||
apps = []
|
||
base = Path(APPS_BASE_DIR)
|
||
|
||
def search(directory: Path, app_name: str, depth: int):
|
||
if depth == 0:
|
||
return
|
||
try:
|
||
for compose_name in COMPOSE_FILES:
|
||
compose_path = directory / compose_name
|
||
if compose_path.exists() and str(directory) not in exclude_dirs:
|
||
rel = directory.relative_to(base / app_name)
|
||
label = app_name if rel == Path(".") else f"{app_name}/{rel}"
|
||
apps.append({
|
||
"name": label,
|
||
"path": str(directory),
|
||
"compose_file": str(compose_path),
|
||
})
|
||
exclude_dirs.add(str(directory))
|
||
return
|
||
for sub in sorted(directory.iterdir()):
|
||
if sub.is_dir():
|
||
search(sub, app_name, depth - 1)
|
||
except PermissionError:
|
||
pass
|
||
|
||
try:
|
||
for entry in sorted(base.iterdir()):
|
||
if entry.is_dir():
|
||
search(entry, entry.name, depth=3)
|
||
except PermissionError:
|
||
print(f" Erreur : permission refusée pour lire {APPS_BASE_DIR}")
|
||
sys.exit(1)
|
||
|
||
return apps
|
||
|
||
|
||
def find_docker_apps() -> list[dict]:
|
||
"""Détecte les apps Compose via docker ps (labels) puis complète avec le filesystem."""
|
||
apps = _apps_from_docker_ps()
|
||
seen_dirs = {a["path"] for a in apps}
|
||
apps += _apps_from_filesystem(seen_dirs)
|
||
return sorted(apps, key=lambda a: a["name"].lower())
|
||
|
||
|
||
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()
|