List VMs and LXC containers from the current cv4pve-cli context and start, shutdown, stop and reboot them, change cores and memory, and grow disks. The app shells out to `cv4pve-cli -o json` and stores no credentials. - Every mutating call uses --wait: without it cv4pve-cli exits 0 even when the task fails. - shutdown, stop and reboot need confirmation; disk growth is increment-only and asks again because it cannot be undone. - After an action the acted-on guest's status is read from status/current, because /cluster/resources lags a few seconds. - Notifies when a cores/memory change is pending until reboot. Tests mock cv4pve-cli (85 tests). Live-tested edit, shutdown, start and reboot on CT 104 (mainframe), restored afterwards; disk resize was not run live. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6.1 KiB
6.1 KiB
Guest TUI MVP
Problem
Manage Proxmox guests (VMs and LXC containers) from the terminal without remembering
cv4pve-cli syntax: list them, start/shutdown/stop/reboot, and change CPU cores,
memory and disk size.
Approach
Thin subprocess wrapper around cv4pve-cli (tested against 2.4.0) plus a Textual app.
The app never handles credentials: it uses whatever context cv4pve-cli config use
selected.
Observed CLI behaviour the design relies on:
get guests -o json-> list of dicts withvmid,type(qemu|lxc),node,name,status,maxcpu,maxmem(bytes),maxdisk(bytes), ...show vm|ct <node> <vmid> -o json-> list of{"key": ..., "value": ...}.api get /nodes/<node>/<qemu|lxc>/<vmid>/pending -o json-> list of{"key", "value", "pending", "delete"};pendingnon-null means not yet applied.- Async actions print a UPID and exit 0 even when the task fails. With
--waitthe CLI waits for the task and exits non-zero (5) with the error text on failure. Every mutating call therefore uses--wait. do stoprefuses to run without-y(verified);-yis accepted by all four lifecycle actions, so it is always passed.- Errors: non-zero exit, message on stdout/stderr. Sync failures are multi-line
(
Error: .../Call: .../Alias: ...). Success is judged by exit code only, never by parsing UPID text. - Each call costs ~0.5 s (.NET startup), so calls run in worker threads.
get guests(/cluster/resources) lags a few seconds behind actions; after an action the app reads the acted-on guest's live status fromapi get /nodes/<node>/<qemu|lxc>/<vmid>/status/current(key/value list).
Module cv4pve_tui/cli.py
CliError(Exception)-str()is the CLI's combined, stripped output;.summaryis one line for the UI: the first non-empty line that is not a UPID,Error:-prefix stripped, plus the following indented detail line if the first line ends with:.run(*args) -> str-subprocess.run(["cv4pve-cli", *args], capture_output=True, text=True, stdin=DEVNULL, timeout=330); non-zero exit ->CliError.kind(guest) -> "vm" | "ct"- fromtype(qemu->vm,lxc->ct).list_guests() -> list[dict]- sorted byvmid.guest_config(guest) -> dict- key/value list flattened to a dict.disks(config, kind) -> dict[str, str]- resizable disks -> size string. VM:scsiN|virtioN|sataN|ideNwith asize=, excludingmedia=cdromand cloud-init. CT:rootfs,mpN.ACTIONS = ("start", "shutdown", "stop", "reboot");action(name, guest)->do <name> <kind> <node> <vmid> -y --wait; unknown name ->ValueError.set_resources(guest, cores=None, memory_mb=None)->set <kind> config <node> <vmid> [--cores N] [--memory M] --wait; values must be positive ints; nothing to set ->ValueError.resize_disk(guest, disk, grow_gb)->do resize <kind> disk <node> <vmid> <disk> +<grow_gb>G --wait;grow_gbmust be a positive int (growth only - Proxmox cannot shrink, and an increment cannot shrink by construction).pending_keys(guest) -> list[str]- keys with a non-nullpending.
Module cv4pve_tui/app.py
DataTableof guests: VMID, name, type, node, status, cores, memory (MiB), disk (GiB). Cursor row = selected guest.- Keys:
sstart,Sshutdown,xstop (hard),rreboot,eedit,grefresh,qquit. shutdown,stop,rebootopen a confirm modal naming the guest;stopsays it is a hard power-off.startneeds no confirmation.eopens an edit modal: cores, memory MiB, disk select + "grow by GiB". Empty field = unchanged. Disk growth gets its own confirm (irreversible).- After
set_resources, ifpending_keyscontainscoresormemory, notify that the change applies at next reboot. - All CLI calls in thread workers; results/errors via
notify(errors showCliError.summary); table refreshes after each action. - A "working" notice is shown when an action starts (
--waitcan block minutes), and a second action on a guest that is already busy is refused. - Action keys on an empty table do nothing (no crash).
UI contract (shared by tests and implementation)
app.pydoesfrom cv4pve_tui import cliand always callscli.<function>(...), so tests monkeypatchcv4pve_tui.cli.<function>.class GuestApp(App);def main()runs it.- Guest table:
DataTablewith idguests, one row per guest, row key = str(vmid), inlist_guests()order. Loaded on mount. ConfirmScreen(ModalScreen[bool]): buttons#yes/#no; its text names the guest (name and vmid); forstopit contains the words "hard power-off".EditScreen(ModalScreen):Input#cores,Input#memory,Select#disk,Input#grow, buttons#apply/#cancel. Inputs are pre-filled with nothing (empty = unchanged); placeholders show current values. Disk options come fromcli.disks(cli.guest_config(guest), cli.kind(guest)). On apply: if cores/memory given ->cli.set_resources(guest, cores=..., memory_mb=...)(only the given ones, as ints); if grow given ->ConfirmScreenfirst, thencli.resize_disk(guest, disk, grow_gb). Non-integer or <= 0 input ->notifywith severity "error" and no CLI call.- Thread workers (
@work(thread=True)orrun_worker(..., thread=True)); tests wait withawait app.workers.wait_for_complete()thenawait pilot.pause().
Out of scope
Create/delete, snapshots, migration, console, backup, in-app context switching, sockets/vCPU/hotplug settings, live auto-refresh.
Steps
- Project skeleton (
uv,pyproject.toml, entry pointcv4pve-tui) -uv run python -c "import cv4pve_tui"succeeds. - Tests for
cli.pywithsubprocess.runmocked - fail before implementation. cli.py- tests pass.- Tests for the app with Textual
run_testandclifunctions mocked. app.py- tests pass.- Read-only smoke test against the real cluster (listing, edit dialog disk options, pending check).
- Live edit (cores/memory), shutdown, start, reboot on CT 104 (mainframe), restored to baseline afterwards. Disk resize not run live (irreversible).
Open questions
None blocking.