""" Routes API pour la gestion des hôtes. """ import uuid from datetime import datetime, timezone from typing import List, Optional from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.ext.asyncio import AsyncSession from app.core.dependencies import get_db, verify_api_key from app.crud.host import HostRepository from app.crud.bootstrap_status import BootstrapStatusRepository from app.schemas.host_api import HostRequest, HostUpdateRequest, HostResponse from app.services import ansible_service, ws_manager router = APIRouter() def _host_to_response(host, bootstrap=None) -> dict: """Convertit un modèle Host DB en réponse API.""" return { "id": host.id, "name": host.name, "ip": host.ip_address, "status": host.status or "unknown", "os": "Linux", "last_seen": host.last_seen, "created_at": host.created_at, "groups": [host.ansible_group] if host.ansible_group else [], "bootstrap_ok": bootstrap.status == "success" if bootstrap else False, "bootstrap_date": bootstrap.last_attempt if bootstrap else None, } @router.get("/groups") async def get_host_groups(api_key_valid: bool = Depends(verify_api_key)): """Récupère la liste des groupes disponibles pour les hôtes.""" return { "env_groups": ansible_service.get_env_groups(), "role_groups": ansible_service.get_role_groups(), "all_groups": ansible_service.get_groups(), } @router.get("/by-name/{host_name}") async def get_host_by_name( host_name: str, api_key_valid: bool = Depends(verify_api_key), db_session: AsyncSession = Depends(get_db), ): """Récupère un hôte par son nom.""" repo = HostRepository(db_session) bs_repo = BootstrapStatusRepository(db_session) host = await repo.get_by_name(host_name) if not host: host = await repo.get_by_ip(host_name) if not host: raise HTTPException(status_code=404, detail=f"Hôte '{host_name}' non trouvé") bootstrap = await bs_repo.latest_for_host(host.id) return _host_to_response(host, bootstrap) @router.post("/refresh") async def refresh_hosts( api_key_valid: bool = Depends(verify_api_key), db_session: AsyncSession = Depends(get_db), ): """Force le rechargement des hôtes depuis l'inventaire Ansible.""" ansible_service.invalidate_cache() hosts = ansible_service.get_hosts_from_inventory() await ws_manager.broadcast({ "type": "hosts_refreshed", "data": {"count": len(hosts)} }) return { "message": f"{len(hosts)} hôtes rechargés depuis l'inventaire Ansible", "count": len(hosts) } @router.post("/sync") async def sync_hosts_from_ansible( api_key_valid: bool = Depends(verify_api_key), db_session: AsyncSession = Depends(get_db), ): """Synchronise les hôtes depuis l'inventaire Ansible vers la base de données.""" repo = HostRepository(db_session) ansible_service.invalidate_cache() inventory_hosts = ansible_service.get_hosts_from_inventory() created_count = 0 updated_count = 0 for inv_host in inventory_hosts: existing = await repo.get_by_name(inv_host.name) if existing: await repo.update( existing, ip_address=inv_host.ansible_host or inv_host.name, ansible_group=inv_host.groups[0] if inv_host.groups else None, ) updated_count += 1 else: await repo.create( id=uuid.uuid4().hex, name=inv_host.name, ip_address=inv_host.ansible_host or inv_host.name, ansible_group=inv_host.groups[0] if inv_host.groups else None, status="unknown", reachable=False, ) created_count += 1 await db_session.commit() await ws_manager.broadcast({ "type": "hosts_synced", "data": { "created": created_count, "updated": updated_count, "total": len(inventory_hosts) } }) return { "message": f"Synchronisation terminée: {created_count} créé(s), {updated_count} mis à jour", "created": created_count, "updated": updated_count, "total": len(inventory_hosts) } @router.get("/{host_id}") async def get_host( host_id: str, api_key_valid: bool = Depends(verify_api_key), db_session: AsyncSession = Depends(get_db), ): """Récupère un hôte par son ID.""" repo = HostRepository(db_session) bs_repo = BootstrapStatusRepository(db_session) host = await repo.get(host_id) if not host: raise HTTPException(status_code=404, detail="Hôte non trouvé") bootstrap = await bs_repo.latest_for_host(host.id) return _host_to_response(host, bootstrap) @router.get("") async def get_hosts( bootstrap_status: Optional[str] = None, limit: int = 100, offset: int = 0, api_key_valid: bool = Depends(verify_api_key), db_session: AsyncSession = Depends(get_db), ): """Récupère la liste des hôtes.""" from app.services import db repo = HostRepository(db_session) bs_repo = BootstrapStatusRepository(db_session) hosts = await repo.list(limit=limit, offset=offset) # Si la base ne contient aucun hôte, fallback sur les données de l'inventaire Ansible if not hosts: hybrid_hosts = db.hosts fallback_results = [] for h in hybrid_hosts: # Appliquer les filtres de bootstrap if bootstrap_status == "ready" and not h.bootstrap_ok: continue if bootstrap_status == "not_configured" and h.bootstrap_ok: continue fallback_results.append({ "id": h.id, "name": h.name, "ip": h.ip, "status": h.status, "os": h.os, "last_seen": h.last_seen, "created_at": h.created_at, "groups": h.groups, "bootstrap_ok": h.bootstrap_ok, "bootstrap_date": h.bootstrap_date, }) return fallback_results result = [] for host in hosts: bootstrap = await bs_repo.latest_for_host(host.id) # Appliquer les filtres de bootstrap if bootstrap_status == "ready" and not (bootstrap and bootstrap.status == "success"): continue if bootstrap_status == "not_configured" and bootstrap and bootstrap.status == "success": continue result.append(_host_to_response(host, bootstrap)) return result @router.post("") async def create_host( host_request: HostRequest, api_key_valid: bool = Depends(verify_api_key), db_session: AsyncSession = Depends(get_db), ): """Crée un nouvel hôte.""" repo = HostRepository(db_session) bs_repo = BootstrapStatusRepository(db_session) # Vérifier si l'hôte existe déjà existing = await repo.get_by_name(host_request.name) if existing: raise HTTPException(status_code=400, detail=f"L'hôte '{host_request.name}' existe déjà") # Valider le groupe d'environnement env_groups = ansible_service.get_env_groups() if host_request.env_group not in env_groups and not host_request.env_group.startswith("env_"): raise HTTPException( status_code=400, detail=f"Le groupe d'environnement doit commencer par 'env_'. Groupes existants: {env_groups}" ) # Valider les groupes de rôles role_groups = ansible_service.get_role_groups() for role in host_request.role_groups: if role not in role_groups and not role.startswith("role_"): raise HTTPException( status_code=400, detail=f"Le groupe de rôle '{role}' doit commencer par 'role_'." ) try: # Ajouter l'hôte à l'inventaire Ansible ansible_service.add_host_to_inventory( hostname=host_request.name, env_group=host_request.env_group, role_groups=host_request.role_groups, ansible_host=host_request.ip, ) # Créer en base host = await repo.create( id=uuid.uuid4().hex, name=host_request.name, ip_address=host_request.ip or host_request.name, ansible_group=host_request.env_group, status="unknown", reachable=False, last_seen=None, ) bootstrap = await bs_repo.latest_for_host(host.id) await db_session.commit() # Notifier les clients WebSocket await ws_manager.broadcast({ "type": "host_created", "data": _host_to_response(host, bootstrap), }) return { "message": f"Hôte '{host_request.name}' ajouté avec succès", "host": _host_to_response(host, bootstrap), "inventory_updated": True, } except HTTPException: raise except Exception as e: await db_session.rollback() raise HTTPException(status_code=500, detail=f"Erreur lors de l'ajout de l'hôte: {str(e)}") @router.put("/{host_name}") async def update_host( host_name: str, update_request: HostUpdateRequest, api_key_valid: bool = Depends(verify_api_key), db_session: AsyncSession = Depends(get_db), ): """Met à jour un hôte existant.""" repo = HostRepository(db_session) bs_repo = BootstrapStatusRepository(db_session) host = await repo.get_by_name(host_name) if not host: host = await repo.get(host_name) if not host: raise HTTPException(status_code=404, detail=f"Hôte '{host_name}' non trouvé") # Valider le groupe d'environnement si fourni if update_request.env_group: env_groups = ansible_service.get_env_groups() if update_request.env_group not in env_groups and not update_request.env_group.startswith("env_"): raise HTTPException(status_code=400, detail="Le groupe d'environnement doit commencer par 'env_'") # Valider les groupes de rôles si fournis if update_request.role_groups: for role in update_request.role_groups: if not role.startswith("role_"): raise HTTPException(status_code=400, detail=f"Le groupe de rôle '{role}' doit commencer par 'role_'") try: ansible_service.update_host_groups( hostname=host_name, env_group=update_request.env_group, role_groups=update_request.role_groups, ansible_host=update_request.ansible_host, ) await repo.update( host, ansible_group=update_request.env_group or host.ansible_group, ) await db_session.commit() bootstrap = await bs_repo.latest_for_host(host.id) await ws_manager.broadcast({ "type": "host_updated", "data": _host_to_response(host, bootstrap), }) return { "message": f"Hôte '{host_name}' mis à jour avec succès", "host": _host_to_response(host, bootstrap), "inventory_updated": True, } except HTTPException: await db_session.rollback() raise except Exception as e: await db_session.rollback() raise HTTPException(status_code=500, detail=f"Erreur lors de la mise à jour: {str(e)}") @router.delete("/by-name/{host_name}") async def delete_host_by_name( host_name: str, api_key_valid: bool = Depends(verify_api_key), db_session: AsyncSession = Depends(get_db), ): """Supprime un hôte par son nom.""" repo = HostRepository(db_session) host = await repo.get_by_name(host_name) if not host: host = await repo.get(host_name) if not host: raise HTTPException(status_code=404, detail=f"Hôte '{host_name}' non trouvé") try: ansible_service.remove_host_from_inventory(host_name) await repo.soft_delete(host.id) await db_session.commit() await ws_manager.broadcast({ "type": "host_deleted", "data": {"name": host_name}, }) return {"message": f"Hôte '{host_name}' supprimé avec succès", "inventory_updated": True} except HTTPException: await db_session.rollback() raise except Exception as e: await db_session.rollback() raise HTTPException(status_code=500, detail=f"Erreur lors de la suppression: {str(e)}") @router.delete("/{host_id}") async def delete_host( host_id: str, api_key_valid: bool = Depends(verify_api_key), db_session: AsyncSession = Depends(get_db), ): """Supprime un hôte par son ID.""" repo = HostRepository(db_session) host = await repo.get(host_id) if not host: raise HTTPException(status_code=404, detail="Hôte non trouvé") return await delete_host_by_name(host.name, api_key_valid, db_session)