refactor(install): always use root loop-file storage
Remove secondary-disk discovery and whole-disk ZFS creation from the installer. New installations now always create a sparse ZFS loop-file pool from free space on /, while existing vpsmgr pools are still adopted. The installer no longer scans, selects, formats, or modifies devices such as /dev/vdb, including virtual ISO/CD devices. Update storage documentation and keep the existing reclaim, pool sizing, and ZFS quota behavior. Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
This commit is contained in:
1 parent
3d524eef05
commit
64349de0c0
3 files changed
+68
-98
No files matched your search
@@ -154,11 +154,11 @@ The panel daemon runs as the dedicated unprivileged `vps` system user
|
||||
|
||||
## Storage
|
||||
|
||||
- The pool is ZFS. On first install, `10-incus.sh` either adopts an existing
|
||||
pool, uses a spare whole-disk block device, or (no spare disk) creates a
|
||||
**sparse loop-file pool** sized to a share of the free space on `/`:
|
||||
80% by default, 90% when ≥ 20 GiB free. The loop file only allocates blocks
|
||||
as the pool actually fills. On very small hosts, cap the ZFS ARC
|
||||
- The pool is ZFS. On first install, `10-incus.sh` adopts an existing pool or
|
||||
creates a **sparse loop-file pool** sized to a share of the free space on
|
||||
`/`: 80% by default, 90% when ≥ 20 GiB free. New installations never scan,
|
||||
select, format, or modify secondary disks. The loop file only allocates
|
||||
blocks as the pool actually fills. On very small hosts, cap the ZFS ARC
|
||||
(`zfs.arc_max`) so container memory keeps priority over the pool's cache.
|
||||
- Containers are ZFS clones of the image: the image's blocks are shared
|
||||
(copy-on-write), so a well-provisioned image costs one copy no matter how
|
||||
|
||||
@@ -34,7 +34,7 @@ same table; `vps config list` shows the live values with this annotation.
|
||||
| `net.ext_if` | operator | re-run `vps install` | external NIC (auto-detected from default route) |
|
||||
| `net.ipv6_subnet` | operator | re-run `vps install` | global IPv6 prefix for pass-through, e.g. `2602:fada:6::/64`; empty = disabled (does not remove IPv6 state already applied, see note below) |
|
||||
| `net.ipv6_mode` | **fixed at install** | — | IPv6 allocation mode: `none` / `prefix` (/112 blocks) / `pool` (per-container address) |
|
||||
| `net.ipv6_pool` | operator | next `vps install` | pool-mode address list (bare global addresses or `/128`); editable via the admin panel's IPv6 Pool page |
|
||||
| `net.ipv6_pool` | operator | applied immediately | pool-mode address list (bare global addresses or `/128`); editable via the admin panel's IPv6 Pool page |
|
||||
| `incus.image` | operator | next `vps add` / reinstall | container image alias |
|
||||
| `incus.image_fallback` | operator | next `vps add` / reinstall | fallback remote image |
|
||||
| `incus.pool` | **fixed at install** | — | storage pool |
|
||||
|
||||
+62
-92
@@ -59,11 +59,11 @@ done
|
||||
[[ -S /var/lib/incus/unix.socket ]] || die "incus daemon socket not ready"
|
||||
|
||||
# --- storage pool ---
|
||||
# ZFS only: Incus quotas, snapshots and clone-on-create (the whole disk model)
|
||||
# depend on it. Incus does NOT bundle the ZFS userspace tools, so
|
||||
# zfsutils-linux must be installed (00-check.sh ensures it). The dir backend is
|
||||
# deliberately not offered — it has no quotas, which silently breaks the disk
|
||||
# limits every container is supposed to have.
|
||||
# ZFS only: Incus quotas, snapshots and clone-on-create depend on it. New
|
||||
# installations use a sparse loop-file pool on /. Incus does NOT bundle the
|
||||
# ZFS userspace tools, so zfsutils-linux must be installed (00-check.sh ensures
|
||||
# it). The dir backend is deliberately not offered — it has no quotas, which
|
||||
# silently breaks the disk limits every container is supposed to have.
|
||||
command -v zpool >/dev/null 2>&1 || die "zfsutils-linux is not installed (zpool missing) — 00-check.sh should have installed it; run it or: apt-get install -y zfsutils-linux"
|
||||
POOL=vpsmgr
|
||||
POOL_EXISTS=0
|
||||
@@ -76,96 +76,71 @@ if incus storage show "$POOL" >/dev/null 2>&1; then
|
||||
log "storage pool '$POOL' exists ($DRIVER)"
|
||||
fi
|
||||
|
||||
# find a spare whole-disk block device (no partitions, not the root disk, unmounted)
|
||||
find_spare_disk(){
|
||||
ROOT_DISK=$(lsblk -rno NAME,MOUNTPOINTS | awk '$2=="/"{print $1; exit}' | sed 's/[0-9]*$//')
|
||||
for d in $(lsblk -rno NAME,TYPE | awk '$2=="disk"{print $1}'); do
|
||||
[[ "$d" == "$ROOT_DISK" ]] && continue
|
||||
[[ -b "/dev/$d" ]] || continue
|
||||
NCHILD=$(lsblk -rno NAME "$d" | tail -n +2 | wc -l)
|
||||
[[ "$NCHILD" -gt 0 ]] && continue
|
||||
grep -q "/dev/$d" /proc/mounts && continue
|
||||
echo "/dev/$d"; return 0
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# Decide how to configure the pool in preseed.
|
||||
# New installations always use a sparse loop-file ZFS pool on /. The installer
|
||||
# deliberately never scans, selects, formats, or modifies secondary block
|
||||
# devices: virtual CD/ISO devices and provider-specific disk layouts are left
|
||||
# completely untouched. Existing vpsmgr pools are adopted unchanged.
|
||||
DRIVER=zfs
|
||||
SPARE=""
|
||||
POOL_SIZE_MB=""
|
||||
if [[ $POOL_EXISTS -eq 1 ]]; then
|
||||
# adopt existing pool
|
||||
# Adopt existing pool.
|
||||
DRIVER=$(incus storage show "$POOL" | awk -F': ' '/driver:/{print $2}')
|
||||
SRC_LINE=" source: \"$POOL\""
|
||||
SIZE_LINE=""
|
||||
else
|
||||
SPARE=$(find_spare_disk || true)
|
||||
if [[ -n "$SPARE" ]]; then
|
||||
log "zfs pool '$POOL' will be created on spare block device $SPARE"
|
||||
SRC_LINE=" source: \"$SPARE\""
|
||||
SIZE_LINE=""
|
||||
else
|
||||
# Loop-file pool: the pool is a sparse file that grows on demand, so the
|
||||
# ceiling is a share of the host's FREE space. Before sizing it, make sure
|
||||
# the host actually has room: when free space is tight, reclaim caches and
|
||||
# build artifacts FIRST (apt cache, autoremove, journal, stale kernels,
|
||||
# /tmp, pip/go caches) so the pool + Incus image store + system still fit.
|
||||
# Without this, a host close to full would carve a pool that then starves
|
||||
# the root filesystem.
|
||||
# Loop-file pool: the pool is sparse and grows on demand inside /. Before
|
||||
# sizing it, reclaim caches and build artifacts when free space is tight so
|
||||
# the pool, Incus image store, and host remain usable.
|
||||
FREE_KB=$(df -k --output=avail / | tail -1 | tr -d ' ')
|
||||
if (( FREE_KB < 25 * 1024 * 1024 )); then
|
||||
log "free space on / is $(( FREE_KB / 1024 / 1024 )) GiB (< 25 GiB) — reclaiming caches before sizing the pool..."
|
||||
# Cleanest wins first; each is idempotent and never touches user data.
|
||||
DEBIAN_FRONTEND=noninteractive apt-get clean 2>/dev/null || true
|
||||
rm -rf /var/lib/apt/lists/* 2>/dev/null || true
|
||||
DEBIAN_FRONTEND=noninteractive apt-get autoremove -y -qq 2>/dev/null || true
|
||||
journalctl --vacuum-time=3d >/dev/null 2>&1 || true
|
||||
rm -rf /tmp/* /var/tmp/* 2>/dev/null || true
|
||||
# Stale kernels: purge ALL linux-image-* except the RUNNING kernel — but
|
||||
# if a NEWER kernel than the running one is already installed (upgraded,
|
||||
# not yet rebooted), keep that newest one too: deleting it would make the
|
||||
# next reboot BOOT AN OLDER KERNEL than the one currently running. The
|
||||
# meta package linux-image-<arch> (no version, e.g. linux-image-amd64)
|
||||
# is excluded — it holds no image payload but keeps apt pulling future
|
||||
# kernels; purging it would silently stop kernel upgrades.
|
||||
RUNNING_KERNEL=$(uname -r)
|
||||
mapfile -t ALL_KERNELS < <(dpkg -l 'linux-image-*' 2>/dev/null | awk '/^ii/{print $2}' | grep -v -- "-dbg" | grep -E -- "-[0-9]+" | sort -V)
|
||||
KEEP=("linux-image-$RUNNING_KERNEL")
|
||||
NEWEST=${ALL_KERNELS[-1]}
|
||||
if [[ -n "$NEWEST" && "$NEWEST" != "linux-image-$RUNNING_KERNEL" ]]; then
|
||||
KEEP+=("$NEWEST")
|
||||
log " keeping $NEWEST (newer than running kernel — reboot pending)"
|
||||
fi
|
||||
OLD_KERNELS=()
|
||||
for k in "${ALL_KERNELS[@]}"; do
|
||||
keep=0
|
||||
for kk in "${KEEP[@]}"; do [[ "$k" == "$kk" ]] && keep=1; done
|
||||
(( keep )) || OLD_KERNELS+=("$k")
|
||||
done
|
||||
if (( ${#OLD_KERNELS[@]} > 0 )); then
|
||||
log " purging ${#OLD_KERNELS[@]} old kernel(s): ${OLD_KERNELS[*]}"
|
||||
DEBIAN_FRONTEND=noninteractive apt-get purge -y -qq "${OLD_KERNELS[@]}" >/dev/null 2>&1 || true
|
||||
fi
|
||||
rm -rf /root/.cache/pip /root/.cache/go-build /root/go/pkg/mod/cache/download 2>/dev/null || true
|
||||
FREE_KB=$(df -k --output=avail / | tail -1 | tr -d ' ')
|
||||
if (( FREE_KB < 25 * 1024 * 1024 )); then
|
||||
log "free space on / is $(( FREE_KB / 1024 / 1024 )) GiB (< 25 GiB) — reclaiming caches before sizing the pool..."
|
||||
# Cleanest wins first; each is idempotent and never touches user data.
|
||||
DEBIAN_FRONTEND=noninteractive apt-get clean 2>/dev/null || true
|
||||
rm -rf /var/lib/apt/lists/* 2>/dev/null || true
|
||||
DEBIAN_FRONTEND=noninteractive apt-get autoremove -y -qq 2>/dev/null || true
|
||||
journalctl --vacuum-time=3d >/dev/null 2>&1 || true
|
||||
rm -rf /tmp/* /var/tmp/* 2>/dev/null || true
|
||||
# Stale kernels: purge ALL linux-image-* except the RUNNING kernel — but
|
||||
# if a NEWER kernel than the running one is already installed (upgraded,
|
||||
# not yet rebooted), keep that newest one too: deleting it would make the
|
||||
# next reboot BOOT AN OLDER KERNEL than the one currently running. The
|
||||
# meta package linux-image-<arch> (no version, e.g. linux-image-amd64) is
|
||||
# excluded — it holds no image payload but keeps apt pulling future
|
||||
# kernels; purging it would silently stop kernel upgrades.
|
||||
RUNNING_KERNEL=$(uname -r)
|
||||
mapfile -t ALL_KERNELS < <(dpkg -l 'linux-image-*' 2>/dev/null | awk '/^ii/{print $2}' | grep -v -- "-dbg" | grep -E -- "-[0-9]+" | sort -V)
|
||||
KEEP=("linux-image-$RUNNING_KERNEL")
|
||||
NEWEST=${ALL_KERNELS[-1]}
|
||||
if [[ -n "$NEWEST" && "$NEWEST" != "linux-image-$RUNNING_KERNEL" ]]; then
|
||||
KEEP+=("$NEWEST")
|
||||
log " keeping $NEWEST (newer than running kernel — reboot pending)"
|
||||
fi
|
||||
OLD_KERNELS=()
|
||||
for k in "${ALL_KERNELS[@]}"; do
|
||||
keep=0
|
||||
for kk in "${KEEP[@]}"; do [[ "$k" == "$kk" ]] && keep=1; done
|
||||
(( keep )) || OLD_KERNELS+=("$k")
|
||||
done
|
||||
if (( ${#OLD_KERNELS[@]} > 0 )); then
|
||||
log " purging ${#OLD_KERNELS[@]} old kernel(s): ${OLD_KERNELS[*]}"
|
||||
DEBIAN_FRONTEND=noninteractive apt-get purge -y -qq "${OLD_KERNELS[@]}" >/dev/null 2>&1 || true
|
||||
fi
|
||||
rm -rf /root/.cache/pip /root/.cache/go-build /root/go/pkg/mod/cache/download 2>/dev/null || true
|
||||
FREE_KB=$(df -k --output=avail / | tail -1 | tr -d ' ')
|
||||
log "after reclaim: $(( FREE_KB / 1024 / 1024 )) GiB free on /"
|
||||
fi
|
||||
# Pool ceiling = a share of the free space, as a sparse loop file that
|
||||
# grows on demand. Small disks keep 80% so the host / Incus image store
|
||||
# keep enough headroom; big disks (>= 20 GiB free) can afford 90%.
|
||||
# No reserve check: the loop file is sparse, so creating the pool costs
|
||||
# nothing — only actual container data grows it, and the percentage
|
||||
# ceiling already keeps the host from being fully starved.
|
||||
PCT=80
|
||||
if (( FREE_KB > 20 * 1024 * 1024 )); then
|
||||
PCT=90
|
||||
fi
|
||||
POOL_SIZE_MB=$(( FREE_KB * PCT / 100 / 1024 ))
|
||||
log "loop-file zfs pool '$POOL' (~${POOL_SIZE_MB} MiB = ${PCT}% of free, created by Incus)"
|
||||
SRC_LINE=""
|
||||
SIZE_LINE=" size: \"${POOL_SIZE_MB}MiB\""
|
||||
log "after reclaim: $(( FREE_KB / 1024 / 1024 )) GiB free on /"
|
||||
fi
|
||||
# Small disks keep 80% so the host and Incus image store retain headroom;
|
||||
# larger free roots can afford 90%. The loop file is sparse, so creation
|
||||
# itself consumes no space; actual container data grows it on demand.
|
||||
PCT=80
|
||||
if (( FREE_KB > 20 * 1024 * 1024 )); then
|
||||
PCT=90
|
||||
fi
|
||||
POOL_SIZE_MB=$(( FREE_KB * PCT / 100 / 1024 ))
|
||||
log "loop-file zfs pool '$POOL' on / (~${POOL_SIZE_MB} MiB = ${PCT}% of free)"
|
||||
SRC_LINE=""
|
||||
SIZE_LINE=" size: \"${POOL_SIZE_MB}MiB\""
|
||||
fi
|
||||
|
||||
# --- incus admin init (preseed) ---
|
||||
@@ -234,13 +209,8 @@ EOF
|
||||
incus network show incusbr0 >/dev/null 2>&1 || incus network create incusbr0 ipv4.address=$V4_GW ipv4.nat=true ipv6.address=none dns.mode=none
|
||||
fi
|
||||
if ! incus storage show "$POOL" >/dev/null 2>&1; then
|
||||
if [[ -n "$SPARE" ]]; then
|
||||
incus storage create "$POOL" zfs source="$SPARE" || die "zfs pool creation on $SPARE failed"
|
||||
elif [[ -n "$POOL_SIZE_MB" ]]; then
|
||||
incus storage create "$POOL" zfs size="${POOL_SIZE_MB}MiB" || die "zfs pool creation (loop file) failed"
|
||||
else
|
||||
die "no disk for the zfs pool and no loop-file size decided"
|
||||
fi
|
||||
[[ -n "$POOL_SIZE_MB" ]] || die "no loop-file size decided for the ZFS pool"
|
||||
incus storage create "$POOL" zfs size="${POOL_SIZE_MB}MiB" || die "zfs pool creation (loop file) failed"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
|
||||
Reference in new issue
Block a user