Run fstrim in a VM via the guest agent
f on the info screen runs fstrim in a running VM with the guest agent enabled. A container gets an info note (no guest agent; use pct fstrim on the node). A stopped VM or one without an agent gets a warning, and nothing runs. Otherwise a confirm comes first, then agent ping and fstrim. The result reports the trimmed total (GiB/MiB), the number of paths and any per-path errors. Review fixes: - Odd agent output no longer turns a finished trim into "fstrim failed". Unreadable output gives "no results". Odd entries are repaired: non-dict entries are dropped, a non-int trimmed becomes 0 and a missing path becomes "?". Well-formed entries are returned unchanged. - fstrim gets a 30-minute CLI timeout instead of 330 s, because a big disk can take longer. - Notifications are never parsed as markup (GuestApp.notify defaults to markup=False). Guest names, paths and CLI or agent errors are untrusted text, so a "[/b]" in them could break the toast. - f before the info screen has loaded says "still loading" instead of a wrong "not running". - "1 path" in the singular. Not run live: the issue asks for approval before fstrim on VM 108. Only the harmless agent ping was run there, and it answered. Closes #19 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit was merged in pull request #31.
This commit is contained in:
1 parent
f5f3effce5
commit
262441916c
6 files changed
+540
-2
No files matched your search
@@ -1504,3 +1504,120 @@ def test_tags_value_is_idempotent_property(text):
|
||||
else:
|
||||
assert cli.tags_value(once) == once
|
||||
assert len(set(once.split(";"))) == len(once.split(";")) # no duplicates
|
||||
|
||||
|
||||
# --- issue #19: guest-agent fstrim (spec: "fstrim in a VM via the guest agent") ----------
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"agent,expected",
|
||||
[
|
||||
("enabled=1", True),
|
||||
("1", True),
|
||||
("1,fstrim_cloned_disks=1", True),
|
||||
("enabled=0", False),
|
||||
("0", False),
|
||||
(None, False),
|
||||
],
|
||||
)
|
||||
def test_agent_enabled(agent, expected):
|
||||
config = {} if agent is None else {"agent": agent}
|
||||
assert cli.agent_enabled(config) is expected
|
||||
|
||||
|
||||
def test_agent_ping_exact_argv(fake):
|
||||
fake.result = (json.dumps([{"key": "result", "value": "{}"}]), "", 0)
|
||||
cli.agent_ping(VMID108)
|
||||
assert fake.argv == [
|
||||
"cv4pve-cli", "do", "vm", "agent", "ping", "mainframe", "108", "-o", "json",
|
||||
] # fmt: skip
|
||||
|
||||
|
||||
def test_agent_ping_nonzero_exit_raises_clierror(monkeypatch):
|
||||
FakeRun(monkeypatch, "Error: QEMU guest agent is not running", returncode=5)
|
||||
with pytest.raises(cli.CliError):
|
||||
cli.agent_ping(VMID108)
|
||||
|
||||
|
||||
FSTRIM_PATHS = [
|
||||
{"path": "/", "trimmed": 1073741824, "minimum": 0},
|
||||
{"path": "/boot", "error": "not supported"},
|
||||
]
|
||||
|
||||
|
||||
def fstrim_stdout(value):
|
||||
return json.dumps([{"key": "result", "value": value}])
|
||||
|
||||
|
||||
def test_fstrim_exact_argv_and_parses_paths(fake):
|
||||
fake.result = (fstrim_stdout(json.dumps({"paths": FSTRIM_PATHS})), "", 0)
|
||||
assert cli.fstrim(VMID108) == FSTRIM_PATHS
|
||||
assert fake.argv == [
|
||||
"cv4pve-cli", "do", "vm", "agent", "fstrim", "mainframe", "108", "-o", "json",
|
||||
] # fmt: skip
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"stdout",
|
||||
[
|
||||
"[]",
|
||||
json.dumps([{"key": "other", "value": "{}"}]),
|
||||
fstrim_stdout("not json at all"),
|
||||
fstrim_stdout("{}"),
|
||||
fstrim_stdout(""),
|
||||
],
|
||||
)
|
||||
def test_fstrim_missing_or_garbage_result_is_empty_list(fake, stdout):
|
||||
fake.result = (stdout, "", 0)
|
||||
assert cli.fstrim(VMID108) == []
|
||||
|
||||
|
||||
def test_fstrim_nonzero_exit_raises_clierror(monkeypatch):
|
||||
FakeRun(monkeypatch, "Error: fstrim failed", returncode=5)
|
||||
with pytest.raises(cli.CliError):
|
||||
cli.fstrim(VMID108)
|
||||
|
||||
|
||||
# --- #19 review findings ----------------------------------------------------------------
|
||||
|
||||
VM19 = {"vmid": 108, "node": "mainframe", "type": "qemu"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"stdout",
|
||||
[
|
||||
"garbage",
|
||||
"",
|
||||
json.dumps({"key": "result"}), # object instead of list
|
||||
json.dumps([{"value": "{}"}]), # item without "key"
|
||||
json.dumps([{"key": "result", "value": {"paths": "x"}}]), # already a dict
|
||||
],
|
||||
)
|
||||
def test_fstrim_unreadable_output_is_empty_not_an_error(fake, stdout):
|
||||
fake.result = (stdout, "", 0)
|
||||
assert cli.fstrim(VM19) == []
|
||||
|
||||
|
||||
def test_fstrim_normalises_odd_path_entries(fake):
|
||||
paths = [
|
||||
{"path": "/", "trimmed": "12"},
|
||||
{"error": "x"},
|
||||
"junk",
|
||||
{"path": "/var", "trimmed": 5},
|
||||
]
|
||||
fake.result = (
|
||||
json.dumps([{"key": "result", "value": json.dumps({"paths": paths})}]),
|
||||
"",
|
||||
0,
|
||||
)
|
||||
assert cli.fstrim(VM19) == [
|
||||
{"path": "/", "trimmed": 0}, # non-int trimmed -> 0
|
||||
{"error": "x", "path": "?"}, # missing path -> "?"; "junk" dropped
|
||||
{"path": "/var", "trimmed": 5}, # well-formed: unchanged
|
||||
]
|
||||
|
||||
|
||||
def test_fstrim_gets_a_longer_timeout_than_other_calls(fake):
|
||||
fake.result = (json.dumps([{"key": "result", "value": "{}"}]), "", 0)
|
||||
cli.fstrim(VM19)
|
||||
assert fake.calls[0][1]["timeout"] >= 1800 # a big disk can take longer than 330 s
|
||||
Reference in new issue
Block a user