# 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=:` ("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 :` (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] `. 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 --mp9 :,mp=/.shrink-new` - PVE allocates and formats; the script reads back mp9's volid and `size=`. 2. `pct mount `; `rsync -aHAX --numeric-ids --whole-file --sparse --one-file-system --exclude=/.shrink-new` old rootfs -> new; `pct unmount `. 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-.lock`): copy the config to `/root/pct-shrink--.conf`; rewrite the main section with awk (exact string compare, no regex on volids) into `.tmp.` 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: ` 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 --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.