From c3cd7c2621b3bad5299f7927bd4ffbf5b6246cc3 Mon Sep 17 00:00:00 2001 From: Bruno Charest Date: Tue, 3 Mar 2026 20:18:22 -0500 Subject: [PATCH] feat: Implement initial Homelab Automation API v2 with new models, routes, and core architecture, including a SQLAlchemy model refactoring script. --- README.md | 22 + app/core/dependencies.py | 8 +- app/factory.py | 2 +- app/models/alert.py | 12 +- app/models/app_setting.py | 2 +- app/models/bootstrap_status.py | 6 +- app/models/container_customization.py | 8 +- app/models/docker_alert.py | 10 +- app/models/docker_container.py | 14 +- app/models/docker_image.py | 6 +- app/models/docker_volume.py | 6 +- app/models/favorite_container.py | 4 +- app/models/favorite_group.py | 6 +- app/models/host.py | 12 +- app/models/host_metrics.py | 80 +- app/models/log.py | 10 +- app/models/playbook_lint.py | 5 +- app/models/schedule.py | 44 +- app/models/schedule_run.py | 14 +- app/models/task.py | 10 +- app/models/terminal_command_log.py | 10 +- app/models/terminal_session.py | 10 +- app/models/user.py | 10 +- app/routes/__init__.py | 2 + app/routes/config.py | 29 +- app/routes/docker.py | 15 + app/routes/health.py | 37 + app/routes/help.py | 32 + app/routes/notifications.py | 36 + app/routes/playbooks.py | 32 + app/routes/users.py | 153 +++ app/services/docker_actions.py | 53 + app/static/help.md | 1209 ++++++++++++++++- app/utils/help_renderer.py | 17 +- app/utils/pdf_generator.py | 64 +- data/homelab.db | Bin 290816 -> 294912 bytes dump_routes.py | 11 + fix_for_314.py | 65 + fix_for_314_v2.py | 50 + fix_for_314_v3.py | 48 + list_routes.py | 14 + logs/tasks_logs/.metadata_cache.json | 2 +- ...w.lab.home_Vérification_de_santé_failed.md | 25 + reset_password.py | 29 + routes_dump.txt | 164 +++ run_dev.ps1 | 2 +- tasks_logs/.metadata_cache.json | 2 +- test_endpoints.py | 27 + test_typing.py | 15 + test_typing_2.py | 9 + 50 files changed, 2265 insertions(+), 188 deletions(-) create mode 100644 app/routes/users.py create mode 100644 dump_routes.py create mode 100644 fix_for_314.py create mode 100644 fix_for_314_v2.py create mode 100644 fix_for_314_v3.py create mode 100644 list_routes.py create mode 100644 logs/tasks_logs/2026/03/03/task_010755_221533_openclaw.lab.home_Vérification_de_santé_failed.md create mode 100644 reset_password.py create mode 100644 routes_dump.txt create mode 100644 test_endpoints.py create mode 100644 test_typing.py create mode 100644 test_typing_2.py diff --git a/README.md b/README.md index e13a91a..03a9245 100644 --- a/README.md +++ b/README.md @@ -316,6 +316,28 @@ curl -H "Authorization: Bearer $TOKEN" http://localhost:8000/api/hosts - Toujours exposer l’API derrière HTTPS (reverse proxy type Nginx/Traefik, certbot, etc.). - Restreindre les IP/autorisations au niveau du pare-feu si possible. +#### 🔐 Récupération et Réinitialisation des accès + +Si vous avez perdu vos identifiants de connexion au Dashboard, voici comment procéder : + +**1. Récupérer le nom d'utilisateur (Username)** +Les utilisateurs sont stockés dans la base de données SQLite. Vous pouvez les lister avec cette commande : +```bash +sqlite3 data/homelab.db "SELECT id, username, role, is_active FROM users;" +``` + +**2. Réinitialiser le mot de passe** +Les mots de passe sont hachés (BCrypt) et ne peuvent **pas** être récupérés en clair. Vous devez les réinitialiser. + +Un script utilitaire est fourni à la racine du projet pour cela : + +```bash +# Usage: python reset_password.py +python reset_password.py admin mon_nouveau_mot_de_passe +``` + +**Note :** Si aucun utilisateur n'existe encore, l'API `POST /api/auth/setup` (mentionnée plus haut) est la méthode recommandée pour créer le premier compte administrateur. + #### Endpoints Principaux **Hôtes** diff --git a/app/core/dependencies.py b/app/core/dependencies.py index 2cc4ad0..adcaf2b 100644 --- a/app/core/dependencies.py +++ b/app/core/dependencies.py @@ -53,8 +53,8 @@ async def verify_api_key( Returns: True si authentifié """ - # Vérifier la clé API - if api_key and api_key == settings.api_key: + # Vérifier la clé API (en tant que header X-API-Key ou Bearer token) + if (api_key and api_key == settings.api_key) or (token and token == settings.api_key): return True # Vérifier le token JWT @@ -86,8 +86,8 @@ async def get_current_user_optional( Returns: Dictionnaire utilisateur si authentifié, None sinon """ - # Vérifier d'abord la clé API (compatibilité legacy) - if api_key and api_key == settings.api_key: + # Vérifier la clé API (en tant que header X-API-Key ou Bearer token) + if (api_key and api_key == settings.api_key) or (token and token == settings.api_key): return {"type": "api_key", "authenticated": True} # Vérifier le token JWT diff --git a/app/factory.py b/app/factory.py index 62824d7..293bc10 100644 --- a/app/factory.py +++ b/app/factory.py @@ -52,7 +52,7 @@ def create_app() -> FastAPI: # Inclure les routers API app.include_router(api_router, prefix="/api") - + # Inclure le router WebSocket (sans préfixe /api) app.include_router(ws_router) diff --git a/app/models/alert.py b/app/models/alert.py index ca5cbc0..87fe35c 100644 --- a/app/models/alert.py +++ b/app/models/alert.py @@ -20,16 +20,16 @@ class Alert(Base): ) id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) - user_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("users.id", ondelete="SET NULL")) + user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id", ondelete="SET NULL"), nullable=True) category: Mapped[str] = mapped_column(String(50), nullable=False) - level: Mapped[Optional[str]] = mapped_column(String(20)) - title: Mapped[Optional[str]] = mapped_column(String(255)) + level: Mapped[str] = mapped_column(String(20), nullable=True) + title: Mapped[str] = mapped_column(String(255), nullable=True) message: Mapped[str] = mapped_column(Text, nullable=False) - source: Mapped[Optional[str]] = mapped_column(String(50)) - details: Mapped[Optional[dict]] = mapped_column(JSON) + source: Mapped[str] = mapped_column(String(50), nullable=True) + details: Mapped[dict] = mapped_column(JSON, nullable=True) - read_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True) + read_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) def __repr__(self) -> str: diff --git a/app/models/app_setting.py b/app/models/app_setting.py index f702a98..055422a 100644 --- a/app/models/app_setting.py +++ b/app/models/app_setting.py @@ -17,7 +17,7 @@ class AppSetting(Base): ) key: Mapped[str] = mapped_column(String(100), primary_key=True) - value: Mapped[Optional[str]] = mapped_column(Text) + value: Mapped[str] = mapped_column(Text, nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now()) diff --git a/app/models/bootstrap_status.py b/app/models/bootstrap_status.py index 63a6b7c..a998059 100644 --- a/app/models/bootstrap_status.py +++ b/app/models/bootstrap_status.py @@ -16,9 +16,9 @@ class BootstrapStatus(Base): id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) host_id: Mapped[str] = mapped_column(String, ForeignKey("hosts.id", ondelete="CASCADE"), nullable=False) status: Mapped[str] = mapped_column(String, nullable=False) - automation_user: Mapped[Optional[str]] = mapped_column(String, nullable=True) - last_attempt: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) - error_message: Mapped[Optional[str]] = mapped_column(Text, nullable=True) + automation_user: Mapped[str] = mapped_column(String, nullable=True) + last_attempt: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=True) + error_message: Mapped[str] = mapped_column(Text, nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) host: Mapped["Host"] = relationship("Host", back_populates="bootstrap_statuses") diff --git a/app/models/container_customization.py b/app/models/container_customization.py index ce37b80..8487daf 100644 --- a/app/models/container_customization.py +++ b/app/models/container_customization.py @@ -14,14 +14,14 @@ class ContainerCustomization(Base): __tablename__ = "container_customizations" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) - user_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=True) + user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=True) host_id: Mapped[str] = mapped_column(String, ForeignKey("hosts.id", ondelete="CASCADE"), nullable=False) container_id: Mapped[str] = mapped_column(String(64), nullable=False) - icon_key: Mapped[Optional[str]] = mapped_column(String(100), nullable=True) - icon_color: Mapped[Optional[str]] = mapped_column(String(20), nullable=True) - bg_color: Mapped[Optional[str]] = mapped_column(String(20), nullable=True) + icon_key: Mapped[str] = mapped_column(String(100), nullable=True) + icon_color: Mapped[str] = mapped_column(String(20), nullable=True) + bg_color: Mapped[str] = mapped_column(String(20), nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now()) diff --git a/app/models/docker_alert.py b/app/models/docker_alert.py index ff474ac..9ded558 100644 --- a/app/models/docker_alert.py +++ b/app/models/docker_alert.py @@ -20,12 +20,12 @@ class DockerAlert(Base): container_name: Mapped[str] = mapped_column(String(255), nullable=False) severity: Mapped[str] = mapped_column(String(20), nullable=False, default="warning") # warning/error/critical state: Mapped[str] = mapped_column(String(20), nullable=False, default="open") # open/closed/acknowledged - message: Mapped[Optional[str]] = mapped_column(Text, nullable=True) + message: Mapped[str] = mapped_column(Text, nullable=True) opened_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) - closed_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True) - acknowledged_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True) - acknowledged_by: Mapped[Optional[str]] = mapped_column(String(100), nullable=True) - last_notified_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True) + closed_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=True) + acknowledged_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=True) + acknowledged_by: Mapped[str] = mapped_column(String(100), nullable=True) + last_notified_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=True) # Relationship to host host: Mapped["Host"] = relationship("Host", back_populates="docker_alerts") diff --git a/app/models/docker_container.py b/app/models/docker_container.py index d173663..5dd1c8c 100644 --- a/app/models/docker_container.py +++ b/app/models/docker_container.py @@ -19,14 +19,14 @@ class DockerContainer(Base): host_id: Mapped[str] = mapped_column(String, ForeignKey("hosts.id", ondelete="CASCADE"), nullable=False) container_id: Mapped[str] = mapped_column(String(64), nullable=False) name: Mapped[str] = mapped_column(String(255), nullable=False) - image: Mapped[Optional[str]] = mapped_column(String(255), nullable=True) + image: Mapped[str] = mapped_column(String(255), nullable=True) state: Mapped[str] = mapped_column(String(20), nullable=False, default="unknown") # running/exited/paused/created/dead - status: Mapped[Optional[str]] = mapped_column(String(255), nullable=True) # Up 2 hours, Exited (0) 5 minutes ago - health: Mapped[Optional[str]] = mapped_column(String(20), nullable=True) # healthy/unhealthy/starting/none - created_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True) - ports: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True) - labels: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True) - compose_project: Mapped[Optional[str]] = mapped_column(String(255), nullable=True) # com.docker.compose.project + status: Mapped[str] = mapped_column(String(255), nullable=True) # Up 2 hours, Exited (0) 5 minutes ago + health: Mapped[str] = mapped_column(String(20), nullable=True) # healthy/unhealthy/starting/none + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=True) + ports: Mapped[dict] = mapped_column(JSON, nullable=True) + labels: Mapped[dict] = mapped_column(JSON, nullable=True) + compose_project: Mapped[str] = mapped_column(String(255), nullable=True) # com.docker.compose.project last_update_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now() ) diff --git a/app/models/docker_image.py b/app/models/docker_image.py index 53d7e49..54bd9e6 100644 --- a/app/models/docker_image.py +++ b/app/models/docker_image.py @@ -18,9 +18,9 @@ class DockerImage(Base): id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) host_id: Mapped[str] = mapped_column(String, ForeignKey("hosts.id", ondelete="CASCADE"), nullable=False) image_id: Mapped[str] = mapped_column(String(64), nullable=False) - repo_tags: Mapped[Optional[list]] = mapped_column(JSON, nullable=True) # ["nginx:latest", "nginx:1.25"] - size: Mapped[Optional[int]] = mapped_column(BigInteger, nullable=True) - created: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True) + repo_tags: Mapped[list] = mapped_column(JSON, nullable=True) # ["nginx:latest", "nginx:1.25"] + size: Mapped[int] = mapped_column(BigInteger, nullable=True) + created: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=True) last_update_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now() ) diff --git a/app/models/docker_volume.py b/app/models/docker_volume.py index ada556a..ab4fb97 100644 --- a/app/models/docker_volume.py +++ b/app/models/docker_volume.py @@ -18,9 +18,9 @@ class DockerVolume(Base): id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) host_id: Mapped[str] = mapped_column(String, ForeignKey("hosts.id", ondelete="CASCADE"), nullable=False) name: Mapped[str] = mapped_column(String(255), nullable=False) - driver: Mapped[Optional[str]] = mapped_column(String(50), nullable=True) - mountpoint: Mapped[Optional[str]] = mapped_column(Text, nullable=True) - scope: Mapped[Optional[str]] = mapped_column(String(20), nullable=True) # local/global + driver: Mapped[str] = mapped_column(String(50), nullable=True) + mountpoint: Mapped[str] = mapped_column(Text, nullable=True) + scope: Mapped[str] = mapped_column(String(20), nullable=True) # local/global last_update_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now() ) diff --git a/app/models/favorite_container.py b/app/models/favorite_container.py index d3522f2..dcc75be 100644 --- a/app/models/favorite_container.py +++ b/app/models/favorite_container.py @@ -14,13 +14,13 @@ class FavoriteContainer(Base): __tablename__ = "favorite_containers" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) - user_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=True) + user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=True) docker_container_id: Mapped[int] = mapped_column( Integer, ForeignKey("docker_containers.id", ondelete="CASCADE"), nullable=False ) - group_id: Mapped[Optional[int]] = mapped_column( + group_id: Mapped[int] = mapped_column( Integer, ForeignKey("favorite_groups.id", ondelete="SET NULL"), nullable=True ) diff --git a/app/models/favorite_group.py b/app/models/favorite_group.py index 3a9e973..262ede4 100644 --- a/app/models/favorite_group.py +++ b/app/models/favorite_group.py @@ -14,11 +14,11 @@ class FavoriteGroup(Base): __tablename__ = "favorite_groups" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) - user_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=True) + user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=True) name: Mapped[str] = mapped_column(String(100), nullable=False) sort_order: Mapped[int] = mapped_column(Integer, nullable=False, server_default=text("0")) - color: Mapped[Optional[str]] = mapped_column(String(20), nullable=True) - icon_key: Mapped[Optional[str]] = mapped_column(String(100), nullable=True) + color: Mapped[str] = mapped_column(String(20), nullable=True) + icon_key: Mapped[str] = mapped_column(String(100), nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now()) diff --git a/app/models/host.py b/app/models/host.py index 15d123e..827f1b1 100644 --- a/app/models/host.py +++ b/app/models/host.py @@ -17,18 +17,18 @@ class Host(Base): name: Mapped[str] = mapped_column(String, nullable=False) ip_address: Mapped[str] = mapped_column(String, nullable=False, unique=True) status: Mapped[str] = mapped_column(String, nullable=False, server_default=text("'unknown'")) - ansible_group: Mapped[Optional[str]] = mapped_column(String, nullable=True) - last_seen: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) + ansible_group: Mapped[str] = mapped_column(String, nullable=True) + last_seen: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=True) reachable: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default=text("0")) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now()) - deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) + deleted_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=True) # Docker-related fields docker_enabled: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default=text("0")) - docker_version: Mapped[Optional[str]] = mapped_column(String(50), nullable=True) - docker_status: Mapped[Optional[str]] = mapped_column(String(20), nullable=True) # online/offline/error - docker_last_collect_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True) + docker_version: Mapped[str] = mapped_column(String(50), nullable=True) + docker_status: Mapped[str] = mapped_column(String(20), nullable=True) # online/offline/error + docker_last_collect_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=True) bootstrap_statuses: Mapped[List["BootstrapStatus"]] = relationship( "BootstrapStatus", back_populates="host", cascade="all, delete-orphan" diff --git a/app/models/host_metrics.py b/app/models/host_metrics.py index 6ed9c41..4f67e3e 100644 --- a/app/models/host_metrics.py +++ b/app/models/host_metrics.py @@ -24,60 +24,60 @@ class HostMetrics(Base): metric_type: Mapped[str] = mapped_column(String(50), nullable=False) # 'system_info', 'disk_usage', 'memory', etc. # Métriques CPU - cpu_count: Mapped[Optional[int]] = mapped_column(Integer) - cpu_model: Mapped[Optional[str]] = mapped_column(String(200)) - cpu_cores: Mapped[Optional[int]] = mapped_column(Integer) - cpu_threads: Mapped[Optional[int]] = mapped_column(Integer) - cpu_threads_per_core: Mapped[Optional[int]] = mapped_column(Integer) - cpu_sockets: Mapped[Optional[int]] = mapped_column(Integer) - cpu_mhz: Mapped[Optional[float]] = mapped_column(Float) - cpu_max_mhz: Mapped[Optional[float]] = mapped_column(Float) - cpu_min_mhz: Mapped[Optional[float]] = mapped_column(Float) - cpu_load_1m: Mapped[Optional[float]] = mapped_column(Float) - cpu_load_5m: Mapped[Optional[float]] = mapped_column(Float) - cpu_load_15m: Mapped[Optional[float]] = mapped_column(Float) - cpu_usage_percent: Mapped[Optional[float]] = mapped_column(Float) - cpu_temperature: Mapped[Optional[float]] = mapped_column(Float) + cpu_count: Mapped[int] = mapped_column(Integer, nullable=True) + cpu_model: Mapped[str] = mapped_column(String(200), nullable=True) + cpu_cores: Mapped[int] = mapped_column(Integer, nullable=True) + cpu_threads: Mapped[int] = mapped_column(Integer, nullable=True) + cpu_threads_per_core: Mapped[int] = mapped_column(Integer, nullable=True) + cpu_sockets: Mapped[int] = mapped_column(Integer, nullable=True) + cpu_mhz: Mapped[float] = mapped_column(Float, nullable=True) + cpu_max_mhz: Mapped[float] = mapped_column(Float, nullable=True) + cpu_min_mhz: Mapped[float] = mapped_column(Float, nullable=True) + cpu_load_1m: Mapped[float] = mapped_column(Float, nullable=True) + cpu_load_5m: Mapped[float] = mapped_column(Float, nullable=True) + cpu_load_15m: Mapped[float] = mapped_column(Float, nullable=True) + cpu_usage_percent: Mapped[float] = mapped_column(Float, nullable=True) + cpu_temperature: Mapped[float] = mapped_column(Float, nullable=True) # Métriques mémoire - memory_total_mb: Mapped[Optional[int]] = mapped_column(Integer) - memory_used_mb: Mapped[Optional[int]] = mapped_column(Integer) - memory_free_mb: Mapped[Optional[int]] = mapped_column(Integer) - memory_usage_percent: Mapped[Optional[float]] = mapped_column(Float) - swap_total_mb: Mapped[Optional[int]] = mapped_column(Integer) - swap_used_mb: Mapped[Optional[int]] = mapped_column(Integer) - swap_usage_percent: Mapped[Optional[float]] = mapped_column(Float) + memory_total_mb: Mapped[int] = mapped_column(Integer, nullable=True) + memory_used_mb: Mapped[int] = mapped_column(Integer, nullable=True) + memory_free_mb: Mapped[int] = mapped_column(Integer, nullable=True) + memory_usage_percent: Mapped[float] = mapped_column(Float, nullable=True) + swap_total_mb: Mapped[int] = mapped_column(Integer, nullable=True) + swap_used_mb: Mapped[int] = mapped_column(Integer, nullable=True) + swap_usage_percent: Mapped[float] = mapped_column(Float, nullable=True) # Métriques disque (stockées en JSON pour flexibilité - plusieurs disques) - disk_info: Mapped[Optional[object]] = mapped_column(JSON) # Liste des points de montage avec usage - disk_devices: Mapped[Optional[object]] = mapped_column(JSON) # Liste des disques + partitions (layout) - disk_root_total_gb: Mapped[Optional[float]] = mapped_column(Float) - disk_root_used_gb: Mapped[Optional[float]] = mapped_column(Float) - disk_root_usage_percent: Mapped[Optional[float]] = mapped_column(Float) + disk_info: Mapped[object] = mapped_column(JSON, nullable=True) # Liste des points de montage avec usage + disk_devices: Mapped[object] = mapped_column(JSON) # Liste des disques + partitions (layout, nullable=True) + disk_root_total_gb: Mapped[float] = mapped_column(Float, nullable=True) + disk_root_used_gb: Mapped[float] = mapped_column(Float, nullable=True) + disk_root_usage_percent: Mapped[float] = mapped_column(Float, nullable=True) # Storage stacks (JSON) - lvm_info: Mapped[Optional[object]] = mapped_column(JSON) - zfs_info: Mapped[Optional[object]] = mapped_column(JSON) + lvm_info: Mapped[object] = mapped_column(JSON, nullable=True) + zfs_info: Mapped[object] = mapped_column(JSON, nullable=True) # Stockage détaillé (JSON) - données normalisées avec métadonnées de collecte - storage_details: Mapped[Optional[dict]] = mapped_column(JSON) + storage_details: Mapped[dict] = mapped_column(JSON, nullable=True) # Informations système - os_name: Mapped[Optional[str]] = mapped_column(String(100)) - os_version: Mapped[Optional[str]] = mapped_column(String(100)) - kernel_version: Mapped[Optional[str]] = mapped_column(String(100)) - hostname: Mapped[Optional[str]] = mapped_column(String(200)) - uptime_seconds: Mapped[Optional[int]] = mapped_column(Integer) - uptime_human: Mapped[Optional[str]] = mapped_column(String(100)) + os_name: Mapped[str] = mapped_column(String(100), nullable=True) + os_version: Mapped[str] = mapped_column(String(100), nullable=True) + kernel_version: Mapped[str] = mapped_column(String(100), nullable=True) + hostname: Mapped[str] = mapped_column(String(200), nullable=True) + uptime_seconds: Mapped[int] = mapped_column(Integer, nullable=True) + uptime_human: Mapped[str] = mapped_column(String(100), nullable=True) # Réseau (stocké en JSON pour flexibilité) - network_info: Mapped[Optional[dict]] = mapped_column(JSON) + network_info: Mapped[dict] = mapped_column(JSON, nullable=True) # Données brutes et métadonnées - raw_data: Mapped[Optional[dict]] = mapped_column(JSON) # Données brutes du playbook - collection_source: Mapped[Optional[str]] = mapped_column(String(100)) # Nom du builtin playbook - collection_duration_ms: Mapped[Optional[int]] = mapped_column(Integer) - error_message: Mapped[Optional[str]] = mapped_column(Text) + raw_data: Mapped[dict] = mapped_column(JSON, nullable=True) # Données brutes du playbook + collection_source: Mapped[str] = mapped_column(String(100), nullable=True) # Nom du builtin playbook + collection_duration_ms: Mapped[int] = mapped_column(Integer, nullable=True) + error_message: Mapped[str] = mapped_column(Text, nullable=True) collected_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) diff --git a/app/models/log.py b/app/models/log.py index 66191ae..3bfe80c 100644 --- a/app/models/log.py +++ b/app/models/log.py @@ -20,12 +20,12 @@ class Log(Base): id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) level: Mapped[str] = mapped_column(String, nullable=False) - source: Mapped[Optional[str]] = mapped_column(String) + source: Mapped[str] = mapped_column(String, nullable=True) message: Mapped[str] = mapped_column(Text, nullable=False) - details: Mapped[Optional[dict]] = mapped_column(JSON) - host_id: Mapped[Optional[str]] = mapped_column(String, nullable=True) - task_id: Mapped[Optional[str]] = mapped_column(String, nullable=True) - schedule_id: Mapped[Optional[str]] = mapped_column(String, nullable=True) + details: Mapped[dict] = mapped_column(JSON, nullable=True) + host_id: Mapped[str] = mapped_column(String, nullable=True) + task_id: Mapped[str] = mapped_column(String, nullable=True) + schedule_id: Mapped[str] = mapped_column(String, nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) diff --git a/app/models/playbook_lint.py b/app/models/playbook_lint.py index 2901ee7..6712052 100644 --- a/app/models/playbook_lint.py +++ b/app/models/playbook_lint.py @@ -1,3 +1,4 @@ +from __future__ import annotations """ Model for storing ansible-lint results per playbook. """ @@ -33,10 +34,10 @@ class PlaybookLintResult(Base): execution_time_ms: Mapped[int] = mapped_column(Integer, nullable=False, default=0) # Full issues list as JSON - issues_json: Mapped[Optional[str]] = mapped_column(Text, nullable=True) + issues_json: Mapped[str] = mapped_column(Text, nullable=True) # Raw output from ansible-lint (for copy to clipboard) - raw_output: Mapped[Optional[str]] = mapped_column(Text, nullable=True) + raw_output: Mapped[str] = mapped_column(Text, nullable=True) # Timestamps created_at: Mapped[datetime] = mapped_column( diff --git a/app/models/schedule.py b/app/models/schedule.py index d93221c..8480b56 100644 --- a/app/models/schedule.py +++ b/app/models/schedule.py @@ -15,35 +15,35 @@ class Schedule(Base): id: Mapped[str] = mapped_column(String, primary_key=True) name: Mapped[str] = mapped_column(String, nullable=False) - description: Mapped[Optional[str]] = mapped_column(Text) + description: Mapped[str] = mapped_column(Text, nullable=True) playbook: Mapped[str] = mapped_column(String, nullable=False) - target_type: Mapped[Optional[str]] = mapped_column(String, default="group") + target_type: Mapped[str] = mapped_column(String, default="group", nullable=True) target: Mapped[str] = mapped_column(String, nullable=False) - extra_vars: Mapped[Optional[Dict[str, Any]]] = mapped_column(JSON) + extra_vars: Mapped[Dict[str, Any]] = mapped_column(JSON, nullable=True) schedule_type: Mapped[str] = mapped_column(String, nullable=False) - schedule_time: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) - recurrence_type: Mapped[Optional[str]] = mapped_column(String) - recurrence_time: Mapped[Optional[str]] = mapped_column(String) - recurrence_days: Mapped[Optional[str]] = mapped_column(Text) - cron_expression: Mapped[Optional[str]] = mapped_column(String) - timezone: Mapped[Optional[str]] = mapped_column(String, default="America/Montreal") - start_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) - end_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) + schedule_time: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=True) + recurrence_type: Mapped[str] = mapped_column(String, nullable=True) + recurrence_time: Mapped[str] = mapped_column(String, nullable=True) + recurrence_days: Mapped[str] = mapped_column(Text, nullable=True) + cron_expression: Mapped[str] = mapped_column(String, nullable=True) + timezone: Mapped[str] = mapped_column(String, default="America/Montreal", nullable=True) + start_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=True) + end_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=True) enabled: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True) - tags: Mapped[Optional[str]] = mapped_column(Text) - next_run: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) - last_run: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) - last_status: Mapped[Optional[str]] = mapped_column(String, default="never") - retry_on_failure: Mapped[Optional[int]] = mapped_column(Integer, default=0) - timeout: Mapped[Optional[int]] = mapped_column(Integer, default=3600) + tags: Mapped[str] = mapped_column(Text, nullable=True) + next_run: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=True) + last_run: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=True) + last_status: Mapped[str] = mapped_column(String, default="never", nullable=True) + retry_on_failure: Mapped[int] = mapped_column(Integer, default=0, nullable=True) + timeout: Mapped[int] = mapped_column(Integer, default=3600, nullable=True) # Type de notification: "none" (aucune), "all" (toujours), "errors" (erreurs seulement) - notification_type: Mapped[Optional[str]] = mapped_column(String, default="all") - run_count: Mapped[Optional[int]] = mapped_column(Integer, default=0) - success_count: Mapped[Optional[int]] = mapped_column(Integer, default=0) - failure_count: Mapped[Optional[int]] = mapped_column(Integer, default=0) + notification_type: Mapped[str] = mapped_column(String, default="all", nullable=True) + run_count: Mapped[int] = mapped_column(Integer, default=0, nullable=True) + success_count: Mapped[int] = mapped_column(Integer, default=0, nullable=True) + failure_count: Mapped[int] = mapped_column(Integer, default=0, nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now()) - deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) + deleted_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=True) runs: Mapped[List["ScheduleRun"]] = relationship( "ScheduleRun", back_populates="schedule", cascade="all, delete-orphan" diff --git a/app/models/schedule_run.py b/app/models/schedule_run.py index 0207af5..c11dbf4 100644 --- a/app/models/schedule_run.py +++ b/app/models/schedule_run.py @@ -15,18 +15,18 @@ class ScheduleRun(Base): id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) schedule_id: Mapped[str] = mapped_column(String, ForeignKey("schedules.id", ondelete="CASCADE"), nullable=False) - task_id: Mapped[Optional[str]] = mapped_column(String, ForeignKey("tasks.id", ondelete="SET NULL")) + task_id: Mapped[str] = mapped_column(String, ForeignKey("tasks.id", ondelete="SET NULL"), nullable=True) status: Mapped[str] = mapped_column(String, nullable=False) started_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) - completed_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) - duration: Mapped[Optional[float]] = mapped_column(Float) - hosts_impacted: Mapped[Optional[int]] = mapped_column(Integer, default=0) - error_message: Mapped[Optional[str]] = mapped_column(Text) - output: Mapped[Optional[str]] = mapped_column(Text) + completed_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=True) + duration: Mapped[float] = mapped_column(Float, nullable=True) + hosts_impacted: Mapped[int] = mapped_column(Integer, default=0, nullable=True) + error_message: Mapped[str] = mapped_column(Text, nullable=True) + output: Mapped[str] = mapped_column(Text, nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) schedule: Mapped["Schedule"] = relationship("Schedule", back_populates="runs") - task: Mapped[Optional["Task"]] = relationship("Task", back_populates="schedule_runs") + task: Mapped["Task"] = relationship("Task", back_populates="schedule_runs") def __repr__(self) -> str: # pragma: no cover - debug helper return f"" diff --git a/app/models/task.py b/app/models/task.py index 1a83ac1..6a5ddd4 100644 --- a/app/models/task.py +++ b/app/models/task.py @@ -17,11 +17,11 @@ class Task(Base): action: Mapped[str] = mapped_column(String, nullable=False) target: Mapped[str] = mapped_column(String, nullable=False) status: Mapped[str] = mapped_column(String, nullable=False, server_default=text("'pending'")) - playbook: Mapped[Optional[str]] = mapped_column(String) - started_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) - completed_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) - error_message: Mapped[Optional[str]] = mapped_column(Text) - result_data: Mapped[Optional[dict]] = mapped_column(JSON) + playbook: Mapped[str] = mapped_column(String, nullable=True) + started_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=True) + completed_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=True) + error_message: Mapped[str] = mapped_column(Text, nullable=True) + result_data: Mapped[dict] = mapped_column(JSON, nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) schedule_runs: Mapped[list["ScheduleRun"]] = relationship("ScheduleRun", back_populates="task") diff --git a/app/models/terminal_command_log.py b/app/models/terminal_command_log.py index 3f61533..7e24e68 100644 --- a/app/models/terminal_command_log.py +++ b/app/models/terminal_command_log.py @@ -38,12 +38,12 @@ class TerminalCommandLog(Base): host_id: Mapped[str] = mapped_column( String, ForeignKey("hosts.id", ondelete="CASCADE"), nullable=False, index=True ) - user_id: Mapped[Optional[str]] = mapped_column( + user_id: Mapped[str] = mapped_column( String, ForeignKey("users.id", ondelete="SET NULL"), nullable=True, index=True ) # Session reference (not FK as sessions may be cleaned up) - terminal_session_id: Mapped[Optional[str]] = mapped_column(String(64), nullable=True, index=True) + terminal_session_id: Mapped[str] = mapped_column(String(64), nullable=True, index=True) # Command data (masked/normalized version only) command: Mapped[str] = mapped_column(Text, nullable=False) @@ -58,11 +58,11 @@ class TerminalCommandLog(Base): # If command was blocked (for audit - no raw command stored) is_blocked: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default=text("0")) - blocked_reason: Mapped[Optional[str]] = mapped_column(String(255), nullable=True) + blocked_reason: Mapped[str] = mapped_column(String(255), nullable=True) # Additional metadata - username: Mapped[Optional[str]] = mapped_column(String(100), nullable=True) - host_name: Mapped[Optional[str]] = mapped_column(String(100), nullable=True) + username: Mapped[str] = mapped_column(String(100), nullable=True) + host_name: Mapped[str] = mapped_column(String(100), nullable=True) # Relationships host = relationship("Host", back_populates="terminal_commands", lazy="selectin") diff --git a/app/models/terminal_session.py b/app/models/terminal_session.py index 0f00392..cc9711c 100644 --- a/app/models/terminal_session.py +++ b/app/models/terminal_session.py @@ -57,15 +57,15 @@ class TerminalSession(Base): host_id: Mapped[str] = mapped_column(String, nullable=False, index=True) host_name: Mapped[str] = mapped_column(String, nullable=False) host_ip: Mapped[str] = mapped_column(String, nullable=False) - user_id: Mapped[Optional[str]] = mapped_column(String, nullable=True) - username: Mapped[Optional[str]] = mapped_column(String, nullable=True) + user_id: Mapped[str] = mapped_column(String, nullable=True) + username: Mapped[str] = mapped_column(String, nullable=True) # Token hash for session authentication (never store plain token) token_hash: Mapped[str] = mapped_column(String(128), nullable=False) # ttyd process management ttyd_port: Mapped[int] = mapped_column(Integer, nullable=False) - ttyd_pid: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) + ttyd_pid: Mapped[int] = mapped_column(Integer, nullable=True) # Session mode: 'embedded' or 'popout' mode: Mapped[str] = mapped_column(String(20), nullable=False, server_default=text("'embedded'")) @@ -74,7 +74,7 @@ class TerminalSession(Base): status: Mapped[str] = mapped_column(String(20), nullable=False, server_default=text("'active'")) # Close reason: 'user_close', 'ttl', 'idle', 'quota', 'server_shutdown', 'client_lost', 'reused' - reason_closed: Mapped[Optional[str]] = mapped_column(String(30), nullable=True) + reason_closed: Mapped[str] = mapped_column(String(30), nullable=True) # Timestamps created_at: Mapped[datetime] = mapped_column( @@ -84,7 +84,7 @@ class TerminalSession(Base): DateTime(timezone=True), nullable=False, server_default=func.now() ) expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) - closed_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True) + closed_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=True) def __repr__(self) -> str: return f"" diff --git a/app/models/user.py b/app/models/user.py index fd70cd2..b5be97a 100644 --- a/app/models/user.py +++ b/app/models/user.py @@ -39,7 +39,7 @@ class User(Base): id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) username: Mapped[str] = mapped_column(String(50), unique=True, nullable=False, index=True) - email: Mapped[Optional[str]] = mapped_column(String(255), unique=True, nullable=True) + email: Mapped[str] = mapped_column(String(255), unique=True, nullable=True) hashed_password: Mapped[str] = mapped_column(String(255), nullable=False) # Role-based access control (prepared for future) @@ -54,7 +54,7 @@ class User(Base): is_superuser: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default=text("0")) # Display name (optional) - display_name: Mapped[Optional[str]] = mapped_column(String(100), nullable=True) + display_name: Mapped[str] = mapped_column(String(100), nullable=True) # Timestamps created_at: Mapped[datetime] = mapped_column( @@ -68,11 +68,11 @@ class User(Base): server_default=func.now(), onupdate=func.now() ) - last_login: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True) - password_changed_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True) + last_login: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=True) + password_changed_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=True) # Soft delete support - deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True) + deleted_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=True) # Relationships terminal_commands: Mapped[List["TerminalCommandLog"]] = relationship( diff --git a/app/routes/__init__.py b/app/routes/__init__.py index bd9a9b3..a40318a 100644 --- a/app/routes/__init__.py +++ b/app/routes/__init__.py @@ -29,6 +29,7 @@ from app.routes.lint import router as lint_router from app.routes.terminal import router as terminal_router from app.routes.config import router as config_router from app.routes.favorites import router as favorites_router +from app.routes.users import router as users_router # Router principal qui agrège tous les sous-routers api_router = APIRouter() @@ -56,6 +57,7 @@ api_router.include_router(lint_router, prefix="/playbooks", tags=["Lint"]) api_router.include_router(terminal_router, prefix="/terminal", tags=["Terminal"]) api_router.include_router(favorites_router, prefix="/favorites", tags=["Favorites"]) api_router.include_router(config_router, tags=["Config"]) +api_router.include_router(users_router, prefix="/users", tags=["Users"]) __all__ = [ "api_router", diff --git a/app/routes/config.py b/app/routes/config.py index 64de8df..70de00d 100644 --- a/app/routes/config.py +++ b/app/routes/config.py @@ -1,12 +1,39 @@ -from fastapi import APIRouter +import sys +import time +from datetime import datetime, timezone + +from fastapi import APIRouter, Depends from app.core.config import settings +from app.core.dependencies import verify_api_key router = APIRouter() +_START_TIME = time.monotonic() + @router.get("/config") async def get_app_config(): + """Récupère la configuration publique de l'application.""" return { "debug_mode": bool(settings.debug_mode), } + + +@router.get("/config/version") +async def get_app_version(api_key_valid: bool = Depends(verify_api_key)): + """Retourne la version de l'application, Python, et l'uptime.""" + uptime_seconds = int(time.monotonic() - _START_TIME) + hours, remainder = divmod(uptime_seconds, 3600) + minutes, seconds = divmod(remainder, 60) + + return { + "app_version": settings.api_version, + "app_title": settings.api_title, + "python_version": sys.version, + "python_executable": sys.executable, + "timezone": settings.scheduler_timezone, + "uptime": f"{hours}h {minutes}m {seconds}s", + "uptime_seconds": uptime_seconds, + "server_time": datetime.now(timezone.utc).isoformat(), + } diff --git a/app/routes/docker.py b/app/routes/docker.py index 57abe97..efa0651 100644 --- a/app/routes/docker.py +++ b/app/routes/docker.py @@ -524,6 +524,21 @@ async def get_volumes( ) +# === Networks === + +@router.get("/hosts/{host_id}/networks") +async def get_networks( + host_id: str, + api_key_valid: bool = Depends(verify_api_key), +): + """Liste les réseaux Docker d'un hôte.""" + try: + result = await docker_actions.list_networks(host_id) + return result + except Exception as e: + raise HTTPException(status_code=500, detail=f"Erreur lors de la récupération des réseaux: {str(e)}") + + # === Alerts === @router.get("/alerts", response_model=DockerAlertListResponse) diff --git a/app/routes/health.py b/app/routes/health.py index ee47b5b..e9a4313 100644 --- a/app/routes/health.py +++ b/app/routes/health.py @@ -112,3 +112,40 @@ async def refresh_hosts(api_key_valid: bool = Depends(verify_api_key)): }) return {"message": f"{len(hosts)} hôtes rechargés depuis l'inventaire Ansible"} + + +@router.post("/check-all") +async def check_all_hosts_health(api_key_valid: bool = Depends(verify_api_key)): + """Exécute un health check sur tous les hôtes en parallèle.""" + results = [] + online = 0 + offline = 0 + + for host in db.hosts: + health_ok = host.status == "online" + status = "online" if health_ok else "offline" + + results.append({ + "host": host.name, + "status": status, + "reachable": health_ok, + "os": host.os, + }) + + if health_ok: + online += 1 + else: + offline += 1 + + await ws_manager.broadcast({ + "type": "health_check_all", + "data": {"online": online, "offline": offline, "total": len(results)}, + }) + + return { + "total": len(results), + "online": online, + "offline": offline, + "results": results, + "timestamp": datetime.now(timezone.utc).isoformat(), + } diff --git a/app/routes/help.py b/app/routes/help.py index f079f9c..b898144 100644 --- a/app/routes/help.py +++ b/app/routes/help.py @@ -64,3 +64,35 @@ async def download_help_pdf(api_key_valid: bool = Depends(verify_api_key)): "Content-Disposition": "attachment; filename=homelab-automation-help.pdf" } ) + + +@router.get("/catalog") +async def get_api_catalog(api_key_valid: bool = Depends(verify_api_key)): + """ + Retourne un catalogue de tous les points d'entrée de l'API en format JSON. + C'est une version simplifiée de la spec OpenAPI (Swagger). + """ + from app.routes import api_router + + catalog = [] + for route in api_router.routes: + # On ne liste que les routes qui ont un chemin et des méthodes (pas les Mount) + if hasattr(route, "path") and hasattr(route, "methods"): + # Exclure les routes internes ou de base si nécessaire + if any(p in route.path for p in ["/openapi.json", "/docs", "/redoc"]): + continue + + catalog.append({ + "path": f"/api{route.path}" if not route.path.startswith("/api") else route.path, + "methods": list(route.methods), + "summary": route.summary if hasattr(route, "summary") and route.summary else route.name, + "description": route.description if hasattr(route, "description") and route.description else "", + "tags": list(route.tags) if hasattr(route, "tags") and route.tags else [] + }) + + return { + "title": "Homelab Automation API Catalog", + "version": "2.0.0", + "endpoints_count": len(catalog), + "endpoints": catalog + } diff --git a/app/routes/notifications.py b/app/routes/notifications.py index 4e5987b..c583de2 100644 --- a/app/routes/notifications.py +++ b/app/routes/notifications.py @@ -57,6 +57,42 @@ async def send_custom_notification( return await notification_service.send_request(request) +@router.put("/config") +async def update_notification_config( + base_url: Optional[str] = None, + default_topic: Optional[str] = None, + enabled: Optional[bool] = None, + timeout: Optional[int] = None, + username: Optional[str] = None, + password: Optional[str] = None, + token: Optional[str] = None, + api_key_valid: bool = Depends(verify_api_key) +): + """Met à jour la configuration des notifications ntfy.""" + current_config = notification_service.config + new_config = NtfyConfig( + base_url=base_url if base_url is not None else current_config.base_url, + default_topic=default_topic if default_topic is not None else current_config.default_topic, + enabled=enabled if enabled is not None else current_config.enabled, + timeout=timeout if timeout is not None else current_config.timeout, + username=username if username is not None else current_config.username, + password=password if password is not None else current_config.password, + token=token if token is not None else current_config.token, + ) + notification_service.reconfigure(new_config) + + return { + "message": "Configuration mise à jour", + "config": { + "enabled": new_config.enabled, + "base_url": new_config.base_url, + "default_topic": new_config.default_topic, + "timeout": new_config.timeout, + "has_auth": new_config.has_auth, + } + } + + @router.post("/toggle") async def toggle_notifications( enabled: bool, diff --git a/app/routes/playbooks.py b/app/routes/playbooks.py index a3f2a91..3a33059 100644 --- a/app/routes/playbooks.py +++ b/app/routes/playbooks.py @@ -16,6 +16,38 @@ from app.services import ansible_service, db router = APIRouter() +@router.get("") +async def list_playbooks( + api_key_valid: bool = Depends(verify_api_key) +): + """Liste tous les playbooks avec métadonnées (taille, date, etc.).""" + playbooks_dir = ansible_service.playbooks_dir + if not playbooks_dir.exists(): + return {"playbooks": [], "count": 0} + + result = [] + for path in sorted(playbooks_dir.glob("*.yml")): + if path.name.startswith("_builtin_"): + continue + stat = path.stat() + result.append({ + "filename": path.name, + "size": stat.st_size, + "modified": datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc).isoformat(), + }) + for path in sorted(playbooks_dir.glob("*.yaml")): + if path.name.startswith("_builtin_"): + continue + stat = path.stat() + result.append({ + "filename": path.name, + "size": stat.st_size, + "modified": datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc).isoformat(), + }) + + return {"playbooks": result, "count": len(result)} + + @router.get("/{filename}/content") async def get_playbook_content( filename: str, diff --git a/app/routes/users.py b/app/routes/users.py new file mode 100644 index 0000000..349f953 --- /dev/null +++ b/app/routes/users.py @@ -0,0 +1,153 @@ +""" +Routes API pour la gestion des utilisateurs (admin). +""" + +from typing import Optional + +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.dependencies import get_db, require_admin +from app.crud.user import UserRepository +from app.schemas.auth import UserCreate, UserUpdate, UserOut +from app.services.auth_service import hash_password + +router = APIRouter() + + +@router.get("", response_model=list[UserOut]) +async def list_users( + limit: int = 100, + offset: int = 0, + include_deleted: bool = False, + user: dict = Depends(require_admin), + db_session: AsyncSession = Depends(get_db), +): + """Liste tous les utilisateurs (admin uniquement).""" + repo = UserRepository(db_session) + users = await repo.list(limit=limit, offset=offset, include_deleted=include_deleted) + return [UserOut.model_validate(u) for u in users] + + +@router.post("", response_model=UserOut, status_code=status.HTTP_201_CREATED) +async def create_user( + user_data: UserCreate, + user: dict = Depends(require_admin), + db_session: AsyncSession = Depends(get_db), +): + """Crée un nouvel utilisateur (admin uniquement).""" + repo = UserRepository(db_session) + + # Vérifier l'unicité du username + existing = await repo.get_by_username(user_data.username) + if existing: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail=f"Le nom d'utilisateur '{user_data.username}' est déjà utilisé", + ) + + # Vérifier l'unicité de l'email si fourni + if user_data.email: + existing_email = await repo.get_by_email(user_data.email) + if existing_email: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail="Cette adresse email est déjà utilisée", + ) + + hashed_password = hash_password(user_data.password) + new_user = await repo.create( + username=user_data.username, + hashed_password=hashed_password, + email=user_data.email, + display_name=user_data.display_name, + role=user_data.role, + is_active=user_data.is_active, + ) + await db_session.commit() + await db_session.refresh(new_user) + + return UserOut.model_validate(new_user) + + +@router.get("/{user_id}", response_model=UserOut) +async def get_user( + user_id: int, + user: dict = Depends(require_admin), + db_session: AsyncSession = Depends(get_db), +): + """Récupère les détails d'un utilisateur (admin uniquement).""" + repo = UserRepository(db_session) + target_user = await repo.get(user_id) + + if not target_user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Utilisateur non trouvé", + ) + + return UserOut.model_validate(target_user) + + +@router.put("/{user_id}", response_model=UserOut) +async def update_user( + user_id: int, + user_data: UserUpdate, + user: dict = Depends(require_admin), + db_session: AsyncSession = Depends(get_db), +): + """Met à jour un utilisateur (admin uniquement).""" + repo = UserRepository(db_session) + target_user = await repo.get(user_id) + + if not target_user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Utilisateur non trouvé", + ) + + update_fields = user_data.model_dump(exclude_unset=True) + if update_fields: + await repo.update(target_user, **update_fields) + await db_session.commit() + await db_session.refresh(target_user) + + return UserOut.model_validate(target_user) + + +@router.delete("/{user_id}") +async def delete_user( + user_id: int, + permanent: bool = False, + user: dict = Depends(require_admin), + db_session: AsyncSession = Depends(get_db), +): + """Désactive ou supprime un utilisateur (admin uniquement).""" + repo = UserRepository(db_session) + target_user = await repo.get(user_id, include_deleted=True) + + if not target_user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Utilisateur non trouvé", + ) + + # Empêcher la suppression de soi-même + current_user_id = user.get("user_id") + if current_user_id and int(current_user_id) == user_id: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Vous ne pouvez pas supprimer votre propre compte", + ) + + if permanent: + await repo.hard_delete(user_id) + else: + await repo.soft_delete(user_id) + + await db_session.commit() + + return { + "message": f"Utilisateur {'supprimé définitivement' if permanent else 'désactivé'}", + "user_id": user_id, + } diff --git a/app/services/docker_actions.py b/app/services/docker_actions.py index 94545b1..60a0061 100644 --- a/app/services/docker_actions.py +++ b/app/services/docker_actions.py @@ -613,5 +613,58 @@ class DockerActionsService: } + async def list_networks( + self, + host_id: str, + ) -> Dict[str, Any]: + """List Docker networks on a host.""" + import json as json_lib + host = await self._get_host_info(host_id) + + try: + conn = await self._ssh_connect(host["ip"]) + try: + use_sudo_state: Dict[str, Optional[bool]] = {"value": None} + result = await self._run_docker( + conn, + 'docker network ls --format "{{json .}}"', + use_sudo_state, + ) + + networks = [] + if result["success"] and result["stdout"].strip(): + for line in result["stdout"].strip().splitlines(): + line = line.strip() + if not line: + continue + try: + net = json_lib.loads(line) + networks.append({ + "id": net.get("ID", ""), + "name": net.get("Name", ""), + "driver": net.get("Driver", ""), + "scope": net.get("Scope", ""), + }) + except json_lib.JSONDecodeError: + continue + + return { + "host_id": host_id, + "host_name": host["name"], + "networks": networks, + "total": len(networks), + } + finally: + conn.close() + except DockerActionError as e: + return { + "host_id": host_id, + "host_name": host.get("name", ""), + "networks": [], + "total": 0, + "error": str(e), + } + + # Singleton instance docker_actions = DockerActionsService() diff --git a/app/static/help.md b/app/static/help.md index 9d1ad65..f868490 100644 --- a/app/static/help.md +++ b/app/static/help.md @@ -251,24 +251,1201 @@ Sauvegarde les fichiers de configuration importants (/etc, configs apps). ## 🔗 Référence API {#help-api} -L'API REST est accessible sur le port configuré. Authentification via header `Authorization: Bearer `. +L'API REST est accessible sur le port configuré (par défaut `8000`). Tous les endpoints (sauf indications contraires) nécessitent un header d'authentification : `Authorization: Bearer `. -| Endpoint | Méthode | Description | -|----------|---------|-------------| -| `/api/hosts` | GET | Liste tous les hosts | -| `/api/hosts` | POST | Ajoute un nouveau host | -| `/api/hosts/{id}` | PUT | Modifie un host existant | -| `/api/hosts/{id}` | DELETE | Supprime un host | -| `/api/tasks/logs` | GET | Récupère les logs de tâches | -| `/api/ansible/playbooks` | GET | Liste les playbooks disponibles | -| `/api/ansible/execute` | POST | Exécute un playbook | -| `/api/schedules` | GET | Liste les planifications | -| `/api/schedules` | POST | Crée une planification | -| `/api/monitoring` | GET | Métriques du dashboard | -| `/api/auth/login` | POST | Authentification utilisateur | -| `/api/auth/me` | GET | Informations utilisateur courant | +### 📖 Exemples d'utilisation + +:::accordion fa-terminal gray Ad-hoc +Exemples d'utilisation pour les endpoints de type **Ad-hoc**. + +- **GET `/api/adhoc/categories`** : Récupère la liste des catégories de commandes ad-hoc. +```bash +curl -X GET "http://localhost:8000/api/adhoc/categories" \ + -H "Authorization: Bearer " +``` + +- **POST `/api/adhoc/categories`** : Crée une nouvelle catégorie de commandes ad-hoc. +```bash +curl -X POST "http://localhost:8000/api/adhoc/categories" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{}' +``` + +- **PUT `/api/adhoc/categories/{category_name}`** : Met à jour une catégorie existante. +```bash +curl -X PUT "http://localhost:8000/api/adhoc/categories/" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{}' +``` + +- **DELETE `/api/adhoc/categories/{category_name}`** : Supprime une catégorie et déplace ses commandes vers 'default'. +```bash +curl -X DELETE "http://localhost:8000/api/adhoc/categories/" \ + -H "Authorization: Bearer " +``` + +- **GET `/api/adhoc/history`** : Récupère l'historique des commandes ad-hoc. +```bash +curl -X GET "http://localhost:8000/api/adhoc/history" \ + -H "Authorization: Bearer " +``` + +- **DELETE `/api/adhoc/history/{command_id}`** : Supprime une commande de l'historique. +```bash +curl -X DELETE "http://localhost:8000/api/adhoc/history/" \ + -H "Authorization: Bearer " +``` + +- **PUT `/api/adhoc/history/{command_id}/category`** : Met à jour la catégorie d'une commande dans l'historique. +```bash +curl -X PUT "http://localhost:8000/api/adhoc/history//category" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{}' +``` +::: + +:::accordion fa-exclamation-triangle orange Alerts +Exemples d'utilisation pour les endpoints de type **Alerts**. + +- **POST `/api/alerts`** : Crée une nouvelle alerte. +```bash +curl -X POST "http://localhost:8000/api/alerts" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{}' +``` + +- **GET `/api/alerts`** : Récupère les alertes avec pagination. +```bash +curl -X GET "http://localhost:8000/api/alerts" \ + -H "Authorization: Bearer " +``` + +- **POST `/api/alerts/mark-all-read`** : Marque toutes les alertes comme lues. +```bash +curl -X POST "http://localhost:8000/api/alerts/mark-all-read" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{}' +``` + +- **GET `/api/alerts/unread-count`** : Récupère le nombre d'alertes non lues. +```bash +curl -X GET "http://localhost:8000/api/alerts/unread-count" \ + -H "Authorization: Bearer " +``` + +- **DELETE `/api/alerts/{alert_id}`** : Supprime une alerte. +```bash +curl -X DELETE "http://localhost:8000/api/alerts/" \ + -H "Authorization: Bearer " +``` + +- **POST `/api/alerts/{alert_id}/read`** : Marque une alerte comme lue. +```bash +curl -X POST "http://localhost:8000/api/alerts//read" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{}' +``` +::: + +:::accordion fa-cogs red Ansible +Exemples d'utilisation pour les endpoints de type **Ansible**. + +- **POST `/api/ansible/adhoc`** : Exécute une commande ad-hoc Ansible. +```bash +curl -X POST "http://localhost:8000/api/ansible/adhoc" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{}' +``` + +- **POST `/api/ansible/bootstrap`** : Compat: Bootstrap un hôte via /api/ansible/bootstrap (historique UI). +```bash +curl -X POST "http://localhost:8000/api/ansible/bootstrap" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{}' +``` + +- **POST `/api/ansible/execute`** : Exécute un playbook Ansible. +```bash +curl -X POST "http://localhost:8000/api/ansible/execute" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{}' +``` + +- **GET `/api/ansible/groups`** : Récupère la liste des groupes Ansible. +```bash +curl -X GET "http://localhost:8000/api/ansible/groups" \ + -H "Authorization: Bearer " +``` + +- **GET `/api/ansible/inventory`** : Récupère l'inventaire Ansible. +```bash +curl -X GET "http://localhost:8000/api/ansible/inventory" \ + -H "Authorization: Bearer " +``` + +- **GET `/api/ansible/playbooks`** : Liste les playbooks Ansible disponibles. +```bash +curl -X GET "http://localhost:8000/api/ansible/playbooks" \ + -H "Authorization: Bearer " +``` + +- **GET `/api/ansible/ssh-config`** : Diagnostic de la configuration SSH. +```bash +curl -X GET "http://localhost:8000/api/ansible/ssh-config" \ + -H "Authorization: Bearer " +``` +::: + +:::accordion fa-lock purple Auth +Exemples d'utilisation pour les endpoints de type **Auth**. + +- **POST `/api/auth/login`** : Connexion via formulaire OAuth2 (form-urlencoded). +```bash +curl -X POST "http://localhost:8000/api/auth/login" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "username=admin&password=password" +``` + +- **POST `/api/auth/login/json`** : Connexion via JSON body. +```bash +curl -X POST "http://localhost:8000/api/auth/login/json" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "username=admin&password=password" +``` + +- **GET `/api/auth/me`** : Récupère les informations de l'utilisateur connecté. +```bash +curl -X GET "http://localhost:8000/api/auth/me" \ + -H "Authorization: Bearer " +``` + +- **PUT `/api/auth/password`** : Change le mot de passe de l'utilisateur connecté. +```bash +curl -X PUT "http://localhost:8000/api/auth/password" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{}' +``` + +- **POST `/api/auth/setup`** : Crée le premier utilisateur admin (uniquement si aucun utilisateur n'existe). +```bash +curl -X POST "http://localhost:8000/api/auth/setup" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{}' +``` + +- **GET `/api/auth/status`** : Vérifie le statut d'authentification et si le setup initial est requis. +```bash +curl -X GET "http://localhost:8000/api/auth/status" \ + -H "Authorization: Bearer " +``` +::: + +:::accordion fa-rocket yellow Bootstrap +Exemples d'utilisation pour les endpoints de type **Bootstrap**. + +- **POST `/api/bootstrap`** : Bootstrap un hôte pour Ansible. +```bash +curl -X POST "http://localhost:8000/api/bootstrap" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{}' +``` + +- **GET `/api/bootstrap/status`** : Récupère le statut de bootstrap de tous les hôtes. +```bash +curl -X GET "http://localhost:8000/api/bootstrap/status" \ + -H "Authorization: Bearer " +``` + +- **GET `/api/bootstrap/status/{host_name}`** : Récupère le statut de bootstrap d'un hôte spécifique. +```bash +curl -X GET "http://localhost:8000/api/bootstrap/status/" \ + -H "Authorization: Bearer " +``` + +- **POST `/api/bootstrap/status/{host_name}`** : Définit manuellement le statut de bootstrap d'un hôte. +```bash +curl -X POST "http://localhost:8000/api/bootstrap/status/" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{}' +``` +::: + +:::accordion fa-book-medical rose Builtin Playbooks +Exemples d'utilisation pour les endpoints de type **Builtin Playbooks**. + +- **GET `/api/builtin-playbooks`** : Liste tous les builtin playbooks disponibles. +```bash +curl -X GET "http://localhost:8000/api/builtin-playbooks" \ + -H "Authorization: Bearer " +``` + +- **POST `/api/builtin-playbooks/collect-all`** : Collecte les métriques de tous les hôtes. +```bash +curl -X POST "http://localhost:8000/api/builtin-playbooks/collect-all" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{}' +``` + +- **POST `/api/builtin-playbooks/execute`** : Exécute un builtin playbook sur une cible. +```bash +curl -X POST "http://localhost:8000/api/builtin-playbooks/execute" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{}' +``` + +- **POST `/api/builtin-playbooks/execute-background`** : Exécute un builtin playbook en arrière-plan. +```bash +curl -X POST "http://localhost:8000/api/builtin-playbooks/execute-background" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{}' +``` + +- **GET `/api/builtin-playbooks/{builtin_id}`** : Récupère les détails d'un builtin playbook. +```bash +curl -X GET "http://localhost:8000/api/builtin-playbooks/" \ + -H "Authorization: Bearer " +``` +::: + +:::accordion fa-cog gray Config +Exemples d'utilisation pour les endpoints de type **Config**. + +- **GET `/api/config`** : Récupère la configuration publique de l'application. +```bash +curl -X GET "http://localhost:8000/api/config" \ + -H "Authorization: Bearer " +``` + +- **GET `/api/config/version`** : Retourne la version de l'application, Python, et l'uptime. +```bash +curl -X GET "http://localhost:8000/api/config/version" \ + -H "Authorization: Bearer " +``` +::: + +:::accordion fab fa-docker cyan Docker +Exemples d'utilisation pour les endpoints de type **Docker**. + +- **GET `/api/docker/alerts`** : List Docker alerts. +```bash +curl -X GET "http://localhost:8000/api/docker/alerts" \ + -H "Authorization: Bearer " +``` + +- **POST `/api/docker/alerts/{alert_id}/acknowledge`** : Acknowledge an alert. +```bash +curl -X POST "http://localhost:8000/api/docker/alerts//acknowledge" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{}' +``` + +- **POST `/api/docker/alerts/{alert_id}/close`** : Close an alert manually. +```bash +curl -X POST "http://localhost:8000/api/docker/alerts//close" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{}' +``` + +- **POST `/api/docker/collect-all`** : Collect Docker info from all enabled hosts. +```bash +curl -X POST "http://localhost:8000/api/docker/collect-all" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{}' +``` + +- **GET `/api/docker/container-customizations`** : Mise à jour / consultation de la ressource. +```bash +curl -X GET "http://localhost:8000/api/docker/container-customizations" \ + -H "Authorization: Bearer " +``` + +- **PUT `/api/docker/container-customizations/{host_id}/{container_id}`** : Mise à jour / consultation de la ressource. +```bash +curl -X PUT "http://localhost:8000/api/docker/container-customizations//" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{}' +``` + +- **DELETE `/api/docker/container-customizations/{host_id}/{container_id}`** : Mise à jour / consultation de la ressource. +```bash +curl -X DELETE "http://localhost:8000/api/docker/container-customizations//" \ + -H "Authorization: Bearer " +``` + +- **GET `/api/docker/containers`** : List all containers across all Docker hosts. +```bash +curl -X GET "http://localhost:8000/api/docker/containers" \ + -H "Authorization: Bearer " +``` + +- **GET `/api/docker/containers/{host_id}/{container_id}/inspect`** : Get detailed container information. +```bash +curl -X GET "http://localhost:8000/api/docker/containers///inspect" \ + -H "Authorization: Bearer " +``` + +- **GET `/api/docker/containers/{host_id}/{container_id}/logs`** : Get container logs. +```bash +curl -X GET "http://localhost:8000/api/docker/containers///logs" \ + -H "Authorization: Bearer " +``` + +- **POST `/api/docker/containers/{host_id}/{container_id}/redeploy`** : Redeploy a container by pulling latest image. +```bash +curl -X POST "http://localhost:8000/api/docker/containers///redeploy" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{}' +``` + +- **POST `/api/docker/containers/{host_id}/{container_id}/remove`** : Remove a container (admin only). +```bash +curl -X POST "http://localhost:8000/api/docker/containers///remove" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{}' +``` + +- **POST `/api/docker/containers/{host_id}/{container_id}/restart`** : Restart a container. +```bash +curl -X POST "http://localhost:8000/api/docker/containers///restart" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{}' +``` + +- **POST `/api/docker/containers/{host_id}/{container_id}/start`** : Start a stopped container. +```bash +curl -X POST "http://localhost:8000/api/docker/containers///start" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{}' +``` + +- **POST `/api/docker/containers/{host_id}/{container_id}/stop`** : Stop a running container. +```bash +curl -X POST "http://localhost:8000/api/docker/containers///stop" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{}' +``` + +- **GET `/api/docker/hosts`** : List all hosts with Docker information. +```bash +curl -X GET "http://localhost:8000/api/docker/hosts" \ + -H "Authorization: Bearer " +``` + +- **POST `/api/docker/hosts/{host_id}/collect`** : Force an immediate Docker collection on a host. +```bash +curl -X POST "http://localhost:8000/api/docker/hosts//collect" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{}' +``` + +- **GET `/api/docker/hosts/{host_id}/containers`** : List containers for a host. +```bash +curl -X GET "http://localhost:8000/api/docker/hosts//containers" \ + -H "Authorization: Bearer " +``` + +- **POST `/api/docker/hosts/{host_id}/enable`** : Enable or disable Docker monitoring on a host. +```bash +curl -X POST "http://localhost:8000/api/docker/hosts//enable" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{}' +``` + +- **GET `/api/docker/hosts/{host_id}/images`** : List images for a host with usage information. +```bash +curl -X GET "http://localhost:8000/api/docker/hosts//images" \ + -H "Authorization: Bearer " +``` + +- **GET `/api/docker/hosts/{host_id}/networks`** : Liste les réseaux Docker d'un hôte. +```bash +curl -X GET "http://localhost:8000/api/docker/hosts//networks" \ + -H "Authorization: Bearer " +``` + +- **GET `/api/docker/hosts/{host_id}/volumes`** : List volumes for a host. +```bash +curl -X GET "http://localhost:8000/api/docker/hosts//volumes" \ + -H "Authorization: Bearer " +``` + +- **DELETE `/api/docker/images/{host_id}/{image_id}`** : Remove a Docker image (admin only). +```bash +curl -X DELETE "http://localhost:8000/api/docker/images//" \ + -H "Authorization: Bearer " +``` + +- **GET `/api/docker/stats`** : Get global Docker statistics. +```bash +curl -X GET "http://localhost:8000/api/docker/stats" \ + -H "Authorization: Bearer " +``` +::: + +:::accordion fa-star yellow Favorites +Exemples d'utilisation pour les endpoints de type **Favorites**. + +- **GET `/api/favorites/containers`** : Mise à jour / consultation de la ressource. +```bash +curl -X GET "http://localhost:8000/api/favorites/containers" \ + -H "Authorization: Bearer " +``` + +- **POST `/api/favorites/containers`** : Mise à jour / consultation de la ressource. +```bash +curl -X POST "http://localhost:8000/api/favorites/containers" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{}' +``` + +- **DELETE `/api/favorites/containers/{favorite_id}`** : Mise à jour / consultation de la ressource. +```bash +curl -X DELETE "http://localhost:8000/api/favorites/containers/" \ + -H "Authorization: Bearer " +``` + +- **GET `/api/favorites/groups`** : Mise à jour / consultation de la ressource. +```bash +curl -X GET "http://localhost:8000/api/favorites/groups" \ + -H "Authorization: Bearer " +``` + +- **POST `/api/favorites/groups`** : Mise à jour / consultation de la ressource. +```bash +curl -X POST "http://localhost:8000/api/favorites/groups" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{}' +``` + +- **PATCH `/api/favorites/groups/{group_id}`** : Mise à jour / consultation de la ressource. +```bash +curl -X PATCH "http://localhost:8000/api/favorites/groups/" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{}' +``` + +- **DELETE `/api/favorites/groups/{group_id}`** : Mise à jour / consultation de la ressource. +```bash +curl -X DELETE "http://localhost:8000/api/favorites/groups/" \ + -H "Authorization: Bearer " +``` +::: + +:::accordion fa-layer-group indigo Groups +Exemples d'utilisation pour les endpoints de type **Groups**. + +- **GET `/api/groups`** : Récupère la liste de tous les groupes Ansible. +```bash +curl -X GET "http://localhost:8000/api/groups" \ + -H "Authorization: Bearer " +``` + +- **POST `/api/groups`** : Crée un nouveau groupe. +```bash +curl -X POST "http://localhost:8000/api/groups" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{}' +``` + +- **GET `/api/groups/{group_name}`** : Récupère les détails d'un groupe. +```bash +curl -X GET "http://localhost:8000/api/groups/" \ + -H "Authorization: Bearer " +``` + +- **PUT `/api/groups/{group_name}`** : Renomme un groupe. +```bash +curl -X PUT "http://localhost:8000/api/groups/" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{}' +``` + +- **DELETE `/api/groups/{group_name}`** : Supprime un groupe. +```bash +curl -X DELETE "http://localhost:8000/api/groups/" \ + -H "Authorization: Bearer " +``` +::: + +:::accordion fa-heartbeat green Health +Exemples d'utilisation pour les endpoints de type **Health**. + +- **GET `/api/health`** : Récupère les métriques système. +```bash +curl -X GET "http://localhost:8000/api/health" \ + -H "Authorization: Bearer " +``` + +- **POST `/api/health/check-all`** : Exécute un health check sur tous les hôtes en parallèle. +```bash +curl -X POST "http://localhost:8000/api/health/check-all" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{}' +``` + +- **GET `/api/health/global`** : Endpoint de healthcheck global utilisé par Docker. +```bash +curl -X GET "http://localhost:8000/api/health/global" \ +``` + +- **POST `/api/health/refresh`** : Force le rechargement des hôtes depuis l'inventaire Ansible. +```bash +curl -X POST "http://localhost:8000/api/health/refresh" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{}' +``` + +- **GET `/api/health/runtime`** : Runtime diagnostics (python executable, asyncssh availability). +```bash +curl -X GET "http://localhost:8000/api/health/runtime" \ + -H "Authorization: Bearer " +``` + +- **GET `/api/health/{host_name}`** : Effectue un health check sur un hôte spécifique. +```bash +curl -X GET "http://localhost:8000/api/health/" \ + -H "Authorization: Bearer " +``` +::: + +:::accordion fa-question-circle cyan Help +Exemples d'utilisation pour les endpoints de type **Help**. + +- **GET `/api/help/catalog`** : Retourne un catalogue de tous les points d'entrée de l'API en format JSON. +```bash +curl -X GET "http://localhost:8000/api/help/catalog" \ + -H "Authorization: Bearer " +``` + +- **GET `/api/help/content`** : Retourne le contenu HTML de la page d'aide généré depuis help.md. +```bash +curl -X GET "http://localhost:8000/api/help/content" \ + -H "Authorization: Bearer " +``` + +- **GET `/api/help/documentation.md`** : Télécharge la documentation d'aide en format Markdown. +```bash +curl -X GET "http://localhost:8000/api/help/documentation.md" \ + -H "Authorization: Bearer " +``` + +- **GET `/api/help/documentation.pdf`** : Télécharge la documentation d'aide en format PDF. +```bash +curl -X GET "http://localhost:8000/api/help/documentation.pdf" \ + -H "Authorization: Bearer " +``` +::: + +:::accordion fa-server blue Hosts +Exemples d'utilisation pour les endpoints de type **Hosts**. + +- **GET `/api/hosts`** : Récupère la liste des hôtes. +```bash +curl -X GET "http://localhost:8000/api/hosts" \ + -H "Authorization: Bearer " +``` + +- **POST `/api/hosts`** : Crée un nouvel hôte. +```bash +curl -X POST "http://localhost:8000/api/hosts" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{}' +``` + +- **GET `/api/hosts/by-name/{host_name}`** : Récupère un hôte par son nom. +```bash +curl -X GET "http://localhost:8000/api/hosts/by-name/" \ + -H "Authorization: Bearer " +``` + +- **DELETE `/api/hosts/by-name/{host_name}`** : Supprime un hôte par son nom. +```bash +curl -X DELETE "http://localhost:8000/api/hosts/by-name/" \ + -H "Authorization: Bearer " +``` + +- **GET `/api/hosts/groups`** : Récupère la liste des groupes disponibles pour les hôtes. +```bash +curl -X GET "http://localhost:8000/api/hosts/groups" \ + -H "Authorization: Bearer " +``` + +- **POST `/api/hosts/refresh`** : Force le rechargement des hôtes depuis l'inventaire Ansible. +```bash +curl -X POST "http://localhost:8000/api/hosts/refresh" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{}' +``` + +- **POST `/api/hosts/sync`** : Synchronise les hôtes depuis l'inventaire Ansible vers la base de données. +```bash +curl -X POST "http://localhost:8000/api/hosts/sync" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{}' +``` + +- **GET `/api/hosts/{host_id}`** : Récupère un hôte par son ID. +```bash +curl -X GET "http://localhost:8000/api/hosts/" \ + -H "Authorization: Bearer " +``` + +- **DELETE `/api/hosts/{host_id}`** : Supprime un hôte par son ID. +```bash +curl -X DELETE "http://localhost:8000/api/hosts/" \ + -H "Authorization: Bearer " +``` + +- **PUT `/api/hosts/{host_name}`** : Met à jour un hôte existant. +```bash +curl -X PUT "http://localhost:8000/api/hosts/" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{}' +``` +::: + +:::accordion fa-broom indigo Lint +Exemples d'utilisation pour les endpoints de type **Lint**. + +- **GET `/api/playbooks/results`** : Récupère tous les résultats de lint stockés en base de données. +```bash +curl -X GET "http://localhost:8000/api/playbooks/results" \ + -H "Authorization: Bearer " +``` + +- **GET `/api/playbooks/results/{filename}`** : Récupère le dernier résultat de lint pour un playbook spécifique. +```bash +curl -X GET "http://localhost:8000/api/playbooks/results/" \ + -H "Authorization: Bearer " +``` + +- **GET `/api/playbooks/rules`** : Liste les règles ansible-lint disponibles. +```bash +curl -X GET "http://localhost:8000/api/playbooks/rules" \ + -H "Authorization: Bearer " +``` + +- **POST `/api/playbooks/{filename}/lint`** : Exécute ansible-lint sur le contenu d'un playbook. +```bash +curl -X POST "http://localhost:8000/api/playbooks//lint" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{}' +``` +::: + +:::accordion fa-file-alt orange Logs +Exemples d'utilisation pour les endpoints de type **Logs**. + +- **GET `/api/logs`** : Récupère les logs récents avec filtrage optionnel. +```bash +curl -X GET "http://localhost:8000/api/logs" \ + -H "Authorization: Bearer " +``` + +- **POST `/api/logs`** : Ajoute une nouvelle entrée de log. +```bash +curl -X POST "http://localhost:8000/api/logs" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{}' +``` + +- **DELETE `/api/logs`** : Efface tous les logs (attention: opération destructive). +```bash +curl -X DELETE "http://localhost:8000/api/logs" \ + -H "Authorization: Bearer " +``` +::: + +:::accordion fa-chart-line purple Metrics +Exemples d'utilisation pour les endpoints de type **Metrics**. + +- **GET `/api/monitoring`** : Récupère les métriques système globales. +```bash +curl -X GET "http://localhost:8000/api/monitoring" \ + -H "Authorization: Bearer " +``` + +- **GET `/api/monitoring/all-hosts`** : Récupère les métriques de tous les hôtes. +```bash +curl -X GET "http://localhost:8000/api/monitoring/all-hosts" \ + -H "Authorization: Bearer " +``` + +- **GET `/api/monitoring/collection-schedule`** : Récupère l'intervalle de collecte des métriques. +```bash +curl -X GET "http://localhost:8000/api/monitoring/collection-schedule" \ + -H "Authorization: Bearer " +``` + +- **POST `/api/monitoring/collection-schedule`** : Définit l'intervalle de collecte des métriques. +```bash +curl -X POST "http://localhost:8000/api/monitoring/collection-schedule" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{}' +``` + +- **GET `/api/monitoring/{host_id}`** : Récupère les métriques d'un hôte spécifique. +```bash +curl -X GET "http://localhost:8000/api/monitoring/" \ + -H "Authorization: Bearer " +``` +::: + +:::accordion fa-bell red Notifications +Exemples d'utilisation pour les endpoints de type **Notifications**. + +- **GET `/api/notifications/config`** : Récupère la configuration actuelle des notifications ntfy. +```bash +curl -X GET "http://localhost:8000/api/notifications/config" \ + -H "Authorization: Bearer " +``` + +- **PUT `/api/notifications/config`** : Met à jour la configuration des notifications ntfy. +```bash +curl -X PUT "http://localhost:8000/api/notifications/config" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{}' +``` + +- **POST `/api/notifications/send`** : Envoie une notification personnalisée via ntfy. +```bash +curl -X POST "http://localhost:8000/api/notifications/send" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{}' +``` + +- **POST `/api/notifications/test`** : Envoie une notification de test. +```bash +curl -X POST "http://localhost:8000/api/notifications/test" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{}' +``` + +- **POST `/api/notifications/toggle`** : Active ou désactive les notifications ntfy. +```bash +curl -X POST "http://localhost:8000/api/notifications/toggle" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{}' +``` +::: + +:::accordion fa-book pink Playbooks +Exemples d'utilisation pour les endpoints de type **Playbooks**. + +- **GET `/api/playbooks`** : Liste tous les playbooks avec métadonnées (taille, date, etc.). +```bash +curl -X GET "http://localhost:8000/api/playbooks" \ + -H "Authorization: Bearer " +``` + +- **DELETE `/api/playbooks/{filename}`** : Supprime un playbook. +```bash +curl -X DELETE "http://localhost:8000/api/playbooks/" \ + -H "Authorization: Bearer " +``` + +- **GET `/api/playbooks/{filename}/content`** : Récupère le contenu d'un playbook (normal ou builtin). +```bash +curl -X GET "http://localhost:8000/api/playbooks//content" \ + -H "Authorization: Bearer " +``` + +- **PUT `/api/playbooks/{filename}/content`** : Sauvegarde le contenu d'un playbook. +```bash +curl -X PUT "http://localhost:8000/api/playbooks//content" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{}' +``` +::: + +:::accordion fa-calendar-alt indigo Schedules +Exemples d'utilisation pour les endpoints de type **Schedules**. + +- **GET `/api/schedules`** : Liste tous les schedules. +```bash +curl -X GET "http://localhost:8000/api/schedules" \ + -H "Authorization: Bearer " +``` + +- **POST `/api/schedules`** : Crée un nouveau schedule. +```bash +curl -X POST "http://localhost:8000/api/schedules" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{}' +``` + +- **GET `/api/schedules/stats`** : Récupère les statistiques des schedules. +```bash +curl -X GET "http://localhost:8000/api/schedules/stats" \ + -H "Authorization: Bearer " +``` + +- **GET `/api/schedules/upcoming`** : Récupère les prochaines exécutions planifiées. +```bash +curl -X GET "http://localhost:8000/api/schedules/upcoming" \ + -H "Authorization: Bearer " +``` + +- **GET `/api/schedules/validate-cron`** : Valide une expression cron. +```bash +curl -X GET "http://localhost:8000/api/schedules/validate-cron" \ + -H "Authorization: Bearer " +``` + +- **GET `/api/schedules/{schedule_id}`** : Récupère les détails d'un schedule. +```bash +curl -X GET "http://localhost:8000/api/schedules/" \ + -H "Authorization: Bearer " +``` + +- **PUT `/api/schedules/{schedule_id}`** : Met à jour un schedule existant. +```bash +curl -X PUT "http://localhost:8000/api/schedules/" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{}' +``` + +- **DELETE `/api/schedules/{schedule_id}`** : Supprime un schedule. +```bash +curl -X DELETE "http://localhost:8000/api/schedules/" \ + -H "Authorization: Bearer " +``` + +- **POST `/api/schedules/{schedule_id}/pause`** : Met en pause un schedule. +```bash +curl -X POST "http://localhost:8000/api/schedules//pause" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{}' +``` + +- **POST `/api/schedules/{schedule_id}/resume`** : Reprend un schedule en pause. +```bash +curl -X POST "http://localhost:8000/api/schedules//resume" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{}' +``` + +- **POST `/api/schedules/{schedule_id}/run`** : Exécute immédiatement un schedule. +```bash +curl -X POST "http://localhost:8000/api/schedules//run" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{}' +``` + +- **GET `/api/schedules/{schedule_id}/runs`** : Récupère l'historique des exécutions d'un schedule. +```bash +curl -X GET "http://localhost:8000/api/schedules//runs" \ + -H "Authorization: Bearer " +``` +::: + +:::accordion fa-hdd blue Server +Exemples d'utilisation pour les endpoints de type **Server**. + +- **GET `/api/server/logs`** : Récupère les logs serveur avec pagination. +```bash +curl -X GET "http://localhost:8000/api/server/logs" \ + -H "Authorization: Bearer " +``` +::: + +:::accordion fa-tasks green Tasks +Exemples d'utilisation pour les endpoints de type **Tasks**. + +- **GET `/api/tasks`** : Récupère la liste des tâches. +```bash +curl -X GET "http://localhost:8000/api/tasks" \ + -H "Authorization: Bearer " +``` + +- **POST `/api/tasks`** : Crée une nouvelle tâche et exécute le playbook correspondant. +```bash +curl -X POST "http://localhost:8000/api/tasks" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{}' +``` + +- **GET `/api/tasks/logs`** : Récupère les logs de tâches depuis les fichiers markdown. +```bash +curl -X GET "http://localhost:8000/api/tasks/logs" \ + -H "Authorization: Bearer " +``` + +- **GET `/api/tasks/logs/dates`** : Récupère les dates disponibles pour le filtrage. +```bash +curl -X GET "http://localhost:8000/api/tasks/logs/dates" \ + -H "Authorization: Bearer " +``` + +- **GET `/api/tasks/logs/stats`** : Récupère les statistiques des logs de tâches. +```bash +curl -X GET "http://localhost:8000/api/tasks/logs/stats" \ + -H "Authorization: Bearer " +``` + +- **GET `/api/tasks/logs/{log_id}`** : Récupère le contenu d'un log de tâche spécifique. +```bash +curl -X GET "http://localhost:8000/api/tasks/logs/" \ + -H "Authorization: Bearer " +``` + +- **DELETE `/api/tasks/logs/{log_id}`** : Supprime un fichier de log de tâche. +```bash +curl -X DELETE "http://localhost:8000/api/tasks/logs/" \ + -H "Authorization: Bearer " +``` + +- **GET `/api/tasks/running`** : Récupère les tâches en cours d'exécution. +```bash +curl -X GET "http://localhost:8000/api/tasks/running" \ + -H "Authorization: Bearer " +``` + +- **GET `/api/tasks/{task_id}`** : Récupère une tâche spécifique. +```bash +curl -X GET "http://localhost:8000/api/tasks/" \ + -H "Authorization: Bearer " +``` + +- **DELETE `/api/tasks/{task_id}`** : Supprime une tâche. +```bash +curl -X DELETE "http://localhost:8000/api/tasks/" \ + -H "Authorization: Bearer " +``` + +- **POST `/api/tasks/{task_id}/cancel`** : Annule une tâche en cours d'exécution. +```bash +curl -X POST "http://localhost:8000/api/tasks//cancel" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{}' +``` +::: + +:::accordion fa-terminal gray Terminal +Exemples d'utilisation pour les endpoints de type **Terminal**. + +- **POST `/api/terminal/cleanup`** : Mise à jour / consultation de la ressource. +```bash +curl -X POST "http://localhost:8000/api/terminal/cleanup" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{}' +``` + +- **GET `/api/terminal/command-history`** : Get command history globally (across all hosts). +```bash +curl -X GET "http://localhost:8000/api/terminal/command-history" \ + -H "Authorization: Bearer " +``` + +- **POST `/api/terminal/command-history/purge`** : Purge command history older than specified days. +```bash +curl -X POST "http://localhost:8000/api/terminal/command-history/purge" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{}' +``` + +- **GET `/api/terminal/connect/{session_id}`** : Mise à jour / consultation de la ressource. +```bash +curl -X GET "http://localhost:8000/api/terminal/connect/" \ + -H "Authorization: Bearer " +``` + +- **GET `/api/terminal/popout/{session_id}`** : Serve the terminal popout page (fullscreen, minimal UI). +```bash +curl -X GET "http://localhost:8000/api/terminal/popout/" \ + -H "Authorization: Bearer " +``` + +- **GET `/api/terminal/proxy/{session_id}`** : Mise à jour / consultation de la ressource. +```bash +curl -X GET "http://localhost:8000/api/terminal/proxy/" \ + -H "Authorization: Bearer " +``` + +- **POST, PUT, DELETE, GET, PATCH `/api/terminal/proxy/{session_id}/{proxy_path:path}`** : Mise à jour / consultation de la ressource. +```bash +curl -X POST "http://localhost:8000/api/terminal/proxy//" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{}' +``` + +- **DELETE `/api/terminal/sessions/{session_id}`** : Mise à jour / consultation de la ressource. +```bash +curl -X DELETE "http://localhost:8000/api/terminal/sessions/" \ + -H "Authorization: Bearer " +``` + +- **POST `/api/terminal/sessions/{session_id}/close-beacon`** : Mise à jour / consultation de la ressource. +```bash +curl -X POST "http://localhost:8000/api/terminal/sessions//close-beacon" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{}' +``` + +- **POST `/api/terminal/sessions/{session_id}/command`** : Mise à jour / consultation de la ressource. +```bash +curl -X POST "http://localhost:8000/api/terminal/sessions//command" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{}' +``` + +- **POST `/api/terminal/sessions/{session_id}/heartbeat`** : Mise à jour / consultation de la ressource. +```bash +curl -X POST "http://localhost:8000/api/terminal/sessions//heartbeat" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{}' +``` + +- **GET `/api/terminal/sessions/{session_id}/probe`** : Mise à jour / consultation de la ressource. +```bash +curl -X GET "http://localhost:8000/api/terminal/sessions//probe" \ + -H "Authorization: Bearer " +``` + +- **GET `/api/terminal/status`** : Mise à jour / consultation de la ressource. +```bash +curl -X GET "http://localhost:8000/api/terminal/status" \ + -H "Authorization: Bearer " +``` + +- **GET `/api/terminal/{host_id}/command-history`** : Get command history for a specific host. +```bash +curl -X GET "http://localhost:8000/api/terminal//command-history" \ + -H "Authorization: Bearer " +``` + +- **DELETE `/api/terminal/{host_id}/command-history`** : Clear command history for a specific host. +```bash +curl -X DELETE "http://localhost:8000/api/terminal//command-history" \ + -H "Authorization: Bearer " +``` + +- **GET `/api/terminal/{host_id}/command-history/unique`** : Get unique commands for a host (deduplicated). +```bash +curl -X GET "http://localhost:8000/api/terminal//command-history/unique" \ + -H "Authorization: Bearer " +``` + +- **POST `/api/terminal/{host_id}/command-history/{command_hash}/pin`** : Toggle the pinned status of a specific command across history. +```bash +curl -X POST "http://localhost:8000/api/terminal//command-history//pin" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{}' +``` + +- **GET `/api/terminal/{host_id}/shell-history`** : Get shell history directly from the remote host via SSH. +```bash +curl -X GET "http://localhost:8000/api/terminal//shell-history" \ + -H "Authorization: Bearer " +``` + +- **POST `/api/terminal/{host_id}/terminal-sessions`** : Mise à jour / consultation de la ressource. +```bash +curl -X POST "http://localhost:8000/api/terminal//terminal-sessions" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{}' +``` +::: + +:::accordion fa-users purple Users +Exemples d'utilisation pour les endpoints de type **Users**. + +- **GET `/api/users`** : Liste tous les utilisateurs (admin uniquement). +```bash +curl -X GET "http://localhost:8000/api/users" \ + -H "Authorization: Bearer " +``` + +- **POST `/api/users`** : Crée un nouvel utilisateur (admin uniquement). +```bash +curl -X POST "http://localhost:8000/api/users" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{}' +``` + +- **GET `/api/users/{user_id}`** : Récupère les détails d'un utilisateur (admin uniquement). +```bash +curl -X GET "http://localhost:8000/api/users/" \ + -H "Authorization: Bearer " +``` + +- **PUT `/api/users/{user_id}`** : Met à jour un utilisateur (admin uniquement). +```bash +curl -X PUT "http://localhost:8000/api/users/" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{}' +``` + +- **DELETE `/api/users/{user_id}`** : Désactive ou supprime un utilisateur (admin uniquement). +```bash +curl -X DELETE "http://localhost:8000/api/users/" \ + -H "Authorization: Bearer " +``` +::: ---- ## 🛠️ Dépannage {#help-troubleshooting} diff --git a/app/utils/help_renderer.py b/app/utils/help_renderer.py index ffdd50e..3661eba 100644 --- a/app/utils/help_renderer.py +++ b/app/utils/help_renderer.py @@ -191,7 +191,7 @@ class HelpMarkdownRenderer: def _process_accordions(self, html: str) -> str: """Traite les accordéons.""" - pattern = r':::accordion\s+(fa-[\w-]+)\s+(\w+)\s+([^\n]+)\n(.*?):::' + pattern = r':::accordion\s+((?:fa[bs]?\s+)?fa-[\w-]+)\s+(\w+)\s+([^\n]+)\n(.*?):::' def replace_accordion(match): icon = match.group(1) @@ -385,18 +385,25 @@ class HelpMarkdownRenderer: return '\n'.join(result) def _process_headings(self, html: str) -> str: - """Traite les titres H3 et H4.""" + """Traite les titres H1, H3 et H4.""" + # H1 + html = re.sub( + r'^#\s+([^\n{]+)(?:\s*\{#[^}]+\})?\s*$', + r'

