Flag guests that no backup job covers

The guest table marks with "!" each guest that Proxmox's not-backed-up
list returns. The frame's bottom border shows the count, e.g.
"mainframe · 2 without backup job". This uses Proxmox's own answer, so
it also counts jobs that select guests by pool.

If the coverage call fails, the guests are still listed and a warning
says the coverage is unknown.

Closes #9

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit was merged in pull request #20.
This commit is contained in:
DuckbootandClaude Opus 5.5 committed 2026-11-02 23:02:35 +01:00
1 parent 5a3ca5a216
commit 520de16cc6
5 files changed
+97 -6

No files matched your search

+43
View File
@@ -49,6 +49,7 @@ class Fake:
self.use_error = None
self.use_hook = None
self.guests_by_context = {}
self.unprotected, self.unprotected_error = set(), None
self.list_hook = None
# delivery A (guest info screen)
self.net_configs = {} # vmid -> extra config keys (netN, ipconfigN)
@@ -103,6 +104,11 @@ class Fake:
self.status_calls.append(guest["vmid"])
return self.live_status.get(guest["vmid"], guest["status"])
def not_backed_up(self):
if self.unprotected_error:
raise self.unprotected_error
return set(self.unprotected)
def contexts(self):
self.contexts_calls += 1
if self.contexts_error:
@@ -195,6 +201,7 @@ def fake(monkeypatch):
"backup_now",
"bridges",
"set_net",
"not_backed_up",
):
monkeypatch.setattr(cli, name, getattr(f, name), raising=False)
for name in ("list_guests", "guest_config", "disks", "action", "set_resources",
@@ -2108,3 +2115,39 @@ async def test_info_resources_reload_after_edit(fake, monkeypatch):
await settle(app, pilot)
assert isinstance(app.screen, appmod.InfoScreen)
assert resource_rows(app)["CPU"].startswith("8 cores")
# --- #9: flag guests that no backup job covers --------------------------------------
def first_cell(app, vmid):
return str(app.query_one("#guests", DataTable).get_row(str(vmid))[0])
async def test_unprotected_guests_are_marked_and_counted(fake):
fake.unprotected = {100}
app = make_app()
async with app.run_test(size=SIZE) as pilot:
await settle(app, pilot)
assert first_cell(app, 100) == "!"
assert first_cell(app, 108) == ""
subtitle = str(app.query_one("#guests_section").border_subtitle)
assert "mainframe" in subtitle and "1 without backup job" in subtitle
async def test_no_count_when_every_guest_is_covered(fake):
app = make_app()
async with app.run_test(size=SIZE) as pilot:
await settle(app, pilot)
assert str(app.query_one("#guests_section").border_subtitle) == "mainframe"
assert first_cell(app, 100) == "" and first_cell(app, 108) == ""
async def test_backup_coverage_failure_still_lists_guests(fake):
fake.unprotected_error = cli.CliError("Error: boom")
app = make_app()
async with app.run_test(size=SIZE) as pilot:
await settle(app, pilot)
assert app.query_one("#guests", DataTable).row_count == 2
assert any(sev == "warning" for _, sev in app.seen) # coverage unknown, said so
assert app.is_running
+18
View File
@@ -985,3 +985,21 @@ def test_apply_ip_rejects_netmask_form_and_zero_prefix(cidr):
family = 6 if ":" in cidr else 4
with pytest.raises(ValueError):
cli.apply_ip({}, family, "static", cidr)
# --- #9: guests not covered by any backup job -----------------------------------------
def test_not_backed_up_returns_vmids(fake):
fake.result = (
json.dumps([{"vmid": 104, "name": "debian", "type": "lxc"}, {"vmid": "110"}]),
"",
0,
)
assert cli.not_backed_up() == {104, 110}
assert fake.argv == ["cv4pve-cli", "get", "cluster", "not-backed-up", "-o", "json"]
def test_not_backed_up_empty(fake):
fake.result = ("[]", "", 0)
assert cli.not_backed_up() == set()