diff --git a/setup/tools/proxmox-lxc-gen.sh b/setup/tools/proxmox-lxc-gen.sh new file mode 100755 index 0000000..f427d89 --- /dev/null +++ b/setup/tools/proxmox-lxc-gen.sh @@ -0,0 +1,587 @@ +#!/usr/bin/env bash +# proxmox-lxc-gen.sh — TUI wizard for configuring and deploying Proxmox LXC containers. +# +# Walks through a step-by-step dialog interface covering every relevant LXC +# parameter, then writes three files to an output directory: +# +# pct-create-.sh — runnable pct create command (execute on Proxmox host) +# lxc-.conf — ready-to-drop /etc/pve/lxc/ config file +# deploy-guide-.txt — step-by-step deployment notes +# +# Optionally SSHs to the Proxmox host to run pct create directly. +# +# Requires: dialog (pacman -S dialog / apt install dialog / dnf install dialog) + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SHELL_SCRIPTS_DIR="$SCRIPT_DIR/../Setup-shell-4-containers" + +# ─── Helpers ────────────────────────────────────────────────────────────────── +RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m' +BLUE='\033[0;34m'; CYAN='\033[0;36m'; NC='\033[0m' +log() { printf "${GREEN}[+]${NC} %s\n" "$*"; } +warn() { printf "${YELLOW}[!]${NC} %s\n" "$*"; } +error() { printf "${RED}[✗]${NC} %s\n" "$*" >&2; } +info() { printf "${CYAN}[i]${NC} %s\n" "$*"; } +section() { printf "\n${BLUE}━━━ %s ━━━${NC}\n" "$*"; } + +[[ $EUID -eq 0 ]] && { error "Run as a normal user (not root)."; exit 1; } + +# ─── dialog dependency ──────────────────────────────────────────────────────── +if ! command -v dialog &>/dev/null; then + warn "dialog not found — attempting install..." + if command -v pacman &>/dev/null; then sudo pacman -S --noconfirm dialog + elif command -v apt-get &>/dev/null; then sudo apt-get install -y dialog + elif command -v dnf &>/dev/null; then sudo dnf install -y dialog + else error "Install dialog manually, then re-run."; exit 1 + fi +fi + +# ─── dialog theme ───────────────────────────────────────────────────────────── +TMP_D="$(mktemp -d)" +trap 'rm -rf "$TMP_D"' EXIT + +BACKTITLE="Proxmox LXC Generator" +export DIALOGRC="$TMP_D/dialogrc" +cat > "$DIALOGRC" <<'DLRC' +use_shadow = ON +use_colors = ON +screen_color = (BLACK,BLACK,ON) +title_color = (CYAN,BLACK,ON) +border_color = (CYAN,BLACK,ON) +button_active_color = (BLACK,CYAN,ON) +button_inactive_color = (WHITE,BLACK,OFF) +menubox_color = (WHITE,BLACK,OFF) +menubox_border_color = (CYAN,BLACK,ON) +item_color = (WHITE,BLACK,OFF) +item_selected_color = (BLACK,CYAN,ON) +tag_color = (CYAN,BLACK,ON) +tag_selected_color = (BLACK,CYAN,ON) +check_color = (WHITE,BLACK,OFF) +check_selected_color = (BLACK,CYAN,ON) +inputbox_color = (WHITE,BLACK,OFF) +inputbox_border_color = (CYAN,BLACK,ON) +uarrow_color = (CYAN,BLACK,ON) +darrow_color = (CYAN,BLACK,ON) +DLRC + +# Dialog wrapper: redirect stdout↔stderr so dialog output is captured. +D() { dialog --backtitle "$BACKTITLE" "$@" 3>&1 1>&2 2>&3; } +abort() { clear; echo "Aborted."; exit 0; } + +# ─── Screen 1: Identity ─────────────────────────────────────────────────────── +IDENTITY=$(D --title " Container Identity " \ + --form "\nSet the container ID, hostname, and optional metadata." \ + 14 66 4 \ + "Container ID (VMID):" 1 1 "100" 1 26 8 9 \ + "Hostname:" 2 1 "" 2 26 32 253 \ + "Description (optional):" 3 1 "" 3 26 36 512 \ + "Tags (comma-separated):" 4 1 "" 4 26 36 512 \ +) || abort + +VMID=$( awk 'NR==1' <<< "$IDENTITY") +CT_HOSTNAME=$(awk 'NR==2' <<< "$IDENTITY") +CT_DESC=$( awk 'NR==3' <<< "$IDENTITY") +CT_TAGS=$( awk 'NR==4' <<< "$IDENTITY") + +[[ -z "$VMID" || ! "$VMID" =~ ^[0-9]{1,9}$ ]] && { + error "VMID must be a positive integer (got: '${VMID}')."; exit 1; } +[[ -z "$CT_HOSTNAME" ]] && { error "Hostname is required."; exit 1; } + +# ─── Screen 2: OS / Template ────────────────────────────────────────────────── +TMPL_DISTRO=$(D --title " OS / Template " \ + --menu "\nSelect the base distribution.\nThe matching CT template will be located via pveam on Proxmox." \ + 20 66 9 \ + "debian" "Debian 12 Bookworm — stable, recommended default" \ + "ubuntu" "Ubuntu 24.04 LTS — popular, snap support" \ + "alpine" "Alpine Linux — minimal, musl, ~10 MB" \ + "arch" "Arch Linux — rolling release" \ + "fedora" "Fedora — recent packages, rpm" \ + "rocky" "Rocky Linux 9 — RHEL-compatible enterprise" \ + "centos" "CentOS Stream 9 — RHEL upstream" \ + "void" "Void Linux — runit init, musl/glibc" \ + "custom" "Custom template — enter filename manually" \ +) || abort + +case "$TMPL_DISTRO" in + debian) CT_OSTYPE="debian"; TMPL_PATTERN="debian-12" ;; + ubuntu) CT_OSTYPE="ubuntu"; TMPL_PATTERN="ubuntu-24.04" ;; + alpine) CT_OSTYPE="alpine"; TMPL_PATTERN="alpine-3" ;; + arch) CT_OSTYPE="archlinux"; TMPL_PATTERN="archlinux-base" ;; + fedora) CT_OSTYPE="fedora"; TMPL_PATTERN="fedora-" ;; + rocky) CT_OSTYPE="centos"; TMPL_PATTERN="rockylinux-9" ;; + centos) CT_OSTYPE="centos"; TMPL_PATTERN="centos-9-stream" ;; + void) CT_OSTYPE="unmanaged"; TMPL_PATTERN="voidlinux-" ;; + custom) CT_OSTYPE="unmanaged"; TMPL_PATTERN="" ;; +esac + +if [[ "$TMPL_DISTRO" == "custom" ]]; then + CT_TEMPLATE=$(D --title " Custom Template " \ + --inputbox \ +"Enter the exact template filename as shown by pveam available. +Example: debian-12-standard_12.7-1_amd64.tar.zst" \ + 10 72 "") || abort + [[ -z "$CT_TEMPLATE" ]] && { error "Template filename is required."; exit 1; } +else + CT_TEMPLATE="$TMPL_PATTERN" # resolved to the full name on Proxmox at deploy time +fi + +# ─── Screen 3: Resources ────────────────────────────────────────────────────── +RESOURCES=$(D --title " Resources " \ + --form "\nAllocate compute and storage resources for the container." \ + 15 66 5 \ + "Memory (MB):" 1 1 "512" 1 22 10 10 \ + "Swap (MB):" 2 1 "512" 2 22 10 10 \ + "CPU cores:" 3 1 "1" 3 22 5 4 \ + "Disk size (GB):" 4 1 "8" 4 22 10 10 \ + "Rootfs storage:" 5 1 "local-lvm" 5 22 24 64 \ +) || abort + +CT_MEMORY=$(awk 'NR==1' <<< "$RESOURCES") +CT_SWAP=$( awk 'NR==2' <<< "$RESOURCES") +CT_CORES=$( awk 'NR==3' <<< "$RESOURCES") +CT_DISK=$( awk 'NR==4' <<< "$RESOURCES") +CT_STORAGE=$(awk 'NR==5' <<< "$RESOURCES") + +for _f in CT_MEMORY CT_SWAP CT_CORES CT_DISK; do + [[ "${!_f}" =~ ^[0-9]+$ ]] || { error "$_f must be a number (got: '${!_f}')."; exit 1; } +done + +# ─── Screen 4: Network ──────────────────────────────────────────────────────── +CT_NET_MODE=$(D --title " Network — IP Assignment " \ + --radiolist "\nChoose how the container obtains its IP address:" \ + 10 56 2 \ + "dhcp" "DHCP (automatic — recommended for most setups)" ON \ + "static" "Static IP address (you specify CIDR + gateway)" OFF \ +) || abort + +CT_IP="" CT_GW="" CT_DNS="8.8.8.8" CT_SEARCHDOMAIN="" + +if [[ "$CT_NET_MODE" == "static" ]]; then + STATIC=$(D --title " Static Network " \ + --form "\nEnter static network settings for this container." \ + 14 68 4 \ + "IP address (CIDR):" 1 1 "192.168.1.100/24" 1 24 22 18 \ + "Gateway:" 2 1 "192.168.1.1" 2 24 16 15 \ + "DNS server:" 3 1 "8.8.8.8" 3 24 16 15 \ + "Search domain:" 4 1 "" 4 24 36 255 \ + ) || abort + CT_IP=$( awk 'NR==1' <<< "$STATIC") + CT_GW=$( awk 'NR==2' <<< "$STATIC") + CT_DNS=$( awk 'NR==3' <<< "$STATIC") + CT_SEARCHDOMAIN=$(awk 'NR==4' <<< "$STATIC") +fi + +NET_ADV=$(D --title " Network — Interface " \ + --form "\nConfigure the virtual NIC attached to this container." \ + 12 60 3 \ + "Bridge:" 1 1 "vmbr0" 1 18 16 16 \ + "VLAN tag (blank=none):" 2 1 "" 2 18 6 4 \ + "Enable firewall [0/1]:" 3 1 "1" 3 18 2 1 \ +) || abort +CT_BRIDGE=$( awk 'NR==1' <<< "$NET_ADV") +CT_VLAN=$( awk 'NR==2' <<< "$NET_ADV") +CT_FIREWALL=$(awk 'NR==3' <<< "$NET_ADV") +[[ "$CT_FIREWALL" != "1" ]] && CT_FIREWALL=0 + +# ─── Screen 5: Security ─────────────────────────────────────────────────────── +UNPRIV=$(D --title " Privilege Level " \ + --radiolist \ +"\nUnprivileged containers map UIDs to avoid host root exposure (recommended). +Privileged containers share the host UID namespace — needed for some services +(e.g. FreeIPA, NFS server) but should only be used when necessary." \ + 13 72 2 \ + "unprivileged" "Unprivileged — UID remapping, recommended" ON \ + "privileged" "Privileged — full host UIDs, reduced isolation" OFF \ +) || abort +[[ "$UNPRIV" == "unprivileged" ]] && CT_UNPRIVILEGED=1 || CT_UNPRIVILEGED=0 + +FEATURES_RAW=$(D --title " LXC Features " \ + --separate-output \ + --checklist \ +"\nSelect kernel features to enable for this container. +Use Space to toggle, Enter to confirm." \ + 18 72 7 \ + "nesting" "nesting=1 — run Docker or nested LXC inside this container" OFF \ + "fuse" "fuse=1 — allow FUSE mounts (sshfs, rclone, etc.)" OFF \ + "keyctl" "keyctl=1 — Linux keyring API (required by some services)" OFF \ + "mounts" "mounts=fuse — permit FUSE bind-mounts" OFF \ + "apparmor" "Unconfined AppArmor profile (privileged workaround)" OFF \ + "cgroup" "cgroup:rw mounts (needed by systemd inside container)" OFF \ + "seccomp" "seccomp=0 — disable seccomp filtering (last resort only)" OFF \ +) || true # OK if nothing selected + +CT_FEATURES="" CT_APPARMOR=0 CT_CGROUP=0 CT_SECCOMP="" +while IFS= read -r _feat; do + [[ -z "$_feat" ]] && continue + case "$_feat" in + nesting) CT_FEATURES+="nesting=1," ;; + fuse) CT_FEATURES+="fuse=1," ;; + keyctl) CT_FEATURES+="keyctl=1," ;; + mounts) CT_FEATURES+="mounts=fuse," ;; + apparmor) CT_APPARMOR=1 ;; + cgroup) CT_CGROUP=1 ;; + seccomp) CT_SECCOMP="lxc.seccomp.profile:" ;; + esac +done <<< "$FEATURES_RAW" +CT_FEATURES="${CT_FEATURES%,}" + +# ─── Screen 6: Options ──────────────────────────────────────────────────────── +# Root password via passwordbox (hidden input), remaining options via form. +CT_ROOT_PW=$(D --title " Root Password " \ + --insecure \ + --passwordbox \ +"Enter the root password for this container. +Leave blank to disable password auth (SSH key recommended)." \ + 10 62 "") || abort + +OPTIONS=$(D --title " Container Options " \ + --form "\nFinal container settings." \ + 13 68 4 \ + "SSH pubkey file (blank=skip):" 1 1 "" 1 34 30 256 \ + "Start on boot [0/1]:" 2 1 "0" 2 34 2 1 \ + "Start after creation [y/n]:" 3 1 "n" 3 34 2 1 \ + "Console/TTY count:" 4 1 "2" 4 34 3 3 \ +) || abort + +CT_SSHKEY=$( awk 'NR==1' <<< "$OPTIONS") +CT_ONBOOT=$( awk 'NR==2' <<< "$OPTIONS") +CT_AUTOSTART=$(awk 'NR==3' <<< "$OPTIONS") +CT_TTY=$( awk 'NR==4' <<< "$OPTIONS") +[[ "$CT_ONBOOT" != "1" ]] && CT_ONBOOT=0 +[[ "${CT_AUTOSTART,,}" == "y"* ]] && CT_AUTOSTART=true || CT_AUTOSTART=false +CT_SSHKEY="${CT_SSHKEY/#\~/$HOME}" +if [[ -n "$CT_SSHKEY" && ! -f "$CT_SSHKEY" ]]; then + warn "SSH key file not found: $CT_SSHKEY (will be ignored)" + CT_SSHKEY="" +fi + +# ─── Screen 7: Proxmox target ───────────────────────────────────────────────── +PVE_FORM=$(D --title " Proxmox Host " \ + --form \ +"\nLeave 'Proxmox host' blank to generate output files only (no deployment). +SSH target format: root@192.168.1.10 or root@pve.example.com" \ + 15 72 4 \ + "Proxmox host (blank=local only):" 1 1 "" 1 36 30 255 \ + "Template storage (pveam target):" 2 1 "local" 2 36 20 64 \ + "Output directory:" 3 1 "$HOME/proxmox-lxc" 3 36 30 255 \ + "Proxmox node name:" 4 1 "pve" 4 36 20 64 \ +) || abort + +PVE_HOST=$( awk 'NR==1' <<< "$PVE_FORM") +TMPL_STORAGE=$(awk 'NR==2' <<< "$PVE_FORM") +OUTPUT_DIR=$( awk 'NR==3' <<< "$PVE_FORM") +PVE_NODE=$( awk 'NR==4' <<< "$PVE_FORM") +OUTPUT_DIR="${OUTPUT_DIR/#\~/$HOME}" +[[ -z "$TMPL_STORAGE" ]] && TMPL_STORAGE="local" +[[ -z "$PVE_NODE" ]] && PVE_NODE="pve" + +# ─── Assemble net0 string ───────────────────────────────────────────────────── +_NET="name=eth0,bridge=${CT_BRIDGE}" +[[ -n "$CT_VLAN" && "$CT_VLAN" =~ ^[0-9]+$ ]] && _NET+=",tag=${CT_VLAN}" +if [[ "$CT_NET_MODE" == "dhcp" ]]; then + _NET+=",ip=dhcp,ip6=auto" +else + _NET+=",ip=${CT_IP}" + [[ -n "$CT_GW" ]] && _NET+=",gw=${CT_GW}" +fi +[[ "$CT_FIREWALL" == "1" ]] && _NET+=",firewall=1" + +# ─── Confirm summary ────────────────────────────────────────────────────────── +SUMMARY=" VMID : $VMID + Hostname : $CT_HOSTNAME + Description : ${CT_DESC:-(none)} + Tags : ${CT_TAGS:-(none)} + + OS / template : $TMPL_DISTRO ($CT_TEMPLATE) + OS type : $CT_OSTYPE + + Memory : ${CT_MEMORY} MB + Swap : ${CT_SWAP} MB + CPU cores : $CT_CORES + Disk : ${CT_DISK} GB on $CT_STORAGE + + Network : $_NET + DNS : ${CT_DNS}${CT_SEARCHDOMAIN:+ / $CT_SEARCHDOMAIN} + + Unprivileged : $([[ "$CT_UNPRIVILEGED" == 1 ]] && echo yes || echo 'NO — privileged') + Features : ${CT_FEATURES:-(none)} + AppArmor : $([[ "$CT_APPARMOR" == 1 ]] && echo unconfined || echo default) + cgroup rw : $([[ "$CT_CGROUP" == 1 ]] && echo yes || echo no) + + Root PW set : $([[ -n "$CT_ROOT_PW" ]] && echo yes || echo no) + SSH pubkey : ${CT_SSHKEY:-(none)} + Start on boot : $CT_ONBOOT + Auto-start : $CT_AUTOSTART + TTY count : $CT_TTY + + Output dir : $OUTPUT_DIR + Proxmox host : ${PVE_HOST:-(local — files only)}" + +D --title " Configuration Summary " \ + --ok-label "Generate" \ + --cancel-label "Abort" \ + --scrolltext \ + --msgbox "$SUMMARY" 38 68 || abort + +clear + +# ─── Generate output files ──────────────────────────────────────────────────── +mkdir -p "$OUTPUT_DIR" +section "Generating output files → $OUTPUT_DIR" + +# ── pct-create script ───────────────────────────────────────────────────────── +PCT_SCRIPT="$OUTPUT_DIR/pct-create-${VMID}.sh" +{ + printf '#!/usr/bin/env bash\n' + printf '# pct-create-%s.sh — generated by proxmox-lxc-gen.sh %s\n' "$VMID" "$(date -Iseconds)" + printf '# Run on the Proxmox host as root.\n\n' + printf 'set -euo pipefail\n\n' + + if [[ "$TMPL_DISTRO" != "custom" ]]; then + printf '# ── Resolve template ──────────────────────────────────────────────────────────\n' + printf 'TEMPLATE=$(pveam available --section system 2>/dev/null \\\n' + printf ' | awk '"'"'/%s/ {print $2; exit}'"'"')\n' "$CT_TEMPLATE" + printf '[[ -z "$TEMPLATE" ]] && TEMPLATE=$(pveam available --section turnkeylinux 2>/dev/null \\\n' + printf ' | awk '"'"'/%s/ {print $2; exit}'"'"') || true\n' "$CT_TEMPLATE" + printf 'if [[ -z "$TEMPLATE" ]]; then\n' + printf ' echo "ERROR: no template matching '"'"'%s'"'"' found." >&2\n' "$CT_TEMPLATE" + printf ' echo "Run: pveam update && pveam available --section system" >&2\n' + printf ' exit 1\n' + printf 'fi\n' + printf 'echo "Resolved template: $TEMPLATE"\n\n' + printf '# Download template if not already cached\n' + printf 'pveam download %s "$TEMPLATE" 2>/dev/null \\\n' "$TMPL_STORAGE" + printf ' && echo "Template downloaded." \\\n' + printf ' || echo "Template already present (or download failed — continuing)"\n\n' + else + printf 'TEMPLATE="%s"\n\n' "$CT_TEMPLATE" + fi + + printf '# ── Create the container ──────────────────────────────────────────────────────\n' + printf 'pct create %s \\\n' "$VMID" + printf ' %s:vztmpl/"$TEMPLATE" \\\n' "$TMPL_STORAGE" + printf ' --hostname "%s" \\\n' "$CT_HOSTNAME" + printf ' --ostype "%s" \\\n' "$CT_OSTYPE" + printf ' --memory %s \\\n' "$CT_MEMORY" + printf ' --swap %s \\\n' "$CT_SWAP" + printf ' --cores %s \\\n' "$CT_CORES" + printf ' --rootfs "%s:%s" \\\n' "$CT_STORAGE" "$CT_DISK" + printf ' --net0 "%s" \\\n' "$_NET" + [[ -n "$CT_DNS" ]] && printf ' --nameserver "%s" \\\n' "$CT_DNS" + [[ -n "$CT_SEARCHDOMAIN" ]] && printf ' --searchdomain "%s" \\\n' "$CT_SEARCHDOMAIN" + printf ' --unprivileged %s \\\n' "$CT_UNPRIVILEGED" + [[ -n "$CT_FEATURES" ]] && printf ' --features "%s" \\\n' "$CT_FEATURES" + printf ' --onboot %s \\\n' "$CT_ONBOOT" + printf ' --tty %s \\\n' "$CT_TTY" + [[ -n "$CT_ROOT_PW" ]] && printf ' --password "%s" \\\n' "$CT_ROOT_PW" + [[ -n "$CT_SSHKEY" ]] && printf ' --ssh-public-keys "%s" \\\n' "$CT_SSHKEY" + [[ -n "$CT_DESC" ]] && printf ' --description "%s" \\\n' "${CT_DESC//\"/\\\"}" + [[ -n "$CT_TAGS" ]] && printf ' --tags "%s" \\\n' "$CT_TAGS" + printf ' --start 0\n\n' + + if [[ "$CT_APPARMOR" == 1 || "$CT_CGROUP" == 1 || -n "$CT_SECCOMP" ]]; then + printf '# ── Apply extra lxc.* options ────────────────────────────────────────────────\n' + printf 'CONF="/etc/pve/lxc/%s.conf"\n' "$VMID" + [[ "$CT_APPARMOR" == 1 ]] && printf 'printf '"'"'lxc.apparmor.profile: unconfined\nlxc.cap.drop:\n'"'"' >> "$CONF"\n' + [[ "$CT_CGROUP" == 1 ]] && printf 'printf '"'"'lxc.mount.auto: proc:rw sys:rw cgroup:rw\nlxc.cgroup2.devices.allow: a\n'"'"' >> "$CONF"\n' + [[ -n "$CT_SECCOMP" ]] && printf 'printf '"'"'lxc.seccomp.profile:\n'"'"' >> "$CONF"\n' + printf '\n' + fi + + if [[ "$CT_AUTOSTART" == true ]]; then + printf '# ── Start the container ──────────────────────────────────────────────────────\n' + printf 'pct start %s\n' "$VMID" + printf 'echo "Container %s is running."\n' "$VMID" + printf 'echo "Access: pct enter %s or ssh root@"\n\n' "$VMID" + fi + + printf 'echo "Done. Container %s created."\n' "$VMID" +} > "$PCT_SCRIPT" +chmod 755 "$PCT_SCRIPT" +log "pct create script: $(basename "$PCT_SCRIPT")" + +# ── LXC conf file ───────────────────────────────────────────────────────────── +LXC_CONF="$OUTPUT_DIR/lxc-${VMID}.conf" +{ + printf '# Generated by proxmox-lxc-gen.sh — %s\n' "$(date -Iseconds)" + printf '# Install: cp %s /etc/pve/lxc/%s.conf\n\n' "$(basename "$LXC_CONF")" "$VMID" + printf 'arch: amd64\n' + printf 'cores: %s\n' "$CT_CORES" + printf 'hostname: %s\n' "$CT_HOSTNAME" + printf 'memory: %s\n' "$CT_MEMORY" + printf 'swap: %s\n' "$CT_SWAP" + printf 'net0: %s\n' "$_NET" + printf 'ostype: %s\n' "$CT_OSTYPE" + printf 'rootfs: %s:vm-%s-disk-0,size=%sG\n' "$CT_STORAGE" "$VMID" "$CT_DISK" + printf 'unprivileged: %s\n' "$CT_UNPRIVILEGED" + printf 'onboot: %s\n' "$CT_ONBOOT" + printf 'tty: %s\n' "$CT_TTY" + [[ -n "$CT_FEATURES" ]] && printf 'features: %s\n' "$CT_FEATURES" + [[ -n "$CT_DNS" ]] && printf 'nameserver: %s\n' "$CT_DNS" + [[ -n "$CT_SEARCHDOMAIN" ]] && printf 'searchdomain: %s\n' "$CT_SEARCHDOMAIN" + [[ -n "$CT_TAGS" ]] && printf 'tags: %s\n' "$CT_TAGS" + [[ -n "$CT_DESC" ]] && printf 'description: %s\n' "$CT_DESC" + if [[ "$CT_APPARMOR" == 1 ]]; then + printf '\n# AppArmor override (applied post-create)\n' + printf 'lxc.apparmor.profile: unconfined\n' + printf 'lxc.cap.drop:\n' + fi + if [[ "$CT_CGROUP" == 1 ]]; then + printf '\n# cgroup access override (required by systemd in containers)\n' + printf 'lxc.mount.auto: proc:rw sys:rw cgroup:rw\n' + printf 'lxc.cgroup2.devices.allow: a\n' + fi + [[ -n "$CT_SECCOMP" ]] && printf '\nlxc.seccomp.profile:\n' +} > "$LXC_CONF" +log "LXC conf: $(basename "$LXC_CONF")" + +# ── Deploy guide ────────────────────────────────────────────────────────────── +GUIDE="$OUTPUT_DIR/deploy-guide-${VMID}.txt" +{ +printf 'Proxmox LXC deploy guide — generated %s +══════════════════════════════════════════════════════════════════════════ + + Container : %s (VMID %s) + OS : %s (%s) + Resources : %s core(s), %s MB RAM, %s GB disk on %s + Network : %s + Privilege : %s + +━━━ Step 1 — Upload files to Proxmox ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + scp %s/pct-create-%s.sh root@:/tmp/ +' \ + "$(date -Iseconds)" \ + "$CT_HOSTNAME" "$VMID" \ + "$TMPL_DISTRO" "$CT_OSTYPE" \ + "$CT_CORES" "$CT_MEMORY" "$CT_DISK" "$CT_STORAGE" \ + "$_NET" \ + "$([[ "$CT_UNPRIVILEGED" == 1 ]] && echo unprivileged || echo 'PRIVILEGED')" \ + "$OUTPUT_DIR" "$VMID" + +printf '━━━ Step 2 — Update template catalogue and create container ━━━━━━━━━━━━━ + + ssh root@ + pveam update + bash /tmp/pct-create-%s.sh + + The script resolves the latest matching template for "%s", + downloads it if not cached, and calls pct create with all + parameters from this wizard. + +━━━ Step 3 — Verify ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + pct list + pct status %s + pct config %s + +━━━ Step 4 — Access the container ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + # Console (always available): + pct start %s + pct enter %s + +' \ + "$VMID" "$CT_TEMPLATE" \ + "$VMID" "$VMID" \ + "$VMID" "$VMID" + +if [[ "$CT_NET_MODE" == "static" ]]; then + printf ' # SSH (static IP configured):\n ssh root@%s\n\n' "${CT_IP%%/*}" +else + printf ' # SSH (find DHCP IP via):\n' + printf ' pct exec %s -- ip -4 addr show eth0\n\n' "$VMID" +fi + +printf '━━━ Step 5 — Post-install shell setup (optional) ━━━━━━━━━━━━━━━━━━━━━━ + + # Run inside the container to set up the shell environment: +' +case "$TMPL_DISTRO" in + debian|ubuntu) + printf ' pct exec %s -- bash /path/to/Setup-shell-4-containers/debian.sh\n' "$VMID" ;; + arch) + printf ' pct exec %s -- bash /path/to/Setup-shell-4-containers/arch.sh\n' "$VMID" ;; + alpine) + printf ' pct exec %s -- ash /path/to/Setup-shell-4-containers/alpine.sh\n' "$VMID" ;; + fedora|rocky|centos) + printf ' pct exec %s -- bash /path/to/Setup-shell-4-containers/fedora.sh\n' "$VMID" ;; + void) + printf ' pct exec %s -- bash /path/to/Setup-shell-4-containers/void.sh\n' "$VMID" ;; + *) + printf ' # See setup/Setup-shell-4-containers/ for distro-specific scripts\n' ;; +esac + +printf '\n━━━ Notes ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n' + +[[ "$CT_UNPRIVILEGED" == 0 ]] && printf \ +' PRIVILEGED container — reduced isolation. If services fail with + permission errors, verify these are in /etc/pve/lxc/%s.conf: + lxc.apparmor.profile: unconfined + lxc.cap.drop:\n\n' "$VMID" + +[[ "$CT_APPARMOR" == 1 ]] && printf \ +' AppArmor profile: unconfined — container has reduced AppArmor isolation. + Only use for trusted workloads (FreeIPA, Docker-in-LXC, etc.).\n\n' + +[[ "$CT_CGROUP" == 1 ]] && printf \ +' cgroup:rw enabled — container has full cgroup access. + Required by systemd services that manage their own cgroups.\n\n' + +[[ -n "$CT_FEATURES" ]] && printf ' LXC features: %s\n\n' "$CT_FEATURES" + +printf \ +' Useful pct commands: + pct set %s --memory 2048 update a setting live + pct snapshot %s before-update take a snapshot + pct rollback %s before-update revert to snapshot + pct destroy %s delete the container (irreversible) + pvesh get /nodes/%s/lxc/%s/status/current + + Generated config: %s + Proxmox path: /etc/pve/lxc/%s.conf +' \ + "$VMID" "$VMID" "$VMID" "$VMID" \ + "$PVE_NODE" "$VMID" \ + "$(basename "$LXC_CONF")" "$VMID" +} > "$GUIDE" +log "Deploy guide: $(basename "$GUIDE")" + +# ─── Optional: SSH deploy ───────────────────────────────────────────────────── +if [[ -n "$PVE_HOST" ]]; then + section "Deploying to $PVE_HOST" + + info "Uploading pct-create-${VMID}.sh..." + scp "$PCT_SCRIPT" "${PVE_HOST}:/tmp/pct-create-${VMID}.sh" + + if [[ -n "$CT_SSHKEY" ]]; then + info "Uploading SSH public key..." + scp "$CT_SSHKEY" "${PVE_HOST}:/tmp/lxc-sshkey-${VMID}.pub" + ssh "$PVE_HOST" \ + "sed -i 's|${CT_SSHKEY}|/tmp/lxc-sshkey-${VMID}.pub|' /tmp/pct-create-${VMID}.sh" + fi + + info "Running pct create on $PVE_HOST (this may take a moment)..." + if ssh "$PVE_HOST" "bash /tmp/pct-create-${VMID}.sh"; then + log "Container $VMID created on $PVE_HOST" + else + warn "pct create reported an error — check the output above" + fi + ssh "$PVE_HOST" "rm -f /tmp/pct-create-${VMID}.sh /tmp/lxc-sshkey-${VMID}.pub" 2>/dev/null || true +fi + +# ─── Final summary ──────────────────────────────────────────────────────────── +section "Done" +printf '\n' +info "Output: $OUTPUT_DIR/" +printf ' %-36s pct create command\n' "pct-create-${VMID}.sh" +printf ' %-36s LXC config (→ /etc/pve/lxc/)\n' "lxc-${VMID}.conf" +printf ' %-36s step-by-step notes\n' "deploy-guide-${VMID}.txt" +printf '\n' +if [[ -z "$PVE_HOST" ]]; then + info "To deploy, copy to your Proxmox host and run:" + printf ' scp %s/pct-create-%s.sh root@:/tmp/\n' "$OUTPUT_DIR" "$VMID" + printf ' ssh root@ "bash /tmp/pct-create-%s.sh"\n' "$VMID" +fi