# 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 with `vmid`, `type` (`qemu`|`lxc`), `node`, `name`, `status`, `maxcpu`, `maxmem` (bytes), `maxdisk` (bytes), ... - `show vm|ct -o json` -> list of `{"key": ..., "value": ...}`. - `api get /nodes////pending -o json` -> list of `{"key", "value", "pending", "delete"}`; `pending` non-null means not yet applied. - Async actions print a UPID and exit 0 **even when the task fails**. With `--wait` the CLI waits for the task and exits non-zero (5) with the error text on failure. Every mutating call therefore uses `--wait`. - `do stop` refuses to run without `-y` (verified); `-y` is 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 from `api get /nodes////status/current` (key/value list). ### Module `cv4pve_tui/cli.py` - `CliError(Exception)` - `str()` is the CLI's combined, stripped output; `.summary` is 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"` - from `type` (`qemu` -> `vm`, `lxc` -> `ct`). - `list_guests() -> list[dict]` - sorted by `vmid`. - `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|ideN` with a `size=`, excluding `media=cdrom` and cloud-init. CT: `rootfs`, `mpN`. - `ACTIONS = ("start", "shutdown", "stop", "reboot")`; `action(name, guest)` -> `do -y --wait`; unknown name -> `ValueError`. - `set_resources(guest, cores=None, memory_mb=None)` -> `set config [--cores N] [--memory M] --wait`; values must be positive ints; nothing to set -> `ValueError`. - `resize_disk(guest, disk, grow_gb)` -> `do resize disk +G --wait`; `grow_gb` must 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-null `pending`. ### Module `cv4pve_tui/app.py` - `DataTable` of guests: VMID, name, type, node, status, cores, memory (MiB), disk (GiB). Cursor row = selected guest. - Keys: `s` start, `S` shutdown, `x` stop (hard), `r` reboot, `e` edit, `g` refresh, `q` quit. - `shutdown`, `stop`, `reboot` open a confirm modal naming the guest; `stop` says it is a hard power-off. `start` needs no confirmation. - `e` opens an edit modal: cores, memory MiB, disk select + "grow by GiB". Empty field = unchanged. Disk growth gets its own confirm (irreversible). - After `set_resources`, if `pending_keys` contains `cores` or `memory`, notify that the change applies at next reboot. - All CLI calls in thread workers; results/errors via `notify` (errors show `CliError.summary`); table refreshes after each action. - A "working" notice is shown when an action starts (`--wait` can 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.py` does `from cv4pve_tui import cli` and always calls `cli.(...)`, so tests monkeypatch `cv4pve_tui.cli.`. - `class GuestApp(App)`; `def main()` runs it. - Guest table: `DataTable` with id `guests`, one row per guest, row key = str(vmid), in `list_guests()` order. Loaded on mount. - `ConfirmScreen(ModalScreen[bool])`: buttons `#yes` / `#no`; its text names the guest (name and vmid); for `stop` it 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 from `cli.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 -> `ConfirmScreen` first, then `cli.resize_disk(guest, disk, grow_gb)`. Non-integer or <= 0 input -> `notify` with severity "error" and no CLI call. - Thread workers (`@work(thread=True)` or `run_worker(..., thread=True)`); tests wait with `await app.workers.wait_for_complete()` then `await pilot.pause()`. ## Out of scope Create/delete, snapshots, migration, console, backup, in-app context switching, sockets/vCPU/hotplug settings, live auto-refresh. ## Steps - [x] Project skeleton (`uv`, `pyproject.toml`, entry point `cv4pve-tui`) - `uv run python -c "import cv4pve_tui"` succeeds. - [x] Tests for `cli.py` with `subprocess.run` mocked - fail before implementation. - [x] `cli.py` - tests pass. - [x] Tests for the app with Textual `run_test` and `cli` functions mocked. - [x] `app.py` - tests pass. - [x] Read-only smoke test against the real cluster (listing, edit dialog disk options, pending check). - [x] 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.