feat(scripts): install Incus via Zabbly LTS repo; unify services as vps.*
- 10-incus.sh replaces 10-lxd.sh: adds the Zabbly incus lts-7.0 apt repo (key fingerprint verified), installs the incus package, creates the vpsmgr pool (ZFS/spare/loop-file, dir fallback) and lxdbr0 via `incus admin init --preseed`, and keeps dns.mode=none. - 00-check.sh targets Debian 12/13 + Ubuntu 22.04/24.04/26.04, drops snapd, checks incus instead of the lxd snap. - 50/60-image scripts use `incus` (was `lxc`). - Service naming unified to vps.*: vps.service (panel), vps-nft.service, vps-ipv6.service. `systemctl restart vps` is now the one command operators need. unit After= incus.service. - uninstall.sh stops vps.* units and removes the Incus package on --purge. - sudoers whitelist for the unprivileged panel (configs/sudoers.d/vps). Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
This commit is contained in:
1 parent
ebc2b1478f
commit
c0faea6c8c
17 files changed
+369
-296
No files matched your search
@@ -38,7 +38,7 @@ net:
|
||||
# empty = IPv6 disabled (default). Containers get global
|
||||
# addresses via SLAAC; no NAT; set at install time.
|
||||
|
||||
lxd:
|
||||
incus:
|
||||
image: "vpsmgr/debian-sshd"
|
||||
image_fallback: "images:debian/13"
|
||||
pool: vpsmgr
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# vpsmgr main nftables config — GENERATED by `vpsmgr install`.
|
||||
# This file is re-applied on boot by vpsmgr-nft.service.
|
||||
# vpsmgr main nftables config — GENERATED by `vps install`.
|
||||
# This file is re-applied on boot by vps-nft.service.
|
||||
# Per-user DNAT rules live in /etc/vpsmgr/nftables.d/user-<name>.nft.
|
||||
table inet vpsmgr {
|
||||
chain prerouting {
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
# Managed by vpsmgr — installed file, do not edit by hand.
|
||||
# Changes are overwritten on the next install.
|
||||
#
|
||||
# The vps panel runs as the unprivileged 'vps' user. These are the ONLY
|
||||
# root-privileged commands it may run, each pinned to its exact invocation:
|
||||
# - nft: reload the vpsmgr nftables ruleset (fw.Reload)
|
||||
# - systemctl: manage the traefik proxy and restart the vps panel itself
|
||||
# - ip: IPv6 pass-through route / neighbor / addr changes
|
||||
# - sysctl: toggle IPv6 forwarding
|
||||
# - service/pkill/ndppd: control the ndppd NDP proxy daemon
|
||||
# Nothing else may be run as root by the panel.
|
||||
vps ALL=(root) NOPASSWD: /usr/sbin/nft add table inet vpsmgr
|
||||
vps ALL=(root) NOPASSWD: /usr/sbin/nft -f /etc/vpsmgr/nftables.conf
|
||||
vps ALL=(root) NOPASSWD: /usr/bin/systemctl enable traefik.service
|
||||
vps ALL=(root) NOPASSWD: /usr/bin/systemctl disable traefik.service
|
||||
vps ALL=(root) NOPASSWD: /usr/bin/systemctl start traefik.service
|
||||
vps ALL=(root) NOPASSWD: /usr/bin/systemctl stop traefik.service
|
||||
vps ALL=(root) NOPASSWD: /usr/bin/systemctl is-active traefik.service
|
||||
vps ALL=(root) NOPASSWD: /usr/bin/systemctl restart vps.service
|
||||
vps ALL=(root) NOPASSWD: /usr/bin/systemctl is-active vps.service
|
||||
vps ALL=(root) NOPASSWD: /sbin/ip -6 route del *
|
||||
vps ALL=(root) NOPASSWD: /sbin/ip -6 route add *
|
||||
vps ALL=(root) NOPASSWD: /sbin/ip -6 route replace *
|
||||
vps ALL=(root) NOPASSWD: /sbin/ip -6 neigh del proxy *
|
||||
vps ALL=(root) NOPASSWD: /sbin/ip -6 neigh add proxy *
|
||||
vps ALL=(root) NOPASSWD: /sbin/ip -6 addr replace *
|
||||
vps ALL=(root) NOPASSWD: /sbin/sysctl -w net.ipv6.conf.all.forwarding=1
|
||||
vps ALL=(root) NOPASSWD: /usr/sbin/service ndppd restart
|
||||
vps ALL=(root) NOPASSWD: /usr/sbin/service ndppd start
|
||||
vps ALL=(root) NOPASSWD: /usr/sbin/service ndppd stop
|
||||
vps ALL=(root) NOPASSWD: /usr/bin/pkill -x ndppd
|
||||
vps ALL=(root) NOPASSWD: /usr/sbin/ndppd -d -p /var/run/ndppd.pid
|
||||
@@ -0,0 +1,17 @@
|
||||
# Managed by vpsmgr — installed file, do not edit by hand.
|
||||
# Changes are overwritten on the next install.
|
||||
[Unit]
|
||||
Description=vps nftables rules
|
||||
After=network-online.target incus.service
|
||||
Wants=network-online.target
|
||||
Before=vps.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
# The config file starts with a delete-table line so the whole ruleset reloads
|
||||
# as ONE atomic batch (a failed rule keeps the previous table intact).
|
||||
ExecStart=/usr/sbin/nft -f /etc/vpsmgr/nftables.conf
|
||||
RemainAfterExit=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,19 @@
|
||||
# Managed by vpsmgr — installed file, do not edit by hand.
|
||||
# Changes are overwritten on the next install.
|
||||
[Unit]
|
||||
Description=vps panel
|
||||
After=network-online.target incus.service
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
# Unprivileged: the panel talks to Incus over its group-readable socket
|
||||
# (incus-admin) and escalates only the exact whitelisted commands via sudo.
|
||||
User=vps
|
||||
Group=vps
|
||||
ExecStart=/usr/local/bin/vps serve
|
||||
Restart=always
|
||||
RestartSec=3
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -1,15 +0,0 @@
|
||||
# Managed by vpsmgr — installed file, do not edit by hand.
|
||||
# Changes are overwritten on the next install.
|
||||
[Unit]
|
||||
Description=vpsmgr nftables rules
|
||||
After=network-online.target lxd.service
|
||||
Wants=network-online.target
|
||||
Before=vpsmgr-panel.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/bin/sh -c 'nft delete table inet vpsmgr 2>/dev/null; exec nft -f /etc/vpsmgr/nftables.conf'
|
||||
RemainAfterExit=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -1,15 +0,0 @@
|
||||
# Managed by vpsmgr — installed file, do not edit by hand.
|
||||
# Changes are overwritten on the next install.
|
||||
[Unit]
|
||||
Description=vpsmgr panel
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/local/bin/vpsmgr serve
|
||||
Restart=always
|
||||
RestartSec=3
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
+11
-16
@@ -7,7 +7,6 @@ set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
ROOT="$PWD"
|
||||
export PATH="$PATH:/snap/bin"
|
||||
|
||||
BUILD_MODE=release
|
||||
if [[ "${1:-}" == "--local-build" ]]; then
|
||||
@@ -20,20 +19,16 @@ if [[ $EUID -ne 0 ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# v0.3 makes breaking changes; a 0.1.x/0.2.x install must NOT be upgraded yet.
|
||||
# Abort before any script runs so the box cannot end up half-upgraded (v0.3
|
||||
# scripts over a v0.2.x binary). A box that already uninstalled is still caught
|
||||
# later by `vps install`, which refuses to adopt an old config.
|
||||
if [[ -x /usr/local/bin/vpsmgr ]]; then
|
||||
OLD_VER="$(/usr/local/bin/vpsmgr version 2>/dev/null || true)"
|
||||
case "$OLD_VER" in
|
||||
0.1.*|0.2.*)
|
||||
echo "error: this is the vpsmgr v0.3 installer, which makes breaking changes." >&2
|
||||
echo " this box still runs vpsmgr $OLD_VER, which cannot be upgraded yet." >&2
|
||||
echo " stay on v0.2.x for now; a migration path will be released later." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
# The Incus-based vpsmgr is a new product line. A leftover binary from the old
|
||||
# LXD-based v0.2.x/early-v0.3 line is not compatible — refuse to run over it
|
||||
# so the box cannot end up half-upgraded. A non-purging uninstall of the old
|
||||
# line leaves no binary (uninstall.sh removes it), so this only fires when the
|
||||
# old panel was never uninstalled.
|
||||
if [[ -x /usr/local/bin/vpsmgr ]] && ! /usr/local/bin/vpsmgr version 2>/dev/null | grep -q "0.3"; then
|
||||
echo "error: an old LXD-based vpsmgr binary is still installed." >&2
|
||||
echo " this installer uses Incus and cannot run over it." >&2
|
||||
echo " uninstall the old version first (bash uninstall.sh --purge)." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Local build: make it obvious WHICH branch will be compiled, and give the user
|
||||
@@ -72,7 +67,7 @@ if [[ -n "${VPSMGR_IPV6_SUBNET:-}" ]]; then
|
||||
systemctl enable ndppd.service >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
for step in 00-check 10-lxd 20-network 30-traefik 40-panel 50-image; do
|
||||
for step in 00-check 10-incus 20-network 30-traefik 40-panel 50-image; do
|
||||
echo
|
||||
echo "===== $step ====="
|
||||
bash "$ROOT/scripts/$step.sh"
|
||||
|
||||
+16
-15
@@ -11,8 +11,9 @@ if [[ $EUID -ne 0 ]]; then die "must run as root"; fi
|
||||
if [[ ! -f /etc/os-release ]]; then die "cannot find /etc/os-release"; fi
|
||||
. /etc/os-release
|
||||
case "${ID:-}:${VERSION_ID:-}" in
|
||||
ubuntu:24.04|ubuntu:26.04) ;;
|
||||
*) die "this installer targets Ubuntu 24.04 / 26.04 (got ${PRETTY_NAME:-unknown})" ;;
|
||||
debian:12|debian:13) ;;
|
||||
ubuntu:22.04|ubuntu:24.04|ubuntu:26.04) ;;
|
||||
*) die "this installer targets Debian 12/13 or Ubuntu 22.04/24.04/26.04 (got ${PRETTY_NAME:-unknown})" ;;
|
||||
esac
|
||||
|
||||
# --- virtualization (require physical or KVM) ---
|
||||
@@ -85,7 +86,7 @@ else
|
||||
fi
|
||||
|
||||
# --- packages ---
|
||||
for p in snapd nftables zstd curl; do
|
||||
for p in nftables zstd curl gpg; do
|
||||
if ! dpkg -s "$p" >/dev/null 2>&1; then
|
||||
log "installing $p"
|
||||
apt-get update -qq
|
||||
@@ -98,18 +99,18 @@ if command -v go >/dev/null 2>&1; then
|
||||
log "go: $(go version 2>/dev/null | awk '{print $3}')"
|
||||
fi
|
||||
|
||||
# --- LXD snap ---
|
||||
if ! snap list lxd >/dev/null 2>&1; then
|
||||
log "lxd snap not installed yet (installed by 10-lxd.sh)"
|
||||
# --- Incus ---
|
||||
if ! command -v incus >/dev/null 2>&1; then
|
||||
log "incus not installed yet (installed by 10-incus.sh)"
|
||||
fi
|
||||
|
||||
# --- UFW conflict ---
|
||||
# LXD manages its own `table inet lxd` nftables rules (DHCP/DNS/forwarding) on
|
||||
# lxdbr0, but UFW's `table ip filter` runs at the same priority with a DROP
|
||||
# policy, so it drops container DHCP/forwarding traffic before LXD's rules
|
||||
# Incus manages its own `table inet incus` nftables rules (DHCP/DNS/forwarding)
|
||||
# on lxdbr0, but UFW's `table ip filter` runs at the same priority with a DROP
|
||||
# policy, so it drops container DHCP/forwarding traffic before Incus's rules
|
||||
# ever see it. Result: containers get no IPv4 (no DHCP, no DNS, no NAT) and
|
||||
# the image build fails. See:
|
||||
# https://canonical.com/lxd/docs/latest/howto/network_bridge_firewalld/
|
||||
# https://linuxcontainers.org/incus/docs/main/howto/network_bridge_firewalld/
|
||||
# vpsmgr manages its own firewall via `table inet vpsmgr` nftables, so the
|
||||
# cleanest fix is to disable UFW during install (idempotent: skipped when it
|
||||
# is already inactive).
|
||||
@@ -119,11 +120,11 @@ if command -v ufw >/dev/null 2>&1 && ufw status 2>/dev/null | grep -q "Status: a
|
||||
V4_NET="${VPSMGR_IPV4_SUBNET:-10.115.0.0/24}"
|
||||
V4_MATCH="$(printf '%s' "${V4_NET%/*}" | sed 's/\./\\./g')"
|
||||
if ufw status verbose 2>/dev/null | grep -qE "lxdbr0|$V4_MATCH"; then
|
||||
log "ufw active but already has LXD/lxdbr0 allow rules — leaving it as-is"
|
||||
log "ufw active but already has Incus/lxdbr0 allow rules — leaving it as-is"
|
||||
else
|
||||
log "ufw is ACTIVE with default-DROP policy — this breaks LXD container IPv4"
|
||||
log " (LXD's own nftables rules are shadowed by ufw's DROP; known issue:"
|
||||
log " https://canonical.com/lxd/docs/latest/howto/network_bridge_firewalld/)"
|
||||
log "ufw is ACTIVE with default-DROP policy — this breaks Incus container IPv4"
|
||||
log " (Incus's own nftables rules are shadowed by ufw's DROP; known issue:"
|
||||
log " https://linuxcontainers.org/incus/docs/main/howto/network_bridge_firewalld/)"
|
||||
log " vpsmgr manages its firewall via nftables; disabling ufw."
|
||||
ufw disable >/dev/null 2>&1 && log " ufw disabled" || die "failed to disable ufw"
|
||||
# Keep it off across reboots. Snap/systemd enable it on boot otherwise.
|
||||
@@ -185,7 +186,7 @@ log "public/panel IP: $PUB_IP (ext iface: ${EXT_IF:-auto})"
|
||||
|
||||
# --- network reachability (warn only) ---
|
||||
if ! curl -sI --max-time 8 https://images.linuxcontainers.org >/dev/null 2>&1; then
|
||||
log " warn: cannot reach images.linuxcontainers.org — LXD image pull will fail unless cached"
|
||||
log " warn: cannot reach images.linuxcontainers.org — Incus image pull will fail unless cached"
|
||||
fi
|
||||
|
||||
echo "[00] checks passed"
|
||||
@@ -0,0 +1,215 @@
|
||||
#!/usr/bin/env bash
|
||||
# 10-incus.sh — install Incus (Zabbly LTS repo), storage pool, bridge.
|
||||
# Targets Debian 12/13 and Ubuntu 22.04/24.04/26.04 via the Zabbly package
|
||||
# repository (the Incus team's official third-party repo, maintained by the
|
||||
# project lead). Version follows the lts-7.0 channel.
|
||||
set -uo pipefail
|
||||
|
||||
log(){ echo "[10] $*"; }
|
||||
die(){ echo "[10] error: $*" >&2; exit 1; }
|
||||
|
||||
# --- ensure the Zabbly repo is present, then install Incus ---
|
||||
ZABBLY_SOURCE="/etc/apt/sources.list.d/zabbly-incus-lts-7.0.sources"
|
||||
if [[ -f "$ZABBLY_SOURCE" ]]; then
|
||||
log "zabbly incus lts-7.0 repo already configured"
|
||||
else
|
||||
log "adding Zabbly Incus LTS 7.0 repository..."
|
||||
install -d -m 0755 /etc/apt/keyrings
|
||||
curl -fsSL https://pkgs.zabbly.com/key.asc -o /etc/apt/keyrings/zabbly.asc \
|
||||
|| die "could not download the Zabbly signing key"
|
||||
# Verify the published fingerprint (4EFC 5906 96CB 15B8 7C73 A3AD 82CC 8797 C838 DCFD).
|
||||
FP=$(gpg --show-keys --with-colons /etc/apt/keyrings/zabbly.asc 2>/dev/null | awk -F: '/^fpr:/{print $10; exit}')
|
||||
if [[ "$FP" != "4EFC590696CB15B87C73A3AD82CC8797C838DCFD" ]]; then
|
||||
die "zabbly key fingerprint mismatch ($FP) — refusing to install from an unverified repo"
|
||||
fi
|
||||
cat > "$ZABBLY_SOURCE" <<EOF
|
||||
Enabled: yes
|
||||
Types: deb
|
||||
URIs: https://pkgs.zabbly.com/incus/lts-7.0
|
||||
Suites: $(. /etc/os-release && echo ${VERSION_CODENAME})
|
||||
Components: main
|
||||
Architectures: $(dpkg --print-architecture)
|
||||
Signed-By: /etc/apt/keyrings/zabbly.asc
|
||||
EOF
|
||||
apt-get update -qq || die "apt-get update failed (check the Zabbly repo)"
|
||||
fi
|
||||
|
||||
if dpkg -s incus >/dev/null 2>&1; then
|
||||
log "incus already installed: $(incus version 2>/dev/null | head -1)"
|
||||
else
|
||||
log "installing incus (Zabbly lts-7.0)..."
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq incus || die "apt install incus failed"
|
||||
fi
|
||||
log "incus version: $(incus version 2>/dev/null | head -1)"
|
||||
|
||||
# Make the incus-admin group available to the vps service user (created by
|
||||
# 40-panel.sh / `vps install`). The panel talks to Incus over its Unix socket,
|
||||
# which is group-readable by incus-admin — membership grants the same control
|
||||
# the root-only LXD snap used to give, without running as root.
|
||||
if id -u vps >/dev/null 2>&1; then
|
||||
usermod -aG incus-admin vps >/dev/null 2>&1 || true
|
||||
log "added user 'vps' to group incus-admin (panel can manage Incus via the API)"
|
||||
fi
|
||||
|
||||
# Wait for the daemon socket to be up (systemd socket activation).
|
||||
for i in $(seq 1 60); do
|
||||
[[ -S /var/lib/incus/unix.socket ]] && break
|
||||
sleep 1
|
||||
done
|
||||
[[ -S /var/lib/incus/unix.socket ]] || die "incus daemon socket not ready"
|
||||
|
||||
# --- storage pool ---
|
||||
# The Debian-package Incus bundles its own userspace tools; on a kernel that
|
||||
# ships the zfs module the pool is created with zfs. Like the LXD snap, no
|
||||
# host zfsutils are needed. Fall back to dir (no quotas) when zfs is
|
||||
# unavailable.
|
||||
POOL=vpsmgr
|
||||
POOL_EXISTS=0
|
||||
if incus storage show "$POOL" >/dev/null 2>&1; then
|
||||
POOL_EXISTS=1
|
||||
log "storage pool '$POOL' exists ($(incus storage show "$POOL" | awk -F': ' '/driver:/{print $2}'))"
|
||||
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
|
||||
DRIVER=zfs
|
||||
SPARE=""
|
||||
POOL_SIZE_MB=""
|
||||
if [[ $POOL_EXISTS -eq 1 ]]; then
|
||||
# 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
|
||||
FREE_KB=$(df -k --output=avail / | tail -1 | tr -d ' ')
|
||||
# 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%.
|
||||
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\""
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- incus admin init (preseed) ---
|
||||
# IPv6 pass-through: when VPSMGR_IPV6_SUBNET is set, lxdbr0 carries the global
|
||||
# prefix (no NAT) and containers SLAAC global addresses from it. The bridge
|
||||
# ADDRESS is deliberately not set here: the Go setup (SetupIPv6Bridge, run at
|
||||
# `vps install`) picks a free address inside the prefix and skips any the host
|
||||
# already uses — hardcoding prefix::1 at init time would claim the most common
|
||||
# upstream-router / host address before that clash check ever runs.
|
||||
V6_IP="none"
|
||||
V6_NAT="false"
|
||||
if [[ -n "${VPSMGR_IPV6_SUBNET:-}" ]]; then
|
||||
log "IPv6 pass-through enabled: lxdbr0 address chosen by \`vps install\` (clash-avoiding)"
|
||||
fi
|
||||
# Container IPv4 subnet (10.<n>.0.0/24): the bridge gateway is .1. Defaults to
|
||||
# 10.115.0.1/24; 00-ip-ask.sh exports the chosen subnet before this step.
|
||||
V4_GW="$(echo "${VPSMGR_IPV4_SUBNET:-10.115.0.0/24}" | cut -d. -f1-3).1/24"
|
||||
if [[ $POOL_EXISTS -eq 0 ]] || ! incus network show lxdbr0 >/dev/null 2>&1; then
|
||||
PRESEED=/tmp/vpsmgr-preseed.yaml
|
||||
cat > "$PRESEED" <<EOF
|
||||
config: {}
|
||||
networks:
|
||||
- config:
|
||||
ipv4.address: $V4_GW
|
||||
ipv4.nat: "true"
|
||||
ipv6.address: $V6_IP
|
||||
ipv6.nat: "$V6_NAT"
|
||||
ipv6.dhcp.stateful: "true"
|
||||
ipv6.routing: "true"
|
||||
# Do not serve instance-name DNS: Incus's dnsmasq would publish
|
||||
# <username>.lxd records for every container, so any tenant could nmap the
|
||||
# subnet and read everyone's username. dns.mode=none keeps DHCP + upstream
|
||||
# forwarding but drops the instance-name records.
|
||||
dns.mode: none
|
||||
description: ""
|
||||
name: lxdbr0
|
||||
type: bridge
|
||||
storage_pools:
|
||||
- config:
|
||||
$SRC_LINE
|
||||
$SIZE_LINE
|
||||
description: ""
|
||||
name: $POOL
|
||||
driver: $DRIVER
|
||||
profiles:
|
||||
- config: {}
|
||||
description: Default Incus profile
|
||||
devices:
|
||||
eth0:
|
||||
name: eth0
|
||||
nictype: bridged
|
||||
parent: lxdbr0
|
||||
type: nic
|
||||
root:
|
||||
path: /
|
||||
pool: $POOL
|
||||
type: disk
|
||||
name: default
|
||||
EOF
|
||||
log "running incus admin init --preseed (driver=$DRIVER, subnet $V4_GW, ipv6=${VPSMGR_IPV6_SUBNET:-none})"
|
||||
if ! incus admin init --preseed < "$PRESEED"; then
|
||||
log "preseed failed — creating missing pieces"
|
||||
if [[ -n "${VPSMGR_IPV6_SUBNET:-}" ]]; then
|
||||
incus network show lxdbr0 >/dev/null 2>&1 || incus network create lxdbr0 ipv4.address=$V4_GW ipv4.nat=true ipv6.address=none ipv6.nat=false ipv6.dhcp.stateful=true ipv6.routing=true dns.mode=none
|
||||
else
|
||||
incus network show lxdbr0 >/dev/null 2>&1 || incus network create lxdbr0 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" || true
|
||||
elif [[ -n "$POOL_SIZE_MB" ]]; then
|
||||
incus storage create "$POOL" zfs size="${POOL_SIZE_MB}MiB" || true
|
||||
fi
|
||||
# last resort: dir backend (no quotas)
|
||||
incus storage show "$POOL" >/dev/null 2>&1 || {
|
||||
log " warn: zfs pool creation failed, using dir backend (quotas disabled)"
|
||||
incus storage create "$POOL" dir
|
||||
}
|
||||
fi
|
||||
fi
|
||||
# ensure default profile devices point at our pool/bridge
|
||||
incus profile set default root.pool "$POOL" 2>/dev/null || true
|
||||
incus profile device set default root pool "$POOL" 2>/dev/null || true
|
||||
incus profile device set default eth0 parent lxdbr0 2>/dev/null || true
|
||||
else
|
||||
log "Incus already initialized (pool+network present)"
|
||||
fi
|
||||
|
||||
# Always re-assert dns.mode=none (fresh install and upgrade alike): this is the
|
||||
# only thing that stops Incus from publishing <username>.lxd DNS/PTR records
|
||||
# that let any tenant enumerate every other user's username with a subnet scan.
|
||||
incus network set lxdbr0 dns.mode=none 2>/dev/null || true
|
||||
|
||||
DRIVER_NOW=$(incus storage show "$POOL" | awk -F': ' '/driver:/{print $2}')
|
||||
log "storage backend: $DRIVER_NOW"
|
||||
if [[ "$DRIVER_NOW" == "dir" ]]; then
|
||||
log " warn: dir backend — disk quotas NOT enforced"
|
||||
fi
|
||||
|
||||
echo "[10] incus ready"
|
||||
@@ -1,172 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# 10-lxd.sh — install LXD snap, storage pool, bridge.
|
||||
set -uo pipefail
|
||||
export PATH="$PATH:/snap/bin"
|
||||
|
||||
log(){ echo "[10] $*"; }
|
||||
die(){ echo "[10] error: $*" >&2; exit 1; }
|
||||
|
||||
# --- ensure LXD snap 5.21/stable ---
|
||||
if snap list lxd >/dev/null 2>&1; then
|
||||
log "lxd snap already installed: $(snap list lxd | awk 'NR==2{print $2}')"
|
||||
else
|
||||
log "installing lxd snap (5.21/stable)..."
|
||||
snap install lxd --channel=5.21/stable || die "snap install lxd failed"
|
||||
fi
|
||||
log "waiting for LXD daemon..."
|
||||
lxd waitready --timeout=120 || die "lxd daemon not ready"
|
||||
|
||||
# --- storage pool ---
|
||||
# No host zfsutils needed: Ubuntu 24.04 kernel ships the zfs kernel module and
|
||||
# the LXD snap bundles the zfs userspace tools, so LXD creates the pool itself.
|
||||
POOL=vpsmgr
|
||||
POOL_EXISTS=0
|
||||
if lxc storage show "$POOL" >/dev/null 2>&1; then
|
||||
POOL_EXISTS=1
|
||||
log "storage pool '$POOL' exists ($(lxc storage show "$POOL" | awk -F': ' '/driver:/{print $2}'))"
|
||||
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
|
||||
DRIVER=zfs
|
||||
SPARE=""
|
||||
POOL_SIZE_MB=""
|
||||
if [[ $POOL_EXISTS -eq 1 ]]; then
|
||||
# adopt existing pool
|
||||
DRIVER=$(lxc 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
|
||||
FREE_KB=$(df -k --output=avail / | tail -1 | tr -d ' ')
|
||||
# Pool ceiling = a share of the free space, as a sparse loop file that
|
||||
# grows on demand. Small disks keep 80% so the host / LXD image store /
|
||||
# snap keep enough headroom; big disks (>= 20 GiB free) can afford 90%.
|
||||
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 LXD)"
|
||||
SRC_LINE=""
|
||||
SIZE_LINE=" size: \"${POOL_SIZE_MB}MiB\""
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- lxd init (preseed) ---
|
||||
# IPv6 pass-through: when VPSMGR_IPV6_SUBNET is set, lxdbr0 carries the global
|
||||
# prefix (no NAT) and containers SLAAC global addresses from it. The bridge
|
||||
# ADDRESS is deliberately not set here: the Go setup (SetupIPv6Bridge, run at
|
||||
# `vps install`) picks a free address inside the prefix and skips any the host
|
||||
# already uses — hardcoding prefix::1 at init time would claim the most common
|
||||
# upstream-router / host address before that clash check ever runs.
|
||||
V6_IP="none"
|
||||
V6_NAT="false"
|
||||
if [[ -n "${VPSMGR_IPV6_SUBNET:-}" ]]; then
|
||||
log "IPv6 pass-through enabled: lxdbr0 address chosen by \`vps install\` (clash-avoiding)"
|
||||
fi
|
||||
# Container IPv4 subnet (10.<n>.0.0/24): the bridge gateway is .1. Defaults to
|
||||
# 10.115.0.1/24; 00-ip-ask.sh exports the chosen subnet before this step.
|
||||
V4_GW="$(echo "${VPSMGR_IPV4_SUBNET:-10.115.0.0/24}" | cut -d. -f1-3).1/24"
|
||||
if [[ $POOL_EXISTS -eq 0 ]] || ! lxc network show lxdbr0 >/dev/null 2>&1; then
|
||||
PRESEED=/tmp/vpsmgr-preseed.yaml
|
||||
cat > "$PRESEED" <<EOF
|
||||
config: {}
|
||||
networks:
|
||||
- config:
|
||||
ipv4.address: $V4_GW
|
||||
ipv4.nat: "true"
|
||||
ipv6.address: $V6_IP
|
||||
ipv6.nat: "$V6_NAT"
|
||||
ipv6.dhcp.stateful: "true"
|
||||
ipv6.routing: "true"
|
||||
# Do not serve instance-name DNS: LXD's dnsmasq would publish
|
||||
# <username>.lxd records for every container, so any tenant could nmap the
|
||||
# subnet and read everyone's username. dns.mode=none keeps DHCP + upstream
|
||||
# forwarding but drops the instance-name records.
|
||||
dns.mode: none
|
||||
description: ""
|
||||
name: lxdbr0
|
||||
type: bridge
|
||||
storage_pools:
|
||||
- config:
|
||||
$SRC_LINE
|
||||
$SIZE_LINE
|
||||
description: ""
|
||||
name: $POOL
|
||||
driver: $DRIVER
|
||||
profiles:
|
||||
- config: {}
|
||||
description: Default LXD profile
|
||||
devices:
|
||||
eth0:
|
||||
name: eth0
|
||||
nictype: bridged
|
||||
parent: lxdbr0
|
||||
type: nic
|
||||
root:
|
||||
path: /
|
||||
pool: $POOL
|
||||
type: disk
|
||||
name: default
|
||||
cluster: null
|
||||
EOF
|
||||
log "running lxd init --preseed (driver=$DRIVER, subnet $V4_GW, ipv6=${VPSMGR_IPV6_SUBNET:-none})"
|
||||
if ! lxd init --preseed < "$PRESEED"; then
|
||||
log "preseed failed — creating missing pieces"
|
||||
if [[ -n "${VPSMGR_IPV6_SUBNET:-}" ]]; then
|
||||
lxc network show lxdbr0 >/dev/null 2>&1 || lxc network create lxdbr0 ipv4.address=$V4_GW ipv4.nat=true ipv6.address=none ipv6.nat=false ipv6.dhcp.stateful=true ipv6.routing=true dns.mode=none
|
||||
else
|
||||
lxc network show lxdbr0 >/dev/null 2>&1 || lxc network create lxdbr0 ipv4.address=$V4_GW ipv4.nat=true ipv6.address=none dns.mode=none
|
||||
fi
|
||||
if ! lxc storage show "$POOL" >/dev/null 2>&1; then
|
||||
if [[ -n "$SPARE" ]]; then
|
||||
lxc storage create "$POOL" zfs source="$SPARE" || true
|
||||
elif [[ -n "$POOL_SIZE_MB" ]]; then
|
||||
lxc storage create "$POOL" zfs size="${POOL_SIZE_MB}MiB" || true
|
||||
fi
|
||||
# last resort: dir backend (no quotas)
|
||||
lxc storage show "$POOL" >/dev/null 2>&1 || {
|
||||
log " warn: zfs pool creation failed, using dir backend (quotas disabled)"
|
||||
lxc storage create "$POOL" dir
|
||||
}
|
||||
fi
|
||||
fi
|
||||
# ensure default profile devices point at our pool/bridge
|
||||
lxc profile set default root.pool "$POOL" 2>/dev/null || true
|
||||
lxc profile device set default root pool "$POOL" 2>/dev/null || true
|
||||
lxc profile device set default eth0 parent lxdbr0 2>/dev/null || true
|
||||
else
|
||||
log "LXD already initialized (pool+network present)"
|
||||
fi
|
||||
|
||||
# Always re-assert dns.mode=none (fresh install and upgrade alike): this is the
|
||||
# only thing that stops LXD from publishing <username>.lxd DNS/PTR records that
|
||||
# let any tenant enumerate every other user's username with a subnet scan.
|
||||
lxc network set lxdbr0 dns.mode=none 2>/dev/null || true
|
||||
|
||||
DRIVER_NOW=$(lxc storage show "$POOL" | awk -F': ' '/driver:/{print $2}')
|
||||
log "storage backend: $DRIVER_NOW"
|
||||
if [[ "$DRIVER_NOW" == "dir" ]]; then
|
||||
log " warn: dir backend — disk quotas NOT enforced"
|
||||
fi
|
||||
|
||||
echo "[10] lxd ready"
|
||||
@@ -1,7 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
# 20-network.sh — sysctl + nftables basics.
|
||||
set -uo pipefail
|
||||
export PATH="$PATH:/snap/bin"
|
||||
|
||||
log(){ echo "[20] $*"; }
|
||||
|
||||
@@ -9,7 +8,7 @@ log(){ echo "[20] $*"; }
|
||||
# run (idempotent). io_uring_disabled=1 lets only init-userns CAP_SYS_ADMIN (the
|
||||
# host's own root services) create io_uring; container tenants — even container
|
||||
# root, which only has userns-scoped caps — get EPERM, closing the biggest
|
||||
# kernel LPE attack surface for tenants. Value 1, not 2: the host stack (LXD,
|
||||
# kernel LPE attack surface for tenants. Value 1, not 2: the host stack (Incus,
|
||||
# ZFS, Go panel/traefik) is untouched. Matches RHEL 9.3+'s shipped default.
|
||||
cat > /etc/sysctl.d/99-vpsmgr.conf <<EOF
|
||||
# Managed by vpsmgr — generated file, do not edit by hand.
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
# 30-traefik.sh — install Traefik binary + static config + systemd service.
|
||||
set -uo pipefail
|
||||
export PATH="$PATH:/snap/bin"
|
||||
|
||||
log(){ echo "[30] $*"; }
|
||||
die(){ echo "[30] error: $*" >&2; exit 1; }
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
# Binary source: prebuilt GitHub release by default, local Go build as fallback
|
||||
# (or when VPSMGR_BUILD_MODE=local, e.g. via ./install.sh --local-build).
|
||||
set -uo pipefail
|
||||
export PATH="$PATH:/snap/bin"
|
||||
|
||||
log(){ echo "[40] $*"; }
|
||||
die(){ echo "[40] error: $*" >&2; exit 1; }
|
||||
|
||||
+18
-19
@@ -6,57 +6,56 @@
|
||||
# build intermediate only) is deleted afterwards so only the modified
|
||||
# vpsmgr/debian-sshd stays on disk.
|
||||
set -uo pipefail
|
||||
export PATH="$PATH:/snap/bin"
|
||||
|
||||
log(){ echo "[50] $*"; }
|
||||
|
||||
lxc info >/dev/null 2>&1 || { echo "[50] error: LXD not ready" >&2; exit 1; }
|
||||
incus info >/dev/null 2>&1 || { echo "[50] error: Incus not ready" >&2; exit 1; }
|
||||
|
||||
# --- base image ---
|
||||
if lxc image show vpsmgr-debian-13 >/dev/null 2>&1; then
|
||||
if incus image show vpsmgr-debian-13 >/dev/null 2>&1; then
|
||||
log "base image vpsmgr-debian-13 already present"
|
||||
else
|
||||
log "pulling images:debian/13 (fallback images:debian/trixie)..."
|
||||
if ! lxc image copy images:debian/13 local: --alias vpsmgr-debian-13; then
|
||||
if ! incus image copy images:debian/13 local: --alias vpsmgr-debian-13; then
|
||||
log " debian/13 alias failed, trying debian/trixie"
|
||||
lxc image copy images:debian/trixie local: --alias vpsmgr-debian-13 \
|
||||
incus image copy images:debian/trixie local: --alias vpsmgr-debian-13 \
|
||||
|| log " warn: image pull failed — add-user needs network and will retry"
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- prebuilt sshd image ---
|
||||
if lxc image show vpsmgr/debian-sshd >/dev/null 2>&1; then
|
||||
if incus image show vpsmgr/debian-sshd >/dev/null 2>&1; then
|
||||
log "sshd image vpsmgr/debian-sshd already present"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! lxc image show vpsmgr-debian-13 >/dev/null 2>&1; then
|
||||
if ! incus image show vpsmgr-debian-13 >/dev/null 2>&1; then
|
||||
log " warn: base image unavailable, skipping sshd image build (add-user will install sshd on the fly)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
log "building sshd image (this takes a few minutes)..."
|
||||
NAME=tmp-sshd-builder
|
||||
lxc delete --force "$NAME" >/dev/null 2>&1 || true
|
||||
if lxc launch vpsmgr-debian-13 "$NAME"; then
|
||||
incus delete --force "$NAME" >/dev/null 2>&1 || true
|
||||
if incus launch vpsmgr-debian-13 "$NAME"; then
|
||||
# wait until usable
|
||||
for i in $(seq 1 60); do
|
||||
if lxc exec "$NAME" -- /bin/true >/dev/null 2>&1; then break; fi
|
||||
if incus exec "$NAME" -- /bin/true >/dev/null 2>&1; then break; fi
|
||||
sleep 2
|
||||
done
|
||||
# DNS can lag agent readiness; running apt before it is up fails the update
|
||||
# and, without a gate, a broken sshd image would still be published.
|
||||
DNS_OK=
|
||||
for i in $(seq 1 30); do
|
||||
if lxc exec "$NAME" -- getent hosts deb.debian.org >/dev/null 2>&1; then
|
||||
if incus exec "$NAME" -- getent hosts deb.debian.org >/dev/null 2>&1; then
|
||||
DNS_OK=1; break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
if [ -z "$DNS_OK" ]; then
|
||||
log " warn: builder never got working DNS; skipping sshd image (add-user will install sshd on the fly)"
|
||||
lxc delete --force "$NAME" >/dev/null 2>&1 || true
|
||||
elif lxc exec "$NAME" -- sh -c 'export DEBIAN_FRONTEND=noninteractive
|
||||
incus delete --force "$NAME" >/dev/null 2>&1 || true
|
||||
elif incus exec "$NAME" -- sh -c 'export DEBIAN_FRONTEND=noninteractive
|
||||
set -e
|
||||
# universal user tooling (small, every container hits these): curl/wget need
|
||||
# ca-certificates or HTTPS fails; dnsutils is bind9-dnsutils on Debian 13.
|
||||
@@ -88,12 +87,12 @@ rm -rf /tmp/* /var/tmp/* 2>/dev/null || true
|
||||
# IPv6 drops at the 1h lease mark). Drop it so each container generates its
|
||||
# own on first boot.
|
||||
rm -f /etc/machine-id /var/lib/dbus/machine-id 2>/dev/null || true'; then
|
||||
lxc stop "$NAME" --timeout=30 || true
|
||||
if lxc publish "$NAME" --alias vpsmgr/debian-sshd; then
|
||||
lxc delete --force "$NAME" || true
|
||||
incus stop "$NAME" --timeout=30 || true
|
||||
if incus publish "$NAME" --alias vpsmgr/debian-sshd; then
|
||||
incus delete --force "$NAME" || true
|
||||
# keep only the modified image — the Debian base was a build intermediate
|
||||
# and is never used to launch containers (fallback is the remote images:).
|
||||
if lxc image delete vpsmgr-debian-13 >/dev/null 2>&1; then
|
||||
if incus image delete vpsmgr-debian-13 >/dev/null 2>&1; then
|
||||
log "removed base image vpsmgr-debian-13 (only vpsmgr/debian-sshd kept)"
|
||||
else
|
||||
log " warn: could not remove base image vpsmgr-debian-13"
|
||||
@@ -101,12 +100,12 @@ rm -f /etc/machine-id /var/lib/dbus/machine-id 2>/dev/null || true'; then
|
||||
log "sshd image published: vpsmgr/debian-sshd"
|
||||
else
|
||||
log " warn: publish FAILED — sshd image NOT built (base image kept; re-run to retry)"
|
||||
lxc delete --force "$NAME" >/dev/null 2>&1 || true
|
||||
incus delete --force "$NAME" >/dev/null 2>&1 || true
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
log " warn: sshd install in builder failed; add-user will install sshd on the fly"
|
||||
lxc delete --force "$NAME" >/dev/null 2>&1 || true
|
||||
incus delete --force "$NAME" >/dev/null 2>&1 || true
|
||||
fi
|
||||
else
|
||||
log " warn: could not launch builder; add-user will install sshd on the fly"
|
||||
|
||||
+20
-20
@@ -10,7 +10,7 @@
|
||||
# Same hygiene as 50-image.sh: sshd + common tools baked in, dnf caches and
|
||||
# logs cleaned before publishing, base image deleted afterwards.
|
||||
set -uo pipefail
|
||||
export PATH="$PATH:/snap/bin"
|
||||
|
||||
|
||||
DISTRO="${1:-alma}"
|
||||
case "$DISTRO" in
|
||||
@@ -21,16 +21,16 @@ esac
|
||||
|
||||
log(){ echo "[60] $*"; }
|
||||
|
||||
lxc info >/dev/null 2>&1 || { echo "[60] error: LXD not ready" >&2; exit 1; }
|
||||
incus info >/dev/null 2>&1 || { echo "[60] error: Incus not ready" >&2; exit 1; }
|
||||
|
||||
if lxc image show "$IMAGE" >/dev/null 2>&1; then
|
||||
if incus image show "$IMAGE" >/dev/null 2>&1; then
|
||||
log "image $IMAGE already present"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! lxc image list "$BASE_ALIAS" --format=csv 2>/dev/null | grep -q .; then
|
||||
if ! incus image list "$BASE_ALIAS" --format=csv 2>/dev/null | grep -q .; then
|
||||
log "pulling $REMOTE_ALIAS (fallback base, deleted after build)..."
|
||||
if ! lxc image copy "$REMOTE_ALIAS" local: --alias "$BASE_ALIAS"; then
|
||||
if ! incus image copy "$REMOTE_ALIAS" local: --alias "$BASE_ALIAS"; then
|
||||
log " warn: image pull failed — nothing built"
|
||||
exit 0
|
||||
fi
|
||||
@@ -40,29 +40,29 @@ fi
|
||||
|
||||
log "building $IMAGE (this takes a few minutes)..."
|
||||
NAME=tmp-rhel-builder
|
||||
lxc delete --force "$NAME" >/dev/null 2>&1 || true
|
||||
if lxc launch "$BASE_ALIAS" "$NAME"; then
|
||||
incus delete --force "$NAME" >/dev/null 2>&1 || true
|
||||
if incus launch "$BASE_ALIAS" "$NAME"; then
|
||||
# wait until usable
|
||||
for i in $(seq 1 60); do
|
||||
if lxc exec "$NAME" -- /bin/true >/dev/null 2>&1; then break; fi
|
||||
if incus exec "$NAME" -- /bin/true >/dev/null 2>&1; then break; fi
|
||||
sleep 2
|
||||
done
|
||||
# RHEL containers bring eth0 up with NetworkManager, which lags the LXD agent
|
||||
# RHEL containers bring eth0 up with NetworkManager, which lags the Incus agent
|
||||
# by a few seconds: running dnf before DHCP has written resolv.conf makes it
|
||||
# die with "Curl error (6): Couldn't resolve host". Wait until DNS answers,
|
||||
# or the builder install fails and a broken image gets published.
|
||||
DNS_OK=
|
||||
for i in $(seq 1 30); do
|
||||
if lxc exec "$NAME" -- getent hosts mirrors.almalinux.org >/dev/null 2>&1; then
|
||||
if incus exec "$NAME" -- getent hosts mirrors.almalinux.org >/dev/null 2>&1; then
|
||||
DNS_OK=1; break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
if [ -z "$DNS_OK" ]; then
|
||||
log " warn: builder never got working DNS; nothing built"
|
||||
lxc delete --force "$NAME" >/dev/null 2>&1 || true
|
||||
lxc image delete "$BASE_ALIAS" >/dev/null 2>&1 || true
|
||||
elif lxc exec "$NAME" -- sh -c '
|
||||
incus delete --force "$NAME" >/dev/null 2>&1 || true
|
||||
incus image delete "$BASE_ALIAS" >/dev/null 2>&1 || true
|
||||
elif incus exec "$NAME" -- sh -c '
|
||||
set -e
|
||||
# universal user tooling (mirrors the Debian image): sshd, curl/wget need
|
||||
# ca-certificates or HTTPS fails; bind-utils is the RHEL nslookup/dig package.
|
||||
@@ -95,11 +95,11 @@ printf "#!/bin/sh\n[ -f /etc/vpsmgr-ipv6.conf ] || exit 0\nip -6 addr replace \"
|
||||
chmod +x /usr/local/sbin/vpsmgr-ipv6
|
||||
printf "[Unit]\nDescription=vpsmgr IPv6 primary address\nAfter=network-online.target\nWants=network-online.target\n[Service]\nType=oneshot\nExecStart=/usr/local/sbin/vpsmgr-ipv6\nRemainAfterExit=yes\n[Install]\nWantedBy=multi-user.target\n" > /etc/systemd/system/vpsmgr-ipv6.service
|
||||
systemctl enable vpsmgr-ipv6.service >/dev/null 2>&1 || true'; then
|
||||
lxc stop "$NAME" --timeout=30 || true
|
||||
if lxc publish "$NAME" --alias "$IMAGE"; then
|
||||
lxc delete --force "$NAME" || true
|
||||
incus stop "$NAME" --timeout=30 || true
|
||||
if incus publish "$NAME" --alias "$IMAGE"; then
|
||||
incus delete --force "$NAME" || true
|
||||
# keep only the modified image — the base was a build intermediate
|
||||
if lxc image delete "$BASE_ALIAS" >/dev/null 2>&1; then
|
||||
if incus image delete "$BASE_ALIAS" >/dev/null 2>&1; then
|
||||
log "removed base image $BASE_ALIAS (only $IMAGE kept)"
|
||||
else
|
||||
log " warn: could not remove base image $BASE_ALIAS"
|
||||
@@ -107,13 +107,13 @@ systemctl enable vpsmgr-ipv6.service >/dev/null 2>&1 || true'; then
|
||||
log "image published: $IMAGE"
|
||||
else
|
||||
log " warn: publish FAILED — $IMAGE NOT built (base image kept; re-run to retry)"
|
||||
lxc delete --force "$NAME" >/dev/null 2>&1 || true
|
||||
incus delete --force "$NAME" >/dev/null 2>&1 || true
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
log " warn: install in builder failed; nothing built"
|
||||
lxc delete --force "$NAME" >/dev/null 2>&1 || true
|
||||
lxc image delete "$BASE_ALIAS" >/dev/null 2>&1 || true
|
||||
incus delete --force "$NAME" >/dev/null 2>&1 || true
|
||||
incus image delete "$BASE_ALIAS" >/dev/null 2>&1 || true
|
||||
fi
|
||||
else
|
||||
log " warn: could not launch builder; nothing built"
|
||||
|
||||
+17
-17
@@ -3,14 +3,13 @@
|
||||
# and the traefik config are KEPT so a plain reinstall adopts the previous
|
||||
# users/domains/settings. --purge also removes those, plus containers/storage.
|
||||
set -uo pipefail
|
||||
export PATH="$PATH:/snap/bin"
|
||||
|
||||
PURGE=0
|
||||
if [[ "${1:-}" == "--purge" ]]; then PURGE=1; fi
|
||||
log(){ echo "[un] $*"; }
|
||||
|
||||
log "stopping services..."
|
||||
for svc in vpsmgr-panel vpsmgr-nft vpsmgr-ipv6 traefik; do
|
||||
for svc in vps vps-nft vps-ipv6 traefik; do
|
||||
systemctl disable --now "$svc.service" >/dev/null 2>&1 || true
|
||||
done
|
||||
systemctl daemon-reload >/dev/null 2>&1 || true
|
||||
@@ -42,11 +41,11 @@ if [[ -n "$V6SUBNET" ]]; then
|
||||
esac
|
||||
done
|
||||
# restore lxdbr0 IPv6 to disabled (matches vpsmgr default)
|
||||
if command -v lxc >/dev/null 2>&1 && lxc network show lxdbr0 >/dev/null 2>&1; then
|
||||
lxc network set lxdbr0 ipv6.address none 2>/dev/null || true
|
||||
lxc network set lxdbr0 ipv6.nat false 2>/dev/null || true
|
||||
lxc network set lxdbr0 ipv6.routing false 2>/dev/null || true
|
||||
lxc network set lxdbr0 ipv6.dhcp.stateful false 2>/dev/null || true
|
||||
if command -v incus >/dev/null 2>&1 && incus network show lxdbr0 >/dev/null 2>&1; then
|
||||
incus network set lxdbr0 ipv6.address none 2>/dev/null || true
|
||||
incus network set lxdbr0 ipv6.nat false 2>/dev/null || true
|
||||
incus network set lxdbr0 ipv6.routing false 2>/dev/null || true
|
||||
incus network set lxdbr0 ipv6.dhcp.stateful false 2>/dev/null || true
|
||||
fi
|
||||
# live sysctls back to defaults
|
||||
sysctl -w net.ipv6.conf.all.forwarding=0 net.ipv6.conf.default.forwarding=0 >/dev/null 2>&1 || true
|
||||
@@ -67,10 +66,8 @@ if [[ $PURGE -eq 0 ]] && [[ -f /etc/vpsmgr/config.yaml ]] && command -v vps >/de
|
||||
fi
|
||||
|
||||
log "removing files..."
|
||||
# The CLI binary is `vps` in v0.3+; also remove a legacy vpsmgr binary left by
|
||||
# a pre-0.3 install.
|
||||
rm -f /usr/local/bin/vps /usr/local/bin/vpsmgr /usr/local/bin/traefik
|
||||
rm -f /etc/systemd/system/vpsmgr-panel.service /etc/systemd/system/vpsmgr-nft.service /etc/systemd/system/vpsmgr-ipv6.service /etc/systemd/system/traefik.service
|
||||
rm -f /usr/local/bin/vps /usr/local/bin/traefik
|
||||
rm -f /etc/systemd/system/vps.service /etc/systemd/system/vps-nft.service /etc/systemd/system/vps-ipv6.service /etc/systemd/system/traefik.service
|
||||
# Restore the host-wide io_uring clamp to the kernel default before dropping
|
||||
# 99-vpsmgr.conf (which sets it to 1 at install time).
|
||||
sysctl -w kernel.io_uring_disabled=0 >/dev/null 2>&1 || true
|
||||
@@ -83,19 +80,22 @@ log " kept /etc/vpsmgr and /etc/traefik (reinstall will adopt them)"
|
||||
if [[ $PURGE -eq 1 ]]; then
|
||||
log "purging vpsmgr config/db/certs and traefik config..."
|
||||
rm -rf /etc/vpsmgr /etc/traefik
|
||||
log "purging LXD instances..."
|
||||
for c in $(lxc list --format=csv -c n 2>/dev/null); do
|
||||
log "purging Incus instances..."
|
||||
for c in $(incus list --format=csv -c n 2>/dev/null); do
|
||||
log " deleting container $c"
|
||||
lxc delete --force "$c" >/dev/null 2>&1 || true
|
||||
incus delete --force "$c" >/dev/null 2>&1 || true
|
||||
done
|
||||
log "removing storage pool..."
|
||||
lxc storage delete vpsmgr >/dev/null 2>&1 || true
|
||||
incus storage delete vpsmgr >/dev/null 2>&1 || true
|
||||
if command -v zpool >/dev/null 2>&1 && zpool list vpsmgr >/dev/null 2>&1; then
|
||||
zpool destroy -f vpsmgr >/dev/null 2>&1 || true
|
||||
fi
|
||||
rm -rf /var/lib/vpsmgr
|
||||
log "removing lxd snap..."
|
||||
snap remove lxd --purge >/dev/null 2>&1 || true
|
||||
log "removing Incus (Zabbly package) and its repo..."
|
||||
DEBIAN_FRONTEND=noninteractive apt-get remove -y -qq incus >/dev/null 2>&1 || true
|
||||
apt-get autoremove -y -qq >/dev/null 2>&1 || true
|
||||
rm -f /etc/apt/sources.list.d/zabbly-incus-lts-7.0.sources /etc/apt/keyrings/zabbly.asc
|
||||
apt-get update -qq >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
if [[ $PURGE -eq 1 ]]; then
|
||||
|
||||
Reference in new issue
Block a user