#!/usr/bin/env bash # lib/build.func — shared host-side helpers for ct/*.sh # # Sourced via: # source <(curl -fsSL https://gitea.luki-net.org/luki-net/proxmox-scripts/raw/branch/main/lib/build.func) # # Caller (ct/.sh) is expected to set: # APP short app name (used for filenames, banner) # APP_DESCRIPTION one-line description (shown in banner) # INSTALL_SCRIPT_URL URL of the matching install/-install.sh # and may override the DEFAULT_* values below. # # Caller may define print_app_summary() to customize the trailing summary. # # NB: We use CT_HOSTNAME (not HOSTNAME) because HOSTNAME is a bash built-in # that always holds the current host's name, which would defeat any prompt. # ── colors / logging ───────────────────────────────────────────────────────── if [[ -z "${_COLORS_LOADED:-}" ]]; then RED="\033[0;31m"; GREEN="\033[0;32m"; YELLOW="\033[1;33m"; BLUE="\033[0;34m"; NC="\033[0m" _COLORS_LOADED=1 fi msg_info() { echo -e "${BLUE}[i]${NC} $*"; } msg_ok() { echo -e "${GREEN}[✓]${NC} $*"; } msg_warn() { echo -e "${YELLOW}[!]${NC} $*"; } msg_err() { echo -e "${RED}[✗]${NC} $*" >&2; } # ── defaults (per-script overridable before sourcing or via env) ───────────── DEFAULT_HOSTNAME="${DEFAULT_HOSTNAME:-lxc}" DEFAULT_DISK="${DEFAULT_DISK:-8}" DEFAULT_CORES="${DEFAULT_CORES:-2}" DEFAULT_RAM="${DEFAULT_RAM:-1024}" DEFAULT_BRIDGE="${DEFAULT_BRIDGE:-vmbr0}" DEFAULT_TEMPLATE_STORAGE="${DEFAULT_TEMPLATE_STORAGE:-local}" DEFAULT_ROOTFS_STORAGE="${DEFAULT_ROOTFS_STORAGE:-local-lvm}" DEFAULT_TEMPLATE_PATTERN="${DEFAULT_TEMPLATE_PATTERN:-debian-12-standard}" DEFAULT_NAMESERVER="${DEFAULT_NAMESERVER:-}" # ── error trap ─────────────────────────────────────────────────────────────── _on_error() { local exit_code=$? msg_err "Installation failed (exit $exit_code)" if [[ -n "${CTID:-}" ]] && pct status "$CTID" >/dev/null 2>&1; then msg_warn "LXC $CTID was created. To clean up: pct stop $CTID && pct destroy $CTID" fi exit "$exit_code" } # ── checks ─────────────────────────────────────────────────────────────────── preflight_pve() { [[ "$EUID" -eq 0 ]] || { msg_err "Must run as root"; exit 1; } command -v pveversion >/dev/null 2>&1 || { msg_err "Not a Proxmox VE host (pveversion not found)"; exit 1; } } show_header() { local app="$1" desc="${2:-}" clear echo "═══════════════════════════════════════════════════════════════" echo " ${app} · Proxmox LXC installer" [[ -n "$desc" ]] && echo " ${desc}" echo " luki-net/proxmox-scripts" echo "═══════════════════════════════════════════════════════════════" echo } # ── prompts ────────────────────────────────────────────────────────────────── # Each prompt is skipped if the corresponding variable is already set in env. # VLAN_TAG and NAMESERVER use ${VAR+x} so that explicitly setting them to "" # via env skips the prompt (= "no VLAN" / "inherit DNS from host"). prompt_lxc_config() { if [[ -z "${CTID:-}" ]]; then read -rp "Container ID [auto]: " CTID [[ -z "${CTID:-}" ]] && CTID=$(pvesh get /cluster/nextid) fi echo " → CTID: $CTID" if [[ -z "${CT_HOSTNAME:-}" ]]; then read -rp "Hostname [$DEFAULT_HOSTNAME]: " CT_HOSTNAME CT_HOSTNAME="${CT_HOSTNAME:-$DEFAULT_HOSTNAME}" fi if [[ -z "${DISK_SIZE:-}" ]]; then read -rp "Disk size in GB [$DEFAULT_DISK]: " DISK_SIZE DISK_SIZE="${DISK_SIZE:-$DEFAULT_DISK}" fi if [[ -z "${CORES:-}" ]]; then read -rp "vCPU cores [$DEFAULT_CORES]: " CORES CORES="${CORES:-$DEFAULT_CORES}" fi if [[ -z "${RAM:-}" ]]; then read -rp "RAM in MB [$DEFAULT_RAM]: " RAM RAM="${RAM:-$DEFAULT_RAM}" fi if [[ -z "${BRIDGE:-}" ]]; then read -rp "Bridge [$DEFAULT_BRIDGE]: " BRIDGE BRIDGE="${BRIDGE:-$DEFAULT_BRIDGE}" fi if [[ -z "${VLAN_TAG+x}" ]]; then read -rp "VLAN tag (empty for none): " VLAN_TAG fi if [[ -z "${TEMPLATE_STORAGE:-}" ]]; then read -rp "Template storage [$DEFAULT_TEMPLATE_STORAGE]: " TEMPLATE_STORAGE TEMPLATE_STORAGE="${TEMPLATE_STORAGE:-$DEFAULT_TEMPLATE_STORAGE}" fi if [[ -z "${ROOTFS_STORAGE:-}" ]]; then read -rp "Rootfs storage [$DEFAULT_ROOTFS_STORAGE]: " ROOTFS_STORAGE ROOTFS_STORAGE="${ROOTFS_STORAGE:-$DEFAULT_ROOTFS_STORAGE}" fi if [[ -z "${IPCFG:-}" ]]; then read -rp "Network: IP/CIDR or 'dhcp' [dhcp]: " IPCFG IPCFG="${IPCFG:-dhcp}" fi GATEWAY="${GATEWAY:-}" if [[ "$IPCFG" != "dhcp" && -z "$GATEWAY" ]]; then read -rp "Gateway: " GATEWAY fi # DNS: only meaningful for static IPs (DHCP gets DNS from the lease). # Default suggestion is the gateway, which doubles as DNS in most homelabs. if [[ "$IPCFG" != "dhcp" && -z "${NAMESERVER+x}" ]]; then local default_ns="${DEFAULT_NAMESERVER:-$GATEWAY}" read -rp "DNS server [$default_ns] (empty = inherit from PVE host): " NAMESERVER NAMESERVER="${NAMESERVER:-$default_ns}" fi } # ── template ───────────────────────────────────────────────────────────────── resolve_debian_template() { msg_info "Resolving template ($DEFAULT_TEMPLATE_PATTERN)..." pveam update >/dev/null 2>&1 || true TEMPLATE=$(pveam available --section system \ | awk -v t="$DEFAULT_TEMPLATE_PATTERN" '$2 ~ t {print $2}' \ | sort -V | tail -n1) [[ -n "$TEMPLATE" ]] || { msg_err "No template matching '$DEFAULT_TEMPLATE_PATTERN' found"; exit 1; } if ! pveam list "$TEMPLATE_STORAGE" 2>/dev/null | grep -q "$TEMPLATE"; then msg_info "Downloading template $TEMPLATE to $TEMPLATE_STORAGE..." pveam download "$TEMPLATE_STORAGE" "$TEMPLATE" fi msg_ok "Template: $TEMPLATE" } # ── LXC ────────────────────────────────────────────────────────────────────── create_lxc() { ROOT_PASSWORD=$(openssl rand -base64 18) local net_opts="name=eth0,bridge=$BRIDGE" [[ -n "${VLAN_TAG:-}" ]] && net_opts+=",tag=$VLAN_TAG" if [[ "$IPCFG" == "dhcp" ]]; then net_opts+=",ip=dhcp" else net_opts+=",ip=$IPCFG,gw=$GATEWAY" fi # Assemble pct args as an array so conditional flags stay clean. local pct_args=( "$CTID" "$TEMPLATE_STORAGE:vztmpl/$TEMPLATE" --hostname "$CT_HOSTNAME" --cores "$CORES" --memory "$RAM" --swap 512 --rootfs "$ROOTFS_STORAGE:$DISK_SIZE" --net0 "$net_opts" --features nesting=1 --unprivileged 1 --onboot 1 --ostype debian --password "$ROOT_PASSWORD" ) [[ -n "${NAMESERVER:-}" ]] && pct_args+=(--nameserver "$NAMESERVER") msg_info "Creating unprivileged LXC $CTID ($CT_HOSTNAME)..." msg_info " net0: $net_opts" [[ -n "${NAMESERVER:-}" ]] && msg_info " dns: $NAMESERVER" pct create "${pct_args[@]}" msg_ok "LXC $CTID created" msg_info "Starting LXC..." pct start "$CTID" sleep 5 msg_info "Waiting for network + DNS..." local i ping_ok=0 dns_ok=0 # We test L3 (gateway/internet) and DNS separately so we can give a useful # error message instead of a generic "network never came up". for i in {1..30}; do if [[ $ping_ok -eq 0 ]] && pct exec "$CTID" -- bash -c "ping -c 1 -W 2 1.1.1.1 >/dev/null 2>&1 || ping -c 1 -W 2 ${GATEWAY:-1.1.1.1} >/dev/null 2>&1"; then ping_ok=1 fi if pct exec "$CTID" -- getent hosts deb.debian.org >/dev/null 2>&1; then dns_ok=1 break fi sleep 2 done if [[ $dns_ok -eq 1 ]]; then msg_ok "Network + DNS up" return 0 fi if [[ $ping_ok -eq 1 ]]; then msg_err "L3 connectivity OK but DNS resolution failed." msg_err "Container can reach the internet but can't resolve names." msg_err "Re-run with NAMESERVER= set, or fix /etc/resolv.conf inside the LXC." else msg_err "Network never came up. Check bridge/VLAN/gateway settings:" msg_err " pct exec $CTID -- ip a" msg_err " pct exec $CTID -- ip r" msg_err " pct exec $CTID -- ping -c2 ${GATEWAY:-}" fi exit 1 } # ── bootstrap installer inside the container ───────────────────────────────── bootstrap_install_script() { local url="$1" msg_info "Installing curl in container..." pct exec "$CTID" -- bash -c "apt-get update -qq && apt-get install -y -qq curl ca-certificates >/dev/null" msg_info "Running installer ($url)..." pct exec "$CTID" -- bash -c "curl -fsSL '$url' -o /root/${APP}-install.sh && bash /root/${APP}-install.sh" } # ── summary ────────────────────────────────────────────────────────────────── get_container_ip() { pct exec "$CTID" -- bash -c "hostname -I | awk '{print \$1}'" | tr -d '\r\n' } # Override in ct/.sh for app-specific output. print_app_summary() { echo " (no app-specific summary defined)" } print_summary() { IP_CT=$(get_container_ip) echo msg_ok "Installation complete!" echo echo "─────────────────────────────────────────────────────────────" echo " $APP LXC #$CTID — $CT_HOSTNAME" echo "─────────────────────────────────────────────────────────────" print_app_summary echo echo " LXC root password: $ROOT_PASSWORD" echo "─────────────────────────────────────────────────────────────" echo } # ── orchestrator ───────────────────────────────────────────────────────────── run_installer() { trap _on_error ERR preflight_pve show_header "$APP" "${APP_DESCRIPTION:-}" prompt_lxc_config resolve_debian_template create_lxc bootstrap_install_script "$INSTALL_SCRIPT_URL" print_summary }