\1

', + html, + flags=re.MULTILINE + ) # H3 html = re.sub( - r'^### ([^\n{]+)$', + r'^###\s+([^\n{]+)(?:\s*\{#[^}]+\})?\s*$', r'

\1

', html, flags=re.MULTILINE ) # H4 html = re.sub( - r'^#### ([^\n]+)$', - r'

\1

', + r'^####\s+([^\n{]+)(?:\s*\{#[^}]+\})?\s*$', + r'

\1

', html, flags=re.MULTILINE ) diff --git a/app/utils/pdf_generator.py b/app/utils/pdf_generator.py index 096973f..1d23183 100644 --- a/app/utils/pdf_generator.py +++ b/app/utils/pdf_generator.py @@ -110,16 +110,41 @@ def emoji_to_png_bytes(emoji: str, size: int = 32) -> Optional[bytes]: return None +import base64 + +def replace_emojis_with_images(text: str) -> str: + """Remplace les emojis par des balises pour ReportLab.""" + emoji_pattern = re.compile( + "[" + "\U0001F600-\U0001F64F" # emoticons + "\U0001F300-\U0001F5FF" # symbols & pictographs + "\U0001F680-\U0001F6FF" # transport & map symbols + "\U0001F1E0-\U0001F1FF" # flags + "\U00002702-\U000027B0" # dingbats + "\U000024C2-\U0001F251" # enclosed characters + "\U0001F900-\U0001F9FF" # supplemental symbols + "\U0001FA00-\U0001FA6F" # chess symbols + "\U0001FA70-\U0001FAFF" # symbols extended + "\U00002600-\U000026FF" # misc symbols + "]" + ) + + def repl(match): + emoji = match.group() + png_bytes = emoji_to_png_bytes(emoji, size=24) + if png_bytes: + b64_img = base64.b64encode(png_bytes).decode('ascii') + # ReportLab image tag must be valid and size specified + return f'' + return emoji + + return emoji_pattern.sub(repl, text) + def markdown_to_pdf_bytes(markdown_content: str, title: str = "Document") -> bytes: - """Convertit du contenu Markdown en PDF. + """Convertit du contenu Markdown en PDF.""" + # Nettoyer les caractères de dessin de boîte pour Courier + markdown_content = markdown_content.replace('├──', '|--').replace('└──', '`--').replace('│', '|') - Args: - markdown_content: Contenu au format Markdown - title: Titre du document - - Returns: - Bytes du PDF généré - """ buffer = io.BytesIO() doc = SimpleDocTemplate( buffer, @@ -130,7 +155,6 @@ def markdown_to_pdf_bytes(markdown_content: str, title: str = "Document") -> byt bottomMargin=2*cm ) - # Styles styles = getSampleStyleSheet() title_style = ParagraphStyle( @@ -282,19 +306,19 @@ def markdown_to_pdf_bytes(markdown_content: str, title: str = "Document") -> byt # Titres if line.startswith('# '): - emojis, text = extract_leading_emojis(line[2:].strip()) - display_text = f"{emojis} {text}".strip() if emojis else text - elements.append(Paragraph(display_text, h1_style)) + text = line[2:].strip() + text = replace_emojis_with_images(text) + elements.append(Paragraph(text, h1_style)) elif line.startswith('## '): - emojis, text = extract_leading_emojis(line[3:].strip()) - display_text = f"{emojis} {text}".strip() if emojis else text - elements.append(Paragraph(display_text, h2_style)) + text = line[3:].strip() + text = replace_emojis_with_images(text) + elements.append(Paragraph(text, h2_style)) elif line.startswith('### '): - emojis, text = extract_leading_emojis(line[4:].strip()) - display_text = f"{emojis} {text}".strip() if emojis else text - elements.append(Paragraph(display_text, h3_style)) + text = line[4:].strip() + text = replace_emojis_with_images(text) + elements.append(Paragraph(text, h3_style)) elif line.startswith('#### '): - text = line[5:].strip() + text = replace_emojis_with_images(line[5:].strip()) elements.append(Paragraph(text, h4_style)) # Listes à puces @@ -303,6 +327,7 @@ def markdown_to_pdf_bytes(markdown_content: str, title: str = "Document") -> byt # Convertir le markdown basique text = re.sub(r'\*\*(.+?)\*\*', r'\1', text) text = re.sub(r'`(.+?)`', r'\1', text) + text = replace_emojis_with_images(text) elements.append(Paragraph(f"• {text}", bullet_style)) # Listes numérotées @@ -333,6 +358,7 @@ def markdown_to_pdf_bytes(markdown_content: str, title: str = "Document") -> byt text = re.sub(r'\*\*(.+?)\*\*', r'\1', text) text = re.sub(r'\*(.+?)\*', r'\1', text) text = re.sub(r'`(.+?)`', r'\1', text) + text = replace_emojis_with_images(text) elements.append(Paragraph(text, body_style)) else: diff --git a/data/homelab.db b/data/homelab.db index 8223fc7d9d09b13609fc984c40f9f3c03d2ce8b6..e4196c8a205ac233c2ddef042acb58012ff31ff5 100644 GIT binary patch delta 4062 zcmb`K4{RG(9mnsV?bz|XpPj^U66eq5kJF7d$(_%3Y*Kv^;m{Bp7#K*3z+f z1Kx-F;81jAWGFV7%0v^X`1DY0CY_m_Fp`vm=RN!d?oS(4G0_flp$gA*8A-UGfcqEi z+})5p?j}q3xjU-c!eN#WhvTuhe|R+JiH}CT0ncy%D{*v`@AL-?k4KZ4@wV7_ zJa%OAXgn25M!(XYj1IStPfp-h63O_;?N!>3O(Z?M$KS?_ZM>J`g%IBv^7-03g+Ng7 z$t9Q<75M@I-mCMHoFNjf5^#0V&i@8ln(2M!&+6_uo;!EQ_GlN=-)fyA;61nkFThzi z0aI`g!f+?p(Ld22(d+0r^nG+3CD48pM2*PIe$4)keVJWgPqD|?D7%{#*lL!vzGuB+ zeZhLxdcrz|lUHZWI%$zC>Z&*7^|$J9A>;|Tb-Cbe(cyeZ>}=NMLa=F-XV((ThkQav z@H7^`z=yoSkm%*KTogkdu|dmuZ%Fjiuj3-;5ko#4$6vReXIBgOVD0*X0uSJZT{;}E zfMAU-_js#yxF3(9O3S^zP*(6dbqeql@Rd4T4Do)4mW#m<&sXShf5_)A*Wtd9Pb`!9 z18Z|%NRj9Z2?4v#3%EmGTQT?GR&ht6mr-%kc)GMM+5w(hNDWXOZ)rzQ+ z6y;Pzr6L@Ps8B??BFYqDSA1l)kXz}xU9{1RS*i?D!`HV+TO*I*jP z-~reVyKusIa6=6MF!(6=E4qf0^(*uW%A;q|S@b>hEi{K_&|x%$6IY6|7P^{DA4_NA z6RoOrtFlFv&8lotWuq!NRW_)yUX^vKtX+{=xn-A%YgAdS$|_YlRavP@hbk*nS+2@5 zRoYc)vpCj<4=P5B(;xnQyKnD(fg`@SKXNcNnHjt{(Vp^-3?}Y5JRJ$lCXWiij-vy8 zW8#76&SW}#AeGvEF#6?=#6<4#4wC!a`eV$q?({F5Y#wiWr`H$Yh5Wl6Wc{4Y$SxCb z0VJT%(`bNQ#*=f2%}yDu1mSSGj+^ZSA!efKBk7q~ES^puz#mIbr3aVJ;fssg6j@rB zPK+jE(M)18#f`+dbTpM&TG+%Tqp8@^0{%WT&1r78nZ(*|S8uY2p_CxpE>{n3F)QMc zuf<*8^v=oH%tRdD+Jz=@W67n3*m!h$3^%6kjCZsh4NSpu^<^GsS#0L9`B+$46Dtq8Tr(mi4

FG5BZo)s{ z6ZinG!!>Mu-T;B23DPJ(4Dy4KAGG|SXqqsu4Dbm8K7wVq3@^g7@B|*)v} zDBa&mVT1c5^rH9CN#teUV!z6A)>p0LR%_|`(m~6omd7n!=6B2wo41twx+GIlW%{Y< zkclxqW$ZJ2WO&RFV6HNAObh)BI!W88iM4@xdA zm(8A3qvf*MldAQ&$17E7xoq|%r-oy*hx1mc<+9h499k}WJ*h&&3tmqu*K*nGNo86t zdp*gn$Ay4o({kDBNl?UBt)7I6_^Q>DSef_df3%nS;;W7HYXnQ2Cg^RYq-EG780QVQ z3<>5uV}&tv83l>cxh5Muo1f}so}_Z0FQ-p1)^~`W+@*3lod10vdy7Wf8w|k%`JlMkxhIjP!a2wQ`KgHDM&g@{ybI;q!+T3+N<*rZQxEM9sdULSpL5N delta 805 zcmZXSYe*DP6vyY@JNul+ncdvBverq8Uh2;5x~-co?pmg$kmMt_ARo9}nr$RfTN$k% zf*wMa#f6A4k;J}aZZbq4ga|~VASe(bdLTk5T78hh$i^Uox`%t<{P`U?|HDZp?aBAH zu5_D)VVDyA|9P>iSzcs$P}c>S*2d~j9BXZAiy9Gj z=wjZG{82|%lS0dXh>19s#JnMe4W(G+M@Da*)EkoYd@aPf1aXXkPw)~R!5z2=#~}h? z*aSB5yZAvIOWEXpF*WugAGrkRWZ)aj!4$lOSMUs;=q-cL54Yh4bn0L;ba%c>=v*rU z^DqtX;WdoI2t0%Vo#}=Pa28s1t^&GiXRWFBx#>b*+s^9B!-rzt>hhAD9aYiRcp~0X zxhK+msv#1MMoJ^4@qo{JGE#A@Jl@n+6W$y1?>kaPZ@H1Y*_W+)vekgB2DYjGZN5B@ z;`vv~QG5Zv*QZUnkt?}m-Q62fhC)bYtv?x{1_fuDuJgV83SR2A$s4sA1>dvssAv0n z)ZuK3H%1dQoR8D#XaPP-gUiSz+MJJ*+U)`yBQAlfVxSL7#Tl_n%oQetc0uN!@`rhz z8|12VpB@hH7pPc{E3~_1_`uV(>;hvXL58g)qa?^7YlBH;7ygF0hGt46!7=9rugU3l zql>%fnNoHGeOJPo4OQWy@?!d>m`&4WCDMXu(u7>}w4LPAK|66Rl{TCu(^)&&W9KIs zyR+DjI)cAlG$m?(kFV+D=OW-I@m8nbk1o60>Kj_3)aD=#t=dV(5xo*-CHi?cYoP~M z61S#0$*=*F)imfL*XZdrWQ5*dPIzr1jXXxknnKE=zEXK8#7P>LB=aL%^n ") + else: + asyncio.run(reset_password(sys.argv[1], sys.argv[2])) diff --git a/routes_dump.txt b/routes_dump.txt new file mode 100644 index 0000000..9ea8d3d --- /dev/null +++ b/routes_dump.txt @@ -0,0 +1,164 @@ +=== APP ROUTES === +{'HEAD', 'GET'} /openapi.json +{'HEAD', 'GET'} /docs +{'HEAD', 'GET'} /docs/oauth2-redirect +{'HEAD', 'GET'} /redoc +N/A /static +{'GET'} /api/auth/status +{'POST'} /api/auth/setup +{'POST'} /api/auth/login +{'POST'} /api/auth/login/json +{'GET'} /api/auth/me +{'PUT'} /api/auth/password +{'GET'} /api/hosts/groups +{'GET'} /api/hosts/by-name/{host_name} +{'POST'} /api/hosts/refresh +{'POST'} /api/hosts/sync +{'GET'} /api/hosts/{host_id} +{'GET'} /api/hosts +{'POST'} /api/hosts +{'PUT'} /api/hosts/{host_name} +{'DELETE'} /api/hosts/by-name/{host_name} +{'DELETE'} /api/hosts/{host_id} +{'GET'} /api/groups +{'GET'} /api/groups/{group_name} +{'POST'} /api/groups +{'PUT'} /api/groups/{group_name} +{'DELETE'} /api/groups/{group_name} +{'GET'} /api/tasks +{'GET'} /api/tasks/running +{'GET'} /api/tasks/logs +{'GET'} /api/tasks/logs/dates +{'GET'} /api/tasks/logs/stats +{'GET'} /api/tasks/logs/{log_id} +{'DELETE'} /api/tasks/logs/{log_id} +{'POST'} /api/tasks/{task_id}/cancel +{'GET'} /api/tasks/{task_id} +{'DELETE'} /api/tasks/{task_id} +{'POST'} /api/tasks +{'GET'} /api/logs +{'POST'} /api/logs +{'DELETE'} /api/logs +{'GET'} /api/ansible/playbooks +{'GET'} /api/ansible/inventory +{'GET'} /api/ansible/groups +{'GET'} /api/ansible/ssh-config +{'POST'} /api/ansible/bootstrap +{'POST'} /api/ansible/execute +{'POST'} /api/ansible/adhoc +{'GET'} /api/playbooks/{filename}/content +{'PUT'} /api/playbooks/{filename}/content +{'DELETE'} /api/playbooks/{filename} +{'GET'} /api/schedules +{'GET'} /api/schedules/stats +{'GET'} /api/schedules/upcoming +{'GET'} /api/schedules/validate-cron +{'GET'} /api/schedules/{schedule_id} +{'POST'} /api/schedules +{'PUT'} /api/schedules/{schedule_id} +{'DELETE'} /api/schedules/{schedule_id} +{'POST'} /api/schedules/{schedule_id}/run +{'POST'} /api/schedules/{schedule_id}/pause +{'POST'} /api/schedules/{schedule_id}/resume +{'GET'} /api/schedules/{schedule_id}/runs +{'GET'} /api/adhoc/history +{'GET'} /api/adhoc/categories +{'POST'} /api/adhoc/categories +{'PUT'} /api/adhoc/categories/{category_name} +{'DELETE'} /api/adhoc/categories/{category_name} +{'PUT'} /api/adhoc/history/{command_id}/category +{'DELETE'} /api/adhoc/history/{command_id} +{'GET'} /api/bootstrap/status +{'GET'} /api/bootstrap/status/{host_name} +{'POST'} /api/bootstrap/status/{host_name} +{'POST'} /api/bootstrap +{'GET'} /api/health +{'GET'} /api/health/global +{'GET'} /api/health/runtime +{'GET'} /api/health/{host_name} +{'POST'} /api/health/refresh +{'GET'} /api/notifications/config +{'POST'} /api/notifications/test +{'POST'} /api/notifications/send +{'POST'} /api/notifications/toggle +{'GET'} /api/help/content +{'GET'} /api/help/documentation.md +{'GET'} /api/help/documentation.pdf +{'GET'} /api/help/catalog +{'GET'} /api/monitoring +{'GET'} /api/monitoring/all-hosts +{'GET'} /api/monitoring/collection-schedule +{'POST'} /api/monitoring/collection-schedule +{'GET'} /api/monitoring/{host_id} +{'GET'} /api/builtin-playbooks +{'GET'} /api/builtin-playbooks/{builtin_id} +{'POST'} /api/builtin-playbooks/execute +{'POST'} /api/builtin-playbooks/execute-background +{'POST'} /api/builtin-playbooks/collect-all +{'GET'} /api/server/logs +{'POST'} /api/alerts +{'GET'} /api/alerts +{'GET'} /api/alerts/unread-count +{'POST'} /api/alerts/{alert_id}/read +{'POST'} /api/alerts/mark-all-read +{'DELETE'} /api/alerts/{alert_id} +{'GET'} /api/docker/container-customizations +{'PUT'} /api/docker/container-customizations/{host_id}/{container_id} +{'DELETE'} /api/docker/container-customizations/{host_id}/{container_id} +{'GET'} /api/docker/hosts +{'POST'} /api/docker/hosts/{host_id}/enable +{'POST'} /api/docker/hosts/{host_id}/collect +{'POST'} /api/docker/collect-all +{'GET'} /api/docker/containers +{'GET'} /api/docker/hosts/{host_id}/containers +{'POST'} /api/docker/containers/{host_id}/{container_id}/start +{'POST'} /api/docker/containers/{host_id}/{container_id}/stop +{'POST'} /api/docker/containers/{host_id}/{container_id}/restart +{'POST'} /api/docker/containers/{host_id}/{container_id}/remove +{'POST'} /api/docker/containers/{host_id}/{container_id}/redeploy +{'GET'} /api/docker/containers/{host_id}/{container_id}/logs +{'GET'} /api/docker/containers/{host_id}/{container_id}/inspect +{'GET'} /api/docker/hosts/{host_id}/images +{'DELETE'} /api/docker/images/{host_id}/{image_id} +{'GET'} /api/docker/hosts/{host_id}/volumes +{'GET'} /api/docker/alerts +{'POST'} /api/docker/alerts/{alert_id}/acknowledge +{'POST'} /api/docker/alerts/{alert_id}/close +{'GET'} /api/docker/stats +{'POST'} /api/playbooks/{filename}/lint +{'GET'} /api/playbooks/results +{'GET'} /api/playbooks/results/{filename} +{'GET'} /api/playbooks/rules +{'GET'} /api/terminal/status +{'GET'} /api/terminal/sessions/{session_id}/probe +{'GET'} /api/terminal/proxy/{session_id} +{'PATCH', 'DELETE', 'GET', 'OPTIONS', 'POST', 'HEAD', 'PUT'} /api/terminal/proxy/{session_id}/{proxy_path:path} +N/A /api/terminal/proxy/{session_id}/ws +{'POST'} /api/terminal/{host_id}/terminal-sessions +{'DELETE'} /api/terminal/sessions/{session_id} +{'POST'} /api/terminal/sessions/{session_id}/heartbeat +{'POST'} /api/terminal/sessions/{session_id}/close-beacon +{'POST'} /api/terminal/cleanup +{'POST'} /api/terminal/sessions/{session_id}/command +{'GET'} /api/terminal/connect/{session_id} +{'GET'} /api/terminal/popout/{session_id} +{'GET'} /api/terminal/{host_id}/command-history +{'GET'} /api/terminal/{host_id}/shell-history +{'GET'} /api/terminal/{host_id}/command-history/unique +{'POST'} /api/terminal/{host_id}/command-history/{command_hash}/pin +{'GET'} /api/terminal/command-history +{'DELETE'} /api/terminal/{host_id}/command-history +{'POST'} /api/terminal/command-history/purge +{'GET'} /api/favorites/groups +{'POST'} /api/favorites/groups +{'PATCH'} /api/favorites/groups/{group_id} +{'DELETE'} /api/favorites/groups/{group_id} +{'GET'} /api/favorites/containers +{'POST'} /api/favorites/containers +{'DELETE'} /api/favorites/containers/{favorite_id} +{'GET'} /api/config +N/A /ws +N/A /terminal/ws/{session_id} +{'GET'} / +{'GET'} /favicon.ico +{'GET'} /api diff --git a/run_dev.ps1 b/run_dev.ps1 index 033fc22..fcc3d70 100644 --- a/run_dev.ps1 +++ b/run_dev.ps1 @@ -15,4 +15,4 @@ if (Test-Path $envFile) { } # Démarrer le backend FastAPI avec uvicorn -python -m uvicorn main:app --host 0.0.0.0 --port 8000 --reload +python -m uvicorn main:app --host 0.0.0.0 --port 8008 --reload diff --git a/tasks_logs/.metadata_cache.json b/tasks_logs/.metadata_cache.json index 2fddc36..836606f 100644 --- a/tasks_logs/.metadata_cache.json +++ b/tasks_logs/.metadata_cache.json @@ -1 +1 @@ -{"C:\\dev\\git\\python\\homelab-automation-api-v2\\tasks_logs\\2025\\12\\23\\task_041616_ca3328_test-host-1.local_Vérification_de_santé_completed.md": {"start_time": "2025-12-24T04:16:16.224889+00:00", "end_time": "2025-12-24T04:16:16.225116+00:00", "duration": "0.0s", "duration_seconds": null, "hosts": [], "category": "Autre", "subcategory": null, "target_type": "host", "source_type": "manual", "task_name": "Vérification de santé", "target": "test-host-1.local", "_mtime": 1766549776.2719624}, "C:\\dev\\git\\python\\homelab-automation-api-v2\\tasks_logs\\2025\\12\\23\\task_041616_cf3208_all_Vérification_de_santé_failed.md": {"start_time": "2025-12-24T04:16:16.755925+00:00", "end_time": "2025-12-24T04:16:16.756095+00:00", "duration": "0.0s", "duration_seconds": null, "hosts": [], "category": "Autre", "subcategory": null, "target_type": "group", "source_type": "manual", "task_name": "Vérification de santé", "target": "all", "_mtime": 1766549776.7802546}, "C:\\dev\\git\\python\\homelab-automation-api-v2\\tasks_logs\\2025\\12\\23\\task_041618_378283_env_test_Vérification_de_santé_completed.md": {"start_time": "2025-12-24T04:16:18.173893+00:00", "end_time": "2025-12-24T04:16:18.174187+00:00", "duration": "0.0s", "duration_seconds": null, "hosts": ["group-host-1", "group-host-2"], "category": "Autre", "subcategory": null, "target_type": "group", "source_type": "manual", "task_name": "Vérification de santé", "target": "env_test", "_mtime": 1766549778.197829}, "C:\\dev\\git\\python\\homelab-automation-api-v2\\tasks_logs\\2025\\12\\23\\task_041618_1e6ba6_role_sbc_Vérification_de_santé_failed.md": {"start_time": "2025-12-24T04:16:18.479521+00:00", "end_time": "2025-12-24T04:16:18.479676+00:00", "duration": "0.0s", "duration_seconds": null, "hosts": ["sbc-01", "sbc-02"], "category": "Autre", "subcategory": null, "target_type": "group", "source_type": "manual", "task_name": "Vérification de santé", "target": "role_sbc", "_mtime": 1766549778.5127187}, "C:\\dev\\git\\python\\homelab-automation-api-v2\\tasks_logs\\2025\\12\\23\\task_041617_a47443_role_sbc_Playbook_Health_Check_failed.md": {"start_time": "2025-12-24T04:16:17.728818+00:00", "end_time": "2025-12-24T04:16:17.756954+00:00", "duration": "1.0s", "duration_seconds": 1, "hosts": ["orangepi"], "category": "Playbook", "subcategory": "Health Check", "target_type": "group", "source_type": "manual", "task_name": "Playbook: Health Check", "target": "role_sbc", "_mtime": 1766549777.763771}, "C:\\dev\\git\\python\\homelab-automation-api-v2\\tasks_logs\\2025\\12\\24\\task_154943_0dfef5_test-host-1.local_Vérification_de_santé_completed.md": {"start_time": "2025-12-24T15:49:43.523258+00:00", "end_time": "2025-12-24T15:49:43.523449+00:00", "duration": "0.0s", "duration_seconds": null, "hosts": [], "category": "Autre", "subcategory": null, "target_type": "host", "source_type": "manual", "task_name": "Vérification de santé", "target": "test-host-1.local", "_mtime": 1766591383.5344203}, "C:\\dev\\git\\python\\homelab-automation-api-v2\\tasks_logs\\2025\\12\\24\\task_154943_7b0547_all_Vérification_de_santé_failed.md": {"start_time": "2025-12-24T15:49:43.720927+00:00", "end_time": "2025-12-24T15:49:43.721080+00:00", "duration": "0.0s", "duration_seconds": null, "hosts": [], "category": "Autre", "subcategory": null, "target_type": "group", "source_type": "manual", "task_name": "Vérification de santé", "target": "all", "_mtime": 1766591383.743436}, "C:\\dev\\git\\python\\homelab-automation-api-v2\\tasks_logs\\2025\\12\\24\\task_154944_d73d9b_env_test_Vérification_de_santé_completed.md": {"start_time": "2025-12-24T15:49:44.310578+00:00", "end_time": "2025-12-24T15:49:44.310710+00:00", "duration": "0.0s", "duration_seconds": null, "hosts": ["group-host-1", "group-host-2"], "category": "Autre", "subcategory": null, "target_type": "group", "source_type": "manual", "task_name": "Vérification de santé", "target": "env_test", "_mtime": 1766591384.323248}, "C:\\dev\\git\\python\\homelab-automation-api-v2\\tasks_logs\\2025\\12\\24\\task_154944_a470ff_role_sbc_Vérification_de_santé_failed.md": {"start_time": "2025-12-24T15:49:44.505926+00:00", "end_time": "2025-12-24T15:49:44.506005+00:00", "duration": "0.0s", "duration_seconds": null, "hosts": ["sbc-01", "sbc-02"], "category": "Autre", "subcategory": null, "target_type": "group", "source_type": "manual", "task_name": "Vérification de santé", "target": "role_sbc", "_mtime": 1766591384.523636}, "C:\\dev\\git\\python\\homelab-automation-api-v2\\tasks_logs\\2025\\12\\24\\task_154944_bdcddf_role_sbc_Playbook_Health_Check_failed.md": {"start_time": "2025-12-24T15:49:44.119736+00:00", "end_time": "2025-12-24T15:49:44.125865+00:00", "duration": "1.0s", "duration_seconds": 1, "hosts": ["orangepi"], "category": "Playbook", "subcategory": "Health Check", "target_type": "group", "source_type": "manual", "task_name": "Playbook: Health Check", "target": "role_sbc", "_mtime": 1766591384.1273415}} \ No newline at end of file +{"C:\\dev\\git\\python\\homelab-automation-api-v2\\tasks_logs\\2025\\12\\23\\task_041616_ca3328_test-host-1.local_Vérification_de_santé_completed.md": {"start_time": "2025-12-24T04:16:16.224889+00:00", "end_time": "2025-12-24T04:16:16.225116+00:00", "duration": "0.0s", "duration_seconds": null, "hosts": [], "category": "Autre", "subcategory": null, "target_type": "host", "source_type": "manual", "task_name": "Vérification de santé", "target": "test-host-1.local", "_mtime": 1766549776.2719624}, "C:\\dev\\git\\python\\homelab-automation-api-v2\\tasks_logs\\2025\\12\\23\\task_041616_cf3208_all_Vérification_de_santé_failed.md": {"start_time": "2025-12-24T04:16:16.755925+00:00", "end_time": "2025-12-24T04:16:16.756095+00:00", "duration": "0.0s", "duration_seconds": null, "hosts": [], "category": "Autre", "subcategory": null, "target_type": "group", "source_type": "manual", "task_name": "Vérification de santé", "target": "all", "_mtime": 1766549776.7802546}, "C:\\dev\\git\\python\\homelab-automation-api-v2\\tasks_logs\\2025\\12\\23\\task_041618_378283_env_test_Vérification_de_santé_completed.md": {"start_time": "2025-12-24T04:16:18.173893+00:00", "end_time": "2025-12-24T04:16:18.174187+00:00", "duration": "0.0s", "duration_seconds": null, "hosts": ["group-host-1", "group-host-2"], "category": "Autre", "subcategory": null, "target_type": "group", "source_type": "manual", "task_name": "Vérification de santé", "target": "env_test", "_mtime": 1766549778.197829}, "C:\\dev\\git\\python\\homelab-automation-api-v2\\tasks_logs\\2025\\12\\23\\task_041618_1e6ba6_role_sbc_Vérification_de_santé_failed.md": {"start_time": "2025-12-24T04:16:18.479521+00:00", "end_time": "2025-12-24T04:16:18.479676+00:00", "duration": "0.0s", "duration_seconds": null, "hosts": ["sbc-01", "sbc-02"], "category": "Autre", "subcategory": null, "target_type": "group", "source_type": "manual", "task_name": "Vérification de santé", "target": "role_sbc", "_mtime": 1766549778.5127187}, "C:\\dev\\git\\python\\homelab-automation-api-v2\\tasks_logs\\2025\\12\\23\\task_041617_a47443_role_sbc_Playbook_Health_Check_failed.md": {"start_time": "2025-12-24T04:16:17.728818+00:00", "end_time": "2025-12-24T04:16:17.756954+00:00", "duration": "1.0s", "duration_seconds": 1, "hosts": ["orangepi"], "category": "Playbook", "subcategory": "Health Check", "target_type": "group", "source_type": "manual", "task_name": "Playbook: Health Check", "target": "role_sbc", "_mtime": 1766549777.763771}, "C:\\dev\\git\\python\\homelab-automation-api-v2\\tasks_logs\\2025\\12\\24\\task_154943_0dfef5_test-host-1.local_Vérification_de_santé_completed.md": {"start_time": "2025-12-24T15:49:43.523258+00:00", "end_time": "2025-12-24T15:49:43.523449+00:00", "duration": "0.0s", "duration_seconds": null, "hosts": [], "category": "Autre", "subcategory": null, "target_type": "host", "source_type": "manual", "task_name": "Vérification de santé", "target": "test-host-1.local", "_mtime": 1766591383.5344203}, "C:\\dev\\git\\python\\homelab-automation-api-v2\\tasks_logs\\2025\\12\\24\\task_154943_7b0547_all_Vérification_de_santé_failed.md": {"start_time": "2025-12-24T15:49:43.720927+00:00", "end_time": "2025-12-24T15:49:43.721080+00:00", "duration": "0.0s", "duration_seconds": null, "hosts": [], "category": "Autre", "subcategory": null, "target_type": "group", "source_type": "manual", "task_name": "Vérification de santé", "target": "all", "_mtime": 1766591383.743436}, "C:\\dev\\git\\python\\homelab-automation-api-v2\\tasks_logs\\2025\\12\\24\\task_154944_d73d9b_env_test_Vérification_de_santé_completed.md": {"start_time": "2025-12-24T15:49:44.310578+00:00", "end_time": "2025-12-24T15:49:44.310710+00:00", "duration": "0.0s", "duration_seconds": null, "hosts": ["group-host-1", "group-host-2"], "category": "Autre", "subcategory": null, "target_type": "group", "source_type": "manual", "task_name": "Vérification de santé", "target": "env_test", "_mtime": 1766591384.323248}, "C:\\dev\\git\\python\\homelab-automation-api-v2\\tasks_logs\\2025\\12\\24\\task_154944_a470ff_role_sbc_Vérification_de_santé_failed.md": {"start_time": "2025-12-24T15:49:44.505926+00:00", "end_time": "2025-12-24T15:49:44.506005+00:00", "duration": "0.0s", "duration_seconds": null, "hosts": ["sbc-01", "sbc-02"], "category": "Autre", "subcategory": null, "target_type": "group", "source_type": "manual", "task_name": "Vérification de santé", "target": "role_sbc", "_mtime": 1766591384.523636}, "C:\\dev\\git\\python\\homelab-automation-api-v2\\tasks_logs\\2025\\12\\24\\task_154944_bdcddf_role_sbc_Playbook_Health_Check_failed.md": {"start_time": "2025-12-24T15:49:44.119736+00:00", "end_time": "2025-12-24T15:49:44.125865+00:00", "duration": "1.0s", "duration_seconds": 1, "hosts": ["orangepi"], "category": "Playbook", "subcategory": "Health Check", "target_type": "group", "source_type": "manual", "task_name": "Playbook: Health Check", "target": "role_sbc", "_mtime": 1766591384.1273415}, "C:\\dev\\git\\python\\homelab-automation-api-v2\\tasks_logs\\2026\\03\\03\\task_000253_236dce_all_Vérification_de_santé_failed.md": {"start_time": "2026-03-04T00:02:53.593732+00:00", "end_time": "2026-03-04T00:02:53.905888+00:00", "duration": "0.3s", "duration_seconds": 0, "hosts": [], "category": "Autre", "subcategory": null, "target_type": "group", "source_type": "manual", "task_name": "Vérification de santé", "target": "all", "_mtime": 1772582573.9389596}, "C:\\dev\\git\\python\\homelab-automation-api-v2\\tasks_logs\\2026\\03\\03\\task_000254_f0d603_test-host-1.local_Vérification_de_santé_completed.md": {"start_time": "2026-03-04T00:02:54.222829+00:00", "end_time": "2026-03-04T00:02:54.222943+00:00", "duration": "0.0s", "duration_seconds": null, "hosts": [], "category": "Autre", "subcategory": null, "target_type": "host", "source_type": "manual", "task_name": "Vérification de santé", "target": "test-host-1.local", "_mtime": 1772582574.2300081}, "C:\\dev\\git\\python\\homelab-automation-api-v2\\tasks_logs\\2026\\03\\03\\task_000254_094dba_test-host_Vérification_de_santé_failed.md": {"start_time": "2026-03-04T00:02:54.138737+00:00", "end_time": "2026-03-04T00:02:54.263018+00:00", "duration": "0.1s", "duration_seconds": 0, "hosts": [], "category": "Autre", "subcategory": null, "target_type": "group", "source_type": "manual", "task_name": "Vérification de santé", "target": "test-host", "_mtime": 1772582574.2660306}, "C:\\dev\\git\\python\\homelab-automation-api-v2\\tasks_logs\\2026\\03\\03\\task_000254_0757ef_all_Vérification_de_santé_failed.md": {"start_time": "2026-03-04T00:02:54.334359+00:00", "end_time": "2026-03-04T00:02:54.334425+00:00", "duration": "0.0s", "duration_seconds": null, "hosts": [], "category": "Autre", "subcategory": null, "target_type": "group", "source_type": "manual", "task_name": "Vérification de santé", "target": "all", "_mtime": 1772582574.34154}, "C:\\dev\\git\\python\\homelab-automation-api-v2\\tasks_logs\\2026\\03\\03\\task_000254_019b47_env_test_Vérification_de_santé_completed.md": {"start_time": "2026-03-04T00:02:54.654602+00:00", "end_time": "2026-03-04T00:02:54.654670+00:00", "duration": "0.0s", "duration_seconds": null, "hosts": ["group-host-1", "group-host-2"], "category": "Autre", "subcategory": null, "target_type": "group", "source_type": "manual", "task_name": "Vérification de santé", "target": "env_test", "_mtime": 1772582574.6624749}, "C:\\dev\\git\\python\\homelab-automation-api-v2\\tasks_logs\\2026\\03\\03\\task_000254_431184_role_sbc_Vérification_de_santé_failed.md": {"start_time": "2026-03-04T00:02:54.750826+00:00", "end_time": "2026-03-04T00:02:54.750892+00:00", "duration": "0.0s", "duration_seconds": null, "hosts": ["sbc-01", "sbc-02"], "category": "Autre", "subcategory": null, "target_type": "group", "source_type": "manual", "task_name": "Vérification de santé", "target": "role_sbc", "_mtime": 1772582574.760851}, "C:\\dev\\git\\python\\homelab-automation-api-v2\\tasks_logs\\2026\\03\\03\\task_000254_b73d6d_role_sbc_Playbook_Health_Check_failed.md": {"start_time": "2026-03-04T00:02:54.544092+00:00", "end_time": "2026-03-04T00:02:54.547636+00:00", "duration": "1.0s", "duration_seconds": 1, "hosts": ["orangepi"], "category": "Playbook", "subcategory": "Health Check", "target_type": "group", "source_type": "manual", "task_name": "Playbook: Health Check", "target": "role_sbc", "_mtime": 1772582574.5481606}} \ No newline at end of file diff --git a/test_endpoints.py b/test_endpoints.py new file mode 100644 index 0000000..98684c4 --- /dev/null +++ b/test_endpoints.py @@ -0,0 +1,27 @@ +import requests + +# Use the API key from .env if possible, or try a dummy one to see the error type +url = "http://localhost:8000/api/help/catalog" +headers = { + "X-API-Key": "test-key" # Will likely fail auth but show if route exists +} + +try: + print(f"Calling {url}...") + response = requests.get(url, headers=headers) + print(f"Status Code: {response.status_code}") + print(f"Response: {response.text}") +except Exception as e: + print(f"Error: {e}") + +# Try another route that definitely exists +url2 = "http://localhost:8000/api/help/content" +url3 = "http://localhost:8000/api/auth/me" +for url in [url2, url3]: + try: + print(f"\nCalling {url}...") + response = requests.get(url, headers=headers) + print(f"Status Code: {response.status_code}") + print(f"Response: {response.text}") + except Exception as e: + print(f"Error: {e}") diff --git a/test_typing.py b/test_typing.py new file mode 100644 index 0000000..a9d2a57 --- /dev/null +++ b/test_typing.py @@ -0,0 +1,15 @@ +import sys +from typing import Union, Any, cast + +print(f"Python version: {sys.version}") + +try: + from typing import Union + types = (int, str) + # This is what SQLAlchemy does: + res = cast(Any, Union).__getitem__(types) + print(f"Success: {res}") +except Exception as e: + print(f"Error: {e}") + import traceback + traceback.print_exc() diff --git a/test_typing_2.py b/test_typing_2.py new file mode 100644 index 0000000..c12925d --- /dev/null +++ b/test_typing_2.py @@ -0,0 +1,9 @@ +from typing import Union + +try: + print(f"Union[int, str]: {Union[int, str]}") + # Let's try what SQLAlchemy tries via casting + from typing import Any, cast + print(f"Casting and getting item: {cast(Any, Union)[int, str]}") +except Exception as e: + print(f"Normal indexing error: {e}")