#!/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 storage type status index local default_found=0 local -a storage_ids=() local -a storage_types=() local -a labels=() local -a values=() while read -r storage type status _; do [[ "$storage" != 'Name' && "$status" == 'active' ]] || continue storage_ids+=("$storage") storage_types+=("$type") [[ "$storage" == "$default_value" ]] && default_found=1 done < <(pvesm status --content rootdir --enabled 1 2>/dev/null) if (( ${#storage_ids[@]} == 0 )); then TAPM_ISO_NFS_FAIL "No active, enabled storage supports LXC volumes." return 1 fi # Put the suggested storage first so pressing ENTER retains the default. if (( default_found == 1 )); then for index in "${!storage_ids[@]}"; do [[ "${storage_ids[$index]}" == "$default_value" ]] || continue labels+=("${storage_ids[$index]} (${storage_types[$index]}) — default") values+=("storage:${storage_ids[$index]}") break done fi for index in "${!storage_ids[@]}"; do [[ $default_found == 1 && "${storage_ids[$index]}" == "$default_value" ]] && continue labels+=("${storage_ids[$index]} (${storage_types[$index]})") values+=("storage:${storage_ids[$index]}") done SELECT_MENU "$label" labels values case "$MENU_SELECTION" in storage:*) printf -v "$variable" '%s' "${MENU_SELECTION#storage:}";; quit) EXIT1; exit 0;; *) return 1;; esac } 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 ha-manager; 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" "PVE-Shared-Storage" [[ "$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" "$hostname" 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 " Resources: 2 vCPU, 2048 MiB RAM, 512 MiB swap, 100 CPU units" 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 " HA resource: ct:${ctid} (started)" echo read -r -p " Create this container and storage (type yes to continue)? " choice [[ "$choice" =~ ^[Yy][Ee][Ss]$ ]] || { 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 \ --cpunits 100 \ --memory 2048 \ --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; } pct exec "$ctid" -- install -d -m 0755 /etc/exports.d || { TAPM_ISO_NFS_FAIL "Could not initialize the NFS exports 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 -e "\n${idsCL[LightCyan]}Adding LXC ${ctid} to Proxmox HA...${idsCL[Default]}" if ! ha-manager add "ct:${ctid}" --state started; then TAPM_ISO_NFS_FAIL \ "Shared storage '${pve_storage_id}' is active, but LXC ${ctid} could not be added to HA." 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 " HA now manages ct:${ctid} with requested state 'started'." 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 }