490 lines
14 KiB
Python
490 lines
14 KiB
Python
from datetime import datetime
|
|
|
|
from pve_backup_report.models import (
|
|
BackupCoverage,
|
|
BackupJob,
|
|
Guest,
|
|
LastBackupResult,
|
|
PbsAccessUser,
|
|
PbsBackupSnapshotSummary,
|
|
PbsDatastoreUsage,
|
|
PbsRetentionPolicy,
|
|
PbsStorage,
|
|
ReportData,
|
|
)
|
|
from pve_backup_report.report_pdf import coverage_sort_key
|
|
from pve_backup_report.report_pdf import coverage_row
|
|
from pve_backup_report.report_pdf import backup_retention_row
|
|
from pve_backup_report.report_pdf import build_table
|
|
from pve_backup_report.report_pdf import build_backup_retention_rows
|
|
from pve_backup_report.report_pdf import build_styles
|
|
from pve_backup_report.report_pdf import display_retention_delta
|
|
from pve_backup_report.report_pdf import expected_retention_versions
|
|
from pve_backup_report.report_pdf import format_duration
|
|
from pve_backup_report.report_pdf import find_snapshot_summary
|
|
from pve_backup_report.report_pdf import format_last_backup
|
|
from pve_backup_report.report_pdf import format_size
|
|
from pve_backup_report.report_pdf import pbs_datastore_usage_row
|
|
from pve_backup_report.report_pdf import pbs_access_user_row
|
|
from pve_backup_report.report_pdf import retention_policy_row
|
|
from pve_backup_report.report_pdf import add_table_of_contents
|
|
from pve_backup_report.report_pdf import unique_report_path
|
|
from pve_backup_report.report_pdf import format_pbs_server
|
|
|
|
|
|
def test_unique_report_path_uses_timestamp(tmp_path) -> None:
|
|
generated_at = datetime(2026, 5, 7, 2, 0, 0)
|
|
|
|
path = unique_report_path(tmp_path, "rapport", generated_at)
|
|
|
|
assert path.name == "rapport-2026-05-07-020000.pdf"
|
|
|
|
|
|
def test_unique_report_path_does_not_overwrite(tmp_path) -> None:
|
|
generated_at = datetime(2026, 5, 7, 2, 0, 0)
|
|
existing = tmp_path / "rapport-2026-05-07-020000.pdf"
|
|
existing.write_text("existing", encoding="utf-8")
|
|
|
|
path = unique_report_path(tmp_path, "rapport", generated_at)
|
|
|
|
assert path.name == "rapport-2026-05-07-020000-1.pdf"
|
|
|
|
|
|
def test_format_pbs_server_with_hostname_mapping() -> None:
|
|
assert (
|
|
format_pbs_server("192.0.2.10", {"192.0.2.10": "backup-display"})
|
|
== "192.0.2.10 (backup-display)"
|
|
)
|
|
|
|
|
|
def test_format_pbs_server_without_mapping() -> None:
|
|
assert format_pbs_server("192.0.2.10", {}) == "192.0.2.10"
|
|
|
|
|
|
def test_format_duration() -> None:
|
|
assert format_duration(3723) == "01:02:03"
|
|
|
|
|
|
def test_format_size() -> None:
|
|
assert format_size(1536) == "1.5 Kio"
|
|
assert format_size(2 * 1024 * 1024 * 1024) == "2.0 Gio"
|
|
|
|
|
|
def test_pbs_datastore_usage_row() -> None:
|
|
usage = PbsDatastoreUsage(
|
|
server_name="PBS01",
|
|
datastore="RAID5",
|
|
total_bytes=10 * 1024 * 1024 * 1024,
|
|
used_bytes=4 * 1024 * 1024 * 1024,
|
|
available_bytes=6 * 1024 * 1024 * 1024,
|
|
)
|
|
|
|
assert pbs_datastore_usage_row(usage) == [
|
|
"PBS01",
|
|
"RAID5",
|
|
"10.0 Gio",
|
|
"4.0 Gio",
|
|
"6.0 Gio",
|
|
]
|
|
|
|
|
|
def test_pbs_access_user_row_displays_permissions() -> None:
|
|
user = PbsAccessUser(
|
|
server_name="PBS01",
|
|
auth_id="backup@pbs!pve",
|
|
user_id="backup@pbs",
|
|
storage_id="BACKUP-PROD",
|
|
datastore="RAID5",
|
|
namespace="serveurs",
|
|
enabled=True,
|
|
expire=0,
|
|
email="admin@example.invalid",
|
|
permissions={"Datastore.Backup": True, "Datastore.Modify": False},
|
|
comment="Compte PVE",
|
|
)
|
|
|
|
assert pbs_access_user_row(user) == [
|
|
"PBS01",
|
|
"backup@pbs!pve",
|
|
"BACKUP-PROD",
|
|
"RAID5",
|
|
"serveurs",
|
|
"oui",
|
|
"aucune",
|
|
"admin@example.invalid",
|
|
"Datastore.Backup",
|
|
"Compte PVE",
|
|
]
|
|
|
|
|
|
def test_build_table_centers_table() -> None:
|
|
table = build_table([["Colonne"], ["Valeur"]], [4])
|
|
|
|
assert table.hAlign == "CENTER"
|
|
|
|
|
|
def test_add_table_of_contents_adds_page_break() -> None:
|
|
story: list[object] = []
|
|
|
|
add_table_of_contents(story, build_styles())
|
|
|
|
assert len(story) == 3
|
|
|
|
|
|
def test_format_last_backup_includes_duration() -> None:
|
|
result = LastBackupResult(
|
|
vmid=100,
|
|
status="succes",
|
|
finished_at=datetime(2026, 5, 7, 2, 14),
|
|
duration_seconds=222,
|
|
)
|
|
|
|
assert format_last_backup(result) == "Succes - 2026-05-07 02:14 - duree 00:03:42"
|
|
|
|
|
|
def test_retention_policy_row_splits_columns() -> None:
|
|
policy = PbsRetentionPolicy(
|
|
policy_id="prune-prod",
|
|
server_name="PBS01",
|
|
datastore="RAID5",
|
|
namespace="serveurs-internes",
|
|
schedule="daily",
|
|
enabled=True,
|
|
keep_last=1,
|
|
keep_hourly=2,
|
|
keep_daily=14,
|
|
keep_weekly=8,
|
|
keep_monthly=3,
|
|
keep_yearly=1,
|
|
max_depth=0,
|
|
)
|
|
|
|
assert retention_policy_row(policy) == [
|
|
"PBS01",
|
|
"RAID5",
|
|
"serveurs-internes",
|
|
"daily",
|
|
"oui",
|
|
1,
|
|
2,
|
|
14,
|
|
8,
|
|
3,
|
|
1,
|
|
0,
|
|
]
|
|
|
|
|
|
def test_find_snapshot_summary_matches_storage_namespace_and_guest() -> None:
|
|
guest = Guest(vmid=100, name="srv", guest_type="qemu")
|
|
item = BackupCoverage(
|
|
guest=guest,
|
|
status="sauvegarde_pbs_planifiee",
|
|
storages=["BACKUP-PRODR5"],
|
|
)
|
|
summary = PbsBackupSnapshotSummary(
|
|
server_name="PBS01",
|
|
vmid=100,
|
|
guest_type="qemu",
|
|
datastore="RAID5",
|
|
namespace="serveurs-internes",
|
|
snapshot_count=13,
|
|
)
|
|
|
|
assert (
|
|
find_snapshot_summary(
|
|
item,
|
|
{"BACKUP-PRODR5": "RAID5"},
|
|
{"BACKUP-PRODR5": "serveurs-internes"},
|
|
{("PBS01", "RAID5", "serveurs-internes", "qemu", 100): summary},
|
|
"PBS01",
|
|
)
|
|
== summary
|
|
)
|
|
|
|
|
|
def test_find_snapshot_summary_matches_pbs02_without_pve_storage_datastore() -> None:
|
|
guest = Guest(vmid=100, name="srv", guest_type="qemu")
|
|
item = BackupCoverage(
|
|
guest=guest,
|
|
status="sauvegarde_pbs_planifiee",
|
|
storages=["BACKUP-PRODR5"],
|
|
)
|
|
summary = PbsBackupSnapshotSummary(
|
|
server_name="PBS02",
|
|
vmid=100,
|
|
guest_type="qemu",
|
|
datastore="PBS2RAID5",
|
|
namespace="serveurs-internes",
|
|
snapshot_count=13,
|
|
)
|
|
|
|
assert (
|
|
find_snapshot_summary(
|
|
item,
|
|
{"BACKUP-PRODR5": "RAID5"},
|
|
{"BACKUP-PRODR5": "serveurs-internes"},
|
|
{("PBS02", "PBS2RAID5", "serveurs-internes", "qemu", 100): summary},
|
|
"PBS02",
|
|
)
|
|
== summary
|
|
)
|
|
|
|
|
|
def test_coverage_sort_key_uses_namespace_then_schedule_then_vmid() -> None:
|
|
first = BackupCoverage(
|
|
guest=Guest(vmid=200, name="b", guest_type="qemu"),
|
|
status="sauvegarde_pbs_planifiee",
|
|
jobs=[BackupJob(job_id="backup-b", schedule="22:00")],
|
|
storages=["STORAGE-B"],
|
|
)
|
|
second = BackupCoverage(
|
|
guest=Guest(vmid=100, name="a", guest_type="qemu"),
|
|
status="sauvegarde_pbs_planifiee",
|
|
jobs=[BackupJob(job_id="backup-a", schedule="21:00")],
|
|
storages=["STORAGE-A"],
|
|
)
|
|
third = BackupCoverage(
|
|
guest=Guest(vmid=50, name="c", guest_type="qemu"),
|
|
status="non_sauvegardee",
|
|
)
|
|
|
|
sorted_items = sorted(
|
|
[first, second, third],
|
|
key=lambda item: coverage_sort_key(
|
|
item,
|
|
{
|
|
"STORAGE-A": "serveurs-internes",
|
|
"STORAGE-B": "Serveurs-PVELAB",
|
|
},
|
|
),
|
|
)
|
|
|
|
assert [item.guest.vmid for item in sorted_items] == [200, 100, 50]
|
|
|
|
|
|
def test_coverage_row_includes_notes_after_name() -> None:
|
|
row = coverage_row(
|
|
BackupCoverage(
|
|
guest=Guest(vmid=100, name="srv", guest_type="qemu", notes="note applicative"),
|
|
status="sauvegarde_pbs_planifiee",
|
|
),
|
|
include_storage=True,
|
|
)
|
|
|
|
assert row[:3] == [100, "srv", "note applicative"]
|
|
|
|
|
|
def test_coverage_row_uses_french_missing_notes_label() -> None:
|
|
row = coverage_row(
|
|
BackupCoverage(
|
|
guest=Guest(vmid=100, name="srv", guest_type="qemu"),
|
|
status="sauvegarde_pbs_planifiee",
|
|
),
|
|
include_storage=True,
|
|
)
|
|
|
|
assert row[2] == "non renseigné"
|
|
|
|
|
|
def test_backup_retention_row_uses_snapshot_summary() -> None:
|
|
summary = PbsBackupSnapshotSummary(
|
|
server_name="PBS01",
|
|
vmid=100,
|
|
guest_type="qemu",
|
|
datastore="RAID5",
|
|
namespace="serveurs-internes",
|
|
snapshot_count=13,
|
|
oldest_backup_at=datetime(2026, 3, 27, 21, 30),
|
|
newest_backup_at=datetime(2026, 5, 7, 21, 30),
|
|
newest_backup_size_bytes=2 * 1024 * 1024 * 1024,
|
|
)
|
|
|
|
assert backup_retention_row(
|
|
summary,
|
|
Guest(vmid=100, name="srv", guest_type="qemu"),
|
|
) == [
|
|
100,
|
|
"srv",
|
|
"serveurs-internes",
|
|
"RAID5",
|
|
"Active sur PVE",
|
|
"13",
|
|
"non renseigne",
|
|
"non renseigne",
|
|
"2026-03-27 21:30",
|
|
"2026-05-07 21:30",
|
|
"2.0 Gio",
|
|
]
|
|
|
|
|
|
def test_display_retention_delta_formats_sign() -> None:
|
|
assert display_retention_delta(13, 12) == "+1"
|
|
assert display_retention_delta(11, 12) == "-1"
|
|
assert display_retention_delta(12, 12) == "0"
|
|
assert display_retention_delta(12, None) == "non renseigne"
|
|
|
|
|
|
def test_expected_retention_versions_uses_active_policy_for_snapshot_namespace() -> None:
|
|
summary = PbsBackupSnapshotSummary(
|
|
server_name="PBS01",
|
|
vmid=100,
|
|
guest_type="qemu",
|
|
datastore="RAID5",
|
|
namespace="serveurs-internes",
|
|
)
|
|
report_data = ReportData(
|
|
pbs_retention_policies=[
|
|
PbsRetentionPolicy(
|
|
policy_id="disabled",
|
|
server_name="PBS01",
|
|
datastore="RAID5",
|
|
namespace="serveurs-internes",
|
|
enabled=False,
|
|
keep_daily=99,
|
|
),
|
|
PbsRetentionPolicy(
|
|
policy_id="active",
|
|
server_name="PBS01",
|
|
datastore="RAID5",
|
|
namespace="serveurs-internes",
|
|
keep_last=1,
|
|
keep_daily=7,
|
|
keep_weekly=4,
|
|
),
|
|
]
|
|
)
|
|
|
|
assert expected_retention_versions(report_data, summary) == 12
|
|
|
|
|
|
def test_build_backup_retention_rows_includes_inactive_guest() -> None:
|
|
report_data = ReportData(
|
|
guests=[
|
|
Guest(vmid=100, name="srv", guest_type="qemu"),
|
|
],
|
|
pbs_storages=[
|
|
PbsStorage(storage_id="backup-storage", namespace="serveurs-internes"),
|
|
PbsStorage(storage_id="pbs-root", namespace=None),
|
|
],
|
|
pbs_retention_policies=[
|
|
PbsRetentionPolicy(
|
|
policy_id="prune-root",
|
|
server_name="PBS01",
|
|
datastore="RAID5",
|
|
namespace="/",
|
|
keep_last=5,
|
|
),
|
|
PbsRetentionPolicy(
|
|
policy_id="prune-serveurs",
|
|
server_name="PBS01",
|
|
datastore="RAID5",
|
|
namespace="serveurs-internes",
|
|
keep_last=1,
|
|
keep_daily=7,
|
|
keep_weekly=4,
|
|
),
|
|
],
|
|
pbs_snapshot_summaries={
|
|
("PBS01", "RAID5", "serveurs-internes", "qemu", 100): PbsBackupSnapshotSummary(
|
|
server_name="PBS01",
|
|
vmid=100,
|
|
guest_type="qemu",
|
|
datastore="RAID5",
|
|
namespace="serveurs-internes",
|
|
snapshot_count=13,
|
|
oldest_backup_at=datetime(2026, 3, 27, 21, 30),
|
|
newest_backup_at=datetime(2026, 5, 7, 21, 30),
|
|
newest_backup_size_bytes=10 * 1024 * 1024,
|
|
),
|
|
("PBS01", "RAID5", "serveurs-internes", "qemu", 200): PbsBackupSnapshotSummary(
|
|
server_name="PBS01",
|
|
vmid=200,
|
|
guest_type="qemu",
|
|
datastore="RAID5",
|
|
namespace="serveurs-internes",
|
|
snapshot_count=4,
|
|
oldest_backup_at=datetime(2026, 4, 1, 21, 30),
|
|
newest_backup_at=datetime(2026, 5, 8, 21, 30),
|
|
newest_backup_size_bytes=3 * 1024 * 1024 * 1024,
|
|
),
|
|
("PBS01", "RAID5", "hors-pve", "qemu", 300): PbsBackupSnapshotSummary(
|
|
server_name="PBS01",
|
|
vmid=300,
|
|
guest_type="qemu",
|
|
datastore="RAID5",
|
|
namespace="hors-pve",
|
|
snapshot_count=2,
|
|
oldest_backup_at=datetime(2026, 4, 2, 21, 30),
|
|
newest_backup_at=datetime(2026, 5, 8, 21, 45),
|
|
),
|
|
("PBS01", "RAID5", "/", "qemu", 400): PbsBackupSnapshotSummary(
|
|
server_name="PBS01",
|
|
vmid=400,
|
|
guest_type="qemu",
|
|
datastore="RAID5",
|
|
namespace="/",
|
|
snapshot_count=5,
|
|
oldest_backup_at=datetime(2026, 4, 3, 21, 30),
|
|
newest_backup_at=datetime(2026, 5, 8, 22, 0),
|
|
newest_backup_size_bytes=512,
|
|
raw={"name": "srv-racine"},
|
|
),
|
|
},
|
|
)
|
|
|
|
rows = build_backup_retention_rows(report_data, "PBS01")
|
|
|
|
assert rows[0] == [
|
|
"VMID",
|
|
"Nom VM/CT",
|
|
"Namespace",
|
|
"Datastore",
|
|
"Etat PVE",
|
|
"Nombre de versions",
|
|
"Nombre attendu de versions",
|
|
"Delta",
|
|
"Plus ancienne",
|
|
"Plus recente",
|
|
"Taille",
|
|
]
|
|
assert rows[1] == [
|
|
400,
|
|
"srv-racine",
|
|
"/",
|
|
"RAID5",
|
|
"Non-active sur PVE",
|
|
"5",
|
|
"5",
|
|
"0",
|
|
"2026-04-03 21:30",
|
|
"2026-05-08 22:00",
|
|
"512 o",
|
|
]
|
|
assert rows[2] == [
|
|
100,
|
|
"srv",
|
|
"serveurs-internes",
|
|
"RAID5",
|
|
"Active sur PVE",
|
|
"13",
|
|
"12",
|
|
"+1",
|
|
"2026-03-27 21:30",
|
|
"2026-05-07 21:30",
|
|
"10.0 Mio",
|
|
]
|
|
assert rows[3] == [
|
|
200,
|
|
"non renseigne",
|
|
"serveurs-internes",
|
|
"RAID5",
|
|
"Non-active sur PVE",
|
|
"4",
|
|
"12",
|
|
"-8",
|
|
"2026-04-01 21:30",
|
|
"2026-05-08 21:30",
|
|
"3.0 Gio",
|
|
]
|