homelab_automation/app/services/adhoc_history_service.py

305 lines
9.9 KiB
Python

"""
Service de gestion de l'historique des commandes ad-hoc.
"""
import uuid
from datetime import datetime, timezone
from typing import List, Optional
from sqlalchemy import select, update, delete
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.database import async_session_maker
from app.schemas.ansible import AdHocHistoryEntry, AdHocHistoryCategory
from app.core.constants import DEFAULT_ADHOC_CATEGORIES
class AdHocHistoryService:
"""Service pour gérer l'historique des commandes ad-hoc en base de données."""
def __init__(self):
self._default_categories_initialized = False
async def _ensure_default_categories(self, session: AsyncSession):
"""S'assure que les catégories par défaut existent."""
if self._default_categories_initialized:
return
from app.crud.log import LogRepository
repo = LogRepository(session)
# Vérifier si des catégories existent déjà
existing = await self.get_categories()
if not existing:
# Créer les catégories par défaut
for cat in DEFAULT_ADHOC_CATEGORIES:
await self._create_category_internal(
session,
name=cat["name"],
description=cat.get("description"),
color=cat.get("color", "#7c3aed"),
icon=cat.get("icon", "fa-folder")
)
await session.commit()
self._default_categories_initialized = True
async def _create_category_internal(
self,
session: AsyncSession,
name: str,
description: str = None,
color: str = "#7c3aed",
icon: str = "fa-folder"
):
"""Crée une catégorie en base (interne)."""
from app.models.log import Log
# Utiliser le modèle Log avec un type spécial pour stocker les catégories
log = Log(
level="ADHOC_CATEGORY",
message=name,
source=description or "",
host_id=None,
task_id=f"{color}|{icon}", # Stocker color et icon dans task_id
)
session.add(log)
async def add_command(
self,
command: str,
target: str,
module: str = "shell",
become: bool = False,
category: str = "default",
description: str = None
) -> AdHocHistoryEntry:
"""Ajoute une commande à l'historique."""
async with async_session_maker() as session:
await self._ensure_default_categories(session)
from app.models.log import Log
cmd_id = f"adhoc_{uuid.uuid4().hex[:12]}"
log = Log(
level="ADHOC_COMMAND",
message=command,
source=category,
host_id=target,
task_id=cmd_id,
)
session.add(log)
await session.commit()
return AdHocHistoryEntry(
id=cmd_id,
command=command,
target=target,
module=module,
become=become,
category=category,
description=description,
created_at=datetime.now(timezone.utc),
last_used=datetime.now(timezone.utc),
use_count=1
)
async def get_commands(
self,
category: str = None,
search: str = None,
limit: int = 50
) -> List[AdHocHistoryEntry]:
"""Récupère les commandes de l'historique."""
async with async_session_maker() as session:
from app.models.log import Log
stmt = select(Log).where(Log.level == "ADHOC_COMMAND")
if category and category != "all":
stmt = stmt.where(Log.source == category)
stmt = stmt.order_by(Log.created_at.desc()).limit(limit)
result = await session.execute(stmt)
logs = result.scalars().all()
commands = []
for log in logs:
if search and search.lower() not in log.message.lower():
continue
commands.append(AdHocHistoryEntry(
id=log.task_id or str(log.id),
command=log.message,
target=log.host_id or "all",
module="shell",
become=False,
category=log.source or "default",
created_at=log.created_at,
last_used=log.created_at,
use_count=1
))
return commands
async def update_command_category(
self,
command_id: str,
category: str,
description: str = None
) -> bool:
"""Met à jour la catégorie d'une commande."""
async with async_session_maker() as session:
from app.models.log import Log
stmt = (
update(Log)
.where(Log.task_id == command_id)
.where(Log.level == "ADHOC_COMMAND")
.values(source=category)
)
result = await session.execute(stmt)
await session.commit()
return result.rowcount > 0
async def delete_command(self, command_id: str) -> bool:
"""Supprime une commande de l'historique."""
async with async_session_maker() as session:
from app.models.log import Log
stmt = (
delete(Log)
.where(Log.task_id == command_id)
.where(Log.level == "ADHOC_COMMAND")
)
result = await session.execute(stmt)
await session.commit()
return result.rowcount > 0
async def get_categories(self) -> List[AdHocHistoryCategory]:
"""Récupère la liste des catégories."""
async with async_session_maker() as session:
from app.models.log import Log
stmt = select(Log).where(Log.level == "ADHOC_CATEGORY")
result = await session.execute(stmt)
logs = result.scalars().all()
if not logs:
# Retourner les catégories par défaut
return [
AdHocHistoryCategory(
name=cat["name"],
description=cat.get("description"),
color=cat.get("color", "#7c3aed"),
icon=cat.get("icon", "fa-folder")
)
for cat in DEFAULT_ADHOC_CATEGORIES
]
categories = []
for log in logs:
# Extraire color et icon depuis task_id
color, icon = "#7c3aed", "fa-folder"
if log.task_id and "|" in log.task_id:
parts = log.task_id.split("|", 1)
color = parts[0]
icon = parts[1] if len(parts) > 1 else "fa-folder"
categories.append(AdHocHistoryCategory(
name=log.message,
description=log.source or None,
color=color,
icon=icon
))
return categories
async def add_category(
self,
name: str,
description: str = None,
color: str = "#7c3aed",
icon: str = "fa-folder"
) -> AdHocHistoryCategory:
"""Ajoute une nouvelle catégorie."""
async with async_session_maker() as session:
await self._create_category_internal(session, name, description, color, icon)
await session.commit()
return AdHocHistoryCategory(
name=name,
description=description,
color=color,
icon=icon
)
async def update_category(
self,
old_name: str,
new_name: str,
description: str = None,
color: str = "#7c3aed",
icon: str = "fa-folder"
) -> bool:
"""Met à jour une catégorie existante."""
async with async_session_maker() as session:
from app.models.log import Log
# Mettre à jour la catégorie
stmt = (
update(Log)
.where(Log.message == old_name)
.where(Log.level == "ADHOC_CATEGORY")
.values(
message=new_name,
source=description or "",
task_id=f"{color}|{icon}"
)
)
result = await session.execute(stmt)
# Mettre à jour les commandes associées si le nom a changé
if old_name != new_name:
stmt2 = (
update(Log)
.where(Log.source == old_name)
.where(Log.level == "ADHOC_COMMAND")
.values(source=new_name)
)
await session.execute(stmt2)
await session.commit()
return result.rowcount > 0
async def delete_category(self, name: str) -> bool:
"""Supprime une catégorie et déplace ses commandes vers 'default'."""
if name == "default":
return False
async with async_session_maker() as session:
from app.models.log import Log
# Déplacer les commandes vers default
stmt1 = (
update(Log)
.where(Log.source == name)
.where(Log.level == "ADHOC_COMMAND")
.values(source="default")
)
await session.execute(stmt1)
# Supprimer la catégorie
stmt2 = (
delete(Log)
.where(Log.message == name)
.where(Log.level == "ADHOC_CATEGORY")
)
result = await session.execute(stmt2)
await session.commit()
return result.rowcount > 0
# Instance singleton du service
adhoc_history_service = AdHocHistoryService()