293 lines
12 KiB
Bash
293 lines
12 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 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; 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" "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; }
|
|
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
|
|
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
|
|
}
|