Add guest info screen with backups, backup now and network
Press i on a guest to see its backups and network. Backups from every backup-capable storage are listed newest first, with the backup jobs that cover the guest. The network part shows the netN settings (bridge, VLAN, MAC, IPv4/IPv6 setting, firewall) and the addresses in use. Press b in the info screen to back the guest up now: snapshot mode and zstd, like the scheduled job, to a storage you pick (the job's storage is preselected). It asks for confirmation, marks the guest busy and uses --wait. Fixed after review and a read-only live check: - prune-backups arrives as a JSON string; parsing it as a dict crashed the app. - CT addresses come from ip-addresses. inet6 holds only the link-local address, so the global SLAAC address was missing. - The info screen uses the guest's live status, and odd API data (null size, unknown address types) no longer kills the app. - The test fake now copies guest dicts; a shared constant was being mutated across tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
70bcb9e7b7
commit
32809b6743
6 files changed
+1057
-2
No files matched your search
@@ -236,3 +236,108 @@ VM consoles (serial/VNC), configurable SSH user/host/options, termproxy/websocke
|
||||
- [x] Interactive test by the user: `t` on CT 104, `exit`, back in the table; screen
|
||||
clean after the reset.
|
||||
- [ ] Push `feat/lxc-shell`, merge to `main` (user-approved).
|
||||
|
||||
---
|
||||
|
||||
# Guest info screen: backups, backup now, network (read-only) - delivery A
|
||||
|
||||
## Problem
|
||||
|
||||
Per guest, see its latest backups and network setup, and start a backup now. Network
|
||||
*editing* is delivery B (separate, live-tested), not part of this.
|
||||
|
||||
## Decisions (user-approved)
|
||||
|
||||
- Backup now uses the scheduled job's settings: `--mode snapshot --compress zstd`.
|
||||
Storage is chosen from the node's backup-capable storages, preselected to the job's
|
||||
storage (`truenas-backup` here), `local` as alternative.
|
||||
- Manual backups get `--notes-template "{{guestname}} (manual)"` so they are
|
||||
recognisable. Note for the user: `truenas-backup` has storage prune `keep-all=1`, but
|
||||
the job has `keep-last 3` per guest; a manual backup there likely counts towards it at
|
||||
the job's next run (not verified).
|
||||
|
||||
Observed CLI behaviour (cv4pve-cli 2.4.0, all read-only probes):
|
||||
|
||||
- `get node storages <node> -o json` -> list of dicts: `storage`, `content`
|
||||
(comma list, e.g. `iso,snippets,import,backup,vztmpl`), `enabled`, `active`, `type`,
|
||||
`avail` (bytes).
|
||||
- `get cluster backup-jobs -o json` -> list of dicts: `id`, `enabled`, `storage`,
|
||||
`schedule` (e.g. `mon,wed,fri 04:00`), `mode`, `compress`, `all` (1 = all guests),
|
||||
`vmid` (comma list or null), `exclude` (comma list or null), `node` (null = all),
|
||||
`next-run` (epoch), `prune-backups` (e.g. `{"keep-last":"3"}`).
|
||||
- `get guest backups <node> <storage> <vmid> -o json` -> list of dicts: `volid`
|
||||
(`truenas-backup:backup/vzdump-lxc-104-2026_11_02-04_11_47.tar.zst`), `ctime` (epoch),
|
||||
`size` (bytes), `format`, `notes`, `protected`, `subtype` (`lxc`|`qemu`), `vmid`.
|
||||
- `do backup guest <node> <vmid> <storage> [--key value]` - no `--yes` option.
|
||||
vzdump params: `mode` snapshot|suspend|stop, `compress` 0|1|gzip|lzo|zstd,
|
||||
`notes-template`.
|
||||
- CT net config: `netN` = `name=eth0,bridge=vmbr0,hwaddr=BC:24:11:36:DE:78,ip=dhcp,ip6=auto,type=veth`
|
||||
(optional `gw`, `gw6`, `tag`, `firewall`, `rate`, `mtu`, ...).
|
||||
- VM net config: `netN` = `virtio=02:24:A6:49:A8:25,bridge=vmbr0[,tag=..][,firewall=1]`
|
||||
(first item is `<model>=<MAC>`); IP settings live in `ipconfigN` (cloud-init), e.g.
|
||||
`ip=dhcp`.
|
||||
- Runtime IPs, CT (running): `get ct interfaces <node> <vmid> -o json` -> list of
|
||||
dicts: `name`, `hwaddr`, `inet` (e.g. `172.16.41.200/23`), `inet6` (may be absent/
|
||||
null), `ip-addresses` (JSON string).
|
||||
- Runtime IPs, VM (running, guest agent): `get vm agent network <node> <vmid> -o json`
|
||||
-> `[{"key": "result", "value": "<JSON string>"}]`; the JSON is a list of
|
||||
`{"name", "hardware-address", "ip-addresses": [{"ip-address", "prefix",
|
||||
"ip-address-type": "ipv4"|"ipv6"}]}`. Fails when the agent is not running.
|
||||
|
||||
### `cli.py`
|
||||
|
||||
- `backup_storages(guest) -> list[str]` - storages on the guest's node whose `content`
|
||||
contains `backup` and that are enabled and active, in API order.
|
||||
- `backup_jobs() -> list[dict]`.
|
||||
- `job_storage(jobs, guest) -> str | None` - storage of the first enabled job covering
|
||||
the guest: (`all` truthy and vmid not in `exclude`) or vmid in `vmid`; and `node` null
|
||||
or equal to the guest's node.
|
||||
- `backup_jobs()` parses `prune-backups` when the CLI returns it as a JSON string
|
||||
(real output: `'{"keep-last":"3"}'`).
|
||||
- `backups(guest, storages) -> list[dict]` - all backups of the guest on those
|
||||
storages, newest first (`ctime` desc). Each dict gets `storage` (from volid prefix).
|
||||
- `backup_now(guest, storage)` -> `do backup guest <node> <vmid> <storage> --mode
|
||||
snapshot --compress zstd --notes-template "{{guestname}} (manual)" --wait`.
|
||||
- `parse_net(value: str) -> dict` - `netN` string to an ordered dict of key -> value
|
||||
(insertion order kept, so B can re-serialise unchanged keys). VM: the first
|
||||
`<model>=<MAC>` item is kept as is (key = model).
|
||||
- `nets(config) -> dict[str, dict]` - `netN` keys (sorted by N) -> `parse_net(value)`.
|
||||
- `interfaces(guest) -> list[dict]` - runtime interfaces as
|
||||
`{"name", "mac", "ipv4": [cidr...], "ipv6": [cidr...]}`, loopback (`lo`) excluded.
|
||||
CT via `get ct interfaces` (addresses from its `ip-addresses` JSON string - `inet6`
|
||||
alone holds only one, e.g. link-local, and misses the global SLAAC address), VM via
|
||||
`get vm agent network`. Only called when the guest's live status is running.
|
||||
|
||||
### `app.py` (UI contract)
|
||||
|
||||
- Key `i` on a guest opens `InfoScreen(guest)` (a `Screen` or `ModalScreen`). Empty
|
||||
table: nothing.
|
||||
- InfoScreen loads in a thread worker: config -> `cli.nets`, `cli.interfaces` (only if
|
||||
status is `running`; a failure there is shown inline as "unavailable: <summary>", not
|
||||
as a fatal error), `cli.backup_storages`, `cli.backup_jobs`, `cli.backups`.
|
||||
- Shows:
|
||||
- `DataTable#backups`: Date (local time, `YYYY-MM-DD HH:MM`), Storage, Size (GiB, one
|
||||
decimal), Format, Notes. Newest first. A `Static#jobs` line naming the job(s) that
|
||||
cover the guest (schedule, storage, `keep-last` if present) or "No backup job
|
||||
covers this guest".
|
||||
- `DataTable#nets`: Iface (net0...), Bridge, VLAN (`tag` or `-`), MAC, IPv4 (CT `ip`,
|
||||
VM `ipconfigN` `ip`, or `-`), IPv6 (CT `ip6`, VM `ipconfigN` `ip6`, or `-`),
|
||||
Firewall (`yes`/`no`).
|
||||
- `DataTable#addresses`: Interface, MAC, IPv4, IPv6 (runtime), or the unavailable
|
||||
line.
|
||||
- Key `b` in InfoScreen: `BackupScreen` modal with `Select#storage` (options =
|
||||
backup storages, preselected `job_storage` or the first), text saying mode snapshot,
|
||||
zstd, and the guest; buttons `#yes` / `#no`; escape = no. On yes: guest marked busy
|
||||
in `app.busy` (refuse with a warning if already busy), "backup started" notify, worker
|
||||
runs `cli.backup_now`, then "backup done"/error notify, busy released, backup list
|
||||
reloaded if the InfoScreen is still open.
|
||||
- `escape` closes InfoScreen; `g` in InfoScreen reloads it.
|
||||
- Every CLI failure -> error notify with `_message(exc)`, app keeps running.
|
||||
|
||||
## Steps
|
||||
|
||||
- [x] Tests (cli + UI contract) - red first.
|
||||
- [x] Implementation - suite green, ruff clean, README keys `i` / `b`.
|
||||
- [x] Read-only live check: open InfoScreen for CT 104 and VM 108, data shown.
|
||||
- [ ] User tests "Backup now" on CT 104.
|
||||
- [ ] Push `feat/guest-info`, merge to `main` (user-approved after the test).
|
||||
Reference in new issue
Block a user