Warn on an untested cv4pve-cli version at startup
At startup the app runs `cv4pve-cli --version`. If it differs from cli.TESTED_VERSION (2.4.0), a warning says which version was found and which one was tested. The app parses output formats that are not a stable API: the config list text, prune-backups as a JSON string, the CT ip-addresses JSON string and the exit codes with --wait. A newer cv4pve-cli could break them in confusing ways. If cv4pve-cli cannot be run at all, an error says so once, instead of every later call failing without explanation. The README notes the tested version. Closes #14 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit was merged in pull request #27.
This commit is contained in:
1 parent
364f8e2137
commit
e7569a7bf7
5 files changed
+83
-1
No files matched your search
@@ -8,7 +8,8 @@ disk size. The table shows CPU % and Mem % of running guests and the info screen
|
|||||||
in-use values (refresh with `g`).
|
in-use values (refresh with `g`).
|
||||||
|
|
||||||
It shells out to `cv4pve-cli` and uses its current context (`cv4pve-cli config use <name>`);
|
It shells out to `cv4pve-cli` and uses its current context (`cv4pve-cli config use <name>`);
|
||||||
it stores no credentials itself. Tested with cv4pve-cli 2.4.0.
|
it stores no credentials itself. Tested with cv4pve-cli 2.4.0; with another version the app
|
||||||
|
warns at startup, because it parses output formats that are not a stable API.
|
||||||
|
|
||||||
## Run
|
## Run
|
||||||
|
|
||||||
|
|||||||
@@ -1040,6 +1040,7 @@ class GuestApp(App):
|
|||||||
def on_mount(self) -> None:
|
def on_mount(self) -> None:
|
||||||
# one place for every sub_title change: mount, switch and failed switch
|
# one place for every sub_title change: mount, switch and failed switch
|
||||||
self.watch(self, "sub_title", self._show_context_frame)
|
self.watch(self, "sub_title", self._show_context_frame)
|
||||||
|
self._check_version()
|
||||||
table = self.query_one("#guests", DataTable)
|
table = self.query_one("#guests", DataTable)
|
||||||
table.add_columns(
|
table.add_columns(
|
||||||
"", # "!" = no backup job covers the guest
|
"", # "!" = no backup job covers the guest
|
||||||
@@ -1084,6 +1085,25 @@ class GuestApp(App):
|
|||||||
self.package_count = count
|
self.package_count = count
|
||||||
self._update_frame()
|
self._update_frame()
|
||||||
|
|
||||||
|
@work(thread=True)
|
||||||
|
def _check_version(self) -> None:
|
||||||
|
try:
|
||||||
|
found = cli.version()
|
||||||
|
except Exception as exc: # noqa: BLE001 - every later call fails too; say why once
|
||||||
|
self.call_from_thread(
|
||||||
|
self.notify,
|
||||||
|
f"cv4pve-cli is not runnable: {_message(exc)}",
|
||||||
|
severity="error",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
if found != cli.TESTED_VERSION:
|
||||||
|
self.call_from_thread(
|
||||||
|
self.notify,
|
||||||
|
f"cv4pve-cli {found} found; this app was tested with {cli.TESTED_VERSION}. "
|
||||||
|
"Some screens may misread its output.",
|
||||||
|
severity="warning",
|
||||||
|
)
|
||||||
|
|
||||||
def _show_context_frame(self, sub_title: str) -> None:
|
def _show_context_frame(self, sub_title: str) -> None:
|
||||||
self.context_name = sub_title.removeprefix("context: ")
|
self.context_name = sub_title.removeprefix("context: ")
|
||||||
self._update_frame()
|
self._update_frame()
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ import json
|
|||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
|
# Output formats this app parses (config list text, prune-backups as a JSON string, the CT
|
||||||
|
# ip-addresses JSON string, exit codes with --wait) were verified against this version.
|
||||||
|
TESTED_VERSION = "2.4.0"
|
||||||
|
|
||||||
|
|
||||||
class CliError(Exception):
|
class CliError(Exception):
|
||||||
"""cv4pve-cli failed; str() is its combined, stripped output."""
|
"""cv4pve-cli failed; str() is its combined, stripped output."""
|
||||||
@@ -472,3 +476,7 @@ def task_log(upid: str) -> str:
|
|||||||
if not isinstance(upid, str) or not upid.startswith("UPID:"):
|
if not isinstance(upid, str) or not upid.startswith("UPID:"):
|
||||||
raise ValueError("not a task id (UPID)")
|
raise ValueError("not a task id (UPID)")
|
||||||
return run("task", "log", upid)
|
return run("task", "log", upid)
|
||||||
|
|
||||||
|
|
||||||
|
def version() -> str:
|
||||||
|
return run("--version").strip()
|
||||||
@@ -50,6 +50,7 @@ class Fake:
|
|||||||
self.use_error = None
|
self.use_error = None
|
||||||
self.use_hook = None
|
self.use_hook = None
|
||||||
self.guests_by_context = {}
|
self.guests_by_context = {}
|
||||||
|
self.cli_version, self.version_error = "2.4.0", None
|
||||||
self.unprotected, self.unprotected_error = set(), None
|
self.unprotected, self.unprotected_error = set(), None
|
||||||
self.list_hook = None
|
self.list_hook = None
|
||||||
# delivery A (guest info screen)
|
# delivery A (guest info screen)
|
||||||
@@ -143,6 +144,11 @@ class Fake:
|
|||||||
raise self.unprotected_error
|
raise self.unprotected_error
|
||||||
return set(self.unprotected)
|
return set(self.unprotected)
|
||||||
|
|
||||||
|
def version(self):
|
||||||
|
if self.version_error:
|
||||||
|
raise self.version_error
|
||||||
|
return self.cli_version
|
||||||
|
|
||||||
def contexts(self):
|
def contexts(self):
|
||||||
self.contexts_calls += 1
|
self.contexts_calls += 1
|
||||||
if self.contexts_error:
|
if self.contexts_error:
|
||||||
@@ -307,6 +313,7 @@ def fake(monkeypatch):
|
|||||||
"bridges",
|
"bridges",
|
||||||
"set_net",
|
"set_net",
|
||||||
"not_backed_up",
|
"not_backed_up",
|
||||||
|
"version",
|
||||||
"snapshots",
|
"snapshots",
|
||||||
"take_snapshot",
|
"take_snapshot",
|
||||||
"rollback_snapshot",
|
"rollback_snapshot",
|
||||||
@@ -3471,3 +3478,36 @@ async def test_frame_subtitle_singular_for_one_package_update(fake):
|
|||||||
async with app.run_test(size=SIZE) as pilot:
|
async with app.run_test(size=SIZE) as pilot:
|
||||||
await settle(app, pilot)
|
await settle(app, pilot)
|
||||||
assert frame_subtitle(app).endswith("· 1 package update")
|
assert frame_subtitle(app).endswith("· 1 package update")
|
||||||
|
|
||||||
|
|
||||||
|
# --- #14: warn on an untested cv4pve-cli version -------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
async def test_no_warning_for_the_tested_cli_version(fake):
|
||||||
|
app = make_app()
|
||||||
|
async with app.run_test(size=SIZE) as pilot:
|
||||||
|
await settle(app, pilot)
|
||||||
|
assert not any("tested with" in m for m, _ in app.seen)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_other_cli_version_warns_once(fake):
|
||||||
|
fake.cli_version = "2.5.0"
|
||||||
|
app = make_app()
|
||||||
|
async with app.run_test(size=SIZE) as pilot:
|
||||||
|
await settle(app, pilot)
|
||||||
|
warned = [m for m, sev in app.seen if sev == "warning" and "tested with" in m]
|
||||||
|
assert (
|
||||||
|
len(warned) == 1
|
||||||
|
and "2.5.0" in warned[0]
|
||||||
|
and cli.TESTED_VERSION in warned[0]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_cli_not_runnable_is_an_error_not_a_crash(fake):
|
||||||
|
fake.version_error = cli.CliError(
|
||||||
|
"[Errno 2] No such file or directory: 'cv4pve-cli'"
|
||||||
|
)
|
||||||
|
app = make_app()
|
||||||
|
async with app.run_test(size=SIZE) as pilot:
|
||||||
|
await settle(app, pilot)
|
||||||
|
assert any("cv4pve-cli" in m for m in errors(app)) and app.is_running
|
||||||
@@ -1375,3 +1375,16 @@ def test_package_updates_failure_raises_clierror(monkeypatch):
|
|||||||
FakeRun(monkeypatch, "Error: node unreachable\n", returncode=1)
|
FakeRun(monkeypatch, "Error: node unreachable\n", returncode=1)
|
||||||
with pytest.raises(cli.CliError):
|
with pytest.raises(cli.CliError):
|
||||||
cli.package_updates("mainframe")
|
cli.package_updates("mainframe")
|
||||||
|
|
||||||
|
|
||||||
|
# --- #14 ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_version_argv_and_strips(fake):
|
||||||
|
fake.result = ("2.4.0\n", "", 0)
|
||||||
|
assert cli.version() == "2.4.0"
|
||||||
|
assert fake.argv == ["cv4pve-cli", "--version"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_tested_version_constant():
|
||||||
|
assert cli.TESTED_VERSION == "2.4.0"
|
||||||
Reference in new issue
Block a user