Add LXC shell via ssh and pct enter

Press t on a running container to suspend the TUI and run
`ssh -t root@<node> pct enter <vmid>`. cv4pve-cli has no shell or exec
for LXC, so this goes around it and needs key-based root SSH to the
PVE node.

- VMs, stopped containers and busy guests get a warning instead.
- Only exit code 255 (ssh failed) is reported as an error. Other codes
  are the last status of the container's shell.
- OSError is caught inside suspend(). Textual's suspend() has no
  try/finally, so an exception escaping it would leave the app
  suspended.
- The guest table reloads when the shell returns.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
DuckbootandClaude Opus 5.5 committed 2026-11-02 20:35:22 +01:00
1 parent 62b79ab29e
commit 40feb125b4
4 files changed
+272 -1

No files matched your search

+52
View File
@@ -179,3 +179,55 @@ Adding/editing/deleting contexts, `config verify`, restoring the original contex
(only `config use` and read-only listing; no guest actions); final context
is `mainframe`.
- [ ] Push `feat/guest-tui-mvp`, merge to `main` (user-approved).
---
# LXC shell
## Problem
Open an interactive shell in a running LXC container from the TUI. cv4pve-cli 2.4.0 has
no console/shell/exec for LXC (verified: no command; the Proxmox API has no LXC exec),
so the app goes around it: SSH to the PVE node as root and run `pct enter <vmid>`.
## Decision
Option 1 from the discussion: `ssh -t root@<node> pct enter <vmid>`. The node name is
the SSH host (resolves via DNS here: `mainframe`, `mainframe-edge`); the user's key is in
root's `authorized_keys` (verified: `ssh -o BatchMode=yes root@mainframe pct status 104`
-> `status: running`). This bypasses cv4pve-cli and its API token.
## `app.py` (UI contract)
- Key `t` ("Shell") on the selected guest.
- Only for `type == "lxc"` and `status == "running"`; otherwise a warning notify that
says why (not a container / not running) and nothing runs. Empty table: nothing.
- Busy guest (action running) -> warning notify, nothing runs.
- Runs on the main thread inside `with self.suspend():` -
`subprocess.run(["ssh", "-t", f"root@{node}", "pct", "enter", str(vmid)])`
(no capture, no timeout: the user's terminal is handed over; `import subprocess` in
app.py so tests patch `cv4pve_tui.app.subprocess.run`).
- Exit 255 (ssh itself failed) -> error notify naming the guest and the code. Other
exit codes are the container shell's last status (`exit` after a failed command
gives 1) and are not errors.
- `OSError` from `subprocess.run` is caught *inside* the `suspend()` block: Textual
8.2.8's `suspend()` has no try/finally, so an exception escaping it leaves the app
suspended.
- The guest table reloads after the shell returns (the user may have stopped or
rebooted the container from inside).
- `textual.app.SuspendNotSupported` (headless / unsupported driver) and `OSError`
(no `ssh` binary) -> error notify, app keeps running.
- Tests: `App.suspend` raises `SuspendNotSupported` under `run_test`, so tests replace
`app.suspend` with a no-op context manager (e.g. `contextlib.nullcontext`) unless
testing that error path.
## Out of scope
VM consoles (serial/VNC), configurable SSH user/host/options, termproxy/websocket.
## Steps
- [x] Tests for the UI contract above - red first.
- [x] Implementation - full suite green, ruff clean. README key table gets `t`.
- [ ] Interactive test by the user: `t` on CT 104, `exit`, back in the table.
- [ ] Push `feat/lxc-shell`, merge to `main` (user-approved).