homelab_automation/app/factory.py
Bruno Charest 05087aa380
Some checks failed
Tests / Backend Tests (Python) (3.10) (push) Has been cancelled
Tests / Backend Tests (Python) (3.11) (push) Has been cancelled
Tests / Backend Tests (Python) (3.12) (push) Has been cancelled
Tests / Frontend Tests (JS) (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / All Tests Passed (push) Has been cancelled
Replace manual upsert logic with SQLite native upsert in Docker CRUD repositories, enhance Ansible backup playbook with better error handling and file permissions, add favicon endpoint, and improve playbook editor UI with syntax highlighting, lint integration, quality badges, and enhanced code editing features
2025-12-17 15:36:49 -05:00

250 lines
9.0 KiB
Python

"""
Factory pour créer l'application FastAPI.
Ce module contient la fonction create_app() qui configure et retourne
une instance FastAPI prête à l'emploi.
"""
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from fastapi.responses import HTMLResponse
from fastapi.responses import FileResponse
from app.core.config import settings
from app.models.database import init_db, async_session_maker
from app.routes import api_router
from app.routes.websocket import router as ws_router
def create_app() -> FastAPI:
"""Crée et configure l'application FastAPI.
Returns:
Instance FastAPI configurée avec tous les routers et middleware
"""
app = FastAPI(
title=settings.api_title,
description=settings.api_description,
version=settings.api_version,
docs_url="/docs",
redoc_url="/redoc",
)
# Configuration CORS
app.add_middleware(
CORSMiddleware,
allow_origins=settings.cors_origins,
allow_credentials=settings.cors_allow_credentials,
allow_methods=settings.cors_allow_methods,
allow_headers=settings.cors_allow_headers,
)
# Monter les fichiers statiques (main.js, etc.)
app.mount("/static", StaticFiles(directory=settings.base_dir, html=False), name="static")
# Inclure les routers API
app.include_router(api_router, prefix="/api")
# Inclure le router WebSocket (sans préfixe /api)
app.include_router(ws_router)
# Routes racine
@app.get("/", response_class=HTMLResponse)
async def root():
"""Page d'accueil - redirige vers le dashboard."""
# Essayer de servir index.html
index_path = settings.base_dir / "index.html"
if index_path.exists():
return index_path.read_text(encoding='utf-8')
return """
<!DOCTYPE html>
<html>
<head>
<title>Homelab Automation</title>
<meta http-equiv="refresh" content="0; url=/docs">
</head>
<body>
<p>Redirecting to <a href="/docs">API Documentation</a>...</p>
</body>
</html>
"""
@app.get("/favicon.ico", include_in_schema=False)
async def favicon():
favicon_path = settings.base_dir / "static" / "favicon.ico"
if favicon_path.exists():
return FileResponse(favicon_path)
return FileResponse(settings.base_dir / "favicon.ico")
@app.get("/api", response_class=HTMLResponse)
async def api_home():
"""Page d'accueil de l'API."""
return """
<!DOCTYPE html>
<html>
<head>
<title>Homelab Automation API</title>
<style>
body { font-family: system-ui, sans-serif; max-width: 800px; margin: 50px auto; padding: 20px; }
h1 { color: #1a1a2e; }
.links { margin-top: 30px; }
.links a { display: inline-block; margin-right: 20px; padding: 10px 20px;
background: #7c3aed; color: white; text-decoration: none; border-radius: 5px; }
.links a:hover { background: #6d28d9; }
</style>
</head>
<body>
<h1>🏠 Homelab Automation API</h1>
<p>API REST pour la gestion automatisée de votre homelab avec Ansible.</p>
<div class="links">
<a href="/docs">📚 Documentation Swagger</a>
<a href="/redoc">📖 Documentation ReDoc</a>
<a href="/">🖥️ Dashboard</a>
</div>
</body>
</html>
"""
# Événements de démarrage et d'arrêt
@app.on_event("startup")
async def startup_event():
"""Événement de démarrage de l'application."""
# Démarrer la capture des logs console en premier
from app.services import console_log_service
console_log_service.start_capture()
print("🚀 Homelab Automation Dashboard démarré")
# Afficher les paramètres d'environnement
print("\n📋 Configuration:")
print(f" BASE_DIR: {settings.base_dir}")
print(f" ANSIBLE_DIR: {settings.ansible_dir}")
print(f" TASKS_LOGS_DIR: {settings.tasks_logs_dir}")
print(f" DATABASE_URL: {settings.async_database_url}")
print(f" SSH_KEY_PATH: {settings.ssh_key_path}")
print(f" SSH_USER: {settings.ssh_user}")
print(f" SSH_REMOTE_USER: {settings.ssh_remote_user}")
print(f" API_KEY: {'*' * 8}...{settings.api_key[-4:] if len(settings.api_key) > 4 else '****'}")
print(f" NTFY_ENABLED: {settings.ntfy_enabled}")
print(f" NTFY_BASE_URL: {settings.ntfy_base_url}")
print(f" NTFY_TOPIC: {settings.ntfy_default_topic}")
print()
# Validation des chemins critiques
validation_ok = True
if not settings.ansible_dir.exists():
print(f"⚠️ ANSIBLE_DIR n'existe pas: {settings.ansible_dir}")
validation_ok = False
else:
print(f"✅ ANSIBLE_DIR OK: {settings.ansible_dir}")
inventory_path = settings.ansible_dir / "inventory" / "hosts.yml"
if not inventory_path.exists():
print(f"⚠️ Inventaire Ansible non trouvé: {inventory_path}")
else:
print(f"✅ Inventaire Ansible OK: {inventory_path}")
playbooks_dir = settings.ansible_dir / "playbooks"
if not playbooks_dir.exists():
print(f"⚠️ Dossier playbooks non trouvé: {playbooks_dir}")
else:
playbook_count = len(list(playbooks_dir.glob("*.yml")))
print(f"✅ Dossier playbooks OK: {playbook_count} playbook(s) trouvé(s)")
print()
# Initialiser la base de données
await init_db()
print("📦 Base de données SQLite initialisée")
# Charger les services
from app.services import (
bootstrap_status_service,
scheduler_service,
notification_service,
)
from app.crud.log import LogRepository
# Charger les statuts bootstrap depuis la BD
await bootstrap_status_service.load_from_db()
# Démarrer le scheduler
await scheduler_service.start_async()
# Ajouter les jobs Docker au scheduler
from app.services.docker_service import docker_service
from app.services.docker_alerts import docker_alerts_service
scheduler_service.scheduler.add_job(
docker_service.collect_all_hosts,
trigger="interval",
seconds=60, # Toutes les minutes
id="docker_collect",
name="Docker Metrics Collection",
replace_existing=True
)
scheduler_service.scheduler.add_job(
docker_alerts_service.check_all_alerts,
trigger="interval",
seconds=30, # Toutes les 30 secondes
id="docker_alerts",
name="Docker Alerts Check",
replace_existing=True
)
print("🐳 Docker monitoring jobs scheduled")
# Afficher l'état du service de notification
ntfy_status = "activé" if notification_service.enabled else "désactivé"
print(f"🔔 Service de notification ntfy: {ntfy_status} ({notification_service.config.base_url})")
# Log de démarrage en base
async with async_session_maker() as session:
repo = LogRepository(session)
await repo.create(
level="INFO",
message="Application démarrée - Services initialisés (BD)",
source="system",
)
await session.commit()
# Notification ntfy au démarrage
startup_notif = notification_service.templates.app_started()
await notification_service.send(
message=startup_notif.message,
topic=startup_notif.topic,
title=startup_notif.title,
priority=startup_notif.priority,
tags=startup_notif.tags,
)
@app.on_event("shutdown")
async def shutdown_event():
"""Événement d'arrêt de l'application."""
print("👋 Arrêt de l'application...")
from app.services import scheduler_service, notification_service
# Arrêter la capture des logs console
from app.services import console_log_service
console_log_service.stop_capture()
# Arrêter le scheduler
scheduler_service.shutdown()
# Notification ntfy à l'arrêt
shutdown_notif = notification_service.templates.app_stopped()
await notification_service.send(
message=shutdown_notif.message,
topic=shutdown_notif.topic,
title=shutdown_notif.title,
priority=shutdown_notif.priority,
tags=shutdown_notif.tags,
)
# Fermer le client HTTP
await notification_service.close()
print("✅ Services arrêtés proprement")
return app