Files
TA-ProxMenu/inc/deploy-iso-nfs-lxc.sh
T
David Schroeder 6bed7068c7 update
2026-07-25 18:45:18 -05:00

264 lines
11 KiB
Bash

#!/usr/bin/env bash
# Deploy a dedicated LXC NFS server and register it as cluster ISO storage.
TAPM_ISO_NFS_VALID_ID() {
[[ "${1:-}" =~ ^[A-Za-z][A-Za-z0-9_-]{0,31}$ ]]
}
TAPM_ISO_NFS_VALID_CTID() {
[[ "${1:-}" =~ ^[1-9][0-9]{2,8}$ ]]
}
TAPM_ISO_NFS_VALID_IPV4_CIDR() {
local value="${1:-}"
local address prefix octet
local -a octets
[[ "$value" == */* ]] || return 1
address="${value%/*}"
prefix="${value#*/}"
[[ "$prefix" =~ ^[0-9]+$ ]] && (( prefix <= 32 )) || return 1
IFS=. read -r -a octets <<<"$address"
(( ${#octets[@]} == 4 )) || return 1
for octet in "${octets[@]}"; do
[[ "$octet" =~ ^[0-9]+$ ]] && (( 10#$octet <= 255 )) || return 1
done
}
TAPM_ISO_NFS_PROMPT() {
local variable="$1"
local label="$2"
local default_value="${3:-}"
local value
if [[ -n "$default_value" ]]; then
read -r -p " ${label} [${default_value}]: " value
printf -v "$variable" '%s' "${value:-$default_value}"
else
read -r -p " ${label}: " value
printf -v "$variable" '%s' "$value"
fi
}
TAPM_ISO_NFS_FAIL() {
echo -e "\n${idsCL[LightRed]}$1${idsCL[Default]}"
return 1
}
TAPM_ISO_NFS_SELECT_STORAGE() {
local variable="$1"
local label="$2"
local default_value="$3"
local value
echo
echo " Active storages that support LXC volumes:"
pvesm status --content rootdir 2>/dev/null |
awk 'NR == 1 || $3 == "active" { printf " %s\n", $1 }'
TAPM_ISO_NFS_PROMPT value "$label" "$default_value"
if ! pvesm status --content rootdir 2>/dev/null |
awk 'NR > 1 && $3 == "active" { print $1 }' |
grep -Fxq -- "$value"; then
TAPM_ISO_NFS_FAIL "Storage '${value}' is not active here or does not support LXC volumes."
return 1
fi
printf -v "$variable" '%s' "$value"
}
TAPM_DEPLOY_ISO_NFS_LXC() {
local ctid default_ctid hostname address_cidr server_ip gateway bridge
local client_cidr root_storage data_storage root_size data_size
local pve_storage_id template_storage template_name template_path
local default_root_storage default_template_storage choice test_file
local container_config
echo
echo -e "${idsCL[LightCyan]}Shared ISO storage using an LXC NFS server${idsCL[Default]}"
echo
echo " This creates a privileged Debian LXC with an unconfined AppArmor profile,"
echo " allocates a dedicated mp0 data volume, exports it to the cluster, and"
echo " registers it in storage.cfg. This reduced isolation is required for the"
echo " kernel NFS service; do not run unrelated or untrusted software in this LXC."
echo " The container's host must be online for ISO storage to remain available."
echo
[[ $EUID -eq 0 ]] ||
{ TAPM_ISO_NFS_FAIL "Run this action as root on a Proxmox VE host."; return 1; }
for command in pct pvesm pveam pvesh; do
command -v "$command" >/dev/null 2>&1 ||
{ TAPM_ISO_NFS_FAIL "Required Proxmox command '${command}' was not found."; return 1; }
done
default_ctid="$(pvesh get /cluster/nextid 2>/dev/null || true)"
TAPM_ISO_NFS_PROMPT ctid "Container ID" "$default_ctid"
TAPM_ISO_NFS_VALID_CTID "$ctid" ||
{ TAPM_ISO_NFS_FAIL "The container ID is invalid."; return 1; }
if pct status "$ctid" >/dev/null 2>&1; then
TAPM_ISO_NFS_FAIL "Container ${ctid} already exists; no changes were made."
return 1
fi
TAPM_ISO_NFS_PROMPT hostname "Container hostname" "iso-nfs"
[[ "$hostname" =~ ^[A-Za-z0-9][A-Za-z0-9.-]{0,62}$ ]] ||
{ TAPM_ISO_NFS_FAIL "The hostname is invalid."; return 1; }
TAPM_ISO_NFS_PROMPT address_cidr "Static IPv4 address with prefix (example: 10.20.30.10/24)"
TAPM_ISO_NFS_VALID_IPV4_CIDR "$address_cidr" ||
{ TAPM_ISO_NFS_FAIL "A valid static IPv4 address and prefix are required."; return 1; }
server_ip="${address_cidr%/*}"
TAPM_ISO_NFS_PROMPT gateway "IPv4 gateway"
TAPM_ISO_NFS_VALID_IPV4_CIDR "${gateway}/32" ||
{ TAPM_ISO_NFS_FAIL "A valid IPv4 gateway is required."; return 1; }
TAPM_ISO_NFS_PROMPT bridge "Proxmox bridge" "vmbr0"
ip link show "$bridge" >/dev/null 2>&1 ||
{ TAPM_ISO_NFS_FAIL "Bridge '${bridge}' does not exist on this host."; return 1; }
client_cidr="$(
python3 -c 'import ipaddress,sys; print(ipaddress.ip_interface(sys.argv[1]).network)' \
"$address_cidr" 2>/dev/null
)" || client_cidr=''
TAPM_ISO_NFS_PROMPT client_cidr "CIDR allowed to mount the export" "$client_cidr"
TAPM_ISO_NFS_VALID_IPV4_CIDR "$client_cidr" ||
{ TAPM_ISO_NFS_FAIL "The allowed client CIDR is invalid."; return 1; }
default_root_storage="$(
pvesm status --content rootdir 2>/dev/null |
awk 'NR > 1 && $3 == "active" { print $1; exit }'
)"
[[ -n "$default_root_storage" ]] ||
{ TAPM_ISO_NFS_FAIL "No active storage supports LXC volumes."; return 1; }
TAPM_ISO_NFS_SELECT_STORAGE root_storage "Root filesystem storage" "$default_root_storage" || return 1
TAPM_ISO_NFS_SELECT_STORAGE data_storage "Dedicated ISO volume storage" "$root_storage" || return 1
TAPM_ISO_NFS_PROMPT root_size "Root filesystem size in GiB" "8"
[[ "$root_size" =~ ^[1-9][0-9]*$ ]] ||
{ TAPM_ISO_NFS_FAIL "The root filesystem size must be a positive integer."; return 1; }
TAPM_ISO_NFS_PROMPT data_size "ISO volume size in GiB" "250"
[[ "$data_size" =~ ^[1-9][0-9]*$ ]] ||
{ TAPM_ISO_NFS_FAIL "The ISO volume size must be a positive integer."; return 1; }
TAPM_ISO_NFS_PROMPT pve_storage_id "Proxmox cluster storage ID" "PVE-Shared-Storage"
TAPM_ISO_NFS_VALID_ID "$pve_storage_id" ||
{ TAPM_ISO_NFS_FAIL "The Proxmox storage ID is invalid."; return 1; }
if pvesm status 2>/dev/null | awk 'NR > 1 { print $1 }' | grep -Fxq -- "$pve_storage_id"; then
TAPM_ISO_NFS_FAIL "Storage ID '${pve_storage_id}' already exists; no changes were made."
return 1
fi
default_template_storage="$(
pvesm status --content vztmpl 2>/dev/null |
awk 'NR > 1 && $3 == "active" { print $1; exit }'
)"
[[ -n "$default_template_storage" ]] ||
{ TAPM_ISO_NFS_FAIL "No active storage supports container templates."; return 1; }
template_storage="$default_template_storage"
echo
echo " Deployment summary"
echo " LXC: ${ctid} (${hostname}), privileged"
echo " Network: ${address_cidr} via ${gateway} on ${bridge}"
echo " Root volume: ${root_storage}:${root_size} GiB"
echo " ISO volume mp0: ${data_storage}:${data_size} GiB -> /srv/iso"
echo " NFS clients: ${client_cidr}"
echo " Cluster storage: ${pve_storage_id}"
echo
read -r -p " Create this container and storage (type YES to continue)? " choice
[[ "$choice" == "YES" ]] || {
echo " Cancelled; no changes were made."
return 0
}
echo -e "\n${idsCL[LightCyan]}Locating a Debian container template...${idsCL[Default]}"
pveam update || { TAPM_ISO_NFS_FAIL "Could not refresh the template catalog."; return 1; }
template_name="$(
pveam available --section system |
awk '$2 ~ /^debian-(13|12)-standard_/ { print $2 }' |
sort -V |
tail -1
)"
[[ -n "$template_name" ]] ||
{ TAPM_ISO_NFS_FAIL "No supported Debian 12/13 standard template was found."; return 1; }
template_path="${template_storage}:vztmpl/${template_name}"
if ! pveam list "$template_storage" 2>/dev/null |
awk 'NR > 1 { print $1 }' |
grep -Fxq -- "$template_path"; then
pveam download "$template_storage" "$template_name" ||
{ TAPM_ISO_NFS_FAIL "The Debian template download failed."; return 1; }
fi
echo -e "\n${idsCL[LightCyan]}Creating LXC ${ctid}...${idsCL[Default]}"
if ! pct create "$ctid" "$template_path" \
--hostname "$hostname" \
--ostype debian \
--unprivileged 0 \
--features nesting=1 \
--cores 2 \
--memory 1024 \
--swap 512 \
--rootfs "${root_storage}:${root_size}" \
--mp0 "${data_storage}:${data_size},mp=/srv/iso,backup=1" \
--net0 "name=eth0,bridge=${bridge},ip=${address_cidr},gw=${gateway},type=veth" \
--onboot 1 \
--startup order=1; then
TAPM_ISO_NFS_FAIL "Container creation failed."
return 1
fi
container_config="/etc/pve/lxc/${ctid}.conf"
if ! grep -q '^lxc\.apparmor\.profile:' "$container_config"; then
printf 'lxc.apparmor.profile: unconfined\n' >>"$container_config" ||
{ TAPM_ISO_NFS_FAIL "Container ${ctid} was created, but its NFS AppArmor setting could not be applied."; return 1; }
fi
pct start "$ctid" ||
{ TAPM_ISO_NFS_FAIL "Container ${ctid} was created but could not be started."; return 1; }
if ! timeout 60 bash -c \
"until pct exec '$ctid' -- test -d /run/systemd/system >/dev/null 2>&1; do sleep 2; done"; then
TAPM_ISO_NFS_FAIL "Container ${ctid} did not become ready within 60 seconds."
return 1
fi
echo -e "\n${idsCL[LightCyan]}Installing and configuring NFS...${idsCL[Default]}"
pct exec "$ctid" -- apt-get update ||
{ TAPM_ISO_NFS_FAIL "Package index refresh failed inside container ${ctid}."; return 1; }
pct exec "$ctid" -- env DEBIAN_FRONTEND=noninteractive \
apt-get install -y nfs-kernel-server ||
{ TAPM_ISO_NFS_FAIL "NFS package installation failed inside container ${ctid}."; return 1; }
pct exec "$ctid" -- install -d -m 0775 /srv/iso/template/iso ||
{ TAPM_ISO_NFS_FAIL "Could not initialize the ISO directory."; return 1; }
printf '/srv/iso %s(rw,sync,no_subtree_check,no_root_squash)\n' "$client_cidr" |
pct exec "$ctid" -- tee /etc/exports.d/proxmox-isos.exports >/dev/null ||
{ TAPM_ISO_NFS_FAIL "Could not write the NFS export configuration."; return 1; }
pct exec "$ctid" -- exportfs -ra ||
{ TAPM_ISO_NFS_FAIL "The NFS export configuration was rejected."; return 1; }
pct exec "$ctid" -- systemctl enable --now nfs-server ||
{ TAPM_ISO_NFS_FAIL "The NFS server could not be started."; return 1; }
pct exec "$ctid" -- exportfs -v | grep -Fq "/srv/iso" ||
{ TAPM_ISO_NFS_FAIL "The expected NFS export is not active."; return 1; }
echo -e "\n${idsCL[LightCyan]}Registering cluster storage...${idsCL[Default]}"
if ! pvesm add nfs "$pve_storage_id" \
--server "$server_ip" \
--export /srv/iso \
--content iso \
--options vers=3; then
TAPM_ISO_NFS_FAIL "The LXC is running, but Proxmox could not add the NFS storage."
return 1
fi
if ! timeout 30 pvesm status --storage "$pve_storage_id" |
awk -v id="$pve_storage_id" 'NR > 1 && $1 == id && $3 == "active" { found=1 } END { exit !found }'; then
pvesm remove "$pve_storage_id" >/dev/null 2>&1 || true
TAPM_ISO_NFS_FAIL "The NFS storage did not become active; its cluster entry was removed."
return 1
fi
test_file="/mnt/pve/${pve_storage_id}/template/iso/.tapm-write-test"
if ! touch "$test_file" || ! rm -f -- "$test_file"; then
pvesm remove "$pve_storage_id" >/dev/null 2>&1 || true
TAPM_ISO_NFS_FAIL "The NFS mount was not writable; its cluster entry was removed."
return 1
fi
echo
echo -e "${idsCL[Green]}Shared ISO storage '${pve_storage_id}' is active.${idsCL[Default]}"
echo " LXC ${ctid} serves ${data_storage}:${data_size} GiB from ${server_ip}:/srv/iso."
echo " Because Proxmox storage configuration is cluster-wide, every cluster node"
echo " can use it when that node can reach ${server_ip} and is allowed by ${client_cidr}."
return 0
}