- The first cut matched the literal host deb.debian.org, which a Debian 13 netinst never contains: it describes the archive as "URIs: mirror+file:///etc/apt/mirrors/debian.list" with "Components: main". So contrib was never appended and "apt-get install zfsutils-linux" still reported "no installation candidate". - A stanza/line now counts as Debian when its Signed-By names the Debian archive keyring, or when it has none (the default trusted keys). A third-party repo signs with its own keyring and is left alone. The deb822 case is parsed per stanza (awk, mawk-compatible) so a mixed file is handled correctly. - Verified on a Debian 13 netinst: after the edit, apt-get update succeeds and zfsutils-linux / zfs-dkms resolve from trixie/contrib. Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
438 lines
20 KiB
Bash
Executable File
438 lines
20 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# 00-check.sh — environment sanity checks.
|
|
set -uo pipefail
|
|
|
|
log(){ echo "[00] $*"; }
|
|
die(){ echo "[00] error: $*" >&2; exit 1; }
|
|
|
|
if [[ $EUID -ne 0 ]]; then die "must run as root"; fi
|
|
|
|
# --- distro ---
|
|
if [[ ! -f /etc/os-release ]]; then die "cannot find /etc/os-release"; fi
|
|
. /etc/os-release
|
|
case "${ID:-}:${VERSION_ID:-}" in
|
|
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) ---
|
|
if command -v systemd-detect-virt >/dev/null 2>&1; then
|
|
VIRT=$(systemd-detect-virt)
|
|
case "$VIRT" in
|
|
openvz|lxc|lxc-libvirt|wsl|container|vm-other|podman|docker)
|
|
die "unsupported environment: '$VIRT'. Need a physical machine or KVM VM."
|
|
;;
|
|
esac
|
|
log "virtualization: ${VIRT:-none (physical)}"
|
|
fi
|
|
|
|
# --- architecture ---
|
|
ARCH=$(uname -m)
|
|
case "$ARCH" in
|
|
x86_64) GOARCH=amd64 ;;
|
|
aarch64) GOARCH=arm64 ;;
|
|
*) die "unsupported architecture: $ARCH (only amd64 / arm64 are supported)" ;;
|
|
esac
|
|
log "architecture: $ARCH ($GOARCH)"
|
|
|
|
# --- hardware (warn only) ---
|
|
CPUS=$(nproc 2>/dev/null || echo 0)
|
|
MEM_KB=$(awk '/MemTotal/{print $2}' /proc/meminfo)
|
|
MEM_GB=$(( MEM_KB / 1024 / 1024 ))
|
|
log "cpu: ${CPUS} cores, mem: ${MEM_GB} GiB"
|
|
[[ $CPUS -lt 2 ]] && log " warn: < 2 CPU may be slow"
|
|
[[ $MEM_GB -lt 2 ]] && log " warn: < 2 GiB RAM is tight"
|
|
|
|
# --- disk ---
|
|
FREE_KB=$(df -k --output=avail / | tail -1 | tr -d ' ')
|
|
log "free disk on /: $(( FREE_KB / 1024 )) MiB"
|
|
if [[ $FREE_KB -lt 5*1024*1024 ]]; then
|
|
log "warn: need at least 5 GiB free on /, only $(( FREE_KB / 1024 / 1024 )) GiB available"
|
|
read -r -p "force continue anyway? [y/N] " ans
|
|
[[ "$ans" == "y" || "$ans" == "Y" ]] || die "aborted: not enough free disk on /"
|
|
fi
|
|
|
|
# --- swap ---
|
|
# Swap is enforced up front in install.sh (before the IPv6 prompts), so it is
|
|
# not handled here. An existing swap is left alone; a missing swap aborts the
|
|
# install there.
|
|
|
|
# --- storage backend ---
|
|
# zfs (default), btrfs, or dir, chosen by install.sh / VPSMGR_STORAGE. zfs and
|
|
# btrfs both provide quotas/snapshots/clone-on-create; dir has none and is
|
|
# meant for throwaway test boxes only.
|
|
STORAGE="${VPSMGR_STORAGE:-zfs}"
|
|
case "$STORAGE" in
|
|
zfs|btrfs|dir) ;;
|
|
*) die "VPSMGR_STORAGE must be zfs, btrfs or dir (got '$STORAGE')" ;;
|
|
esac
|
|
log "storage backend: $STORAGE"
|
|
|
|
# --- packages ---
|
|
# Debian minimal images are notoriously bare (sometimes even curl is missing),
|
|
# so every tool the installer needs is ensured here, before any later step
|
|
# depends on it:
|
|
# - zfsutils-linux: Incus does NOT bundle the ZFS userspace tools; the
|
|
# storage pool is ZFS-only by default, so zpool/zfs must be present or
|
|
# pool creation fails (skipped in dir mode — no kernel module needed)
|
|
# - matching kernel headers (plus build-essential): Debian only. The
|
|
# linux-headers-amd64 meta package is for the normal amd64 kernel; cloud
|
|
# kernels need linux-headers-cloud-amd64 so DKMS can build for the kernel
|
|
# that is actually running. Ubuntu ships the ZFS module PREBUILT inside
|
|
# the kernel (linux-modules), so it needs none of the DKMS toolchain — no
|
|
# compile time at all.
|
|
# - ca-certificates: without it every curl HTTPS call (Zabbly key, the
|
|
# HAProxy repository, image pulls) fails with a certificate error
|
|
# - python3: used by 00-ip-ask.sh (prefix/subnet validation) and the
|
|
# check-ipv6-support.sh probe
|
|
# - tar/xz-utils: archive extraction; curl: downloads; gpg: Zabbly key and
|
|
# HAProxy repository key verification; nftables/zstd: firewall + Incus image
|
|
# compression
|
|
# - sudo: hard requirement — the panel runs unprivileged and escalates ONLY
|
|
# the sudoers whitelist commands via `sudo -n`. Without it every
|
|
# privileged operation (nft reload, haproxy/systemctl, IPv6 wiring, ndppd)
|
|
# fails at runtime. visudo ships with the sudo package.
|
|
KERNEL_REL=$(uname -r)
|
|
BASE_DEPS="sudo ca-certificates python3 tar xz-utils nftables zstd curl gpg"
|
|
|
|
# zfsutils-linux and zfs-dkms live in Debian's 'contrib' component (ZFS's
|
|
# CDDL license is not DFSG-free), but a minimal Debian install enables only
|
|
# 'main' (plus 'non-free-firmware' when firmware was selected). Without
|
|
# contrib, 'apt-get install zfsutils-linux' fails with "has no installation
|
|
# candidate" and zfs-dkms is never pulled in, so no module can be built and
|
|
# the pool cannot be created.
|
|
#
|
|
# Enable contrib idempotently, touching only the Debian archive entries.
|
|
# Matching a hostname is not enough: a netinst describes the archive through
|
|
# a 'mirror+file://' indirection, and the mirror may be a regional/hoster
|
|
# one — while third-party repos (tailscale, docker, ...) sign with their own
|
|
# keyring and must NOT be given a contrib they lack ('apt-get update' would
|
|
# then 404). So a stanza/line counts as Debian when its Signed-By names the
|
|
# Debian archive keyring, or when it has none (the default trusted keys).
|
|
enable_debian_contrib(){
|
|
local f tmp
|
|
while IFS= read -r f; do
|
|
[[ -z "$f" ]] && continue
|
|
case "$f" in
|
|
*.sources)
|
|
tmp="$f.vpsmgr-tmp"
|
|
if awk '
|
|
function emit(){
|
|
if (n == 0) return
|
|
deb = 1
|
|
for (i = 1; i <= n; i++)
|
|
if (buf[i] ~ /^[ \t]*Signed-By:/ && buf[i] !~ /debian-archive-keyring/) deb = 0
|
|
if (deb)
|
|
for (i = 1; i <= n; i++)
|
|
if (buf[i] ~ /^[ \t]*Components:/ && buf[i] ~ /(^|[ \t])main([ \t]|$)/ && buf[i] !~ /contrib/) buf[i] = buf[i] " contrib"
|
|
for (i = 1; i <= n; i++) print buf[i]
|
|
n = 0
|
|
}
|
|
/^[ \t]*$/ { emit(); print ""; next }
|
|
{ buf[++n] = $0 }
|
|
END { emit() }
|
|
' "$f" > "$tmp"; then
|
|
cat "$tmp" > "$f"; rm -f "$tmp"
|
|
else
|
|
rm -f "$tmp"
|
|
fi
|
|
;;
|
|
*)
|
|
sed -i -E '
|
|
/^deb[[:space:]]/!b
|
|
/signed-by=/ { /signed-by=[^]]*debian-archive-keyring/!b }
|
|
/[[:space:]]main([[:space:]]|$)/ { /contrib/! s/[[:space:]]*$/ contrib/ }
|
|
' "$f"
|
|
;;
|
|
esac
|
|
done < <(ls /etc/apt/sources.list /etc/apt/sources.list.d/*.sources /etc/apt/sources.list.d/*.list 2>/dev/null)
|
|
}
|
|
|
|
if [[ "$STORAGE" == "zfs" ]]; then
|
|
BASE_DEPS="$BASE_DEPS zfsutils-linux"
|
|
if [[ "$ID" == "debian" ]]; then
|
|
KERNEL_HEADERS_PKG=linux-headers-amd64
|
|
case "$KERNEL_REL" in
|
|
*-cloud-*) KERNEL_HEADERS_PKG=linux-headers-cloud-amd64 ;;
|
|
esac
|
|
BASE_DEPS="$BASE_DEPS $KERNEL_HEADERS_PKG build-essential"
|
|
log "Debian detected — ZFS module will be DKMS-compiled (headers: $KERNEL_HEADERS_PKG)"
|
|
if ! grep -rhE '^[^#].*\bcontrib\b' /etc/apt/sources.list /etc/apt/sources.list.d/ 2>/dev/null | grep -q .; then
|
|
log "enabling the Debian contrib component (needed for zfsutils-linux / zfs-dkms)"
|
|
enable_debian_contrib
|
|
apt-get update -qq
|
|
fi
|
|
else
|
|
log "Ubuntu detected — ZFS module is prebuilt in the kernel, no compilation"
|
|
# Ubuntu minimal/cloud images often ship with only the 'main' component; the
|
|
# installer needs packages from universe (ndppd for IPv6 pass-through, among
|
|
# others). Enable it idempotently.
|
|
if ! grep -rhE "^[^#].*universe" /etc/apt/sources.list /etc/apt/sources.list.d/ 2>/dev/null | grep -q .; then
|
|
log "enabling the Ubuntu universe repository (needed for ndppd etc.)"
|
|
apt-get update -qq
|
|
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq software-properties-common >/dev/null 2>&1 || true
|
|
add-apt-repository -y universe >/dev/null 2>&1 || die "could not enable the universe repository"
|
|
apt-get update -qq
|
|
fi
|
|
fi
|
|
elif [[ "$STORAGE" == "btrfs" ]]; then
|
|
# The btrfs driver needs the userspace tools (btrfs / mkfs.btrfs): Incus uses
|
|
# mkfs.btrfs when it creates a loop-backed pool and the btrfs tool to manage
|
|
# subvolumes/snapshots/quotas for the native-subvolume case alike.
|
|
BASE_DEPS="$BASE_DEPS btrfs-progs"
|
|
log "btrfs backend selected — installing btrfs-progs; no ZFS module/kernel headers needed"
|
|
else
|
|
log "dir backend — no ZFS/btrfs tools needed; WARNING: disk quotas, snapshots and clone-on-create are NOT available"
|
|
fi
|
|
for p in $BASE_DEPS; do
|
|
if ! dpkg -s "$p" >/dev/null 2>&1; then
|
|
log "installing $p"
|
|
apt-get update -qq
|
|
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq "$p" || log " warn: could not install $p"
|
|
fi
|
|
done
|
|
|
|
# A headers meta-package may track a newer kernel than the one currently
|
|
# running. Try the exact package once, then fail with an actionable message
|
|
# instead of letting DKMS silently build for another installed kernel.
|
|
if [[ "$STORAGE" == "zfs" && "$ID" == "debian" && ! -e "/lib/modules/$KERNEL_REL/build" ]]; then
|
|
EXACT_HEADERS_PKG="linux-headers-$KERNEL_REL"
|
|
if apt-cache show "$EXACT_HEADERS_PKG" >/dev/null 2>&1; then
|
|
log "matching headers are missing for $KERNEL_REL — installing $EXACT_HEADERS_PKG"
|
|
apt-get update -qq
|
|
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq "$EXACT_HEADERS_PKG" || true
|
|
fi
|
|
fi
|
|
if [[ "$STORAGE" == "zfs" && "$ID" == "debian" && ! -e "/lib/modules/$KERNEL_REL/build" ]]; then
|
|
die "matching kernel headers are missing for $KERNEL_REL — install linux-headers-$KERNEL_REL or reboot into a kernel with installed headers"
|
|
fi
|
|
|
|
# ZFS module present and loadable for the RUNNING kernel? On Debian a kernel
|
|
# upgrade can leave the DKMS module missing (headers were unavailable at
|
|
# postinst time); rebuild it for the running kernel rather than failing later
|
|
# at pool creation. On Ubuntu the module ships with the kernel, so this is a
|
|
# no-op. Skipped entirely in dir mode.
|
|
if [[ "$STORAGE" == "zfs" ]]; then
|
|
if ! modprobe zfs 2>/dev/null; then
|
|
log "ZFS module missing for $KERNEL_REL — rebuilding via dkms (takes a minute)..."
|
|
# A module whose postinst ran before the headers existed is registered
|
|
# without a kernel ("zfs/<ver>: added"); the field carries a trailing
|
|
# colon there, so strip it (dkms build rejects "zfs/<ver>:").
|
|
DKMS_VERSIONS=$(dkms status 2>/dev/null | awk -F'[,/ ]+' '/^zfs\//{v=$2; sub(/:$/,"",v); if(v!="") print v}' | sort -u)
|
|
[[ -n "$DKMS_VERSIONS" ]] || die "no ZFS DKMS version found — check 'dkms status'"
|
|
for v in $DKMS_VERSIONS; do
|
|
if ! DKMS_OUT=$(dkms build "zfs/$v" -k "$KERNEL_REL" 2>&1); then
|
|
printf '%s\n' "$DKMS_OUT" >&2
|
|
die "DKMS failed to build zfs/$v for $KERNEL_REL — check the matching kernel headers"
|
|
fi
|
|
if ! DKMS_OUT=$(dkms install "zfs/$v" -k "$KERNEL_REL" 2>&1); then
|
|
printf '%s\n' "$DKMS_OUT" >&2
|
|
die "DKMS failed to install zfs/$v for $KERNEL_REL"
|
|
fi
|
|
done
|
|
modprobe zfs || die "ZFS kernel module failed to load for $KERNEL_REL — check 'dkms status' and 'dmesg | grep zfs'"
|
|
fi
|
|
log "zfs: $(zpool version 2>/dev/null | head -1)"
|
|
|
|
# Make ZFS boot-safe: the module must be loaded and the pool imported on every
|
|
# boot, or Incus's storage pool is unavailable after a reboot. zfsutils-linux
|
|
# normally enables these in its postinst; ensure them explicitly so a kernel
|
|
# update + manual reboot "just works": module loads, pool imports, datasets
|
|
# mount, then Incus and the panel come up on top.
|
|
for unit in zfs-import-cache.service zfs-mount.service zfs-zed.service zfs.target; do
|
|
systemctl enable "$unit" >/dev/null 2>&1 || log " warn: could not enable $unit"
|
|
done
|
|
log "zfs boot units enabled (import + mount on every boot)"
|
|
fi
|
|
|
|
# --- Go toolchain (only needed for local build; installed lazily by 40-panel.sh) ---
|
|
if command -v go >/dev/null 2>&1; then
|
|
log "go: $(go version 2>/dev/null | awk '{print $3}')"
|
|
fi
|
|
|
|
# --- Incus ---
|
|
if ! command -v incus >/dev/null 2>&1; then
|
|
log "incus not installed yet (installed by 10-incus.sh)"
|
|
fi
|
|
|
|
# --- UFW conflict ---
|
|
# Incus manages its own `table inet incus` nftables rules (DHCP/DNS/forwarding)
|
|
# on incusbr0, 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://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).
|
|
if command -v ufw >/dev/null 2>&1 && ufw status 2>/dev/null | grep -q "Status: active"; then
|
|
# Already handled from a previous install? Never disable again silently —
|
|
# the user may have re-enabled it on purpose.
|
|
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 "incusbr0|$V4_MATCH"; then
|
|
log "ufw active but already has Incus/incusbr0 allow rules — leaving it as-is"
|
|
else
|
|
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.
|
|
if command -v systemctl >/dev/null 2>&1; then
|
|
systemctl disable ufw.service >/dev/null 2>&1 || true
|
|
systemctl stop ufw.service >/dev/null 2>&1 || true
|
|
fi
|
|
fi
|
|
else
|
|
log "ufw: not active (or not installed) — no conflict"
|
|
fi
|
|
|
|
# --- port occupancy ---
|
|
# Ports reserved for vpsmgr must be free on a fresh install. On adoption the
|
|
# panel and the domain proxy are already running and owned by vpsmgr — those
|
|
# listeners
|
|
# are excluded by process name. Checks TCP and UDP (the user port block is
|
|
# DNAT-ed for both).
|
|
#
|
|
# 80/443 are handled specially: if either is already taken, the install does
|
|
# NOT fail — it proceeds with HAProxy installed but DISABLED (net.haproxy
|
|
# false, not started/autostarted), so a host that already serves 80/443 is
|
|
# still usable; the operator can re-enable later via `vps config set
|
|
# net.haproxy true`. The user-port ranges (from net.user_ports, exported by
|
|
# 00-ip-ask.sh as VPSMGR_USER_PORTS; default 10000-29999) stay hard failures.
|
|
#
|
|
# SSH ports (30000-31999) are deliberately NOT checked here: they are allocated
|
|
# per-container at `vps add` time with a "probe the port, reassign on conflict"
|
|
# loop (mgr.allocSSHPort binds the candidate to detect a live listener), so an
|
|
# occupied port no longer blocks the install — the host simply skips it. This
|
|
# keeps the install usable on machines that happen to run something in that
|
|
# range.
|
|
#
|
|
# 00-check runs as a separate bash process, so its env cannot reach the rest of
|
|
# the installer. The decision therefore goes through a root-only marker file
|
|
# that install.sh turns into an env var for 30-haproxy.sh and the panel.
|
|
#
|
|
# The marker keeps its historical name (.install-traefik-off) even though the
|
|
# proxy is HAProxy now: an operator may run an older installer checkout that
|
|
# reads and writes exactly this path, and the new script honors it as well.
|
|
# Clear any stale marker from a previous run so a reinstall re-detects.
|
|
rm -f /etc/vpsmgr/.install-traefik-off 2>/dev/null || true
|
|
if [[ "${VPSMGR_DISABLE_V4FORWARD:-0}" == "1" ]]; then
|
|
log "v4 forwarding disabled by installer — skipping reserved port checks"
|
|
else
|
|
# Only the ports the configured user-port ranges actually reserve must be
|
|
# free — checking the whole 10000-29999 regardless of the ranges would block a
|
|
# small-footprint install that never uses the upper ports. VPSMGR_USER_PORTS is
|
|
# the comma-separated (already whole-hundred aligned) ranges exported by
|
|
# 00-ip-ask.sh; default 10000-29999. SSH ports (30000-31999) are NOT checked
|
|
# here — they are probed per-container at add time (see allocSSHPort).
|
|
USER_PORTS="${VPSMGR_USER_PORTS:-10000-29999}"
|
|
port_in_user_ranges(){
|
|
local p="$1"
|
|
local r lo hi
|
|
while IFS=',' read -ra RS; do
|
|
for r in "${RS[@]}"; do
|
|
r="${r// /}"
|
|
[[ -z "$r" ]] && continue
|
|
lo="${r%%-*}"; hi="${r##*-}"
|
|
if (( p >= lo && p <= hi )); then
|
|
return 0
|
|
fi
|
|
done
|
|
done <<<"$USER_PORTS"
|
|
return 1
|
|
}
|
|
port_hard_reserved(){
|
|
local p="$1"
|
|
port_in_user_ranges "$p"
|
|
return $?
|
|
}
|
|
log "checking reserved ports (80/443, $USER_PORTS)..."
|
|
CONFLICTS=""
|
|
PORT_80_443_TAKEN=0
|
|
PORT_80_443_PROC=""
|
|
while IFS= read -r line; do
|
|
[[ -z "$line" ]] && continue
|
|
ADDR=$(awk '{print $4}' <<<"$line")
|
|
[[ -z "$ADDR" ]] && continue
|
|
PORT="${ADDR##*:}"
|
|
[[ "$PORT" =~ ^[0-9]+$ ]] || continue
|
|
PROC=$(sed -n 's/.*users:(("\([^"]*\)".*/\1/p' <<<"$line")
|
|
case "${PROC##*/}" in
|
|
# A listener we own is never a conflict: on adoption/upgrade the domain
|
|
# proxy is already running. 'haproxy' is the current proxy, 'traefik' the
|
|
# pre-HAProxy one (a host that has not been upgraded yet), 'vpsmgr' the
|
|
# panel itself.
|
|
vpsmgr|traefik|haproxy) continue ;;
|
|
esac
|
|
if [[ "$PORT" == "80" || "$PORT" == "443" ]]; then
|
|
# 80/443 occupied by a non-vpsmgr process: not a hard failure — force the
|
|
# domain proxy OFF instead (still installed, kept stopped).
|
|
PORT_80_443_TAKEN=1
|
|
PORT_80_443_PROC="${PROC:-unknown process}"
|
|
continue
|
|
fi
|
|
if port_hard_reserved "$PORT"; then
|
|
CONFLICTS+=" $PORT (${ADDR}) — bound by ${PROC:-unknown process}"$'\n'
|
|
fi
|
|
done < <(ss -H -tlnp 2>/dev/null; ss -H -ulnp 2>/dev/null)
|
|
if [[ -n "$CONFLICTS" ]]; then
|
|
rm -f /etc/vpsmgr/.install-traefik-off 2>/dev/null || true
|
|
die "ports reserved for vpsmgr are already in use:
|
|
$CONFLICTS
|
|
Free these ports (or remove the programs above) and re-run install."
|
|
fi
|
|
if [[ "$PORT_80_443_TAKEN" == "1" ]]; then
|
|
# Belt-and-suspenders for update/adoption: the scan already skips our proxy's
|
|
# own listeners by process name, but if `ss` could not attribute the listener
|
|
# (PROC empty) while OUR proxy is already active, that active proxy is what
|
|
# holds 80/443 — do not misread it as a third-party conflict and turn our own
|
|
# domain proxy off. Both service names are checked: 'haproxy' now, 'traefik'
|
|
# on a host that has not run the upgraded installer yet.
|
|
if systemctl is-active --quiet haproxy.service 2>/dev/null \
|
|
|| systemctl is-active --quiet traefik.service 2>/dev/null; then
|
|
log "80/443 held by the running vpsmgr domain proxy — keeping it enabled (no force-off)"
|
|
else
|
|
mkdir -p /etc/vpsmgr
|
|
# The marker keeps its historical name: an older installer checkout still
|
|
# writes and reads this exact path, and the new one honors it too.
|
|
touch /etc/vpsmgr/.install-traefik-off
|
|
log "port 80 and/or 443 is in use (${PORT_80_443_PROC}) — will NOT fail;"
|
|
log "the domain proxy will be installed but DISABLED (net.haproxy false, not started/autostarted)."
|
|
log "re-enable later with: vps config set net.haproxy true"
|
|
fi
|
|
else
|
|
log "reserved ports are free"
|
|
fi
|
|
fi
|
|
|
|
# --- detect public ip / ext iface ---
|
|
# ext iface: prefer the IPv4 default route, then the IPv6 default route (covers
|
|
# IPv6-only hosts and policy-routed boxes whose v4 default lives in a non-main
|
|
# table), then the first non-virtual UP interface.
|
|
EXT_IF=$(ip route show default 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="dev"){print $(i+1); exit}}')
|
|
if [[ -z "$EXT_IF" ]]; then
|
|
EXT_IF=$(ip -6 route show default 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="dev"){print $(i+1); exit}}')
|
|
fi
|
|
if [[ -z "$EXT_IF" ]]; then
|
|
EXT_IF=$(ip -o link show up 2>/dev/null | grep -v -E 'lo|incusbr|virbr|docker|veth|warp|wg' | awk -F': ' '{print $2; exit}')
|
|
fi
|
|
PUB_IP=""
|
|
if [[ -n "$EXT_IF" ]]; then
|
|
PUB_IP=$(ip -4 -o addr show dev "$EXT_IF" scope global | awk '{print $4}' | cut -d/ -f1 | head -1)
|
|
fi
|
|
if [[ -z "$PUB_IP" ]]; then
|
|
PUB_IP=$(hostname -I | awk '{print $1}')
|
|
log " warn: no public IPv4 detected on $EXT_IF, using $PUB_IP (private/other) as fallback"
|
|
fi
|
|
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 — Incus image pull will fail unless cached"
|
|
fi
|
|
|
|
echo "[00] checks passed"
|