400 lines
13 KiB
Python
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)
|
|
|
|
|