Add email delivery check command
This commit is contained in:
@@ -92,9 +92,11 @@ docker compose build
|
||||
```sh
|
||||
docker compose run --rm pve-backup-report --check-config
|
||||
docker compose run --rm pve-backup-report --check-api
|
||||
docker compose run --rm pve-backup-report --check-email
|
||||
```
|
||||
|
||||
`--check-api` teste les endpoints PVE principaux. Si `/cluster/backup` retourne `HTTP 403 - Permission check failed (/, Sys.Audit)`, le token fonctionne mais il lui manque le privilège `Sys.Audit` sur `/`.
|
||||
`--check-email` envoie un email de test avec la configuration SMTP du fichier `.env`, sans générer de PDF.
|
||||
|
||||
### 📄 Génération du rapport
|
||||
|
||||
@@ -156,6 +158,7 @@ pytest
|
||||
```sh
|
||||
pve-backup-report --check-config
|
||||
pve-backup-report --check-api
|
||||
pve-backup-report --check-email
|
||||
pve-backup-report --dump-inventory
|
||||
pve-backup-report --dump-coverage
|
||||
pve-backup-report --dump-report-data
|
||||
@@ -170,6 +173,7 @@ Sans installation éditable, depuis le dépôt :
|
||||
```sh
|
||||
PYTHONPATH=src python3 -m pve_backup_report --check-config
|
||||
PYTHONPATH=src python3 -m pve_backup_report --check-api
|
||||
PYTHONPATH=src python3 -m pve_backup_report --check-email
|
||||
PYTHONPATH=src python3 -m pve_backup_report --dump-inventory
|
||||
PYTHONPATH=src python3 -m pve_backup_report --dump-coverage
|
||||
PYTHONPATH=src python3 -m pve_backup_report --dump-report-data
|
||||
|
||||
@@ -92,9 +92,11 @@ docker compose build
|
||||
```sh
|
||||
docker compose run --rm pve-backup-report --check-config
|
||||
docker compose run --rm pve-backup-report --check-api
|
||||
docker compose run --rm pve-backup-report --check-email
|
||||
```
|
||||
|
||||
`--check-api` tests the main PVE endpoints. If `/cluster/backup` returns `HTTP 403 - Permission check failed (/, Sys.Audit)`, the token works but is missing the `Sys.Audit` privilege on `/`.
|
||||
`--check-email` sends a test email with the SMTP configuration from `.env`, without generating a PDF.
|
||||
|
||||
### 📄 Generating the report
|
||||
|
||||
@@ -156,6 +158,7 @@ pytest
|
||||
```sh
|
||||
pve-backup-report --check-config
|
||||
pve-backup-report --check-api
|
||||
pve-backup-report --check-email
|
||||
pve-backup-report --dump-inventory
|
||||
pve-backup-report --dump-coverage
|
||||
pve-backup-report --dump-report-data
|
||||
@@ -170,6 +173,7 @@ Without an editable install, from the repository:
|
||||
```sh
|
||||
PYTHONPATH=src python3 -m pve_backup_report --check-config
|
||||
PYTHONPATH=src python3 -m pve_backup_report --check-api
|
||||
PYTHONPATH=src python3 -m pve_backup_report --check-email
|
||||
PYTHONPATH=src python3 -m pve_backup_report --dump-inventory
|
||||
PYTHONPATH=src python3 -m pve_backup_report --dump-coverage
|
||||
PYTHONPATH=src python3 -m pve_backup_report --dump-report-data
|
||||
|
||||
@@ -45,6 +45,14 @@ This command tests `/nodes`, `/storage`, `/cluster` and the endpoint configured
|
||||
|
||||
If `/cluster/backup` returns `HTTP 403 - Permission check failed (/, Sys.Audit)`, the token works but lacks the `Sys.Audit` privilege on `/`.
|
||||
|
||||
### Email Delivery Test
|
||||
|
||||
```sh
|
||||
docker compose run --rm pve-backup-report --check-email
|
||||
```
|
||||
|
||||
This command sends a test email with the SMTP configuration from `.env`, without generating a PDF. It requires `REPORT_EMAIL_ENABLED=true` and returns an error if the SMTP connection, STARTTLS, authentication or sending fails.
|
||||
|
||||
### PDF Report Generation
|
||||
|
||||
```sh
|
||||
@@ -124,6 +132,7 @@ Installed commands:
|
||||
```sh
|
||||
pve-backup-report --check-config
|
||||
pve-backup-report --check-api
|
||||
pve-backup-report --check-email
|
||||
pve-backup-report --dump-inventory
|
||||
pve-backup-report --dump-coverage
|
||||
pve-backup-report --dump-report-data
|
||||
@@ -137,6 +146,7 @@ Without editable installation:
|
||||
```sh
|
||||
PYTHONPATH=src python3 -m pve_backup_report --check-config
|
||||
PYTHONPATH=src python3 -m pve_backup_report --check-api
|
||||
PYTHONPATH=src python3 -m pve_backup_report --check-email
|
||||
PYTHONPATH=src python3 -m pve_backup_report --dump-inventory
|
||||
PYTHONPATH=src python3 -m pve_backup_report --dump-coverage
|
||||
PYTHONPATH=src python3 -m pve_backup_report --dump-report-data
|
||||
|
||||
@@ -45,6 +45,14 @@ Cette commande teste `/nodes`, `/storage`, `/cluster` et l'endpoint configure da
|
||||
|
||||
Si `/cluster/backup` retourne `HTTP 403 - Permission check failed (/, Sys.Audit)`, le token fonctionne mais il lui manque le privilege `Sys.Audit` sur `/`.
|
||||
|
||||
### Test d'envoi email
|
||||
|
||||
```sh
|
||||
docker compose run --rm pve-backup-report --check-email
|
||||
```
|
||||
|
||||
Cette commande envoie un email de test avec la configuration SMTP du fichier `.env`, sans generer de PDF. Elle exige `REPORT_EMAIL_ENABLED=true` et retourne une erreur si la connexion SMTP, STARTTLS, l'authentification ou l'envoi echoue.
|
||||
|
||||
### Generation du rapport PDF
|
||||
|
||||
```sh
|
||||
@@ -124,6 +132,7 @@ Commandes installees :
|
||||
```sh
|
||||
pve-backup-report --check-config
|
||||
pve-backup-report --check-api
|
||||
pve-backup-report --check-email
|
||||
pve-backup-report --dump-inventory
|
||||
pve-backup-report --dump-coverage
|
||||
pve-backup-report --dump-report-data
|
||||
@@ -137,6 +146,7 @@ Sans installation editable :
|
||||
```sh
|
||||
PYTHONPATH=src python3 -m pve_backup_report --check-config
|
||||
PYTHONPATH=src python3 -m pve_backup_report --check-api
|
||||
PYTHONPATH=src python3 -m pve_backup_report --check-email
|
||||
PYTHONPATH=src python3 -m pve_backup_report --dump-inventory
|
||||
PYTHONPATH=src python3 -m pve_backup_report --dump-coverage
|
||||
PYTHONPATH=src python3 -m pve_backup_report --dump-report-data
|
||||
|
||||
@@ -24,7 +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.email_report import EmailReportError, send_report_email, send_test_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
|
||||
@@ -50,6 +50,11 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
action="store_true",
|
||||
help="teste les endpoints PVE /nodes, /storage et /cluster/backup puis quitte",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--check-email",
|
||||
action="store_true",
|
||||
help="envoie un email de test avec la configuration SMTP puis quitte",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dump-inventory",
|
||||
action="store_true",
|
||||
@@ -156,6 +161,15 @@ def run(argv: Sequence[str] | None = None) -> int:
|
||||
)
|
||||
return 3 if has_error else 0
|
||||
|
||||
if args.check_email:
|
||||
try:
|
||||
send_test_email(config.email)
|
||||
except EmailReportError as exc:
|
||||
logger.error("Verification email echouee: %s", exc)
|
||||
return 6
|
||||
logger.info("Email de test envoye a %s", ", ".join(config.email.smtp_to))
|
||||
return 0
|
||||
|
||||
if args.dump_inventory:
|
||||
report_data = collect_data_or_log_error(config, "inventaire")
|
||||
if report_data is None:
|
||||
|
||||
@@ -20,7 +20,25 @@ def send_report_email(config: EmailConfig, pdf_path: Path) -> None:
|
||||
raise EmailReportError("configuration email incomplete")
|
||||
|
||||
message = build_report_message(config, pdf_path)
|
||||
send_email_message(config, message)
|
||||
|
||||
|
||||
def send_test_email(config: EmailConfig) -> None:
|
||||
if not config.enabled:
|
||||
raise EmailReportError("REPORT_EMAIL_ENABLED=false")
|
||||
if config.smtp_host is None or config.smtp_from is None or not config.smtp_to:
|
||||
raise EmailReportError("configuration email incomplete")
|
||||
|
||||
message = build_base_message(config, f"[TEST] {config.subject}")
|
||||
message.set_content(
|
||||
"Bonjour,\n\n"
|
||||
"Ceci est un email de test envoye par PVE Backup Report.\n\n"
|
||||
"Cordialement.\n"
|
||||
)
|
||||
send_email_message(config, message)
|
||||
|
||||
|
||||
def send_email_message(config: EmailConfig, message: EmailMessage) -> None:
|
||||
try:
|
||||
if config.smtp_ssl:
|
||||
with smtplib.SMTP_SSL(
|
||||
@@ -44,13 +62,7 @@ def send_report_email(config: EmailConfig, pdf_path: Path) -> None:
|
||||
|
||||
|
||||
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["Date"] = formatdate(localtime=True)
|
||||
message["Message-ID"] = make_msgid(domain=message_id_domain(config.smtp_from))
|
||||
message["Auto-Submitted"] = "auto-generated"
|
||||
message = build_base_message(config, config.subject)
|
||||
message.set_content(
|
||||
"Bonjour,\n\n"
|
||||
"Veuillez trouver ci-joint le rapport de sauvegardes Proxmox VE.\n\n"
|
||||
@@ -65,6 +77,17 @@ def build_report_message(config: EmailConfig, pdf_path: Path) -> EmailMessage:
|
||||
return message
|
||||
|
||||
|
||||
def build_base_message(config: EmailConfig, subject: str) -> EmailMessage:
|
||||
message = EmailMessage()
|
||||
message["Subject"] = subject
|
||||
message["From"] = config.smtp_from or ""
|
||||
message["To"] = ", ".join(config.smtp_to)
|
||||
message["Date"] = formatdate(localtime=True)
|
||||
message["Message-ID"] = make_msgid(domain=message_id_domain(config.smtp_from))
|
||||
message["Auto-Submitted"] = "auto-generated"
|
||||
return message
|
||||
|
||||
|
||||
def message_id_domain(address: str | None) -> str | None:
|
||||
if address is None:
|
||||
return None
|
||||
|
||||
@@ -41,6 +41,12 @@ def test_cli_has_dump_pbs_users() -> None:
|
||||
assert args.dump_pbs_users is True
|
||||
|
||||
|
||||
def test_cli_has_check_email() -> None:
|
||||
args = build_parser().parse_args(["--check-email"])
|
||||
|
||||
assert args.check_email is True
|
||||
|
||||
|
||||
def test_dump_report_data_does_not_export_sensitive_raw_fields(
|
||||
monkeypatch,
|
||||
capsys,
|
||||
@@ -246,3 +252,39 @@ def test_generate_pdf_sends_email_when_enabled(tmp_path, monkeypatch) -> None:
|
||||
|
||||
assert run(["--generate-pdf"]) == 0
|
||||
assert sent == [("smtp.example.invalid", pdf_path)]
|
||||
|
||||
|
||||
def test_check_email_sends_test_email(monkeypatch) -> None:
|
||||
config = AppConfig(
|
||||
pve_api_url="https://pve.example.invalid:8006",
|
||||
pve_api_token_id="backup-report@pve!report",
|
||||
pve_api_token_secret="secret",
|
||||
report_output_dir=Path("reports"),
|
||||
report_timezone="Europe/Paris",
|
||||
pve_verify_tls=True,
|
||||
pve_ca_bundle=None,
|
||||
pve_timeout_seconds=30,
|
||||
pve_backup_jobs_endpoint="/cluster/backup",
|
||||
pve_task_history_limit=500,
|
||||
pve_task_log_limit=5000,
|
||||
pbs_hostnames={},
|
||||
pbs_servers=(),
|
||||
log_level="INFO",
|
||||
report_filename_prefix="rapport-sauvegardes-pve",
|
||||
email=EmailConfig(
|
||||
enabled=True,
|
||||
smtp_host="smtp.example.invalid",
|
||||
smtp_from="report@example.invalid",
|
||||
smtp_to=("admin@example.invalid",),
|
||||
),
|
||||
)
|
||||
sent = []
|
||||
|
||||
monkeypatch.setattr("pve_backup_report.cli.load_config", lambda: config)
|
||||
monkeypatch.setattr(
|
||||
"pve_backup_report.cli.send_test_email",
|
||||
lambda email_config: sent.append(email_config.smtp_host),
|
||||
)
|
||||
|
||||
assert run(["--check-email"]) == 0
|
||||
assert sent == ["smtp.example.invalid"]
|
||||
|
||||
@@ -3,8 +3,10 @@ from pathlib import Path
|
||||
from pve_backup_report.config import EmailConfig
|
||||
from pve_backup_report.email_report import (
|
||||
build_report_message,
|
||||
EmailReportError,
|
||||
message_id_domain,
|
||||
send_report_email,
|
||||
send_test_email,
|
||||
)
|
||||
|
||||
|
||||
@@ -41,6 +43,46 @@ def test_message_id_domain_uses_sender_domain() -> None:
|
||||
assert message_id_domain(None) is None
|
||||
|
||||
|
||||
def test_send_test_email_builds_message_without_attachment(monkeypatch) -> None:
|
||||
sent = []
|
||||
config = EmailConfig(
|
||||
enabled=True,
|
||||
smtp_host="smtp.example.invalid",
|
||||
smtp_from="report@example.invalid",
|
||||
smtp_to=("admin@example.invalid",),
|
||||
subject="Rapport PVE",
|
||||
)
|
||||
|
||||
monkeypatch.setattr(
|
||||
"pve_backup_report.email_report.send_email_message",
|
||||
lambda email_config, message: sent.append((email_config, message)),
|
||||
)
|
||||
|
||||
send_test_email(config)
|
||||
|
||||
assert sent[0][0] == config
|
||||
message = sent[0][1]
|
||||
assert message["Subject"] == "[TEST] Rapport PVE"
|
||||
assert message["To"] == "admin@example.invalid"
|
||||
assert list(message.iter_attachments()) == []
|
||||
|
||||
|
||||
def test_send_test_email_requires_enabled_config() -> None:
|
||||
config = EmailConfig(
|
||||
enabled=False,
|
||||
smtp_host="smtp.example.invalid",
|
||||
smtp_from="report@example.invalid",
|
||||
smtp_to=("admin@example.invalid",),
|
||||
)
|
||||
|
||||
try:
|
||||
send_test_email(config)
|
||||
except EmailReportError as exc:
|
||||
assert "REPORT_EMAIL_ENABLED=false" in str(exc)
|
||||
else:
|
||||
raise AssertionError("EmailReportError attendu")
|
||||
|
||||
|
||||
def test_send_report_email_uses_starttls_and_auth(tmp_path: Path, monkeypatch) -> None:
|
||||
pdf_path = tmp_path / "rapport.pdf"
|
||||
pdf_path.write_bytes(b"%PDF-1.7")
|
||||
|
||||
Reference in New Issue
Block a user