Initial commit

This commit is contained in:
2026-05-13 16:04:17 +02:00
commit b66612d672
43 changed files with 10515 additions and 0 deletions
+489
View File
@@ -0,0 +1,489 @@
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",
]