Add email delivery for generated reports
This commit is contained in:
@@ -24,6 +24,7 @@ from pve_backup_report.coverage import (
|
||||
STATUS_PBS_PLANNED,
|
||||
analyze_backup_coverage,
|
||||
)
|
||||
from pve_backup_report.email_report import EmailReportError, send_report_email
|
||||
from pve_backup_report.logging_config import configure_logging
|
||||
from pve_backup_report.pbs_client import PbsApiError, PbsClient
|
||||
from pve_backup_report.pve_client import PveApiError, PveClient
|
||||
@@ -212,6 +213,16 @@ def run(argv: Sequence[str] | None = None) -> int:
|
||||
return 4
|
||||
|
||||
logger.info("Rapport PDF genere: %s", pdf_path)
|
||||
if config.email.enabled:
|
||||
try:
|
||||
send_report_email(config.email, pdf_path)
|
||||
except EmailReportError as exc:
|
||||
logger.error("Envoi email echoue: %s", exc)
|
||||
return 6
|
||||
logger.info(
|
||||
"Rapport PDF envoye par email a %s",
|
||||
", ".join(config.email.smtp_to),
|
||||
)
|
||||
return 0 if not any(issue.severity == "error" for issue in report_data.issues) else 5
|
||||
|
||||
if args.debug_last_backup_vmid is not None:
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
import os
|
||||
import re
|
||||
from collections.abc import Mapping
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
|
||||
from pve_backup_report.i18n import TranslationError, normalize_language
|
||||
@@ -33,6 +33,21 @@ class PbsServerConfig:
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class EmailConfig:
|
||||
enabled: bool = False
|
||||
smtp_host: str | None = None
|
||||
smtp_port: int = 587
|
||||
smtp_username: str | None = None
|
||||
smtp_password: str | None = None
|
||||
smtp_from: str | None = None
|
||||
smtp_to: tuple[str, ...] = ()
|
||||
smtp_starttls: bool = True
|
||||
smtp_ssl: bool = False
|
||||
smtp_timeout_seconds: int = 30
|
||||
subject: str = "Rapport sauvegardes PVE"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AppConfig:
|
||||
pve_api_url: str
|
||||
@@ -51,6 +66,7 @@ class AppConfig:
|
||||
log_level: str
|
||||
report_filename_prefix: str
|
||||
report_language: str = "fr"
|
||||
email: EmailConfig = field(default_factory=EmailConfig)
|
||||
|
||||
@property
|
||||
def configured_pbs_servers(self) -> tuple[PbsServerConfig, ...]:
|
||||
@@ -103,10 +119,80 @@ def load_config(env_file: str | Path | None = ".env") -> AppConfig:
|
||||
"REPORT_FILENAME_PREFIX",
|
||||
"rapport-sauvegardes-pve",
|
||||
),
|
||||
email=parse_email_config(os.environ),
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
def parse_email_config(environ: Mapping[str, str]) -> EmailConfig:
|
||||
enabled = parse_bool(
|
||||
environ.get("REPORT_EMAIL_ENABLED", "false"),
|
||||
"REPORT_EMAIL_ENABLED",
|
||||
)
|
||||
smtp_ssl = parse_bool(
|
||||
environ.get("REPORT_EMAIL_SMTP_SSL", "false"),
|
||||
"REPORT_EMAIL_SMTP_SSL",
|
||||
)
|
||||
smtp_starttls = parse_bool(
|
||||
environ.get("REPORT_EMAIL_SMTP_STARTTLS", "true"),
|
||||
"REPORT_EMAIL_SMTP_STARTTLS",
|
||||
)
|
||||
if smtp_ssl and smtp_starttls:
|
||||
raise ConfigError(
|
||||
"REPORT_EMAIL_SMTP_SSL et REPORT_EMAIL_SMTP_STARTTLS ne peuvent pas etre actifs ensemble"
|
||||
)
|
||||
|
||||
smtp_username = parse_optional_string(environ.get("REPORT_EMAIL_SMTP_USERNAME"))
|
||||
smtp_password = parse_optional_string(environ.get("REPORT_EMAIL_SMTP_PASSWORD"))
|
||||
validate_optional_group(
|
||||
"REPORT_EMAIL_SMTP_AUTH",
|
||||
{
|
||||
"REPORT_EMAIL_SMTP_USERNAME": smtp_username,
|
||||
"REPORT_EMAIL_SMTP_PASSWORD": smtp_password,
|
||||
},
|
||||
)
|
||||
|
||||
smtp_to = parse_string_list(environ.get("REPORT_EMAIL_TO", ""), "REPORT_EMAIL_TO")
|
||||
config = EmailConfig(
|
||||
enabled=enabled,
|
||||
smtp_host=parse_optional_string(environ.get("REPORT_EMAIL_SMTP_HOST")),
|
||||
smtp_port=parse_int(
|
||||
environ.get("REPORT_EMAIL_SMTP_PORT", "587"),
|
||||
"REPORT_EMAIL_SMTP_PORT",
|
||||
),
|
||||
smtp_username=smtp_username,
|
||||
smtp_password=smtp_password,
|
||||
smtp_from=parse_optional_string(environ.get("REPORT_EMAIL_FROM")),
|
||||
smtp_to=smtp_to,
|
||||
smtp_starttls=smtp_starttls,
|
||||
smtp_ssl=smtp_ssl,
|
||||
smtp_timeout_seconds=parse_int(
|
||||
environ.get("REPORT_EMAIL_SMTP_TIMEOUT_SECONDS", "30"),
|
||||
"REPORT_EMAIL_SMTP_TIMEOUT_SECONDS",
|
||||
),
|
||||
subject=(
|
||||
parse_optional_string(environ.get("REPORT_EMAIL_SUBJECT"))
|
||||
or "Rapport sauvegardes PVE"
|
||||
),
|
||||
)
|
||||
|
||||
if enabled:
|
||||
missing = []
|
||||
if config.smtp_host is None:
|
||||
missing.append("REPORT_EMAIL_SMTP_HOST")
|
||||
if config.smtp_from is None:
|
||||
missing.append("REPORT_EMAIL_FROM")
|
||||
if not config.smtp_to:
|
||||
missing.append("REPORT_EMAIL_TO")
|
||||
if missing:
|
||||
raise ConfigError(
|
||||
"configuration email incomplete: variables manquantes "
|
||||
+ ", ".join(missing)
|
||||
)
|
||||
|
||||
return config
|
||||
|
||||
|
||||
PBS_SERVER_ENV_PATTERN = re.compile(r"^PBS(\d+)_([A-Z0-9_]+)$")
|
||||
PBS_SERVER_REQUIRED_KEYS = ("API_URL", "API_TOKEN_ID", "API_TOKEN_SECRET")
|
||||
PBS_SERVER_KNOWN_KEYS = {
|
||||
@@ -272,3 +358,13 @@ def parse_mapping(value: str, name: str) -> dict[str, str]:
|
||||
raise ConfigError(f"{name} contient une entree vide")
|
||||
mapping[key] = mapped_value
|
||||
return mapping
|
||||
|
||||
|
||||
def parse_string_list(value: str | None, name: str) -> tuple[str, ...]:
|
||||
if value is None or value.strip() == "":
|
||||
return ()
|
||||
|
||||
items = tuple(item.strip() for item in value.split(",") if item.strip())
|
||||
if not items:
|
||||
raise ConfigError(f"{name} contient une entree vide")
|
||||
return items
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import smtplib
|
||||
from email.message import EmailMessage
|
||||
from pathlib import Path
|
||||
|
||||
from pve_backup_report.config import EmailConfig
|
||||
from pve_backup_report.sanitization import sanitize_message
|
||||
|
||||
|
||||
class EmailReportError(RuntimeError):
|
||||
"""Erreur d'envoi du rapport par email."""
|
||||
|
||||
|
||||
def send_report_email(config: EmailConfig, pdf_path: Path) -> None:
|
||||
if not config.enabled:
|
||||
return
|
||||
if config.smtp_host is None or config.smtp_from is None or not config.smtp_to:
|
||||
raise EmailReportError("configuration email incomplete")
|
||||
|
||||
message = build_report_message(config, pdf_path)
|
||||
|
||||
try:
|
||||
if config.smtp_ssl:
|
||||
with smtplib.SMTP_SSL(
|
||||
config.smtp_host,
|
||||
config.smtp_port,
|
||||
timeout=config.smtp_timeout_seconds,
|
||||
) as smtp:
|
||||
authenticate_and_send(smtp, config, message)
|
||||
return
|
||||
|
||||
with smtplib.SMTP(
|
||||
config.smtp_host,
|
||||
config.smtp_port,
|
||||
timeout=config.smtp_timeout_seconds,
|
||||
) as smtp:
|
||||
if config.smtp_starttls:
|
||||
smtp.starttls()
|
||||
authenticate_and_send(smtp, config, message)
|
||||
except (OSError, smtplib.SMTPException) as exc:
|
||||
raise EmailReportError(sanitize_message(exc)) from exc
|
||||
|
||||
|
||||
def build_report_message(config: EmailConfig, pdf_path: Path) -> EmailMessage:
|
||||
message = EmailMessage()
|
||||
message["Subject"] = config.subject
|
||||
message["From"] = config.smtp_from or ""
|
||||
message["To"] = ", ".join(config.smtp_to)
|
||||
message.set_content(
|
||||
"Bonjour,\n\n"
|
||||
"Veuillez trouver ci-joint le rapport de sauvegardes Proxmox VE.\n\n"
|
||||
"Cordialement.\n"
|
||||
)
|
||||
message.add_attachment(
|
||||
pdf_path.read_bytes(),
|
||||
maintype="application",
|
||||
subtype="pdf",
|
||||
filename=pdf_path.name,
|
||||
)
|
||||
return message
|
||||
|
||||
|
||||
def authenticate_and_send(
|
||||
smtp: smtplib.SMTP | smtplib.SMTP_SSL,
|
||||
config: EmailConfig,
|
||||
message: EmailMessage,
|
||||
) -> None:
|
||||
if config.smtp_username is not None and config.smtp_password is not None:
|
||||
smtp.login(config.smtp_username, config.smtp_password)
|
||||
smtp.send_message(message)
|
||||
Reference in New Issue
Block a user