feat(tools): add proxmox-lxc-gen.sh — TUI wizard for LXC containers
Interactive dialog-based wizard that walks through every relevant
Proxmox LXC parameter and emits three files:
pct-create-<vmid>.sh — runnable pct create command
lxc-<vmid>.conf — drop-in /etc/pve/lxc/ config
deploy-guide-<vmid>.txt — step-by-step deployment notes
TUI screens (dialog, cyan theme):
1. Identity — VMID, hostname, description, tags
2. OS/template — distro menu (debian/ubuntu/alpine/arch/fedora/
rocky/centos/void/custom); pveam pattern resolved
at deploy time so the latest version is always used
3. Resources — memory, swap, cores, disk size, rootfs storage pool
4. Network — DHCP or static (IP/CIDR + gateway + DNS + search
domain); bridge, VLAN tag, firewall flag
5. Security — unprivileged/privileged; feature checklist
(nesting, fuse, keyctl, mounts, apparmor unconfined,
cgroup:rw, seccomp disable)
6. Options — root password (passwordbox, hidden); SSH pubkey
file; start-on-boot; auto-start; TTY count
7. Proxmox host — optional SSH target for direct provisioning;
template storage; output directory; node name
8. Summary — scrollable review before generating
If a Proxmox host is provided, the script SCPs the create script and
runs it over SSH. Matches DIALOGRC theme style of freeipa-image.sh
(cyan accent instead of magenta to visually distinguish).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
feat/astal-menu
parent
0dd7ce3b51
commit
b4ae7ae4ce
|
|
@ -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-<vmid>.sh — runnable pct create command (execute on Proxmox host)
|
||||||
|
# lxc-<vmid>.conf — ready-to-drop /etc/pve/lxc/ config file
|
||||||
|
# deploy-guide-<vmid>.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@<container-ip>"\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@<proxmox>:/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@<proxmox>
|
||||||
|
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@<proxmox>:/tmp/\n' "$OUTPUT_DIR" "$VMID"
|
||||||
|
printf ' ssh root@<proxmox> "bash /tmp/pct-create-%s.sh"\n' "$VMID"
|
||||||
|
fi
|
||||||
Loading…
Reference in New Issue