400 lines
13 KiB
Python

"""
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)