393 lines
12 KiB
Python
393 lines
12 KiB
Python
"""
|
|
Schémas Pydantic pour le système de notifications ntfy.
|
|
"""
|
|
|
|
from typing import Optional, List, Dict, Any, Literal
|
|
from pydantic import BaseModel, Field, field_validator
|
|
import os
|
|
|
|
|
|
class NtfyConfig(BaseModel):
|
|
"""Configuration du service de notification ntfy."""
|
|
|
|
base_url: str = Field(
|
|
default="http://localhost:8150",
|
|
description="URL de base du serveur ntfy"
|
|
)
|
|
default_topic: str = Field(
|
|
default="homelab-events",
|
|
description="Topic par défaut pour les notifications"
|
|
)
|
|
enabled: bool = Field(
|
|
default=True,
|
|
description="Activer/désactiver les notifications"
|
|
)
|
|
timeout: int = Field(
|
|
default=5,
|
|
ge=1,
|
|
le=30,
|
|
description="Timeout en secondes pour les requêtes HTTP"
|
|
)
|
|
username: Optional[str] = Field(
|
|
default=None,
|
|
description="Nom d'utilisateur pour l'authentification Basic"
|
|
)
|
|
password: Optional[str] = Field(
|
|
default=None,
|
|
description="Mot de passe pour l'authentification Basic"
|
|
)
|
|
token: Optional[str] = Field(
|
|
default=None,
|
|
description="Token Bearer pour l'authentification"
|
|
)
|
|
msg_types: List[str] = Field(
|
|
default_factory=lambda: ["ALL"],
|
|
description=(
|
|
"Types de notifications à envoyer: ALL, ERR, WARN "
|
|
"(liste séparée par des virgules, ex: 'ERR,WARN')"
|
|
),
|
|
)
|
|
|
|
@classmethod
|
|
def from_env(cls) -> "NtfyConfig":
|
|
"""Crée une configuration à partir des variables d'environnement."""
|
|
enabled_str = os.environ.get("NTFY_ENABLED", "true").lower()
|
|
enabled = enabled_str in ("true", "1", "yes", "on")
|
|
# NTFY_MSG_TYPE: ALL, ERR, WARN, ou combinaison séparée par des virgules
|
|
raw_types = os.environ.get("NTFY_MSG_TYPE", "ALL")
|
|
tokens = [t.strip().upper() for t in raw_types.split(",") if t.strip()]
|
|
# Normaliser et filtrer les valeurs invalides
|
|
valid_tokens = {"ALL", "ERR", "WARN"}
|
|
selected = [t for t in tokens if t in valid_tokens]
|
|
if not selected or "ALL" in selected:
|
|
msg_types = ["ALL"]
|
|
else:
|
|
# Supprimer les doublons en conservant l'ordre
|
|
seen = set()
|
|
msg_types = []
|
|
for t in selected:
|
|
if t not in seen:
|
|
seen.add(t)
|
|
msg_types.append(t)
|
|
|
|
return cls(
|
|
base_url=os.environ.get("NTFY_BASE_URL", "http://localhost:8150"),
|
|
default_topic=os.environ.get("NTFY_DEFAULT_TOPIC", "homelab-events"),
|
|
enabled=enabled,
|
|
timeout=int(os.environ.get("NTFY_TIMEOUT", "5")),
|
|
username=os.environ.get("NTFY_USERNAME") or None,
|
|
password=os.environ.get("NTFY_PASSWORD") or None,
|
|
token=os.environ.get("NTFY_TOKEN") or None,
|
|
msg_types=msg_types,
|
|
)
|
|
|
|
@property
|
|
def has_auth(self) -> bool:
|
|
"""Vérifie si l'authentification est configurée."""
|
|
return bool(self.token) or (bool(self.username) and bool(self.password))
|
|
|
|
@property
|
|
def allowed_levels(self) -> set[str]:
|
|
"""Retourne les niveaux logiques autorisés: INFO, WARN, ERR.
|
|
|
|
Mapping:
|
|
- ALL -> {INFO, WARN, ERR}
|
|
- ERR -> {ERR}
|
|
- WARN -> {WARN}
|
|
- ERR,WARN -> {ERR, WARN}
|
|
"""
|
|
# Si ALL est présent, tout est autorisé
|
|
if "ALL" in self.msg_types:
|
|
return {"INFO", "WARN", "ERR"}
|
|
|
|
levels: set[str] = set()
|
|
for t in self.msg_types:
|
|
if t == "ERR":
|
|
levels.add("ERR")
|
|
elif t == "WARN":
|
|
levels.add("WARN")
|
|
|
|
# Fallback: si config vide/invalide, tout autoriser
|
|
return levels or {"INFO", "WARN", "ERR"}
|
|
|
|
|
|
class NtfyAction(BaseModel):
|
|
"""Action attachée à une notification ntfy."""
|
|
|
|
action: Literal["view", "broadcast", "http"] = Field(
|
|
default="view",
|
|
description="Type d'action"
|
|
)
|
|
label: str = Field(
|
|
...,
|
|
description="Texte du bouton d'action"
|
|
)
|
|
url: Optional[str] = Field(
|
|
default=None,
|
|
description="URL pour les actions 'view' et 'http'"
|
|
)
|
|
method: Optional[str] = Field(
|
|
default=None,
|
|
description="Méthode HTTP pour l'action 'http'"
|
|
)
|
|
headers: Optional[Dict[str, str]] = Field(
|
|
default=None,
|
|
description="Headers HTTP pour l'action 'http'"
|
|
)
|
|
body: Optional[str] = Field(
|
|
default=None,
|
|
description="Corps de la requête pour l'action 'http'"
|
|
)
|
|
clear: bool = Field(
|
|
default=False,
|
|
description="Effacer la notification après l'action"
|
|
)
|
|
|
|
|
|
class NotificationRequest(BaseModel):
|
|
"""Requête pour envoyer une notification."""
|
|
|
|
topic: Optional[str] = Field(
|
|
default=None,
|
|
description="Topic cible (utilise le topic par défaut si non spécifié)"
|
|
)
|
|
message: str = Field(
|
|
...,
|
|
min_length=1,
|
|
max_length=4096,
|
|
description="Corps du message"
|
|
)
|
|
title: Optional[str] = Field(
|
|
default=None,
|
|
max_length=250,
|
|
description="Titre de la notification"
|
|
)
|
|
priority: Optional[int] = Field(
|
|
default=None,
|
|
ge=1,
|
|
le=5,
|
|
description="Priorité: 1=min, 2=low, 3=default, 4=high, 5=urgent"
|
|
)
|
|
tags: Optional[List[str]] = Field(
|
|
default=None,
|
|
description="Tags/emojis (ex: ['warning', 'skull'])"
|
|
)
|
|
click: Optional[str] = Field(
|
|
default=None,
|
|
description="URL à ouvrir au clic sur la notification"
|
|
)
|
|
attach: Optional[str] = Field(
|
|
default=None,
|
|
description="URL d'une pièce jointe"
|
|
)
|
|
actions: Optional[List[NtfyAction]] = Field(
|
|
default=None,
|
|
description="Actions attachées à la notification"
|
|
)
|
|
delay: Optional[str] = Field(
|
|
default=None,
|
|
description="Délai avant envoi (ex: '30m', '1h', '2025-01-01T10:00:00')"
|
|
)
|
|
|
|
|
|
class NotificationResponse(BaseModel):
|
|
"""Réponse après envoi d'une notification."""
|
|
|
|
success: bool = Field(
|
|
...,
|
|
description="Indique si l'envoi a réussi"
|
|
)
|
|
topic: str = Field(
|
|
...,
|
|
description="Topic utilisé"
|
|
)
|
|
message_id: Optional[str] = Field(
|
|
default=None,
|
|
description="ID du message retourné par ntfy"
|
|
)
|
|
error: Optional[str] = Field(
|
|
default=None,
|
|
description="Message d'erreur en cas d'échec"
|
|
)
|
|
|
|
|
|
# ===== Helpers pour les notifications courantes =====
|
|
|
|
class NotificationTemplates:
|
|
"""Templates de notifications prédéfinis pour les cas d'usage courants."""
|
|
|
|
@staticmethod
|
|
def app_started() -> NotificationRequest:
|
|
"""Notification de démarrage de l'application."""
|
|
return NotificationRequest(
|
|
topic=None,
|
|
title="✅ Homelab Dashboard démarré",
|
|
message="L'application Homelab Automation Dashboard est maintenant en ligne et opérationnelle.",
|
|
priority=3,
|
|
tags=["white_check_mark", "rocket"]
|
|
)
|
|
|
|
@staticmethod
|
|
def app_stopped() -> NotificationRequest:
|
|
"""Notification d'arrêt de l'application."""
|
|
return NotificationRequest(
|
|
topic=None,
|
|
title="⚠️ Homelab Dashboard arrêté",
|
|
message="L'application Homelab Automation Dashboard a été arrêtée.",
|
|
priority=4,
|
|
tags=["warning", "octagonal_sign"]
|
|
)
|
|
|
|
@staticmethod
|
|
def backup_success(
|
|
hostname: str,
|
|
duration: str,
|
|
size: Optional[str] = None
|
|
) -> NotificationRequest:
|
|
"""Notification de succès de backup."""
|
|
details = [f"• Hôte : {hostname}"]
|
|
if duration:
|
|
details.append(f"• Durée : {duration}")
|
|
if size:
|
|
details.append(f"• Taille : {size}")
|
|
|
|
return NotificationRequest(
|
|
topic=None,
|
|
title="✅ Backup terminé avec succès",
|
|
message="\n".join(details),
|
|
priority=3,
|
|
tags=["white_check_mark", "floppy_disk"]
|
|
)
|
|
|
|
@staticmethod
|
|
def backup_failed(
|
|
hostname: str,
|
|
error: str
|
|
) -> NotificationRequest:
|
|
"""Notification d'échec de backup."""
|
|
return NotificationRequest(
|
|
topic=None,
|
|
title="❌ Échec du backup",
|
|
message=f"• Hôte : {hostname}\n• Erreur : {error}",
|
|
priority=5,
|
|
tags=["x", "warning"]
|
|
)
|
|
|
|
@staticmethod
|
|
def bootstrap_started(hostname: str) -> NotificationRequest:
|
|
"""Notification de début de bootstrap."""
|
|
return NotificationRequest(
|
|
topic=None,
|
|
title="🔧 Bootstrap en cours",
|
|
message=f"Configuration initiale en cours pour l'hôte {hostname}.",
|
|
priority=3,
|
|
tags=["wrench", "computer"]
|
|
)
|
|
|
|
@staticmethod
|
|
def bootstrap_success(hostname: str) -> NotificationRequest:
|
|
"""Notification de succès de bootstrap."""
|
|
return NotificationRequest(
|
|
topic=None,
|
|
title="✅ Bootstrap terminé avec succès",
|
|
message=f"L'hôte {hostname} est maintenant configuré et prêt pour Ansible.",
|
|
priority=3,
|
|
tags=["white_check_mark", "computer"]
|
|
)
|
|
|
|
@staticmethod
|
|
def bootstrap_failed(hostname: str, error: str) -> NotificationRequest:
|
|
"""Notification d'échec de bootstrap."""
|
|
return NotificationRequest(
|
|
topic=None,
|
|
title="❌ Échec du bootstrap",
|
|
message=f"• Hôte : {hostname}\n• Erreur : {error}",
|
|
priority=5,
|
|
tags=["x", "warning"]
|
|
)
|
|
|
|
@staticmethod
|
|
def health_status_changed(
|
|
hostname: str,
|
|
new_status: Literal["up", "down"],
|
|
details: Optional[str] = None
|
|
) -> NotificationRequest:
|
|
"""Notification de changement d'état de santé."""
|
|
if new_status == "down":
|
|
return NotificationRequest(
|
|
topic=None,
|
|
title="🔴 Hôte inaccessible",
|
|
message=f"L'hôte {hostname} ne répond plus." + (f"\n• Détails : {details}" if details else ""),
|
|
priority=5,
|
|
tags=["red_circle", "warning"]
|
|
)
|
|
else:
|
|
return NotificationRequest(
|
|
topic=None,
|
|
title="🟢 Hôte de nouveau accessible",
|
|
message=f"L'hôte {hostname} est de nouveau en ligne." + (f"\n• Détails : {details}" if details else ""),
|
|
priority=3,
|
|
tags=["green_circle", "white_check_mark"]
|
|
)
|
|
|
|
@staticmethod
|
|
def task_completed(
|
|
task_name: str,
|
|
target: str,
|
|
duration: Optional[str] = None
|
|
) -> NotificationRequest:
|
|
"""Notification de tâche terminée."""
|
|
lines = [
|
|
f"• Tâche : {task_name}",
|
|
f"• Cible : {target}",
|
|
]
|
|
if duration:
|
|
lines.append(f"• Durée : {duration}")
|
|
|
|
return NotificationRequest(
|
|
topic=None,
|
|
title="✅ Tâche exécutée avec succès",
|
|
message="\n".join(lines),
|
|
priority=3,
|
|
tags=["white_check_mark", "gear"]
|
|
)
|
|
|
|
@staticmethod
|
|
def task_failed(
|
|
task_name: str,
|
|
target: str,
|
|
error: str
|
|
) -> NotificationRequest:
|
|
"""Notification d'échec de tâche."""
|
|
return NotificationRequest(
|
|
topic=None,
|
|
title="❌ Échec de la tâche",
|
|
message=f"• Tâche : {task_name}\n• Cible : {target}\n• Erreur : {error}",
|
|
priority=5,
|
|
tags=["x", "warning"]
|
|
)
|
|
|
|
@staticmethod
|
|
def schedule_executed(
|
|
schedule_name: str,
|
|
success: bool,
|
|
details: Optional[str] = None
|
|
) -> NotificationRequest:
|
|
"""Notification d'exécution de schedule."""
|
|
if success:
|
|
return NotificationRequest(
|
|
topic=None,
|
|
title="✅ Planification exécutée avec succès",
|
|
message=f"• Schedule : {schedule_name}" + (f"\n• Détails : {details}" if details else ""),
|
|
priority=3,
|
|
tags=["white_check_mark", "calendar"]
|
|
)
|
|
else:
|
|
return NotificationRequest(
|
|
topic=None,
|
|
title="❌ Échec de la planification",
|
|
message=f"• Schedule : {schedule_name}" + (f"\n• Détails : {details}" if details else ""),
|
|
priority=5,
|
|
tags=["x", "calendar"]
|
|
)
|