diff --git a/lib/build.func b/lib/build.func new file mode 100644 index 0000000..9c3c9f8 --- /dev/null +++ b/lib/build.func @@ -0,0 +1,211 @@ +#!/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. + +# ── 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}" + +# ── 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. +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 "${HOSTNAME:-}" ]]; then + read -rp "Hostname [$DEFAULT_HOSTNAME]: " HOSTNAME + HOSTNAME="${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 "${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 +} + +# ── 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" + if [[ "$IPCFG" == "dhcp" ]]; then + net_opts+=",ip=dhcp" + else + net_opts+=",ip=$IPCFG,gw=$GATEWAY" + fi + + msg_info "Creating unprivileged LXC $CTID ($HOSTNAME)..." + pct create "$CTID" "$TEMPLATE_STORAGE:vztmpl/$TEMPLATE" \ + --hostname "$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" + msg_ok "LXC $CTID created" + + msg_info "Starting LXC..." + pct start "$CTID" + sleep 5 + + msg_info "Waiting for network..." + local i + for i in {1..30}; do + if pct exec "$CTID" -- getent hosts deb.debian.org >/dev/null 2>&1; then + break + fi + sleep 2 + done + msg_ok "Network up" +} + +# ── 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 — $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 +}