520 lines
18 KiB
Python

"""
Routes API pour l'exécution Ansible.
"""
import uuid
import subprocess
from datetime import datetime, timezone
from time import perf_counter
from typing import Optional, Dict, Any
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.config import settings
from app.core.dependencies import get_db, verify_api_key
from app.crud.task import TaskRepository
from app.schemas.ansible import AnsibleExecutionRequest, AdHocCommandRequest, AdHocCommandResult
from app.schemas.task_api import Task
from app.schemas.common import LogEntry
from app.services import (
ansible_service,
ws_manager,
notification_service,
adhoc_history_service,
db,
)
from app.services.task_log_service import TaskLogService
from app.utils.ssh_utils import find_ssh_private_key
router = APIRouter()
# Instance du service de logs de tâches
task_log_service = TaskLogService(settings.tasks_logs_dir)
@router.get("/playbooks")
async def get_ansible_playbooks(
target: Optional[str] = None,
api_key_valid: bool = Depends(verify_api_key)
):
"""Liste les playbooks Ansible disponibles."""
if target:
playbooks = ansible_service.get_compatible_playbooks(target)
else:
playbooks = ansible_service.get_playbooks()
return {
"playbooks": playbooks,
"categories": ansible_service.get_playbook_categories(),
"ansible_dir": str(settings.ansible_dir),
"filter": target
}
@router.get("/inventory")
async def get_ansible_inventory(
group: Optional[str] = None,
api_key_valid: bool = Depends(verify_api_key)
):
"""Récupère l'inventaire Ansible."""
return {
"hosts": [h.dict() for h in ansible_service.get_hosts_from_inventory(group_filter=group)],
"groups": ansible_service.get_groups(),
"inventory_path": str(ansible_service.inventory_path),
"filter": group
}
@router.get("/groups")
async def get_ansible_groups(api_key_valid: bool = Depends(verify_api_key)):
"""Récupère la liste des groupes Ansible."""
return {"groups": ansible_service.get_groups()}
@router.get("/ssh-config")
async def get_ssh_config(api_key_valid: bool = Depends(verify_api_key)):
"""Diagnostic de la configuration SSH."""
from pathlib import Path
import shutil
ssh_key_path = Path(settings.ssh_key_path)
ssh_dir = ssh_key_path.parent
available_files = []
if ssh_dir.exists():
available_files = [f.name for f in ssh_dir.iterdir()]
private_key_exists = ssh_key_path.exists()
public_key_exists = Path(settings.ssh_key_path + ".pub").exists()
pub_keys_found = []
for key_type in ["id_rsa", "id_ed25519", "id_ecdsa", "id_dsa"]:
key_path = ssh_dir / f"{key_type}.pub"
if key_path.exists():
pub_keys_found.append(str(key_path))
active_private_key = find_ssh_private_key()
return {
"ssh_key_path": settings.ssh_key_path,
"ssh_dir": str(ssh_dir),
"ssh_dir_exists": ssh_dir.exists(),
"private_key_exists": private_key_exists,
"public_key_exists": public_key_exists,
"available_files": available_files,
"public_keys_found": pub_keys_found,
"active_private_key": active_private_key,
"ssh_user": settings.ssh_user,
"sshpass_available": shutil.which("sshpass") is not None,
}
@router.post("/execute")
async def execute_ansible_playbook(
request: AnsibleExecutionRequest,
api_key_valid: bool = Depends(verify_api_key),
db_session: AsyncSession = Depends(get_db)
):
"""Exécute un playbook Ansible."""
start_time_dt = datetime.now(timezone.utc)
# Valider la compatibilité playbook-target
playbooks = ansible_service.get_playbooks()
playbook_info = next(
(pb for pb in playbooks if pb['filename'] == request.playbook or pb['name'] == request.playbook.replace('.yml', '').replace('.yaml', '')),
None
)
if playbook_info:
playbook_hosts = playbook_info.get('hosts', 'all')
if not ansible_service.is_target_compatible_with_playbook(request.target, playbook_hosts):
raise HTTPException(
status_code=400,
detail=f"Le playbook '{request.playbook}' (hosts: {playbook_hosts}) n'est pas compatible avec la cible '{request.target}'."
)
# Créer une tâche en BD
task_repo = TaskRepository(db_session)
task_id = f"pb_{uuid.uuid4().hex[:12]}"
playbook_name = request.playbook.replace('.yml', '').replace('-', ' ').title()
db_task = await task_repo.create(
id=task_id,
action=f"playbook:{request.playbook}",
target=request.target,
playbook=request.playbook,
status="running",
)
await task_repo.update(db_task, started_at=start_time_dt)
await db_session.commit()
# Créer aussi en mémoire
task = Task(
id=task_id,
name=f"Playbook: {playbook_name}",
host=request.target,
status="running",
progress=0,
start_time=start_time_dt
)
db.tasks.insert(0, task)
try:
result = await ansible_service.execute_playbook(
playbook=request.playbook,
target=request.target,
extra_vars=request.extra_vars,
check_mode=request.check_mode,
verbose=request.verbose
)
# Mettre à jour la tâche
task.status = "completed" if result["success"] else "failed"
task.progress = 100
task.end_time = datetime.now(timezone.utc)
task.duration = f"{result.get('execution_time', 0):.1f}s"
task.output = result.get("stdout", "")
task.error = result.get("stderr", "") if not result["success"] else None
# Ajouter un log
log_entry = LogEntry(
id=db.get_next_id("logs"),
timestamp=datetime.now(timezone.utc),
level="INFO" if result["success"] else "ERROR",
message=f"Playbook {request.playbook} exécuté sur {request.target}: {'succès' if result['success'] else 'échec'}",
source="ansible",
host=request.target
)
db.logs.insert(0, log_entry)
# Sauvegarder le log markdown
try:
log_path = task_log_service.save_task_log(
task=task,
output=result.get("stdout", ""),
error=result.get("stderr", "")
)
created_log = task_log_service.index_log_file(log_path)
if created_log:
await ws_manager.broadcast({
"type": "task_log_created",
"data": created_log.dict()
})
except Exception as log_error:
print(f"Erreur sauvegarde log markdown: {log_error}")
await ws_manager.broadcast({
"type": "ansible_execution",
"data": result
})
# Mettre à jour la BD
await task_repo.update(
db_task,
status=task.status,
completed_at=task.end_time,
error_message=task.error,
result_data={"output": result.get("stdout", "")[:5000]}
)
await db_session.commit()
# Notification
if result["success"]:
await notification_service.notify_task_completed(
task_name=task.name,
target=request.target,
duration=task.duration
)
else:
await notification_service.notify_task_failed(
task_name=task.name,
target=request.target,
error=result.get("stderr", "Erreur inconnue")[:200]
)
result["task_id"] = task_id
return result
except FileNotFoundError as e:
task.status = "failed"
task.end_time = datetime.now(timezone.utc)
task.error = str(e)
try:
log_path = task_log_service.save_task_log(task=task, error=str(e))
created_log = task_log_service.index_log_file(log_path)
if created_log:
await ws_manager.broadcast({
"type": "task_log_created",
"data": created_log.dict()
})
except Exception:
pass
await task_repo.update(db_task, status="failed", completed_at=task.end_time, error_message=str(e))
await db_session.commit()
await notification_service.notify_task_failed(task_name=task.name, target=request.target, error=str(e)[:200])
raise HTTPException(status_code=404, detail=str(e))
except Exception as e:
task.status = "failed"
task.end_time = datetime.now(timezone.utc)
task.error = str(e)
try:
log_path = task_log_service.save_task_log(task=task, error=str(e))
created_log = task_log_service.index_log_file(log_path)
if created_log:
await ws_manager.broadcast({
"type": "task_log_created",
"data": created_log.dict()
})
except Exception:
pass
await task_repo.update(db_task, status="failed", completed_at=task.end_time, error_message=str(e))
await db_session.commit()
await notification_service.notify_task_failed(task_name=task.name, target=request.target, error=str(e)[:200])
raise HTTPException(status_code=500, detail=str(e))
@router.post("/adhoc", response_model=AdHocCommandResult)
async def execute_adhoc_command(
request: AdHocCommandRequest,
api_key_valid: bool = Depends(verify_api_key),
db_session: AsyncSession = Depends(get_db)
):
"""Exécute une commande ad-hoc Ansible."""
start_time_perf = perf_counter()
start_time_dt = datetime.now(timezone.utc)
# Créer une tâche en BD
task_repo = TaskRepository(db_session)
task_id = f"adhoc_{uuid.uuid4().hex[:12]}"
task_name = f"Ad-hoc: {request.command[:40]}{'...' if len(request.command) > 40 else ''}"
db_task = await task_repo.create(
id=task_id,
action=f"adhoc:{request.module}",
target=request.target,
playbook=None,
status="running",
)
await task_repo.update(db_task, started_at=start_time_dt)
await db_session.commit()
# Créer aussi en mémoire
task = Task(
id=task_id,
name=task_name,
host=request.target,
status="running",
progress=0,
start_time=start_time_dt
)
db.tasks.insert(0, task)
# Construire la commande ansible
ansible_cmd = [
"ansible",
request.target,
"-i", str(settings.ansible_dir / "inventory" / "hosts.yml"),
"-m", request.module,
"-a", request.command,
"--timeout", str(request.timeout),
]
if request.become:
ansible_cmd.append("--become")
private_key = find_ssh_private_key()
if private_key:
ansible_cmd.extend(["--private-key", private_key])
if settings.ssh_user:
ansible_cmd.extend(["-u", settings.ssh_user])
try:
result = subprocess.run(
ansible_cmd,
capture_output=True,
text=True,
timeout=request.timeout + 10,
cwd=str(settings.ansible_dir)
)
duration = perf_counter() - start_time_perf
success = result.returncode == 0
task.status = "completed" if success else "failed"
task.progress = 100
task.end_time = datetime.now(timezone.utc)
task.duration = f"{round(duration, 2)}s"
task.output = result.stdout
task.error = result.stderr if result.stderr else None
log_path = task_log_service.save_task_log(task, output=result.stdout, error=result.stderr or "", source_type='adhoc')
created_log = task_log_service.index_log_file(log_path)
if created_log:
await ws_manager.broadcast({
"type": "task_log_created",
"data": created_log.dict()
})
log_entry = LogEntry(
id=db.get_next_id("logs"),
timestamp=datetime.now(timezone.utc),
level="INFO" if success else "WARN",
message=f"Ad-hoc [{request.module}] sur {request.target}: {request.command[:50]}{'...' if len(request.command) > 50 else ''}",
source="ansible-adhoc",
host=request.target
)
db.logs.insert(0, log_entry)
await ws_manager.broadcast({
"type": "adhoc_executed",
"data": {
"target": request.target,
"command": request.command,
"success": success,
"task_id": task_id
}
})
await adhoc_history_service.add_command(
command=request.command,
target=request.target,
module=request.module,
become=request.become,
category=request.category or "default"
)
await task_repo.update(
db_task,
status=task.status,
completed_at=task.end_time,
error_message=task.error,
result_data={"output": result.stdout[:5000] if result.stdout else None}
)
await db_session.commit()
if success:
await notification_service.notify_task_completed(
task_name=task.name,
target=request.target,
duration=task.duration
)
else:
await notification_service.notify_task_failed(
task_name=task.name,
target=request.target,
error=(result.stderr or "Erreur inconnue")[:200]
)
return AdHocCommandResult(
target=request.target,
command=request.command,
success=success,
return_code=result.returncode,
stdout=result.stdout,
stderr=result.stderr if result.stderr else None,
duration=round(duration, 2)
)
except subprocess.TimeoutExpired:
duration = perf_counter() - start_time_perf
task.status = "failed"
task.progress = 100
task.end_time = datetime.now(timezone.utc)
task.duration = f"{round(duration, 2)}s"
task.error = f"Timeout après {request.timeout} secondes"
try:
log_path = task_log_service.save_task_log(task, error=task.error, source_type='adhoc')
created_log = task_log_service.index_log_file(log_path)
if created_log:
await ws_manager.broadcast({
"type": "task_log_created",
"data": created_log.dict()
})
except Exception:
pass
await task_repo.update(db_task, status="failed", completed_at=task.end_time, error_message=task.error)
await db_session.commit()
await notification_service.notify_task_failed(task_name=task.name, target=request.target, error=task.error[:200])
return AdHocCommandResult(
target=request.target,
command=request.command,
success=False,
return_code=-1,
stdout="",
stderr=f"Timeout après {request.timeout} secondes",
duration=round(duration, 2)
)
except FileNotFoundError:
duration = perf_counter() - start_time_perf
error_msg = "ansible non trouvé. Vérifiez que Ansible est installé."
task.status = "failed"
task.progress = 100
task.end_time = datetime.now(timezone.utc)
task.duration = f"{round(duration, 2)}s"
task.error = error_msg
try:
log_path = task_log_service.save_task_log(task, error=error_msg, source_type='adhoc')
created_log = task_log_service.index_log_file(log_path)
if created_log:
await ws_manager.broadcast({
"type": "task_log_created",
"data": created_log.dict()
})
except Exception:
pass
await task_repo.update(db_task, status="failed", completed_at=task.end_time, error_message=error_msg)
await db_session.commit()
await notification_service.notify_task_failed(task_name=task.name, target=request.target, error=error_msg[:200])
return AdHocCommandResult(
target=request.target,
command=request.command,
success=False,
return_code=-1,
stdout="",
stderr=error_msg,
duration=round(duration, 2)
)
except Exception as e:
duration = perf_counter() - start_time_perf
error_msg = f"Erreur interne: {str(e)}"
task.status = "failed"
task.progress = 100
task.end_time = datetime.now(timezone.utc)
task.duration = f"{round(duration, 2)}s"
task.error = error_msg
try:
log_path = task_log_service.save_task_log(task, error=error_msg, source_type='adhoc')
created_log = task_log_service.index_log_file(log_path)
if created_log:
await ws_manager.broadcast({
"type": "task_log_created",
"data": created_log.dict()
})
except Exception:
pass
await task_repo.update(db_task, status="failed", completed_at=task.end_time, error_message=error_msg)
await db_session.commit()
await notification_service.notify_task_failed(task_name=task.name, target=request.target, error=error_msg[:200])
return AdHocCommandResult(
target=request.target,
command=request.command,
success=False,
return_code=-1,
stdout="",
stderr=error_msg,
duration=round(duration, 2)
)