diff --git a/README.md b/README.md index 5b51fc8..a2de0eb 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,7 @@ restart Restart core local Proxmox management services cpu Detect and optionally apply a migration-safe CPU model maintenance Toggle local HA maintenance mode keepalived Deploy Keepalived across the cluster +iso-nfs Create an LXC NFS server and cluster-wide shared ISO storage ``` ## Companion files @@ -71,6 +72,12 @@ portal without changing ProxMenu. The Keepalived deployment additionally requires a healthy, quorate Proxmox cluster and passwordless root SSH between cluster nodes. +The shared ISO storage wizard creates a privileged Debian LXC with a dedicated +secondary volume, restricts its NFS export to a supplied client CIDR, and adds +the export to Proxmox's cluster-wide storage configuration. The container host +and network path to the container must remain available for nodes to use the +ISO repository. + The legacy Dell OMSA installer is limited to supported PowerEdge x30/x40 systems running Proxmox VE 9 on Debian 13 (Trixie), amd64. diff --git a/defaults.inc b/defaults.inc index 921b6d0..8d60755 100755 --- a/defaults.inc +++ b/defaults.inc @@ -3,7 +3,7 @@ action="${1:-}" FOLDER='/opt/idssys/ta-proxmenu' -VERS='2026.7.25-35' +VERS='2026.7.25-37' noupdate=' ' @@ -20,14 +20,6 @@ TAPM_BROKER_URL='https://tapm.scity.us' S1_BROKER_PACKAGE='sentinelone-linux' S1_PACKAGE='tapm-sentinelone.deb' -if [[ -d /mnt/pve/PVE-Shared-Storage/template/iso ]]; then - DLDIR='/mnt/pve/PVE-Shared-Storage/template/iso' -else - DLDIR='/var/lib/vz/template/iso' -fi - - - # if [ -f /etc/apt/sources.list.d/gyptazy.list ]; then # rm -f /etc/apt/sources.list.d/gyptazy.list /etc/apt/keyrings/gyptazy.asc # echo "deb https://repo.gyptazy.com/stable /" > /etc/apt/sources.list.d/proxlb.list diff --git a/inc/deploy-iso-nfs-lxc.sh b/inc/deploy-iso-nfs-lxc.sh new file mode 100644 index 0000000..a36132f --- /dev/null +++ b/inc/deploy-iso-nfs-lxc.sh @@ -0,0 +1,263 @@ +#!/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 +} diff --git a/proxmenu-scripts.sh b/proxmenu-scripts.sh index 27f7aa5..db1ad20 100755 --- a/proxmenu-scripts.sh +++ b/proxmenu-scripts.sh @@ -6,6 +6,7 @@ source /opt/idssys/defaults/default.inc source /opt/idssys/ta-proxmenu/defaults.inc source /opt/idssys/ta-proxmenu/inc/git-update.inc +source /opt/idssys/ta-proxmenu/inc/deploy-iso-nfs-lxc.sh ACTION_REQUESTED=0 [[ -n "${action:-}" ]] && ACTION_REQUESTED=1 @@ -174,8 +175,9 @@ TAPM_AUTHORIZE() { host_fingerprint="$(sha256sum /etc/machine-id | cut -d' ' -f1)" exchange_response="$( TAPM_CODE="$deploycode" TAPM_FINGERPRINT="$host_fingerprint" TAPM_HOSTNAME="$(hostname)" \ + TAPM_LAN_IP="${RNIP:-}" \ TAPM_REQUESTED_ACTION="$required_action" TAPM_REQUESTED_PACKAGE="$required_package" \ - python3 -c 'import json, os, sys; json.dump({"code": os.environ["TAPM_CODE"], "host_fingerprint": os.environ["TAPM_FINGERPRINT"], "hostname": os.environ["TAPM_HOSTNAME"], "requested_action": os.environ["TAPM_REQUESTED_ACTION"], "requested_package": os.environ["TAPM_REQUESTED_PACKAGE"]}, sys.stdout)' | + python3 -c 'import json, os, sys; json.dump({"code": os.environ["TAPM_CODE"], "host_fingerprint": os.environ["TAPM_FINGERPRINT"], "hostname": os.environ["TAPM_HOSTNAME"], "lan_ip": os.environ["TAPM_LAN_IP"], "requested_action": os.environ["TAPM_REQUESTED_ACTION"], "requested_package": os.environ["TAPM_REQUESTED_PACKAGE"]}, sys.stdout)' | curl --fail --silent --show-error \ --header 'Content-Type: application/json' \ --data-binary @- "${TAPM_BROKER_URL}/api/v1/exchange" @@ -783,6 +785,11 @@ VIRTIO_STABLE_CHECKED=0 VIRTIO_STABLE_STATUS='unknown' VIRTIO_STABLE_FILE='' VIRTIO_LAST_FILE='' +DLDIR='' +VIRTIO_SELECTED_STORAGE='' +declare -a VIRTIO_STORAGE_IDS=() +declare -a VIRTIO_STORAGE_TYPES=() +declare -a VIRTIO_STORAGE_DIRS=() TAPM_VIRTIO_FILENAME_FROM_URL() { local url="${1%%\?*}" @@ -792,12 +799,103 @@ TAPM_VIRTIO_FILENAME_FROM_URL() { printf '%s\n' "$filename" } +TAPM_VIRTIO_LABELED_FILENAME() { + local source_filename="$1" + local label="$2" + local version='' + + [[ "$label" =~ ^[a-z0-9-]+$ ]] || return 1 + if [[ "$source_filename" =~ ^virtio-win-(0\.1\.[0-9]+)\.iso$ ]]; then + version="${BASH_REMATCH[1]}" + elif [[ "$source_filename" != 'virtio-win.iso' ]]; then + return 1 + fi + + printf 'virtio-win-%s%s.iso\n' "$label" "${version:+-${version}}" +} + +TAPM_REFRESH_ISO_STORAGES() { + local storage + local type + local status + local probe_path + + VIRTIO_STORAGE_IDS=() + VIRTIO_STORAGE_TYPES=() + VIRTIO_STORAGE_DIRS=() + command -v pvesm >/dev/null 2>&1 || return 1 + + while read -r storage type status _; do + [[ "$storage" != 'Name' && "$status" == 'active' ]] || continue + probe_path="$( + pvesm path "${storage}:iso/tapm-path-probe.iso" 2>/dev/null + )" || continue + [[ "$probe_path" == /*/* ]] || continue + + VIRTIO_STORAGE_IDS+=("$storage") + VIRTIO_STORAGE_TYPES+=("$type") + VIRTIO_STORAGE_DIRS+=("${probe_path%/*}") + done < <(pvesm status --content iso --enabled 1 2>/dev/null) + + (( ${#VIRTIO_STORAGE_IDS[@]} > 0 )) +} + +TAPM_SELECT_ISO_STORAGE() { + local index + local -a labels=() + local -a values=() + + if ! TAPM_REFRESH_ISO_STORAGES; then + echo -e "${idsCL[LightRed]}No active, enabled Proxmox storage supporting ISO images was found.${idsCL[Default]}" + ENTER2CONTINUE + return 1 + fi + + if (( ${#VIRTIO_STORAGE_IDS[@]} == 1 )); then + index=0 + else + for index in "${!VIRTIO_STORAGE_IDS[@]}"; do + labels+=("${VIRTIO_STORAGE_IDS[$index]} (${VIRTIO_STORAGE_TYPES[$index]}) — ${VIRTIO_STORAGE_DIRS[$index]}") + values+=("storage:${index}") + done + SELECT_MENU "Select ISO Storage" labels values + case "$MENU_SELECTION" in + storage:*) index="${MENU_SELECTION#storage:}";; + quit) EXIT1; exit 0;; + *) return 1;; + esac + fi + + VIRTIO_SELECTED_STORAGE="${VIRTIO_STORAGE_IDS[$index]}" + DLDIR="${VIRTIO_STORAGE_DIRS[$index]}" + echo -e "\n${idsCL[LightCyan]}ISO storage: ${VIRTIO_SELECTED_STORAGE} (${DLDIR})${idsCL[Default]}" +} + +TAPM_VIRTIO_FILE_EXISTS() { + local filename="$1" + local directory + + for directory in "${VIRTIO_STORAGE_DIRS[@]}"; do + [[ -f "${directory}/${filename}" ]] && return 0 + done + return 1 +} + +TAPM_ANY_LOCAL_VIRTIO_ISOS() { + local directory + + for directory in "${VIRTIO_STORAGE_DIRS[@]}"; do + compgen -G "${directory}/virtio-win*.iso" >/dev/null && return 0 + done + return 1 +} + TAPM_REFRESH_VIRTIO_LOCAL_STATUS() { [[ -n "$VIRTIO_STABLE_FILE" ]] || return - if [[ -f "${DLDIR}/${VIRTIO_STABLE_FILE}" ]]; then + if TAPM_VIRTIO_FILE_EXISTS "$VIRTIO_STABLE_FILE"; then VIRTIO_STABLE_STATUS='current' - elif compgen -G "${DLDIR}/virtio-win*.iso" >/dev/null; then + elif TAPM_ANY_LOCAL_VIRTIO_ISOS; then VIRTIO_STABLE_STATUS='update' else VIRTIO_STABLE_STATUS='available' @@ -807,6 +905,7 @@ TAPM_REFRESH_VIRTIO_LOCAL_STATUS() { TAPM_CHECK_VIRTIO_STABLE() { local force="${1:-0}" local effective_url + local source_filename if (( VIRTIO_STABLE_CHECKED == 1 && force == 0 )); then return @@ -823,7 +922,9 @@ TAPM_CHECK_VIRTIO_STABLE() { --output /dev/null --write-out '%{url_effective}' \ "$VIRTIO_STABLE_URL" 2>/dev/null )" || return 1 - VIRTIO_STABLE_FILE="$(TAPM_VIRTIO_FILENAME_FROM_URL "$effective_url")" || + source_filename="$(TAPM_VIRTIO_FILENAME_FROM_URL "$effective_url")" || + return 1 + VIRTIO_STABLE_FILE="$(TAPM_VIRTIO_LABELED_FILENAME "$source_filename" latest)" || return 1 TAPM_REFRESH_VIRTIO_LOCAL_STATUS } @@ -844,7 +945,8 @@ TAPM_VALID_VIRTIO_ISO() { TAPM_DOWNLOAD_VIRTIO_ISO() { local url="$1" local expected_filename="${2:-}" - local description="${3:-VirtIO driver ISO}" + local filename_label="$3" + local description="${4:-VirtIO driver ISO}" local effective_url local filename local final_file @@ -896,6 +998,11 @@ TAPM_DOWNLOAD_VIRTIO_ISO() { echo -e "${idsCL[LightRed]}The VirtIO archive returned ${filename}; expected ${expected_filename}.${idsCL[Default]}" return 1 fi + filename="$(TAPM_VIRTIO_LABELED_FILENAME "$filename" "$filename_label")" || { + rm -f -- "$partial_file" + echo -e "${idsCL[LightRed]}Could not create a safe local filename for the VirtIO ISO.${idsCL[Default]}" + return 1 + } if ! TAPM_VALID_VIRTIO_ISO "$partial_file"; then rm -f -- "$partial_file" @@ -916,11 +1023,13 @@ TAPM_DOWNLOAD_VIRTIO_ISO() { echo -e "\n${idsCL[Green]}VirtIO ISO downloaded and validated.${idsCL[Default]}" printf ' File: %s\n' "$filename" printf ' Size: %s\n' "$(numfmt --to=iec-i --suffix=B "$size")" + printf ' Storage: %s\n' "$VIRTIO_SELECTED_STORAGE" printf ' Destination: %s\n' "$DLDIR" } DOWNLOAD_VIRTIO_STABLE() { - if ! TAPM_DOWNLOAD_VIRTIO_ISO "$VIRTIO_STABLE_URL" '' \ + TAPM_SELECT_ISO_STORAGE || return + if ! TAPM_DOWNLOAD_VIRTIO_ISO "$VIRTIO_STABLE_URL" '' latest \ 'current stable VirtIO drivers'; then FINISH_FAILED_ACTION return @@ -934,11 +1043,13 @@ DOWNLOAD_VIRTIO_STABLE() { DOWNLOAD_VIRTIO_ARCHIVE() { local release="$1" local description="$2" + local filename_label="${3:-archive}" local iso_version="${release%-*}" local filename="virtio-win-${iso_version}.iso" local url="https://fedorapeople.org/groups/virt/virtio-win/direct-downloads/archive-virtio/virtio-win-${release}/${filename}" - if ! TAPM_DOWNLOAD_VIRTIO_ISO "$url" "$filename" "$description"; then + TAPM_SELECT_ISO_STORAGE || return + if ! TAPM_DOWNLOAD_VIRTIO_ISO "$url" "$filename" "$filename_label" "$description"; then FINISH_FAILED_ACTION return fi @@ -961,6 +1072,7 @@ DOWNLOAD_CUSTOM_VIRTIO_ARCHIVE() { } SHOW_LOCAL_VIRTIO_ISOS() { + local index local file local found=0 @@ -968,18 +1080,25 @@ SHOW_LOCAL_VIRTIO_ISOS() { echo echo -e " ${idsCL[LightCyan]}Downloaded VirtIO ISOs${idsCL[Default]}" echo - echo " Destination: ${DLDIR}" - echo - while IFS= read -r -d '' file; do - found=1 - printf ' %-34s %10s %s\n' \ - "${file##*/}" \ - "$(numfmt --to=iec-i --suffix=B "$(stat -c '%s' "$file")")" \ - "$(date --date="@$(stat -c '%Y' "$file")" '+%F %R')" - done < <( - find "$DLDIR" -maxdepth 1 -type f -name 'virtio-win*.iso' \ - -print0 2>/dev/null | sort -z - ) + if TAPM_REFRESH_ISO_STORAGES; then + for index in "${!VIRTIO_STORAGE_IDS[@]}"; do + echo -e " ${idsCL[LightCyan]}${VIRTIO_STORAGE_IDS[$index]}${idsCL[Default]} (${VIRTIO_STORAGE_TYPES[$index]})" + echo " ${VIRTIO_STORAGE_DIRS[$index]}" + while IFS= read -r -d '' file; do + found=1 + printf ' %-42s %10s %s\n' \ + "${file##*/}" \ + "$(numfmt --to=iec-i --suffix=B "$(stat -c '%s' "$file")")" \ + "$(date --date="@$(stat -c '%Y' "$file")" '+%F %R')" + done < <( + find "${VIRTIO_STORAGE_DIRS[$index]}" -maxdepth 1 -type f \ + -name 'virtio-win*.iso' -print0 2>/dev/null | sort -z + ) + echo + done + else + echo -e " ${idsCL[LightRed]}No active ISO-capable storage was found.${idsCL[Default]}" + fi (( found == 1 )) || echo " No VirtIO ISOs are currently downloaded." echo read -r -p " Press ENTER to return..." _ @@ -1000,10 +1119,12 @@ VIRTIO_MENU() { ) echo -en "\n${idsCL[LightCyan]}Checking the current stable VirtIO release...${idsCL[Default]} " + TAPM_REFRESH_ISO_STORAGES || true TAPM_CHECK_VIRTIO_STABLE 1 || true echo while true; do + TAPM_REFRESH_ISO_STORAGES || true TAPM_REFRESH_VIRTIO_LOCAL_STATUS case "$VIRTIO_STABLE_STATUS" in current) @@ -1030,13 +1151,13 @@ VIRTIO_MENU() { "View downloaded VirtIO ISOs" "Refresh stable-version check" ) - [[ -f "${DLDIR}/virtio-win-0.1.240.iso" ]] && + TAPM_VIRTIO_FILE_EXISTS 'virtio-win-server-2016-0.1.240.iso' && labels[1]+=" (downloaded)" - [[ -f "${DLDIR}/virtio-win-0.1.189.iso" ]] && + TAPM_VIRTIO_FILE_EXISTS 'virtio-win-server-2012r2-0.1.189.iso' && labels[2]+=" (downloaded)" - [[ -f "${DLDIR}/virtio-win-0.1.172.iso" ]] && + TAPM_VIRTIO_FILE_EXISTS 'virtio-win-server-2008r2-0.1.172.iso' && labels[3]+=" (downloaded)" - [[ -f "${DLDIR}/virtio-win-0.1.141.iso" ]] && + TAPM_VIRTIO_FILE_EXISTS 'virtio-win-server-2008-0.1.141.iso' && labels[4]+=" (downloaded)" SELECT_MENU "VirtIO Driver Downloads" labels values @@ -1044,19 +1165,19 @@ VIRTIO_MENU() { stable) DOWNLOAD_VIRTIO_STABLE;; server2016) DOWNLOAD_VIRTIO_ARCHIVE 0.1.240-1 \ - "Windows Server 2016 compatibility drivers" + "Windows Server 2016 compatibility drivers" server-2016 ;; server2012) DOWNLOAD_VIRTIO_ARCHIVE 0.1.189-1 \ - "Windows Server 2012/R2 compatibility drivers" + "Windows Server 2012/R2 compatibility drivers" server-2012r2 ;; server2008r2) DOWNLOAD_VIRTIO_ARCHIVE 0.1.172-1 \ - "Windows Server 2008 R2 compatibility drivers" + "Windows Server 2008 R2 compatibility drivers" server-2008r2 ;; server2008) DOWNLOAD_VIRTIO_ARCHIVE 0.1.141-1 \ - "Windows Server 2008 compatibility drivers" + "Windows Server 2008 compatibility drivers" server-2008 ;; archive) DOWNLOAD_CUSTOM_VIRTIO_ARCHIVE;; local) SHOW_LOCAL_VIRTIO_ISOS;; @@ -1736,7 +1857,8 @@ HOST_SETUP_MENU() { labels+=("Detect CPU model for live migrations") values+=("cpu") - if compgen -G "${DLDIR}/virtio-win*.iso" >/dev/null; then + TAPM_REFRESH_ISO_STORAGES >/dev/null 2>&1 || true + if TAPM_ANY_LOCAL_VIRTIO_ISOS; then labels+=("VirtIO driver downloads (local ISOs available)") else labels+=("VirtIO driver downloads") @@ -1810,7 +1932,7 @@ MONITORING_MENU() { CLUSTER_MENU() { local -a labels - local -a values=("maintenance" "services" "keepalived") + local -a values=("maintenance" "services" "keepalived" "iso_nfs") while true; do if ha-manager status | grep -F "$(hostname -s)" | grep -q "maintenance mode"; then @@ -1823,12 +1945,17 @@ CLUSTER_MENU() { dpkg-query -W -f='${Status}' keepalived 2>/dev/null | grep -q "install ok installed" && labels+=("Deploy/reconfigure Keepalived (installed locally)") || labels+=("Deploy Keepalived on all cluster hosts") + labels+=("Create shared ISO storage using an LXC NFS server") SELECT_MENU "Cluster & Maintenance" labels values case "$MENU_SELECTION" in maintenance) MAINTENANCE_MODE;; services) SERVICE_RECOVERY_MENU;; keepalived) INSTALL_KEEPALIVE;; + iso_nfs) + TAPM_DEPLOY_ISO_NFS_LXC + FINISH_ACTION + ;; back) return;; quit) EXIT1; exit 0;; esac @@ -2110,6 +2237,7 @@ if (( ACTION_REQUESTED == 1 )); then cpu) DETECT_CPU;; maintenance|mm) MAINTENANCE_MODE;; keepalived) INSTALL_KEEPALIVE;; + iso-nfs|iso_nfs) TAPM_DEPLOY_ISO_NFS_LXC;; *) MAIN_MENU;; esac else