Files
TA-ProxMenu/proxmenu-scripts.sh
T

2597 lines
75 KiB
Bash
Executable File

#!/usr/bin/env bash
# TA-Proxmenu - Proxmox Setup Scripts for TA Use
[ "${2:-}" != "q" ] && source /opt/idssys/defaults/colors.inc
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/ha-status.inc
source /opt/idssys/ta-proxmenu/inc/post-install.inc
source /opt/idssys/ta-proxmenu/inc/deploy-iso-nfs-lxc.sh
source /opt/idssys/ta-proxmenu/inc/deploy-pulse-lxc.sh
source /opt/idssys/ta-proxmenu/inc/virtio-helpers.inc
ACTION_REQUESTED=0
[[ -n "${action:-}" ]] && ACTION_REQUESTED=1
FINISH_ACTION() {
(( ACTION_REQUESTED == 1 )) && exit 0
ENTER2CONTINUE
}
FINISH_FAILED_ACTION() {
(( ACTION_REQUESTED == 1 )) && exit 1
ENTER2CONTINUE
return 1
}
declare -a TAPM_TEMP_DIRS=()
TAPM_TEMP_DIR=''
TAPM_CLEAN_TEMP_DIR() {
local temp_dir="${1:-}"
if [[ "$temp_dir" == /tmp/ta-proxmenu-* && -d "$temp_dir" ]]; then
rm -rf -- "$temp_dir"
fi
}
TAPM_CLEAN_ALL_TEMP_DIRS() {
local temp_dir
for temp_dir in "${TAPM_TEMP_DIRS[@]}"; do
TAPM_CLEAN_TEMP_DIR "$temp_dir"
done
}
TAPM_CREATE_TEMP_DIR() {
local label="${1:-installer}"
TAPM_TEMP_DIR="$(mktemp -d "/tmp/ta-proxmenu-${label}.XXXXXX")" || {
echo -e "${idsCL[LightRed]}Unable to create a temporary installer directory.${idsCL[Default]}"
return 1
}
if ! chmod 0700 "$TAPM_TEMP_DIR"; then
TAPM_CLEAN_TEMP_DIR "$TAPM_TEMP_DIR"
echo -e "${idsCL[LightRed]}Unable to secure the temporary installer directory.${idsCL[Default]}"
return 1
fi
TAPM_TEMP_DIRS+=("$TAPM_TEMP_DIR")
}
TAPM_VALID_HTTPS_URL() {
local url="${1:-}"
[[ "$url" == https://* &&
"$url" != *$'\n'* &&
"$url" != *$'\r'* &&
"$url" != *'"'* &&
"$url" != *\\* &&
"$url" != *[[:space:]]* ]]
}
TAPM_DOWNLOAD_HTTPS() {
local url="$1"
local destination="$2"
local label="${3:-Installer}"
if ! TAPM_VALID_HTTPS_URL "$url"; then
echo -e "${idsCL[LightRed]}${label} requires a valid HTTPS URL.${idsCL[Default]}"
return 1
fi
if ! printf 'url = "%s"\n' "$url" |
curl --fail --location --silent --show-error \
--proto '=https' --proto-redir '=https' \
--output "$destination" --config -; then
echo -e "${idsCL[LightRed]}${label} download failed.${idsCL[Default]}"
return 1
fi
if [[ ! -s "$destination" ]]; then
echo -e "${idsCL[LightRed]}${label} download was empty.${idsCL[Default]}"
return 1
fi
}
TAPM_DOWNLOAD_SHA256() {
local url="$1"
local destination="$2"
local expected_sha256="${3,,}"
local label="${4:-Installer}"
local actual_sha256
TAPM_DOWNLOAD_HTTPS "$url" "$destination" "$label" || return 1
actual_sha256="$(sha256sum "$destination" | cut -d' ' -f1)"
if [[ "$actual_sha256" != "$expected_sha256" ]]; then
rm -f -- "$destination"
echo -e "${idsCL[LightRed]}${label} checksum did not match; the file was removed.${idsCL[Default]}"
return 1
fi
}
TAPM_PACKAGE_INSTALLED() {
dpkg-query -W -f='${Status}' "$1" 2>/dev/null |
grep -q '^install ok installed$'
}
TAPM_WAIT_FOR_SERVICE() {
local service="$1"
local attempts="${2:-30}"
local attempt=0
while (( attempt < attempts )); do
systemctl is-active --quiet "$service" && return 0
sleep 1
((attempt++))
done
return 1
}
TAPM_WAIT_FOR_TCP_PORT() {
local port="$1"
local attempts="${2:-30}"
local attempt=0
while (( attempt < attempts )); do
if ss -H -lnt 2>/dev/null |
awk -v port="$port" '$4 ~ (":" port "$") { found=1 } END { exit !found }'; then
return 0
fi
sleep 1
((attempt++))
done
return 1
}
trap TAPM_CLEAN_ALL_TEMP_DIRS EXIT
TAPM_CLEAR_AUTHORIZATION() {
unset TAPM_SESSION_TOKEN TAPM_PACKAGE_URL TAPM_PACKAGE_SHA256 TAPM_PACKAGE_VERSION
}
TAPM_AUTHORIZE() {
local required_action="${1:-}"
local required_package="${2:-}"
local authorization_label="${3:-this installation}"
local deploycode exchange_response host_fingerprint
local -a access
TAPM_CLEAR_AUTHORIZATION
if ! command -v python3 >/dev/null 2>&1; then
echo -e "${idsCL[LightRed]}Python 3 is required to authorize ${authorization_label}.${idsCL[Default]}"
return 1
fi
echo
echo -en "${idsCL[LightYellow]}Paste the TAPM deployment code: ${idsCL[Default]}"
read -r -s deploycode
echo
deploycode="${deploycode^^}"
if [[ ! "$deploycode" =~ ^TAPM-[0-9A-HJKMNP-TV-Z]{5}-[0-9A-HJKMNP-TV-Z]{5}$ ]]; then
unset deploycode
echo -e "${idsCL[LightRed]}The TAPM deployment code is not valid.${idsCL[Default]}"
return 1
fi
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"], "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"
)" || {
unset deploycode host_fingerprint exchange_response
echo -e "${idsCL[LightRed]}TAPM could not authorize ${authorization_label}.${idsCL[Default]}"
return 1
}
unset deploycode host_fingerprint
mapfile -t access < <(
printf '%s' "$exchange_response" |
TAPM_REQUIRED_ACTION="$required_action" TAPM_REQUIRED_PACKAGE="$required_package" \
python3 -c '
import json, os, sys
data = json.load(sys.stdin)
required_action = os.environ["TAPM_REQUIRED_ACTION"]
required_package = os.environ["TAPM_REQUIRED_PACKAGE"]
if required_action and required_action not in data.get("actions", []):
raise SystemExit(1)
package = None
if required_package:
package = next((item for item in data.get("packages", []) if item.get("slug") == required_package), None)
if not package:
raise SystemExit(1)
print(data.get("session_token", ""))
print(package.get("download_url", "") if package else "")
print(package.get("sha256", "") if package else "")
print(package.get("version", "") if package else "")
'
) || true
unset exchange_response
if [[ ${#access[@]} -ne 4 ||
! "${access[0]}" =~ ^[A-Za-z0-9._~-]+$ ]]; then
unset access
echo -e "${idsCL[LightRed]}This deployment code does not authorize ${authorization_label}.${idsCL[Default]}"
return 1
fi
if [[ -n "$required_package" &&
( -z "${access[1]}" || ! "${access[2]}" =~ ^[0-9a-fA-F]{64}$ ) ]]; then
unset access
echo -e "${idsCL[LightRed]}The authorized package metadata is incomplete.${idsCL[Default]}"
return 1
fi
TAPM_SESSION_TOKEN="${access[0]}"
TAPM_PACKAGE_URL="${access[1]}"
TAPM_PACKAGE_SHA256="${access[2],,}"
TAPM_PACKAGE_VERSION="${access[3]}"
unset access
return 0
}
INSTALL_PULSE() {
if ! TAPM_DEPLOY_PULSE_LXC; then
echo -e "${idsCL[LightRed]}Pulse deployment failed.${idsCL[Default]}"
FINISH_FAILED_ACTION
return
fi
FINISH_ACTION
}
INSTALL_ACRONIS() {
local installer
local temp_dir
local url='https://us5-cloud.acronis.com/bc/api/ams/links/agents/redirect?language=multi&channel=CURRENT&system=linux&architecture=64&productType=enterprise&login=010180ae-63c4-4495-bed0-4ec934c25af9&white_labeled=0'
echo
if ! TAPM_AUTHORIZE "install-acronis" "" "Acronis installation"; then
FINISH_FAILED_ACTION
return
fi
TAPM_CLEAR_AUTHORIZATION
if ! TAPM_CREATE_TEMP_DIR acronis; then
FINISH_FAILED_ACTION
return
fi
temp_dir="$TAPM_TEMP_DIR"
installer="${temp_dir}/acronisinstall"
if ! TAPM_DOWNLOAD_HTTPS "$url" "$installer" 'Acronis installer' ||
! chmod 0700 "$installer" ||
! (cd "$temp_dir" && ./acronisinstall) ||
! TAPM_PACKAGE_INSTALLED cyberprotect; then
TAPM_CLEAN_TEMP_DIR "$temp_dir"
echo -e "${idsCL[LightRed]}Acronis installation failed or could not be verified.${idsCL[Default]}"
FINISH_FAILED_ACTION
return
fi
TAPM_CLEAN_TEMP_DIR "$temp_dir"
echo
echo -e "\n${idsCL[Green]}Acronis has been installed and verified.${idsCL[Default]}"
FINISH_ACTION
}
INSTALL_GLANCES() {
echo
if ! DEBIAN_FRONTEND=noninteractive apt-get install glances -y ||
! TAPM_PACKAGE_INSTALLED glances; then
echo -e "\n${idsCL[LightRed]}Glances installation failed or could not be verified.${idsCL[Default]}"
FINISH_FAILED_ACTION
return
fi
echo -e "\n${idsCL[Green]}Glances has been installed and verified.${idsCL[Default]}"
FINISH_ACTION
}
INSTALL_SCREENCONNECT() {
local SCURL=''
local installer
local temp_dir
echo
if ! TAPM_AUTHORIZE "install-screenconnect" "" "ScreenConnect installation"; then
FINISH_FAILED_ACTION
return
fi
TAPM_CLEAR_AUTHORIZATION
echo -en "\n${idsCL[LightYellow]}Paste the URL provided from the Build Installer: ${idsCL[Default]}"
read -r -s SCURL
echo
[[ -n "$SCURL" ]] || { echo "No URL supplied."; FINISH_FAILED_ACTION; return; }
if ! TAPM_CREATE_TEMP_DIR screenconnect; then
unset SCURL
FINISH_FAILED_ACTION
return
fi
temp_dir="$TAPM_TEMP_DIR"
installer="${temp_dir}/screenconnect.deb"
if ! TAPM_DOWNLOAD_HTTPS "$SCURL" "$installer" 'ScreenConnect installer'; then
unset SCURL
TAPM_CLEAN_TEMP_DIR "$temp_dir"
FINISH_FAILED_ACTION
return
fi
unset SCURL
if ! dpkg -i "$installer"; then
if ! DEBIAN_FRONTEND=noninteractive apt-get install --fix-broken -y; then
TAPM_CLEAN_TEMP_DIR "$temp_dir"
echo -e "${idsCL[LightRed]}ScreenConnect dependency installation failed.${idsCL[Default]}"
FINISH_FAILED_ACTION
return
fi
fi
DEBIAN_FRONTEND=noninteractive apt-get remove 'connectwis*' -y >/dev/null 2>&1 || true
if ! dpkg -i "$installer" ||
! TAPM_WAIT_FOR_SERVICE 'connectwise*'; then
TAPM_CLEAN_TEMP_DIR "$temp_dir"
echo -e "${idsCL[LightRed]}ScreenConnect installation failed or its service is not active.${idsCL[Default]}"
FINISH_FAILED_ACTION
return
fi
TAPM_CLEAN_TEMP_DIR "$temp_dir"
systemctl disable --now proxmenux-monitor >/dev/null 2>&1 || true
echo -e "\n${idsCL[Green]}ScreenConnect has been installed and verified.${idsCL[Default]}"
FINISH_ACTION
}
INSTALL_RMM() {
local RMMURL=''
local TOKEN=''
local installer
local temp_dir
echo
if ! TAPM_AUTHORIZE "install-rmm" "" "RMM installation"; then
FINISH_FAILED_ACTION
return
fi
TAPM_CLEAR_AUTHORIZATION
echo -en "\n${idsCL[LightYellow]}Paste the Linux Server URL provided from the Download Agent screen: ${idsCL[Default]}"
read -r -s RMMURL
echo
[[ -n "$RMMURL" ]] || { echo "No URL supplied."; FINISH_FAILED_ACTION; return; }
if [[ "$RMMURL" != *TKN* || "$RMMURL" != */RUN* ]]; then
echo "Unable to extract the RMM token from the URL."
unset RMMURL
FINISH_FAILED_ACTION
return
fi
TOKEN="${RMMURL#*TKN}"
TOKEN="${TOKEN%%/RUN*}"
if [[ -z "$TOKEN" ]]; then
unset RMMURL TOKEN
echo "The RMM token is empty."
FINISH_FAILED_ACTION
return
fi
if ! TAPM_CREATE_TEMP_DIR rmm; then
unset RMMURL TOKEN
FINISH_FAILED_ACTION
return
fi
temp_dir="$TAPM_TEMP_DIR"
installer="${temp_dir}/rmminstall"
if ! TAPM_DOWNLOAD_HTTPS "$RMMURL" "$installer" 'RMM installer'; then
unset RMMURL TOKEN
TAPM_CLEAN_TEMP_DIR "$temp_dir"
FINISH_FAILED_ACTION
return
fi
unset RMMURL
if ! TOKEN="$TOKEN" bash "$installer"; then
unset TOKEN
TAPM_CLEAN_TEMP_DIR "$temp_dir"
echo -e "${idsCL[LightRed]}RMM installation failed.${idsCL[Default]}"
FINISH_FAILED_ACTION
return
fi
unset TOKEN
TAPM_CLEAN_TEMP_DIR "$temp_dir"
if ! systemctl restart ITSPlatform ||
! TAPM_WAIT_FOR_SERVICE ITSPlatform; then
echo -e "${idsCL[LightRed]}RMM installed, but the ITSPlatform service is not active.${idsCL[Default]}"
FINISH_FAILED_ACTION
return
fi
echo -e "\n${idsCL[Green]}RMM has been installed and verified.${idsCL[Default]}"
FINISH_ACTION
}
INSTALL_S1() {
local installer
local s1token=''
local temp_dir
if ! TAPM_AUTHORIZE "" "$S1_BROKER_PACKAGE" "SentinelOne installation"; then
FINISH_FAILED_ACTION
return
fi
if ! TAPM_CREATE_TEMP_DIR sentinelone; then
TAPM_CLEAR_AUTHORIZATION
FINISH_FAILED_ACTION
return
fi
temp_dir="$TAPM_TEMP_DIR"
installer="${temp_dir}/${S1_PACKAGE}"
if ! TAPM_VALID_HTTPS_URL "$TAPM_PACKAGE_URL"; then
TAPM_CLEAR_AUTHORIZATION
TAPM_CLEAN_TEMP_DIR "$temp_dir"
echo -e "${idsCL[LightRed]}The authorized SentinelOne package URL is not valid HTTPS.${idsCL[Default]}"
FINISH_FAILED_ACTION
return
fi
if ! printf 'header = "Authorization: Bearer %s"\nurl = "%s"\n' \
"$TAPM_SESSION_TOKEN" "$TAPM_PACKAGE_URL" |
curl --fail --location --silent --show-error --config - \
--proto '=https' --proto-redir '=https' \
--output "$installer"; then
TAPM_CLEAR_AUTHORIZATION
TAPM_CLEAN_TEMP_DIR "$temp_dir"
echo -e "${idsCL[LightRed]}The SentinelOne installer download failed.${idsCL[Default]}"
FINISH_FAILED_ACTION
return
fi
if [[ ! -s "$installer" ||
"$(sha256sum "$installer" | cut -d' ' -f1)" != "$TAPM_PACKAGE_SHA256" ]]; then
TAPM_CLEAR_AUTHORIZATION
TAPM_CLEAN_TEMP_DIR "$temp_dir"
echo -e "${idsCL[LightRed]}The SentinelOne installer checksum did not match. The file was removed.${idsCL[Default]}"
FINISH_FAILED_ACTION
return
fi
TAPM_CLEAR_AUTHORIZATION
echo -en "${idsCL[LightYellow]}Paste the customers SentinelOne Site Token: ${idsCL[Default]}"
read -r -s s1token
echo
[[ -n "$s1token" ]] || {
TAPM_CLEAN_TEMP_DIR "$temp_dir"
echo "No SentinelOne site token supplied."
FINISH_FAILED_ACTION
return
}
if ! dpkg -i "$installer" ||
! /opt/sentinelone/bin/sentinelctl management token set "$s1token" ||
! /opt/sentinelone/bin/sentinelctl control start ||
! TAPM_PACKAGE_INSTALLED sentinelagent; then
unset s1token
TAPM_CLEAN_TEMP_DIR "$temp_dir"
echo -e "${idsCL[LightRed]}SentinelOne installation failed or could not be verified.${idsCL[Default]}"
FINISH_FAILED_ACTION
return
fi
unset s1token
TAPM_CLEAN_TEMP_DIR "$temp_dir"
echo -e "\n${idsCL[Green]}SentinelOne Agent has been installed and verified. Make sure it is added to a \"DETECT ONLY\" policy.${idsCL[Default]}"
FINISH_ACTION
}
TAPM_SYSTEM_PRODUCT_NAME() {
local product_name=''
if [[ -r /sys/class/dmi/id/product_name ]]; then
read -r product_name </sys/class/dmi/id/product_name
elif command -v dmidecode >/dev/null 2>&1; then
product_name="$(dmidecode -s system-product-name 2>/dev/null)"
fi
printf '%s\n' "$product_name"
}
TAPM_OMSA_SUPPORTED_HARDWARE() {
local product_name
product_name="$(TAPM_SYSTEM_PRODUCT_NAME)"
[[ "$product_name" =~ PowerEdge[[:space:]]+[[:alpha:]]+[0-9](3|4)[0-9][[:alnum:]-]* ]]
}
TAPM_OMSA_SUPPORTED_PLATFORM() {
local architecture
local codename
local os_version
local pve_version
architecture="$(dpkg --print-architecture 2>/dev/null)"
os_version="$(. /etc/os-release 2>/dev/null; printf '%s' "${VERSION_ID:-}")"
codename="$(. /etc/os-release 2>/dev/null; printf '%s' "${VERSION_CODENAME:-}")"
pve_version="$(pveversion 2>/dev/null | head -n 1)"
[[ "$architecture" == "amd64" &&
"$os_version" == "13" &&
"$codename" == "trixie" &&
"$pve_version" == pve-manager/9.* ]]
}
INSTALL_OMSA() {
local base_url='https://archive.ubuntu.com/ubuntu/pool'
local dell_key
local dell_keyring
local dell_source
local index
local product_name
local temp_dir
local -a filenames=(
'libwsman-curl-client-transport1_2.6.5-0ubuntu16_amd64.deb'
'libwsman-client4t64_2.6.5-0ubuntu16_amd64.deb'
'libxml2-16_2.15.2+dfsg-0.1_amd64.deb'
'libwsman1t64_2.6.5-0ubuntu16_amd64.deb'
'libwsman-server1t64_2.6.5-0ubuntu16_amd64.deb'
'libcimcclient0_2.2.8-0ubuntu2_amd64.deb'
'openwsman_2.6.5-0ubuntu16_amd64.deb'
'cim-schema_2.48.0-0ubuntu1_all.deb'
'libsfcutil0_1.0.1-0ubuntu4_amd64.deb'
'sfcb_1.4.9-0ubuntu7_amd64.deb'
'libcmpicppimpl0_2.0.3-0ubuntu2_amd64.deb'
'libssl1.1_1.1.1w-0+deb11u1_amd64.deb'
)
local -a urls=(
"${base_url}/universe/o/openwsman/${filenames[0]}"
"${base_url}/universe/o/openwsman/${filenames[1]}"
'https://snapshot.debian.org/file/32f51d914435fd29ce31af7ba17525f6276ea58d'
"${base_url}/universe/o/openwsman/${filenames[3]}"
"${base_url}/universe/o/openwsman/${filenames[4]}"
"${base_url}/universe/s/sblim-sfcc/${filenames[5]}"
"${base_url}/universe/o/openwsman/${filenames[6]}"
"${base_url}/multiverse/c/cim-schema/${filenames[7]}"
"${base_url}/universe/s/sblim-sfc-common/${filenames[8]}"
"${base_url}/multiverse/s/sblim-sfcb/${filenames[9]}"
"${base_url}/universe/s/sblim-cmpi-devel/${filenames[10]}"
"https://deb.debian.org/debian/pool/main/o/openssl/${filenames[11]}"
)
local -a checksums=(
'42fdd34722ac1304427f80c4176ee781d057f05669d485f0ee1da4d87df7488c'
'6d1855a2e8263e9a578b4c0bb7a963a6d99dc8d2ef41e287725799dcad0c6cb3'
'8571682a07f329bb462569502b57aced4866e5b95c2db3ec7e5414a5b3bbdc14'
'61b91e8f234c5f2f87b4bce3534bc2f2304d50cd2fd95f426f12fc73d80e27b4'
'5d3f948ab605b4973b399f53bd62bddd70eb01161769a0d39a811399fe7c2daf'
'14b9ac374f88bd44e57395e87faa76d99d02e242c813fe30083d5bbfafec5870'
'62b30fcf41dae0c1d841f67a46049b7dfa4dfffe314e4226a776eb134605b7fc'
'a87d16d41e81092c7ada43824a97cf79fab18c4a3722ef6f0476ad697a3d9ab7'
'ba890cf5f2359befd3da1e5763672ef8b03d5424fa4e3d1ef21c9d52884af247'
'3eb5dce0a873f8eb77174fdd5e02ac55a989f7a73bd5ef8c8aae501d225d7524'
'284acfbb6d675496046ee46e6d5ea6c70ceafa3781cf1eece04f612cbadf117c'
'aadf8b4b197335645b230c2839b4517aa444fd2e8f434e5438c48a18857988f7'
)
local -a package_paths=()
echo
product_name="$(TAPM_SYSTEM_PRODUCT_NAME)"
if ! TAPM_OMSA_SUPPORTED_HARDWARE; then
echo -e "${idsCL[LightRed]}Dell OMSA legacy installation is limited to PowerEdge x30/x40 systems.${idsCL[Default]}"
echo "Detected system: ${product_name:-Unknown}"
FINISH_FAILED_ACTION
return
fi
if ! TAPM_OMSA_SUPPORTED_PLATFORM; then
echo -e "${idsCL[LightRed]}This legacy OMSA package set requires PVE 9 on Debian 13 (Trixie), amd64.${idsCL[Default]}"
FINISH_FAILED_ACTION
return
fi
echo -e "${idsCL[LightYellow]}Installing legacy Dell OMSA 11.0 compatibility packages on ${product_name}.${idsCL[Default]}"
if ! TAPM_CREATE_TEMP_DIR omsa; then
FINISH_FAILED_ACTION
return
fi
temp_dir="$TAPM_TEMP_DIR"
dell_key="${temp_dir}/dell-openmanage.asc"
dell_keyring="${temp_dir}/linux.dell.com.gpg"
dell_source="${temp_dir}/linux.dell.com.list"
if ! TAPM_DOWNLOAD_SHA256 \
'https://linux.dell.com/repo/pgp_pubkeys/0x1285491434D8786F.asc' \
"$dell_key" \
'92f9622bf300f1fc8a4ef12d8e5efef6511b089c63c15e635cfc7429499e86d4' \
'Dell OpenManage signing key'; then
TAPM_CLEAN_TEMP_DIR "$temp_dir"
FINISH_FAILED_ACTION
return
fi
for index in "${!filenames[@]}"; do
package_paths+=("${temp_dir}/${filenames[$index]}")
if ! TAPM_DOWNLOAD_SHA256 "${urls[$index]}" "${package_paths[$index]}" \
"${checksums[$index]}" "${filenames[$index]}"; then
TAPM_CLEAN_TEMP_DIR "$temp_dir"
FINISH_FAILED_ACTION
return
fi
done
if ! apt-get update ||
! DEBIAN_FRONTEND=noninteractive apt-get install -y \
gnupg libcurl4t64 libncurses6 libxslt1.1 libgpm2 libtinfo6 ||
! gpg --batch --yes --dearmor --output "$dell_keyring" "$dell_key"; then
TAPM_CLEAN_TEMP_DIR "$temp_dir"
echo -e "${idsCL[LightRed]}Unable to prepare the Dell OMSA repository.${idsCL[Default]}"
FINISH_FAILED_ACTION
return
fi
if ! printf '%s\n' \
'deb [signed-by=/etc/apt/keyrings/linux.dell.com.gpg] https://linux.dell.com/repo/community/openmanage/11000/jammy jammy main' \
>"$dell_source" ||
! mkdir -p /etc/apt/keyrings ||
! install -m 0644 "$dell_keyring" /etc/apt/keyrings/linux.dell.com.gpg ||
! install -m 0644 "$dell_source" /etc/apt/sources.list.d/linux.dell.com.list; then
TAPM_CLEAN_TEMP_DIR "$temp_dir"
echo -e "${idsCL[LightRed]}Unable to write the Dell OMSA repository configuration.${idsCL[Default]}"
FINISH_FAILED_ACTION
return
fi
if ! apt-get update; then
TAPM_CLEAN_TEMP_DIR "$temp_dir"
echo -e "${idsCL[LightRed]}The Dell OMSA repository could not be refreshed.${idsCL[Default]}"
FINISH_FAILED_ACTION
return
fi
if ! dpkg -i "${package_paths[@]}"; then
if ! DEBIAN_FRONTEND=noninteractive apt-get install --fix-broken -y ||
! dpkg -i "${package_paths[@]}"; then
TAPM_CLEAN_TEMP_DIR "$temp_dir"
echo -e "${idsCL[LightRed]}The legacy OMSA compatibility packages could not be installed.${idsCL[Default]}"
FINISH_FAILED_ACTION
return
fi
fi
if ! DEBIAN_FRONTEND=noninteractive apt-get install -y srvadmin-all ||
! TAPM_PACKAGE_INSTALLED srvadmin-all ||
[[ ! -x /opt/dell/srvadmin/sbin/srvadmin-services.sh ]] ||
! /opt/dell/srvadmin/sbin/srvadmin-services.sh start ||
! TAPM_WAIT_FOR_TCP_PORT 1311 30; then
TAPM_CLEAN_TEMP_DIR "$temp_dir"
echo -e "${idsCL[LightRed]}Dell OMSA installation failed or port 1311 did not become available.${idsCL[Default]}"
FINISH_FAILED_ACTION
return
fi
TAPM_CLEAN_TEMP_DIR "$temp_dir"
echo -e "\n${idsCL[Green]}Dell OMSA has been installed and verified.${idsCL[Default]}"
echo -e "\n${idsCL[LightCyan]}Available at: ${idsCL[LightGreen]}https://${RNIP}:1311${idsCL[Default]}"
FINISH_ACTION
}
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_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 TAPM_VIRTIO_FILE_EXISTS "$VIRTIO_STABLE_FILE"; then
VIRTIO_STABLE_STATUS='current'
elif TAPM_ANY_LOCAL_VIRTIO_ISOS; then
VIRTIO_STABLE_STATUS='update'
else
VIRTIO_STABLE_STATUS='available'
fi
}
TAPM_CHECK_VIRTIO_STABLE() {
local force="${1:-0}"
local effective_url
local source_filename
if (( VIRTIO_STABLE_CHECKED == 1 && force == 0 )); then
return
fi
VIRTIO_STABLE_CHECKED=1
VIRTIO_STABLE_STATUS='unavailable'
VIRTIO_STABLE_FILE=''
effective_url="$(
curl --fail --location --silent --show-error --head \
--proto '=https' --proto-redir '=https' \
--connect-timeout 5 --max-time 15 \
--output /dev/null --write-out '%{url_effective}' \
"$VIRTIO_STABLE_URL" 2>/dev/null
)" || return 1
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
}
TAPM_VALID_VIRTIO_ISO() {
local iso_file="$1"
local iso_signature
local size
size="$(stat -c '%s' "$iso_file" 2>/dev/null)" || return 1
(( size >= 10 * 1024 * 1024 )) || return 1
iso_signature="$(
dd if="$iso_file" bs=1 skip=32769 count=5 status=none 2>/dev/null
)"
[[ "$iso_signature" == 'CD001' ]]
}
TAPM_DOWNLOAD_VIRTIO_ISO() {
local url="$1"
local expected_filename="${2:-}"
local filename_label="$3"
local description="${4:-VirtIO driver ISO}"
local effective_url
local filename
local final_file
local partial_file
local size
VIRTIO_LAST_FILE=''
if ! TAPM_VALID_HTTPS_URL "$url" ||
[[ "$url" != https://fedorapeople.org/groups/virt/virtio-win/direct-downloads/* ]]; then
echo -e "${idsCL[LightRed]}The VirtIO download URL is not an approved Fedora VirtIO-Win URL.${idsCL[Default]}"
return 1
fi
if [[ ! -d "$DLDIR" ]] && ! install -d -m 0755 "$DLDIR"; then
echo -e "${idsCL[LightRed]}Could not create the ISO destination: ${DLDIR}.${idsCL[Default]}"
return 1
fi
if [[ ! -w "$DLDIR" ]]; then
echo -e "${idsCL[LightRed]}The ISO destination is not writable: ${DLDIR}.${idsCL[Default]}"
return 1
fi
partial_file="$(mktemp "${DLDIR}/.tapm-virtio.part.XXXXXX")" || {
echo -e "${idsCL[LightRed]}Could not create a temporary file on ${DLDIR}.${idsCL[Default]}"
return 1
}
chmod 0600 "$partial_file"
echo -e "\n${idsCL[LightCyan]}Downloading ${description}...${idsCL[Default]}\n"
effective_url="$(
curl --fail --location --show-error --progress-bar \
--proto '=https' --proto-redir '=https' \
--connect-timeout 10 \
--output "$partial_file" --write-out '%{url_effective}' \
"$url"
)" || {
rm -f -- "$partial_file"
echo -e "\n${idsCL[LightRed]}The VirtIO ISO download failed. Existing ISOs were not changed.${idsCL[Default]}"
return 1
}
filename="$(TAPM_VIRTIO_FILENAME_FROM_URL "$effective_url")" || {
rm -f -- "$partial_file"
echo -e "${idsCL[LightRed]}The VirtIO source returned an unexpected filename.${idsCL[Default]}"
return 1
}
if [[ -n "$expected_filename" && "$filename" != "$expected_filename" ]]; then
rm -f -- "$partial_file"
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"
echo -e "${idsCL[LightRed]}The downloaded file did not pass ISO validation and was removed.${idsCL[Default]}"
return 1
fi
final_file="${DLDIR}/${filename}"
if ! chmod 0644 "$partial_file" ||
! mv -f -- "$partial_file" "$final_file"; then
rm -f -- "$partial_file"
echo -e "${idsCL[LightRed]}Could not install the validated VirtIO ISO in ${DLDIR}.${idsCL[Default]}"
return 1
fi
size="$(stat -c '%s' "$final_file")"
VIRTIO_LAST_FILE="$filename"
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() {
TAPM_SELECT_ISO_STORAGE || return
if ! TAPM_DOWNLOAD_VIRTIO_ISO "$VIRTIO_STABLE_URL" '' latest \
'current stable VirtIO drivers'; then
FINISH_FAILED_ACTION
return
fi
VIRTIO_STABLE_FILE="$VIRTIO_LAST_FILE"
VIRTIO_STABLE_STATUS='current'
FINISH_ACTION
}
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}"
TAPM_SELECT_ISO_STORAGE || return
if ! TAPM_DOWNLOAD_VIRTIO_ISO "$url" "$filename" "$filename_label" "$description"; then
FINISH_FAILED_ACTION
return
fi
FINISH_ACTION
}
DOWNLOAD_CUSTOM_VIRTIO_ARCHIVE() {
local release
echo
read -r -p "Archived VirtIO release (example: 0.1.229-1; blank cancels): " release
[[ -n "$release" ]] || return
if [[ ! "$release" =~ ^0\.1\.[0-9]+-[0-9]+$ ]]; then
echo -e "${idsCL[LightRed]}Use a release in the form 0.1.229-1.${idsCL[Default]}"
ENTER2CONTINUE
return
fi
DOWNLOAD_VIRTIO_ARCHIVE "$release" "VirtIO archive ${release}"
}
SHOW_LOCAL_VIRTIO_ISOS() {
local index
local file
local found=0
MENU_HEADER
echo
echo -e " ${idsCL[LightCyan]}Downloaded VirtIO ISOs${idsCL[Default]}"
echo
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..." _
}
VIRTIO_MENU() {
local stable_label
local -a labels
local -a values=(
"stable"
"server2016"
"server2012"
"server2008r2"
"server2008"
"archive"
"local"
"refresh"
)
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)
stable_label="Current stable drivers (${VIRTIO_STABLE_FILE} downloaded)"
;;
update)
stable_label="Current stable drivers (${VIRTIO_STABLE_FILE} update available)"
;;
available)
stable_label="Current stable drivers (${VIRTIO_STABLE_FILE})"
;;
*)
stable_label="Current stable drivers (version check unavailable)"
;;
esac
labels=(
"$stable_label"
"Windows Server 2016 compatibility ISO (0.1.240)"
"Windows Server 2012/R2 compatibility ISO (0.1.189)"
"Windows Server 2008 R2 compatibility ISO (0.1.172)"
"Windows Server 2008 compatibility ISO (0.1.141)"
"Download another archived VirtIO release"
"View downloaded VirtIO ISOs"
"Refresh stable-version check"
)
TAPM_VIRTIO_FILE_EXISTS 'virtio-win-server-2016-0.1.240.iso' &&
labels[1]+=" (downloaded)"
TAPM_VIRTIO_FILE_EXISTS 'virtio-win-server-2012r2-0.1.189.iso' &&
labels[2]+=" (downloaded)"
TAPM_VIRTIO_FILE_EXISTS 'virtio-win-server-2008r2-0.1.172.iso' &&
labels[3]+=" (downloaded)"
TAPM_VIRTIO_FILE_EXISTS 'virtio-win-server-2008-0.1.141.iso' &&
labels[4]+=" (downloaded)"
SELECT_MENU "VirtIO Driver Downloads" labels values
case "$MENU_SELECTION" in
stable) DOWNLOAD_VIRTIO_STABLE;;
server2016)
DOWNLOAD_VIRTIO_ARCHIVE 0.1.240-1 \
"Windows Server 2016 compatibility drivers" server-2016
;;
server2012)
DOWNLOAD_VIRTIO_ARCHIVE 0.1.189-1 \
"Windows Server 2012/R2 compatibility drivers" server-2012r2
;;
server2008r2)
DOWNLOAD_VIRTIO_ARCHIVE 0.1.172-1 \
"Windows Server 2008 R2 compatibility drivers" server-2008r2
;;
server2008)
DOWNLOAD_VIRTIO_ARCHIVE 0.1.141-1 \
"Windows Server 2008 compatibility drivers" server-2008
;;
archive) DOWNLOAD_CUSTOM_VIRTIO_ARCHIVE;;
local) SHOW_LOCAL_VIRTIO_ISOS;;
refresh)
echo -en "\n${idsCL[LightCyan]}Refreshing the stable VirtIO release...${idsCL[Default]} "
TAPM_CHECK_VIRTIO_STABLE 1 || true
echo
;;
back) return;;
quit) EXIT1; exit 0;;
esac
done
}
TAPM_INSTALL_PROXCLMC() {
local key_url='https://git.gyptazy.com/api/packages/gyptazy/debian/repository.key'
local keyring='/etc/apt/keyrings/gyptazy.asc'
local repository_file='/etc/apt/sources.list.d/gyptazy.list'
local repository_line='deb [signed-by=/etc/apt/keyrings/gyptazy.asc] https://packages.gyptazy.com/api/packages/gyptazy/debian trixie main'
local temp_dir
local temp_key
TAPM_PACKAGE_INSTALLED proxclmc && command -v proxclmc >/dev/null 2>&1 &&
return 0
TAPM_CREATE_TEMP_DIR proxclmc || return 1
temp_dir="$TAPM_TEMP_DIR"
temp_key="${temp_dir}/gyptazy.asc"
TAPM_DOWNLOAD_HTTPS "$key_url" "$temp_key" "ProxCLMC repository key" ||
return 1
if ! command -v gpg >/dev/null 2>&1 ||
! gpg --batch --show-keys "$temp_key" >/dev/null 2>&1; then
echo -e "${idsCL[LightRed]}The downloaded ProxCLMC repository key is not a valid OpenPGP key.${idsCL[Default]}"
return 1
fi
if ! install -d -m 0755 /etc/apt/keyrings ||
! install -m 0644 "$temp_key" "$keyring" ||
! printf '%s\n' "$repository_line" >"$repository_file" ||
! chmod 0644 "$repository_file"; then
echo -e "${idsCL[LightRed]}Could not configure the ProxCLMC package repository.${idsCL[Default]}"
return 1
fi
if ! apt-get update ||
! apt-get install -y proxclmc ||
! command -v proxclmc >/dev/null 2>&1; then
echo -e "${idsCL[LightRed]}ProxCLMC could not be installed.${idsCL[Default]}"
return 1
fi
TAPM_CLEAN_TEMP_DIR "$temp_dir"
}
TAPM_CLUSTER_QEMU_GUESTS() {
pvesh get /cluster/resources --type vm --output-format json 2>/dev/null |
python3 -c '
import json
import sys
for guest in json.load(sys.stdin):
if guest.get("type") != "qemu":
continue
print(
guest.get("vmid", ""),
str(guest.get("name", ""))
.replace("\x1f", " ")
.replace("\r", " ")
.replace("\n", " "),
guest.get("node", ""),
"yes" if guest.get("template") in (1, True, "1") else "no",
sep="\x1f",
)
'
}
TAPM_QEMU_CPU_MODEL() {
local vmid="$1"
local node="$2"
local config_file
local config_output
local cpu_model
[[ "$vmid" =~ ^[1-9][0-9]{2,8}$ ]] || return 1
[[ "$node" =~ ^[A-Za-z0-9][A-Za-z0-9._-]{0,62}$ ]] || return 1
config_file="/etc/pve/nodes/${node}/qemu-server/${vmid}.conf"
if [[ -r "$config_file" ]]; then
cpu_model="$(
awk '
$1 == "cpu:" {
sub(/^[^:]*:[[:space:]]*/, "")
print
exit
}
' "$config_file"
)" || return 1
[[ -n "$cpu_model" ]] || cpu_model='kvm64'
printf '%s\n' "$cpu_model"
return 0
fi
# Fall back to the node-aware API if the shared pmxcfs entry is briefly
# unavailable, such as while cluster membership is changing.
config_output="$(
pvesh get "/nodes/${node}/qemu/${vmid}/config" \
--output-format json 2>/dev/null
)" || return 1
cpu_model="$(
QEMU_CONFIG_JSON="$config_output" python3 -c '
import json, os
try:
config = json.loads(os.environ["QEMU_CONFIG_JSON"])
except (TypeError, ValueError):
raise SystemExit(1)
cpu = config.get("cpu", "kvm64")
print(cpu if isinstance(cpu, str) and cpu.strip() else "kvm64")
' 2>/dev/null
)" || return 1
[[ -n "$cpu_model" ]] || cpu_model='kvm64'
printf '%s\n' "$cpu_model"
}
TAPM_SET_QEMU_CPU_MODEL() {
local vmid="$1"
local node="$2"
local cpu_model="$3"
[[ "$vmid" =~ ^[1-9][0-9]{2,8}$ ]] || return 1
[[ "$node" =~ ^[A-Za-z0-9][A-Za-z0-9._-]{0,62}$ ]] || return 1
[[ "$cpu_model" =~ ^x86-64-v(1|2-AES|3|4)$ ]] || return 1
pvesh set "/nodes/${node}/qemu/${vmid}/config" \
--cpu "$cpu_model"
}
DETECT_CPU() {
local answer
local cpu_model
local current_cpu
local guest
local guest_output
local name
local node
local template
local vmid
local -a changes=()
local -a failures=()
local -a successes=()
for command in apt-get curl gpg install pvesh python3; do
if ! command -v "$command" >/dev/null 2>&1; then
echo -e "${idsCL[LightRed]}${command} is required for CPU compatibility detection.${idsCL[Default]}"
FINISH_FAILED_ACTION
return
fi
done
if ! TAPM_INSTALL_PROXCLMC; then
FINISH_FAILED_ACTION
return
fi
echo -e "\n${idsCL[LightCyan]}Analyzing CPU compatibility across the cluster...${idsCL[Default]}"
cpu_model="$(proxclmc --list-only 2>/dev/null)" || {
echo -e "${idsCL[LightRed]}ProxCLMC could not determine a compatible CPU model.${idsCL[Default]}"
FINISH_FAILED_ACTION
return
}
cpu_model="${cpu_model//$'\r'/}"
cpu_model="${cpu_model//$'\n'/}"
if [[ ! "$cpu_model" =~ ^x86-64-v(1|2-AES|3|4)$ ]]; then
echo -e "${idsCL[LightRed]}ProxCLMC returned an unexpected CPU model: ${cpu_model:-empty}.${idsCL[Default]}"
FINISH_FAILED_ACTION
return
fi
guest_output="$(TAPM_CLUSTER_QEMU_GUESTS)" || {
echo -e "${idsCL[LightRed]}Could not read cluster QEMU resources.${idsCL[Default]}"
FINISH_FAILED_ACTION
return
}
if [[ -z "$guest_output" ]]; then
echo -e "\n${idsCL[LightCyan]}No QEMU VMs or templates were found in the cluster.${idsCL[Default]}"
FINISH_ACTION
return
fi
while IFS=$'\x1f' read -r vmid name node template; do
[[ -n "$vmid" ]] || continue
current_cpu="$(TAPM_QEMU_CPU_MODEL "$vmid" "$node")" || {
echo -e "${idsCL[LightRed]}Could not read the CPU configuration for VMID ${vmid}.${idsCL[Default]}"
FINISH_FAILED_ACTION
return
}
[[ "$current_cpu" == "$cpu_model" ]] && continue
changes+=(
"${vmid}"$'\x1f'"${name}"$'\x1f'"${node}"$'\x1f'"${template}"$'\x1f'"${current_cpu}"
)
done <<< "$guest_output"
if (( ${#changes[@]} == 0 )); then
echo -e "\n${idsCL[Green]}All QEMU VMs and templates already use ${cpu_model}.${idsCL[Default]}"
FINISH_ACTION
return
fi
printf '\nRecommended cluster CPU model: %s\n' "$cpu_model"
printf '\n%-7s %-28s %-20s %-10s %-24s %s\n' \
'VMID' 'NAME' 'NODE' 'TEMPLATE' 'CURRENT CPU' 'PROPOSED CPU'
printf '%-7s %-28s %-20s %-10s %-24s %s\n' \
'-------' '----------------------------' '--------------------' \
'----------' '------------------------' '----------------'
for guest in "${changes[@]}"; do
IFS=$'\x1f' read -r vmid name node template current_cpu <<< "$guest"
printf '%-7s %-28.28s %-20.20s %-10s %-24.24s %s\n' \
"$vmid" "${name:-unnamed}" "$node" "$template" "$current_cpu" "$cpu_model"
done
echo -en "\n${idsCL[LightCyan]}Apply this CPU model to ${#changes[@]} VM(s) and template(s) (y/N)?${idsCL[Default]} "
read -r -n 1 answer
echo
[[ "$answer" =~ ^[Yy]$ ]] || return
for guest in "${changes[@]}"; do
IFS=$'\x1f' read -r vmid name node template current_cpu <<< "$guest"
if TAPM_SET_QEMU_CPU_MODEL "$vmid" "$node" "$cpu_model" &&
[[ "$(TAPM_QEMU_CPU_MODEL "$vmid" "$node")" == "$cpu_model" ]]; then
successes+=("$guest")
else
failures+=("$guest")
fi
done
printf '\nCPU model update summary:\n'
printf ' Successful: %d\n' "${#successes[@]}"
printf ' Failed: %d\n' "${#failures[@]}"
if (( ${#failures[@]} > 0 )); then
printf '\nFailed VM/template updates:\n'
for guest in "${failures[@]}"; do
IFS=$'\x1f' read -r vmid name node template current_cpu <<< "$guest"
printf ' %s (%s) on %s\n' "$vmid" "${name:-unnamed}" "$node"
done
echo -e "\n${idsCL[LightRed]}One or more CPU model updates failed.${idsCL[Default]}"
FINISH_FAILED_ACTION
return
fi
echo -e "\n${idsCL[Green]}The CPU model was updated to ${cpu_model}.${idsCL[Default]}"
echo -e "${idsCL[LightCyan]}Running VMs must be fully shut down and started again before the new CPU model takes effect.${idsCL[Default]}"
FINISH_ACTION
}
RESTART_SERVICE_GROUP() {
local description="$1"
shift
local -a services=("$@")
local service
local failed=0
local choice
if (( ${RESTART_ASSUME_YES:-0} == 1 )); then
choice="y"
else
echo -en "${idsCL[LightCyan]}Restart ${description} on this host (Y/n)?${idsCL[Default]} "
read -r -n 1 choice
echo
fi
[[ "$choice" =~ ^[Nn]$ ]] && return
echo -e "\n${idsCL[Yellow]}Restarting ${description}...${idsCL[Default]}"
if ! systemctl restart "${services[@]}"; then
failed=1
fi
echo
for service in "${services[@]}"; do
if systemctl is-active --quiet "$service"; then
echo -e " ${idsCL[Green]}[active]${idsCL[Default]} ${service}"
else
echo -e " ${idsCL[Red]}[failed]${idsCL[Default]} ${service}"
failed=1
fi
done
if (( failed == 0 )); then
echo -e "\n${idsCL[Green]}Service restart completed successfully.${idsCL[Default]}"
else
echo -e "\n${idsCL[Red]}One or more services failed to restart.${idsCL[Default]}"
echo "Review: journalctl -u <service> --since '-10 minutes'"
fi
FINISH_ACTION
}
RESTART_CLUSTER_FILESYSTEM() {
local choice
echo -e "${idsCL[LightYellow]}This temporarily interrupts /etc/pve (pmxcfs) and cluster configuration access.${idsCL[Default]}"
echo -e "${idsCL[LightYellow]}It does not restart Corosync or the HA daemons.${idsCL[Default]}"
if systemctl is-active --quiet corosync &&
! timeout 5 pvecm status 2>/dev/null |
grep -Eq '^Quorate:[[:space:]]+Yes[[:space:]]*$'; then
echo -e "\n${idsCL[Red]}The cluster is not quorate. pve-cluster will not be restarted.${idsCL[Default]}"
ENTER2CONTINUE
return
fi
echo -en "\n${idsCL[LightCyan]}Restart pve-cluster on this host (y/N)?${idsCL[Default]} "
read -r -n 1 choice
echo
[[ "$choice" =~ ^[Yy]$ ]] || return
if systemctl restart pve-cluster &&
systemctl is-active --quiet pve-cluster &&
timeout 15 bash -c 'until test -d /etc/pve/nodes; do sleep 1; done'; then
echo -e "\n${idsCL[Green]}pve-cluster restarted and /etc/pve is available.${idsCL[Default]}"
else
echo -e "\n${idsCL[Red]}pve-cluster did not recover normally.${idsCL[Default]}"
echo "Review: journalctl -u pve-cluster --since '-10 minutes'"
fi
ENTER2CONTINUE
}
RESTART_PVE_SERVICES() {
local requested_choice="${1:-}"
local RESTART_ASSUME_YES=0
[[ "$requested_choice" =~ ^[Nn]$ ]] && return
[[ "$requested_choice" =~ ^[Yy]$ ]] && RESTART_ASSUME_YES=1
RESTART_SERVICE_GROUP "core Proxmox management services" \
pvedaemon pveproxy pvestatd pvescheduler
}
HA_NODE_IN_MAINTENANCE() {
local node="$1"
local ha_status
ha_status="$(ha-manager status 2>/dev/null)" || return 2
grep -F "lrm ${node} " <<< "$ha_status" |
grep -q "maintenance mode"
}
WAIT_FOR_HA_MAINTENANCE_STATE() {
local node="$1"
local expected_state="$2"
local attempts="${3:-30}"
local attempt=0
local active=0
local state_result
while (( attempt < attempts )); do
active=0
HA_NODE_IN_MAINTENANCE "$node"
state_result=$?
[[ "$state_result" -eq 0 ]] && active=1
if [[ "$state_result" -gt 1 ]]; then
sleep 1
((attempt++))
continue
fi
if [[ "$expected_state" == "enabled" && "$active" -eq 1 ]] ||
[[ "$expected_state" == "disabled" && "$active" -eq 0 ]]; then
return 0
fi
sleep 1
((attempt++))
done
return 1
}
MAINTENANCE_MODE() {
local choice
local local_node
local maintenance_state
local_node="$(hostname -s)"
HA_NODE_IN_MAINTENANCE "$local_node"
maintenance_state=$?
if [[ "$maintenance_state" -gt 1 ]]; then
echo -e "\n${idsCL[LightRed]}Could not read HA maintenance status for ${local_node}.${idsCL[Default]}"
FINISH_FAILED_ACTION
return
fi
if [[ "$maintenance_state" -eq 0 ]]; then
echo -en "${idsCL[LightCyan]}Take the local host out of maintenance mode (Y/n)?${idsCL[Default]} "
else
echo -en "${idsCL[LightCyan]}Put the local host into maintenance mode (Y/n)?${idsCL[Default]} "
fi
read -r -n 1 choice
case "$choice" in
[Nn]) echo;;
*) echo
if [[ "$maintenance_state" -eq 0 ]]; then
if ! ha-manager crm-command node-maintenance disable "$local_node" ||
! WAIT_FOR_HA_MAINTENANCE_STATE "$local_node" disabled; then
echo -e "\n${idsCL[LightRed]}Failed to take ${local_node} out of HA maintenance mode.${idsCL[Default]}"
FINISH_FAILED_ACTION
return
fi
echo -e "\n${idsCL[Green]}This host will be taken out of maintenance mode${idsCL[Default]}\n"
else
if ! ha-manager crm-command node-maintenance enable "$local_node"; then
echo -e "\n${idsCL[LightRed]}Failed to request HA maintenance mode for ${local_node}.${idsCL[Default]}"
FINISH_FAILED_ACTION
return
fi
echo -e "\n${idsCL[Green]}This host will be entered into maintenance mode${idsCL[Default]}\n"
if ! bash /opt/idssys/ta-proxmenu/inc/evacuate-proxmox-node.sh; then
echo -e "\n${idsCL[LightRed]}Evacuation did not complete. ${local_node} remains in HA maintenance mode.${idsCL[Default]}"
FINISH_FAILED_ACTION
return
fi
fi
FINISH_ACTION
;;
esac
}
INSTALL_KEEPALIVE() {
echo
bash /opt/idssys/ta-proxmenu/inc/deploy-proxmox-keepalived.sh
echo -e "\n${idsCL[Green]}Keepalive has been installed${idsCL[Default]}"
FINISH_ACTION
}
UPDATE_CACHE_DIR='/var/cache/ta-proxmenu'
UPDATE_CACHE_FILE="${UPDATE_CACHE_DIR}/update-status"
UPDATE_CACHE_SECONDS=14400
UPDATE_CHECK_PID=''
UPDATE_STATUS='unknown'
UPDATE_REMOTE_COMMIT=''
WRITE_UPDATE_CACHE() {
local status="$1"
local branch="$2"
local local_commit="$3"
local remote_commit="$4"
local cache_temp
mkdir -p "$UPDATE_CACHE_DIR" || return 1
cache_temp="$(mktemp "${UPDATE_CACHE_DIR}/.update-status.XXXXXX")" || return 1
printf '%s|%s|%s|%s|%s\n' \
"$(date +%s)" "$branch" "$local_commit" "$remote_commit" "$status" \
>"$cache_temp"
chmod 0644 "$cache_temp"
mv -f "$cache_temp" "$UPDATE_CACHE_FILE"
}
UPDATE_CHECK_WORKER() {
local branch
local local_commit
local remote_commit
local status
branch="$(git -C "$FOLDER" branch --show-current 2>/dev/null)"
local_commit="$(git -C "$FOLDER" rev-parse HEAD 2>/dev/null)"
if [[ -z "$branch" || -z "$local_commit" ]]; then
WRITE_UPDATE_CACHE "unavailable" "$branch" "$local_commit" ""
return
fi
if TAPM_GIT_WORKTREE_DIRTY "$FOLDER"; then
WRITE_UPDATE_CACHE "dirty" "$branch" "$local_commit" ""
return
fi
if ! TAPM_GIT_FETCH_BRANCH "$FOLDER" "$branch" 30 >/dev/null 2>&1; then
WRITE_UPDATE_CACHE "unavailable" "$branch" "$local_commit" ""
return
fi
remote_commit="$(git -C "$FOLDER" rev-parse "refs/remotes/origin/${branch}" 2>/dev/null)"
status="$(TAPM_GIT_BRANCH_STATE "$FOLDER" "$branch")"
WRITE_UPDATE_CACHE "$status" "$branch" "$local_commit" "$remote_commit"
}
START_UPDATE_CHECK() {
if [[ -n "$UPDATE_CHECK_PID" ]] &&
kill -0 "$UPDATE_CHECK_PID" 2>/dev/null; then
return
fi
UPDATE_CHECK_WORKER >/dev/null 2>&1 &
UPDATE_CHECK_PID="$!"
}
LOAD_UPDATE_STATUS() {
local checked_at
local cached_branch
local cached_local
local cached_remote
local cached_status
local current_branch
local current_commit
local current_dirty=0
local now
UPDATE_STATUS='unknown'
UPDATE_REMOTE_COMMIT=''
current_branch="$(git -C "$FOLDER" branch --show-current 2>/dev/null)"
current_commit="$(git -C "$FOLDER" rev-parse HEAD 2>/dev/null)"
TAPM_GIT_WORKTREE_DIRTY "$FOLDER" && current_dirty=1
now="$(date +%s)"
if [[ -r "$UPDATE_CACHE_FILE" ]]; then
IFS='|' read -r checked_at cached_branch cached_local cached_remote cached_status \
<"$UPDATE_CACHE_FILE"
if [[ "$checked_at" =~ ^[0-9]+$ ]] &&
(( now - checked_at < UPDATE_CACHE_SECONDS )) &&
[[ "$cached_branch" == "$current_branch" ]] &&
[[ "$cached_local" == "$current_commit" ]] &&
[[ "$cached_status" =~ ^(current|behind|ahead|diverged|dirty|unavailable)$ ]] &&
{ [[ "$cached_status" == "dirty" && "$current_dirty" -eq 1 ]] ||
[[ "$cached_status" != "dirty" && "$current_dirty" -eq 0 ]]; }; then
UPDATE_STATUS="$cached_status"
UPDATE_REMOTE_COMMIT="$cached_remote"
return
fi
fi
UPDATE_STATUS='checking'
START_UPDATE_CHECK
}
UPDATE_LAST_CHECKED_DISPLAY() {
local checked_at
[[ -r "$UPDATE_CACHE_FILE" ]] || {
printf '%s\n' 'never'
return
}
IFS='|' read -r checked_at _ <"$UPDATE_CACHE_FILE"
[[ "$checked_at" =~ ^[0-9]+$ ]] || {
printf '%s\n' 'unknown'
return
}
date --date="@${checked_at}" '+%Y-%m-%d %H:%M %Z' 2>/dev/null ||
printf '%s\n' 'unknown'
}
FORCE_UPDATE_CHECK() {
local attempts=0
rm -f "$UPDATE_CACHE_FILE"
UPDATE_CHECK_PID=''
START_UPDATE_CHECK
echo -en "${idsCL[LightCyan]}Checking current branch for updates"
while (( attempts < 20 )); do
sleep 0.5
LOAD_UPDATE_STATUS
[[ "$UPDATE_STATUS" != "checking" ]] && break
echo -n "."
((attempts++))
done
echo -e "${idsCL[Default]}"
case "$UPDATE_STATUS" in
behind)
echo -e "${idsCL[LightYellow]}An update is available for the current branch.${idsCL[Default]}"
;;
current)
echo -e "${idsCL[Green]}TA-ProxMenu is current.${idsCL[Default]}"
;;
ahead)
echo -e "${idsCL[LightYellow]}The current branch has local commits not on origin.${idsCL[Default]}"
;;
diverged)
echo -e "${idsCL[LightRed]}The current branch has diverged from origin; automatic update is disabled.${idsCL[Default]}"
;;
dirty)
echo -e "${idsCL[LightYellow]}The TA-ProxMenu repository has local file changes.${idsCL[Default]}"
;;
*)
echo -e "${idsCL[Red]}The update status could not be determined.${idsCL[Default]}"
;;
esac
ENTER2CONTINUE
}
MENU_HEADER() {
local version_display
# Reuse a settled update result instead of running Git checks on every
# arrow-key redraw. Continue refreshing only while the worker is pending.
if [[ "$UPDATE_STATUS" == 'unknown' || "$UPDATE_STATUS" == 'checking' ]]; then
LOAD_UPDATE_STATUS
fi
case "$UPDATE_STATUS" in
behind)
version_display="${idsCL[LightYellow]}${VERS} ** UPDATE AVAILABLE **${idsCL[Default]}"
;;
ahead)
version_display="${idsCL[LightYellow]}${VERS} ** LOCAL COMMITS **${idsCL[Default]}"
;;
diverged)
version_display="${idsCL[LightRed]}${VERS} ** BRANCH DIVERGED **${idsCL[Default]}"
;;
dirty)
version_display="${idsCL[LightYellow]}${VERS} ** LOCAL CHANGES **${idsCL[Default]}"
;;
checking)
version_display="${idsCL[LightCyan]}${VERS} (checking for updates)${idsCL[Default]}"
;;
*)
version_display="${idsCL[White]}${VERS}${idsCL[Default]}"
;;
esac
clear
echo
echo -e " ${idsCL[Green]}TA-ProxMenu - Proxmox Setup Scripts${idsCL[Default]} ${version_display}"
echo -e "${idsCL[Green]}---------------------------------------------------------------------------${idsCL[Default]}"
echo -e " Hostname: ${idsCL[Cyan]}$(hostname -s)${idsCL[Default]}"
echo -e " IP Address: ${idsCL[Cyan]}${RNIP:-Unavailable}${idsCL[Default]}"
echo -e "${idsCL[Green]}---------------------------------------------------------------------------${idsCL[Default]}"
}
SELECT_MENU() {
local title="$1"
local labels_name="$2"
local values_name="$3"
local allow_back="${4:-1}"
local -n labels_ref="$labels_name"
local -n values_ref="$values_name"
local selected=0
local key
local sequence
local index
while true; do
MENU_HEADER
echo
echo -e " ${idsCL[LightCyan]}${title}${idsCL[Default]}"
echo
for index in "${!labels_ref[@]}"; do
if (( index == selected )); then
printf '\e[7m %d %-64s\e[0m\n' "$((index + 1))" "${labels_ref[$index]}"
else
printf ' %d %s\n' "$((index + 1))" "${labels_ref[$index]}"
fi
done
echo
if (( allow_back == 1 )); then
echo " ↑/↓ Navigate Enter Select Number Quick Select ←/Esc/B Back Q Quit"
else
echo " ↑/↓ Navigate Enter Select Number Quick Select Q Quit"
fi
key=""
if [[ "$UPDATE_STATUS" == "checking" ]]; then
IFS= read -rsn1 -t 1 key || continue
else
IFS= read -rsn1 key
fi
case "$key" in
"")
MENU_SELECTION="${values_ref[$selected]}"
return 0
;;
[1-9])
index=$((10#$key - 1))
if (( index < ${#values_ref[@]} )); then
MENU_SELECTION="${values_ref[$index]}"
return 0
fi
;;
[Qq])
MENU_SELECTION="quit"
return 0
;;
[Bb])
if (( allow_back == 1 )); then
MENU_SELECTION="back"
return 0
fi
;;
$'\e')
sequence=""
IFS= read -rsn2 -t 0.1 sequence || true
case "$sequence" in
"[A"|"OA")
(( selected = (selected - 1 + ${#labels_ref[@]}) % ${#labels_ref[@]} ))
;;
"[B"|"OB")
(( selected = (selected + 1) % ${#labels_ref[@]} ))
;;
"[C"|"OC")
MENU_SELECTION="${values_ref[$selected]}"
return 0
;;
"[D"|"OD"|"")
if (( allow_back == 1 )); then
MENU_SELECTION="back"
return 0
fi
;;
"[H")
selected=0
;;
"[F")
selected=$((${#labels_ref[@]} - 1))
;;
esac
;;
esac
done
}
TAPM_POST_PROFILE_SUMMARY() {
echo
echo -e " ${idsCL[LightCyan]}Recommended TAPM host profile${idsCL[Default]}"
echo
echo " [x] System utilities and CPU-appropriate microcode"
echo " [x] Chrony time synchronization (existing timezone preserved)"
echo " [x] Kernel panic recovery (30 seconds)"
echo " [x] Conservative inotify limits"
echo " [x] Persistent journald (1 GiB / 30 days)"
echo " [x] Verify and repair log rotation"
echo " [x] Host memory behavior (swappiness 10)"
echo " [x] Network safeguards"
echo " [x] BBR and TCP Fast Open"
echo " [x] Native parallel gzip support for vzdump"
echo " [ ] APT network compatibility / conditional IPv4 (optional)"
echo
echo " Global vzdump bandwidth and I/O-priority overrides are not applied."
}
TAPM_POST_FAILURE_RECOVERY() {
local backup_dir="$1"
local mode="$2"
local -a labels=(
"Retry the selected profile"
"Restore the pre-change backup"
"Stop and return to the menu"
)
local -a values=(retry restore stop)
while true; do
echo
[[ -n "$backup_dir" ]] && echo " Pre-change backup: ${backup_dir}"
SELECT_MENU "Host Configuration Recovery" labels values
case "$MENU_SELECTION" in
retry)
if TAPM_POST_APPLY_PROFILE "$mode"; then
echo -e "\n${idsCL[Green]}The profile retry completed successfully.${idsCL[Default]}"
echo " New backup: ${TAPM_POST_LAST_BACKUP}"
return 0
fi
echo -e "\n${idsCL[LightRed]}${TAPM_POST_LAST_ERROR:-The retry failed.}${idsCL[Default]}"
;;
restore)
if [[ -n "$backup_dir" ]] &&
TAPM_POST_RESTORE_BACKUP "$backup_dir"; then
echo -e "\n${idsCL[Green]}The pre-change configuration was restored.${idsCL[Default]}"
return 0
fi
echo -e "\n${idsCL[LightRed]}The pre-change backup could not be restored.${idsCL[Default]}"
;;
stop|back) return 1 ;;
quit) EXIT1; exit 0 ;;
esac
done
}
TAPM_POST_APPLY_RECOMMENDED() {
local backup_dir
local confirmation
TAPM_POST_DEFAULT_SELECTIONS
TAPM_POST_PROFILE_SUMMARY
read -r -p " Apply this profile to the current host (type yes to continue)? " \
confirmation
[[ "${confirmation,,}" == yes ]] || return
if TAPM_POST_APPLY_PROFILE apply; then
echo -e "\n${idsCL[Green]}The TAPM host profile was applied and verified.${idsCL[Default]}"
echo " Backup: ${TAPM_POST_LAST_BACKUP}"
FINISH_ACTION
return
fi
echo -e "\n${idsCL[LightRed]}${TAPM_POST_LAST_ERROR:-Host profile application failed.}${idsCL[Default]}"
backup_dir="$TAPM_POST_LAST_BACKUP"
if TAPM_POST_FAILURE_RECOVERY "$backup_dir" apply; then
FINISH_ACTION
else
FINISH_FAILED_ACTION
fi
}
TAPM_POST_MIGRATE_PROXMENUX() {
local backup_dir
local confirmation
if ! TAPM_POST_PROXMENUX_DETECTED; then
echo -e "\n${idsCL[LightYellow]}No ProxMenux installation or recognized artifacts were detected.${idsCL[Default]}"
FINISH_FAILED_ACTION
return
fi
TAPM_POST_DEFAULT_SELECTIONS
TAPM_POST_PROFILE_SUMMARY
TAPM_POST_MIGRATION_PLAN
echo
echo -e " ${idsCL[LightYellow]}Migration will repair recognized ProxMenux settings, restore gzip,"
echo -e " apply the TAPM profile, and then remove ProxMenux itself.${idsCL[Default]}"
echo " Shared packages such as dialog, jq, curl, Git, and Python are retained."
echo
read -r -p " Migrate this host and remove ProxMenux (type yes to continue)? " \
confirmation
[[ "${confirmation,,}" == yes ]] || return
if TAPM_POST_APPLY_PROFILE migrate; then
echo -e "\n${idsCL[Green]}The host was migrated to the TAPM profile and ProxMenux was removed.${idsCL[Default]}"
echo " Backup: ${TAPM_POST_LAST_BACKUP}"
FINISH_ACTION
return
fi
echo -e "\n${idsCL[LightRed]}${TAPM_POST_LAST_ERROR:-ProxMenux migration failed.}${idsCL[Default]}"
echo " ProxMenux removal is the final migration step; earlier failures leave it installed."
backup_dir="$TAPM_POST_LAST_BACKUP"
if TAPM_POST_FAILURE_RECOVERY "$backup_dir" migrate; then
FINISH_ACTION
else
FINISH_FAILED_ACTION
fi
}
TAPM_POST_FLAG_LABEL() {
local value="$1"
local label="$2"
(( value == 1 )) && printf '[x] %s' "$label" || printf '[ ] %s' "$label"
}
TAPM_POST_TOGGLE_FLAG() {
local variable_name="$1"
local current_value="${!variable_name}"
(( current_value == 1 )) &&
printf -v "$variable_name" '%d' 0 ||
printf -v "$variable_name" '%d' 1
}
TAPM_POST_CUSTOMIZE() {
local -a labels
local -a values
TAPM_POST_DEFAULT_SELECTIONS
while true; do
labels=(
"$(TAPM_POST_FLAG_LABEL "$TAPM_POST_DO_UTILITIES" "System utilities")"
"$(TAPM_POST_FLAG_LABEL "$TAPM_POST_DO_TIME" "Time synchronization")"
"$(TAPM_POST_FLAG_LABEL "$TAPM_POST_DO_PANIC" "Kernel panic recovery")"
"$(TAPM_POST_FLAG_LABEL "$TAPM_POST_DO_LIMITS" "Conservative inotify limits")"
"$(TAPM_POST_FLAG_LABEL "$TAPM_POST_DO_JOURNALD" "Journald — 1 GiB / 30 days")"
"$(TAPM_POST_FLAG_LABEL "$TAPM_POST_DO_LOGROTATE" "Verify and repair log rotation")"
"$(TAPM_POST_FLAG_LABEL "$TAPM_POST_DO_MEMORY" "Host memory behavior")"
"$(TAPM_POST_FLAG_LABEL "$TAPM_POST_DO_NETWORK" "Network safeguards")"
"$(TAPM_POST_FLAG_LABEL "$TAPM_POST_DO_BBR" "BBR and TCP Fast Open")"
"$(TAPM_POST_FLAG_LABEL "$TAPM_POST_DO_PIGZ" "Native parallel gzip for vzdump")"
"$(TAPM_POST_FLAG_LABEL "$TAPM_POST_DO_APT_NETWORK" "APT network compatibility check")"
"Apply selected profile"
)
values=(
utilities time panic limits journald logrotate memory network bbr pigz
apt_network apply
)
SELECT_MENU "Customize TAPM Host Profile" labels values
case "$MENU_SELECTION" in
utilities) TAPM_POST_TOGGLE_FLAG TAPM_POST_DO_UTILITIES ;;
time) TAPM_POST_TOGGLE_FLAG TAPM_POST_DO_TIME ;;
panic) TAPM_POST_TOGGLE_FLAG TAPM_POST_DO_PANIC ;;
limits) TAPM_POST_TOGGLE_FLAG TAPM_POST_DO_LIMITS ;;
journald) TAPM_POST_TOGGLE_FLAG TAPM_POST_DO_JOURNALD ;;
logrotate) TAPM_POST_TOGGLE_FLAG TAPM_POST_DO_LOGROTATE ;;
memory) TAPM_POST_TOGGLE_FLAG TAPM_POST_DO_MEMORY ;;
network) TAPM_POST_TOGGLE_FLAG TAPM_POST_DO_NETWORK ;;
bbr) TAPM_POST_TOGGLE_FLAG TAPM_POST_DO_BBR ;;
pigz) TAPM_POST_TOGGLE_FLAG TAPM_POST_DO_PIGZ ;;
apt_network) TAPM_POST_TOGGLE_FLAG TAPM_POST_DO_APT_NETWORK ;;
apply)
local backup_dir
local confirmation
echo
read -r -p " Apply the selected profile (type yes to continue)? " \
confirmation
[[ "${confirmation,,}" == yes ]] || continue
if TAPM_POST_APPLY_PROFILE custom; then
echo -e "\n${idsCL[Green]}The custom TAPM profile was applied.${idsCL[Default]}"
echo " Backup: ${TAPM_POST_LAST_BACKUP}"
FINISH_ACTION
return
fi
echo -e "\n${idsCL[LightRed]}${TAPM_POST_LAST_ERROR:-Custom profile application failed.}${idsCL[Default]}"
backup_dir="$TAPM_POST_LAST_BACKUP"
if TAPM_POST_FAILURE_RECOVERY "$backup_dir" custom; then
FINISH_ACTION
else
FINISH_FAILED_ACTION
fi
return
;;
back) return ;;
quit) EXIT1; exit 0 ;;
esac
done
}
TAPM_POST_SHOW_AUDIT() {
MENU_HEADER
echo
TAPM_POST_AUDIT
FINISH_ACTION
}
TAPM_POST_SHOW_REPORT() {
local report='/var/lib/ta-proxmenu/post-install/last-report.txt'
MENU_HEADER
echo
if [[ -f "$report" ]]; then
cat "$report"
else
echo -e " ${idsCL[LightYellow]}No completed TAPM host configuration report is available.${idsCL[Default]}"
fi
FINISH_ACTION
}
TAPM_POST_RESTORE_MENU() {
local backup_base='/var/backups/ta-proxmenu/post-install'
local -a labels=()
local -a values=()
local backup_dir
local confirmation
if [[ -d "$backup_base" ]]; then
while IFS= read -r backup_dir; do
[[ -f "${backup_dir}/manifest.tsv" ]] || continue
labels+=("${backup_dir##*/}")
values+=("$backup_dir")
done < <(find "$backup_base" -mindepth 1 -maxdepth 1 -type d |
sort -r)
fi
if (( ${#labels[@]} == 0 )); then
echo -e "\n${idsCL[LightYellow]}No TAPM host configuration backups were found.${idsCL[Default]}"
FINISH_FAILED_ACTION
return
fi
SELECT_MENU "Restore TAPM Host Configuration Backup" labels values
case "$MENU_SELECTION" in
back) return ;;
quit) EXIT1; exit 0 ;;
esac
backup_dir="$MENU_SELECTION"
echo
echo " Selected backup: $backup_dir"
echo " Restoring may also reinstate files or services removed during migration."
read -r -p " Restore this backup (type restore to continue)? " confirmation
[[ "${confirmation,,}" == restore ]] || return
if TAPM_POST_RESTORE_BACKUP "$backup_dir"; then
echo -e "\n${idsCL[Green]}The selected configuration backup was restored.${idsCL[Default]}"
FINISH_ACTION
return
fi
echo -e "\n${idsCL[LightRed]}The selected backup could not be fully restored.${idsCL[Default]}"
FINISH_FAILED_ACTION
}
TAPM_POST_VZDUMP_APPLY() {
local mode="$1"
local bandwidth="${2:-}"
local ionice="${3:-}"
TAPM_POST_LAST_ERROR=''
if ! TAPM_POST_PRECHECK; then
echo -e "\n${idsCL[LightRed]}$TAPM_POST_LAST_ERROR${idsCL[Default]}"
FINISH_FAILED_ACTION
return
fi
if ! TAPM_POST_BACKUP >/dev/null; then
echo -e "\n${idsCL[LightRed]}The vzdump configuration backup failed.${idsCL[Default]}"
FINISH_FAILED_ACTION
return
fi
case "$mode" in
defaults)
TAPM_POST_REMOVE_COLON_KEY /etc/vzdump.conf bwlimit &&
TAPM_POST_REMOVE_COLON_KEY /etc/vzdump.conf ionice
;;
maximum)
TAPM_POST_SET_COLON_KEY /etc/vzdump.conf bwlimit 0 &&
TAPM_POST_SET_COLON_KEY /etc/vzdump.conf ionice 5
;;
custom)
TAPM_POST_SET_COLON_KEY /etc/vzdump.conf bwlimit "$bandwidth" &&
TAPM_POST_SET_COLON_KEY /etc/vzdump.conf ionice "$ionice"
;;
esac
if (( $? == 0 )); then
echo -e "\n${idsCL[Green]}vzdump performance settings were updated.${idsCL[Default]}"
echo " Backup: ${TAPM_POST_LAST_BACKUP}"
FINISH_ACTION
return
fi
echo -e "\n${idsCL[LightRed]}vzdump performance settings could not be updated.${idsCL[Default]}"
FINISH_FAILED_ACTION
}
TAPM_POST_VZDUMP_MENU() {
local -a labels=(
"Use Proxmox defaults — recommended"
"Maximum throughput — unlimited, ionice 5"
"Set a custom bandwidth limit"
"Restore a TAPM configuration backup"
)
local -a values=(defaults maximum custom restore)
local confirmation
local bandwidth_mib
local bandwidth_kib
local ionice
SELECT_MENU "Configure vzdump Performance" labels values
case "$MENU_SELECTION" in
defaults)
TAPM_POST_VZDUMP_APPLY defaults
;;
maximum)
echo
echo -e "${idsCL[LightYellow]}This can saturate shared storage and increase guest latency.${idsCL[Default]}"
read -r -p " Enable maximum throughput (type yes to continue)? " confirmation
[[ "${confirmation,,}" == yes ]] &&
TAPM_POST_VZDUMP_APPLY maximum
;;
custom)
echo
while true; do
read -r -p " Bandwidth limit in MiB/s (0 means unlimited): " bandwidth_mib
[[ "$bandwidth_mib" =~ ^[0-9]+$ ]] && break
echo " Enter a nonnegative whole number."
done
while true; do
read -r -p " I/O priority [0-8, Proxmox default is 7]: " ionice
[[ "$ionice" =~ ^[0-8]$ ]] && break
echo " Enter a value from 0 through 8."
done
bandwidth_kib=$((bandwidth_mib * 1024))
TAPM_POST_VZDUMP_APPLY custom "$bandwidth_kib" "$ionice"
;;
restore) TAPM_POST_RESTORE_MENU ;;
back) return ;;
quit) EXIT1; exit 0 ;;
esac
}
TAPM_HOST_CONFIGURATION_MENU() {
local -a labels
local -a values
while true; do
labels=(
"Audit current host"
"Apply/repair recommended TAPM profile"
"Customize TAPM profile"
)
values=(audit apply customize)
if TAPM_POST_PROXMENUX_DETECTED; then
labels+=("** Migrate from ProxMenux — RECOMMENDED **")
values+=(migrate)
fi
labels+=(
"Configure vzdump performance"
"Restore a TAPM configuration backup"
"View last configuration report"
)
values+=(vzdump restore report)
SELECT_MENU "TAPM Host Configuration" labels values
case "$MENU_SELECTION" in
audit) TAPM_POST_SHOW_AUDIT ;;
apply) TAPM_POST_APPLY_RECOMMENDED ;;
customize) TAPM_POST_CUSTOMIZE ;;
migrate) TAPM_POST_MIGRATE_PROXMENUX ;;
vzdump) TAPM_POST_VZDUMP_MENU ;;
restore) TAPM_POST_RESTORE_MENU ;;
report) TAPM_POST_SHOW_REPORT ;;
back) return ;;
quit) EXIT1; exit 0 ;;
esac
done
}
SHOW_ABOUT() {
MENU_HEADER
echo
echo -e " ${idsCL[LightCyan]}About TA-ProxMenu${idsCL[Default]}"
echo
echo " Version: ${VERS}"
echo " Install: /opt/idssys/ta-proxmenu"
echo " Branch: $(git -C /opt/idssys/ta-proxmenu branch --show-current 2>/dev/null || echo unknown)"
echo
read -r -p " Press ENTER to return..." _
}
HOST_SETUP_MENU() {
local -a labels
local -a values
while true; do
labels=("TAPM Host Configuration")
values=("post_install")
TAPM_POST_PROXMENUX_DETECTED &&
labels[0]="TAPM Host Configuration — ProxMenux migration recommended"
labels+=("Detect CPU model for live migrations")
values+=("cpu")
# Discover ISO storage only after the VirtIO submenu is selected.
labels+=("VirtIO driver downloads")
values+=("virtio")
command -v glances >/dev/null 2>&1 &&
labels+=("Glances CLI monitor (installed)") ||
labels+=("Install Glances CLI monitor")
values+=("glances")
if TAPM_OMSA_SUPPORTED_HARDWARE; then
TAPM_PACKAGE_INSTALLED srvadmin-all &&
labels+=("Dell OMSA - Legacy Dell Hosts (installed)") ||
labels+=("Install Dell OMSA - Legacy Dell Hosts")
values+=("omsa")
fi
SELECT_MENU "Host Setup" labels values
case "$MENU_SELECTION" in
post_install) TAPM_HOST_CONFIGURATION_MENU;;
cpu) DETECT_CPU;;
virtio) VIRTIO_MENU;;
glances) INSTALL_GLANCES;;
omsa) INSTALL_OMSA;;
back) return;;
quit) EXIT1; exit 0;;
esac
done
}
MONITORING_MENU() {
local -a labels
local -a values=("pulse" "rmm" "acronis" "sentinelone" "screenconnect")
local cluster_resources
local pulse_status
while true; do
TAPM_PULSE_RESOURCE_INSTALLED_FROM_CONFIGS
pulse_status=$?
if (( pulse_status == 2 )); then
cluster_resources="$(
pvesh get /cluster/resources --type vm --output-format json 2>/dev/null
)"
TAPM_PULSE_RESOURCE_INSTALLED "$cluster_resources"
pulse_status=$?
fi
if (( pulse_status == 0 )); then
labels=("Pulse monitoring (installed)")
else
labels=($'Install \e[36mPulse\e[39m monitoring')
fi
if systemctl is-active --quiet ITSPlatform; then
labels+=("ConnectWise RMM agent (installed)")
else
labels+=($'Install \e[36mConnectWise RMM\e[39m agent')
fi
if dpkg-query -W -f='${Status}' cyberprotect 2>/dev/null |
grep -q "install ok installed"; then
labels+=("Acronis backup agent (installed)")
else
labels+=($'Install \e[36mAcronis\e[39m backup agent')
fi
if dpkg-query -W -f='${Status}' sentinelagent 2>/dev/null |
grep -q "install ok installed"; then
labels+=("SentinelOne agent (installed)")
else
labels+=($'Install \e[36mSentinelOne\e[39m agent')
fi
if systemctl is-active --quiet 'connectwise*'; then
labels+=("ScreenConnect agent (installed)")
else
labels+=($'Install \e[36mScreenConnect\e[39m agent')
fi
SELECT_MENU "Monitoring & Agents" labels values
case "$MENU_SELECTION" in
pulse) INSTALL_PULSE;;
rmm) INSTALL_RMM;;
acronis) INSTALL_ACRONIS;;
sentinelone) INSTALL_S1;;
screenconnect) INSTALL_SCREENCONNECT;;
back) return;;
quit) EXIT1; exit 0;;
esac
done
}
CLUSTER_MENU() {
local -a labels
local -a values=("maintenance" "services" "keepalived" "iso_nfs")
local maintenance_status
local node
while true; do
node="$(hostname -s)"
TAPM_HA_NODE_IN_MAINTENANCE "$node"
maintenance_status=$?
if (( maintenance_status == 2 )); then
ha-manager status |
grep -F "$node" |
grep -q "maintenance mode"
maintenance_status=$?
fi
if (( maintenance_status == 0 )); then
labels=("Take this host out of maintenance mode")
else
labels=("Put this host into maintenance mode and evacuate guests")
fi
labels+=("Proxmox service recovery")
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
done
}
SERVICE_RECOVERY_MENU() {
local -a labels=(
"Restart Web UI and API (pveproxy, pvedaemon)"
"Restart statistics collection (pvestatd)"
"Restart task scheduler (pvescheduler)"
"Restart core management services"
"Restart cluster filesystem (pve-cluster)"
)
local -a values=("web" "statistics" "scheduler" "core" "clusterfs")
while true; do
SELECT_MENU "Proxmox Service Recovery" labels values
case "$MENU_SELECTION" in
web)
RESTART_SERVICE_GROUP "Web UI and API services" pveproxy pvedaemon
;;
statistics)
RESTART_SERVICE_GROUP "statistics collection" pvestatd
;;
scheduler)
RESTART_SERVICE_GROUP "task scheduler" pvescheduler
;;
core)
RESTART_PVE_SERVICES
;;
clusterfs)
RESTART_CLUSTER_FILESYSTEM
;;
back) return;;
quit) EXIT1; exit 0;;
esac
done
}
SWITCH_SCRIPT_BRANCH() {
local target_branch="$1"
local current_branch
local choice
local target_state=''
current_branch="$(git -C "$FOLDER" branch --show-current 2>/dev/null)"
if [[ "$target_branch" == "$current_branch" ]]; then
echo -e "\n${idsCL[Green]}TA-ProxMenu is already using '${target_branch}'.${idsCL[Default]}"
ENTER2CONTINUE
return
fi
if [[ -n "$(git -C "$FOLDER" status --porcelain --untracked-files=normal)" ]]; then
echo -e "\n${idsCL[Red]}The installed TA-ProxMenu repository has local changes.${idsCL[Default]}"
echo "Branch switching was refused to protect those files."
echo
git -C "$FOLDER" status --short
ENTER2CONTINUE
return
fi
echo -en "\n${idsCL[LightCyan]}Switch TA-ProxMenu from '${current_branch}' to '${target_branch}' (y/N)?${idsCL[Default]} "
read -r -n 1 choice
echo
[[ "$choice" =~ ^[Yy]$ ]] || return
if ! TAPM_GIT_FETCH_BRANCH "$FOLDER" "$target_branch" 30; then
echo -e "${idsCL[Red]}Failed to fetch '${target_branch}' from origin.${idsCL[Default]}"
ENTER2CONTINUE
return
fi
if git -C "$FOLDER" show-ref --verify --quiet "refs/heads/${target_branch}"; then
target_state="$(
TAPM_GIT_RELATION "$FOLDER" "refs/heads/${target_branch}" \
"refs/remotes/origin/${target_branch}"
)" || {
echo -e "${idsCL[Red]}Could not compare local and remote '${target_branch}'.${idsCL[Default]}"
ENTER2CONTINUE
return
}
case "$target_state" in
ahead)
echo -e "${idsCL[LightYellow]}The local '${target_branch}' branch has commits not on origin.${idsCL[Default]}"
echo "Branch switching was refused to preserve those commits."
ENTER2CONTINUE
return
;;
diverged)
echo -e "${idsCL[LightRed]}The local '${target_branch}' branch has diverged from origin.${idsCL[Default]}"
echo "Branch switching was refused; manual Git review is required."
ENTER2CONTINUE
return
;;
esac
git -C "$FOLDER" switch "$target_branch" || {
ENTER2CONTINUE
return
}
else
git -C "$FOLDER" switch --create "$target_branch" \
--track "origin/${target_branch}" || {
ENTER2CONTINUE
return
}
fi
if [[ "$target_state" == "behind" ]]; then
if ! TAPM_GIT_FAST_FORWARD "$FOLDER" "$target_branch"; then
echo -e "${idsCL[Red]}Failed to fast-forward '${target_branch}'; no commits were discarded.${idsCL[Default]}"
ENTER2CONTINUE
return
fi
fi
rm -f "$UPDATE_CACHE_FILE"
echo -e "\n${idsCL[Green]}Now using TA-ProxMenu branch '${target_branch}'.${idsCL[Default]}"
sleep 1
exec /opt/idssys/ta-proxmenu/run.sh
}
BRANCH_MANAGEMENT_MENU() {
local -a labels=()
local -a values=()
local branch
local branch_version
local current_branch
MENU_HEADER
echo
echo -e " ${idsCL[LightCyan]}Refreshing remote branch list...${idsCL[Default]}"
if ! timeout 30 git -C "$FOLDER" fetch origin \
'+refs/heads/*:refs/remotes/origin/*' --prune >/dev/null 2>&1; then
echo -e "\n${idsCL[Red]}Could not retrieve branches from origin.${idsCL[Default]}"
ENTER2CONTINUE
return
fi
current_branch="$(git -C "$FOLDER" branch --show-current 2>/dev/null)"
while IFS= read -r branch; do
[[ -n "$branch" && "$branch" != "HEAD" ]] || continue
branch_version="$(
git -C "$FOLDER" show "refs/remotes/origin/${branch}:defaults.inc" \
2>/dev/null |
awk -F"'" '/^VERS=/{ print $2; exit }'
)"
if [[ "$branch" == "$current_branch" ]]; then
labels+=("${branch} (current, version ${branch_version:-unknown})")
else
labels+=("${branch} (version ${branch_version:-unknown})")
fi
values+=("branch:${branch}")
done < <(
git -C "$FOLDER" for-each-ref \
--format='%(refname:strip=3)' refs/remotes/origin |
sort -V
)
if (( ${#labels[@]} == 0 )); then
echo -e "\n${idsCL[Red]}No remote branches were found.${idsCL[Default]}"
ENTER2CONTINUE
return
fi
while true; do
SELECT_MENU "Git Branch Management" labels values
case "$MENU_SELECTION" in
branch:*)
SWITCH_SCRIPT_BRANCH "${MENU_SELECTION#branch:}"
;;
back) return;;
quit) EXIT1; exit 0;;
esac
done
}
UTILITIES_MENU() {
local -a labels
local -a values=("install_update" "check_update" "branches" "about")
local choice
local last_checked
while true; do
LOAD_UPDATE_STATUS
case "$UPDATE_STATUS" in
behind)
labels=("Install available update")
;;
current)
labels=("TA-ProxMenu is current")
;;
ahead)
labels=("Local branch is ahead of origin")
;;
diverged)
labels=("Local branch has diverged from origin")
;;
dirty)
labels=("Local repository has uncommitted changes")
;;
checking)
labels=("Update check in progress")
;;
*)
labels=("Update status unavailable")
;;
esac
last_checked="$(UPDATE_LAST_CHECKED_DISPLAY)"
labels+=(
"Check again now "$'\e[2m'"(last checked: ${last_checked})"$'\e[22m'
"Git branch management"
"Version and installation information"
)
SELECT_MENU "TA-ProxMenu Management" labels values
case "$MENU_SELECTION" in
install_update)
if [[ "$UPDATE_STATUS" != "behind" ]]; then
echo -e "\n${idsCL[LightCyan]}No available update is currently detected.${idsCL[Default]}"
ENTER2CONTINUE
continue
fi
echo -en "\n${idsCL[LightCyan]}Install the update for the current branch (y/N)?${idsCL[Default]} "
read -r -n 1 choice
echo
if [[ "$choice" =~ ^[Yy]$ ]] &&
/opt/idssys/ta-proxmenu/run.sh update; then
exec /opt/idssys/ta-proxmenu/run.sh
fi
ENTER2CONTINUE
;;
check_update) FORCE_UPDATE_CHECK;;
branches) BRANCH_MANAGEMENT_MENU;;
about) SHOW_ABOUT;;
back) return;;
quit) EXIT1; exit 0;;
esac
done
}
MAIN_MENU() {
local -a labels=(
"Host Setup"
"Monitoring & Agents"
"Cluster & Maintenance"
"TA-ProxMenu Management"
"Quit"
)
local -a values=("host" "monitoring" "cluster" "utilities" "quit")
while true; do
SELECT_MENU "Main Menu" labels values 0
case "$MENU_SELECTION" in
host) HOST_SETUP_MENU;;
monitoring) MONITORING_MENU;;
cluster) CLUSTER_MENU;;
utilities) UTILITIES_MENU;;
quit) EXIT1; exit 0;;
esac
done
}
if (( ACTION_REQUESTED == 1 )); then
case "$action" in
pulse) INSTALL_PULSE;;
rmm) INSTALL_RMM;;
omsa) INSTALL_OMSA;;
glances) INSTALL_GLANCES;;
acronis) INSTALL_ACRONIS;;
post-install|post_install|proxmenux) TAPM_HOST_CONFIGURATION_MENU;;
virtio) VIRTIO_MENU;;
sentinelone|s1) INSTALL_S1;;
screenconnect) INSTALL_SCREENCONNECT;;
restart) RESTART_PVE_SERVICES "${2:-}";;
cpu) DETECT_CPU;;
maintenance|mm) MAINTENANCE_MODE;;
keepalived) INSTALL_KEEPALIVE;;
iso-nfs|iso_nfs) TAPM_DEPLOY_ISO_NFS_LXC;;
*) MAIN_MENU;;
esac
else
MAIN_MENU
fi
exit 0