Restore a container backup with a smaller root disk
Proxmox only grows disks. The restore dialog gets an optional "Root disk GiB" field for container backups; with a size, the backup is restored as a new guest with --rootfs <storage>:<N> (Proxmox's advanced restore mode). - A smaller disk is only allowed for a new VMID, checked in the dialog and again in cli.restore(): with force, Proxmox deletes the old disks before it unpacks, and a failed unpack then removes the guest. - The size must be a positive integer (0 gives an unlimited subvol on ZFS/dir storage) and above the used root disk space of a running guest. - The confirm screen is a separate DANGER screen with a red border: it says mount points are dropped and their backed-up files land in the root disk, raw lxc.* lines can come back under root@pam, root disk options are not carried over, and the used space is no fit check. - Never --unique or ignore-unpack-errors. Design: docs/plans/lxc-shrink.md (section A). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9b472e8cf6
commit
82902edfc8
5 files changed
+457
-8
No files matched your search
@@ -0,0 +1,121 @@
|
||||
# Shrink a container's root disk
|
||||
|
||||
## Problem
|
||||
|
||||
Proxmox only grows disks (`resize_vm` dies with "unable to shrink disk size"). A
|
||||
container whose rootfs was sized too large cannot be made smaller from the web UI or
|
||||
this TUI. Two routes exist, verified against pve-container 6.1.18 (master) source:
|
||||
|
||||
1. API: restore a backup with an explicit `rootfs=<storage>:<GiB>` ("advanced restore
|
||||
mode", pct.adoc). Reachable through cv4pve-cli (`do restore ct ... --rootfs S:N`
|
||||
forwards the key; checked with `--dry-run`).
|
||||
2. Node shell: copy the files into a new, smaller volume and swap it in.
|
||||
|
||||
Both are dangerous; the TUI must say so plainly.
|
||||
|
||||
## Approach
|
||||
|
||||
### A. TUI: smaller rootfs on restore (container backups only)
|
||||
|
||||
- `RestoreScreen` gets an optional field "Root disk GiB" for lxc backups. Empty =
|
||||
today's behaviour, unchanged.
|
||||
- With a size set, only **New guest** is allowed. Overwrite + size is refused: with
|
||||
`force=1` PVE destroys every old volume before it extracts, and a failed extraction
|
||||
then deletes the new disks and the config too; the container is gone.
|
||||
- `cli.restore()` gets `rootfs_gb: int | None`; when set it checks it with
|
||||
`_positive` (a size of 0 makes PVE allocate an *unlimited* subvol on ZFS/dir) and
|
||||
adds `--rootfs <storage>:<N>` (same storage as the dialog's choice). Never
|
||||
`ignore-unpack-errors`.
|
||||
- The confirm screen for a shrink restore is a separate, red warning:
|
||||
- every mpX of the backup is dropped from the new config; files of `backup=1`
|
||||
mount points land inside the root disk and count against its size; `backup=0`
|
||||
mount points are not in the backup at all;
|
||||
- raw `lxc.*` lines (including `lxc.mount.entry` bind mounts) may come back when
|
||||
cv4pve-cli runs as root@pam: do not run both guests at once;
|
||||
- rootfs options (acl, quota, mountoptions, replicate) are not carried over;
|
||||
- names the guest's *current* mount points, saying the backup may differ;
|
||||
- shows rootfs used space when the guest is running (`status/current` `disk`;
|
||||
0 when stopped, shown as "unknown"), and says this is not a fit check: the
|
||||
backup may be older, mount point data and ext4 overhead (5 % reserve, inodes)
|
||||
come on top;
|
||||
- a too-small disk makes the restore task fail; the original is untouched because
|
||||
this is always a new VMID.
|
||||
- Hard refusal only where it is certain: size not a positive integer, or size <=
|
||||
used rootfs GiB of a running guest.
|
||||
- Known limit, unchanged from today's restore: `run(timeout=3600)` can give up while
|
||||
the PVE task still runs.
|
||||
|
||||
### B. Node script `scripts/pct-shrink-rootfs` (bash, run as root on the node)
|
||||
|
||||
Usage: `pct-shrink-rootfs [--yes] <vmid> <new-size-GiB>`. Without `--yes` it only
|
||||
measures and prints the plan; measuring runs `pct mount`/`pct unmount`, which sets
|
||||
and clears `lock: mounted` (so even a dry run touches the config briefly).
|
||||
|
||||
Refuses (exit non-zero, nothing changed) when: not root; `pct` missing; CT not
|
||||
stopped; config has any `[...]` section (snapshots, `[pve:pending]`); `lock:` set;
|
||||
`template: 1` or `protection: 1`; HA-managed (`ha-manager status`); a replication
|
||||
job exists (`pvesr list`); rootfs is not a volume; **any `mpN` or `lxc.mount.entry`**
|
||||
(rootfs-only CTs: no shadowed files, `df` measures rootfs alone, `pct mount` cannot
|
||||
fail on another storage); new size >= current size; size not a positive integer.
|
||||
|
||||
Fit check after `pct mount` (`df -B1` and `df -i` on the rootfs): used bytes * 1.1 <=
|
||||
N GiB * 0.93 (5 % root reserve + ~2 % ext4 metadata), and used inodes * 1.1 <= the
|
||||
new disk's inodes (N * 65536, mkfs.ext4 default ratio). On any error after
|
||||
`pct mount` the trap runs `pct unmount`; if that fails it stops and prints the manual
|
||||
cleanup (`pct unmount`, `pct unlock`), touching nothing else.
|
||||
|
||||
Steps (copy route, any storage):
|
||||
|
||||
1. `pct set <id> --mp9 <storage>:<N>,mp=/.shrink-new` - PVE allocates and formats;
|
||||
the script reads back mp9's volid and `size=`.
|
||||
2. `pct mount <id>`; `rsync -aHAX --numeric-ids --whole-file --sparse
|
||||
--one-file-system --exclude=/.shrink-new` old rootfs -> new; `pct unmount <id>`.
|
||||
On rsync failure: unmount, then free the new volume explicitly - `pct set
|
||||
--delete mp9` only moves it to `unusedN`, so the script then deletes the
|
||||
`unusedK` whose value is exactly the new volid (found by value, never by index).
|
||||
3. Swap, under PVE's config flock (`flock /run/lock/lxc/pve-config-<id>.lock`):
|
||||
copy the config to `/root/pct-shrink-<id>-<timestamp>.conf`; rewrite the main
|
||||
section with awk (exact string compare, no regex on volids) into
|
||||
`<conf>.tmp.<pid>` and rename it, as PVE's `file_set_contents` does on pmxcfs;
|
||||
refuse if rootfs or mp9 changed since the copy: `rootfs:` gets the
|
||||
new volid with the old options and mp9's `size=`, the `mp9:` line goes, and
|
||||
`unusedK: <old volid>` is added at the first free K. No `pct rescan`: the script
|
||||
knows both volids and the size, so rescan's side effects (every orphan of the
|
||||
VMID added as unused, `size=0` on pve-container < 6.1.9) are avoided.
|
||||
4. Print the exact `unusedK` key and value of the old rootfs: start the CT, check
|
||||
it, then `pct set <id> --delete unusedK`. The script never deletes the old volume.
|
||||
|
||||
The in-place ZFS `refquota` shortcut is left out: one route for every storage is
|
||||
less code to get wrong, and the copy keeps the original until the user deletes it.
|
||||
chattr flags (`+i`, `+a`) are not copied by rsync; the script says so.
|
||||
|
||||
Tests: pytest runs the script with stub `pct`, `df`, `rsync`, `flock`,
|
||||
`ha-manager`, `pvesr` on `PATH` and a temp config dir (`PCT_CONF_DIR` override,
|
||||
default `/etc/pve/lxc`; the plan output prints the directory used). bats is not
|
||||
installed and CI only runs pytest. ShellCheck via the edit hook.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Shrinking mpX volumes, and containers that have any mount point (script).
|
||||
- Overwrite-in-place restore with a smaller disk.
|
||||
- Restoring back to the original VMID (second backup/restore cycle).
|
||||
- Running the node script from the TUI (it needs a root shell on the node).
|
||||
- Moving `backup=0` mount points to the new guest.
|
||||
|
||||
## Steps
|
||||
|
||||
- [x] `cli.restore(rootfs_gb=...)` adds `--rootfs S:N`; tests in `test_cli.py`.
|
||||
- [x] RestoreScreen: size field for lxc, refuse with overwrite, refuse <= used of a
|
||||
running guest; shrink confirm text with the warnings above; tests in
|
||||
`test_app.py`.
|
||||
- [x] `scripts/pct-shrink-rootfs` + `tests/test_pct_shrink.py`: each refusal, dry
|
||||
run changes no config, happy path (exact new config, `unusedK` at first free
|
||||
K with existing `unused0`), dir-style volid with `/` and `.`, rsync failure
|
||||
frees the new volume by value, unmount failure stops.
|
||||
- [x] README: key table row and a warning paragraph; script usage.
|
||||
- [ ] Full `uv run pytest`, ruff, shellcheck green.
|
||||
|
||||
## Open questions
|
||||
|
||||
- Should a shrink restore pass `--unique 1` (new MACs)? Today's restore does not, it
|
||||
warns instead. Proposal: keep that, the warning already covers it.
|
||||
Reference in new issue
Block a user