Restore a guest from a backup in the info screen
CI / pytest and ruff (pull_request) Successful in 7m25s
CI / pytest and ruff (push) Successful in 6m56s

r on the info screen restores the backup under the cursor of the
backup list.

- New guest (default): the next free VMID from `get cluster nextid`,
  editable. The original is left untouched. An id already used by a
  listed guest is refused.
- Overwrite: replaces the guest itself. The guest must be stopped; its
  live status is read at apply and again right before the call. The
  VMID must be typed to confirm, then an "Overwrite" confirm follows.

--storage is always passed. It is preselected to the storage of the
guest's rootfs or first disk, and the choices are the node's storages
that can hold CT (rootdir) or VM (images) disks. Start after restore
is off by default, because a copy keeps the original's MAC address
and IP.

Safety:
- cli.restore refuses force (overwrite) for any VMID other than the
  guest's own (explicit raise). A hypothesis property checks that no
  other VMID ever reaches the CLI with force.
- The volid and storage are validated before they become arguments.
- A backup of the other guest type is refused. A reused VMID can carry
  a qemu backup for a CT or the other way round, and whether PVE
  rejects a mismatched archive before removing the old guest is not
  verified.
- An earlier guest's backup (visible with a) is labelled with that
  guest's name in the dialog and the confirm.
- r before the info screen has loaded says "still loading".
- Restore gets a 1-hour CLI timeout.

Not run live: a live test would create a guest that then has to be
deleted, which the issue reserves for the user's approval. A
read-only check opened the dialog for CT 104 (next id 105, local-lvm
preselected) and closed it without any call.

Closes #15

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit was merged in pull request #32.
This commit is contained in:
DuckbootandClaude Opus 5.5 committed 2026-11-03 01:32:05 +01:00
1 parent 262441916c
commit 0df928ab11
6 files changed
+862 -3

No files matched your search

+60
View File
@@ -735,3 +735,63 @@ CLI facts:
- yes -> busy guard, worker: `cli.agent_ping` then `cli.fstrim`; done notify with the
trimmed total in GiB/MiB and the number of paths (errors per path mentioned);
CliError -> error notify (ping failure says the agent did not answer).
---
# Restore a guest from a backup (issue #15)
## Decision
From the info screen's backup list, key `r` ("Restore backup"; `R` stays snapshot
rollback). Two targets: a new VMID (default, original untouched) or overwriting the
guest itself (destructive). Not run live: a live test would create a guest that then
needs deleting, which the issue reserves for the user's approval. Tested with mocks.
CLI facts (cv4pve-cli 2.4.0, read-only probes):
- `do restore ct|vm <node> <vmid> <volume> [--key value] -y --wait` - "creates/overwrites
the target vmid". API params: `storage` (target storage for the disks), `force`
(bool; required to overwrite an existing guest), `start` (bool), `unique` (VM: new
MACs).
- `get cluster nextid -o json` -> a JSON number, e.g. `105`.
- Backup entries carry `volid` (`truenas-backup:backup/vzdump-lxc-104-...tar.zst`),
`subtype` (`lxc`|`qemu`), `vmid`.
- Node storages: `content` lists `rootdir` (CT disks) and/or `images` (VM disks).
### `cli.py`
- `next_id() -> int`.
- `valid_volid(volid) -> str` - ValueError unless it matches
`^[A-Za-z0-9][A-Za-z0-9_.-]*:backup/vzdump-(lxc|qemu)-\d+-\S+$` (positional argument).
- `restore_storages(guest) -> list[str]` - enabled+active storages on the guest's node
whose `content` has `rootdir` (CT) or `images` (VM), API order.
- `restore(guest, volid, vmid, storage, force=False, start=False)` ->
`do restore <kind> <node> <vmid> <volid> --storage <storage> [--force 1] [--start 1]
-y --wait`; kind from the backup's guest kind (the guest dict's `type`); `vmid` a
positive int; volid validated; `force=True` only allowed when `vmid ==
guest["vmid"]` (ValueError otherwise - never overwrite some other guest).
### `app.py` (UI contract)
- `r` on the info screen: the backup under the cursor of `#backups` (the visible rows;
hidden earlier-guest backups are not restorable unless shown with `a`). No backup ->
info notify.
- `RestoreScreen` (DialogScreen, title "Restore <date> backup of <name> (<vmid>)"):
`RadioSet#target` with `RadioButton#new` (default, "New guest") and
`RadioButton#overwrite` ("Overwrite <name> (<vmid>)"); `Input#newid` prefilled with
`cli.next_id()` (used for "new"); `Select#storage` (`restore_storages`, preselected
to the storage of the guest's rootfs/first disk if listed); `Checkbox#start` (off);
`Input#confirm` (typed VMID, only needed for overwrite); `#apply`/`#cancel`; escape =
cancel. A note says a new copy keeps the original's network settings (same static
IP), so leave Start off until it is changed.
- Apply, new: `newid` must be a positive int not used by any guest in the current
listing (app.guests) -> else error notify, no call. Then `ConfirmScreen` titled
"Restore" summarising backup, target VMID, storage, start; yes -> busy guard on the
source guest, worker `cli.restore(guest, volid, newid, storage, force=False,
start=...)`, notify, main list reloads.
- Apply, overwrite: the guest must be stopped (live `cli.status` at apply time) -> else
error notify; `Input#confirm` must equal the VMID -> else error notify; then
`ConfirmScreen` titled "Overwrite" saying the guest's current disks and config are
replaced and cannot be undone; yes -> `cli.restore(guest, volid, guest vmid, storage,
force=True, start=...)`.
- All CliErrors -> error notify; busy released; markup-safe texts.