#!/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 /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 --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 allow_space="${5:-0}" local initial_selected="${6:-0}" local -n labels_ref="$labels_name" local -n values_ref="$values_name" local selected="$initial_selected" local key local sequence local index [[ "$selected" =~ ^[0-9]+$ ]] || selected=0 (( selected < ${#labels_ref[@]} )) || selected=0 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 && allow_space == 1 )); then echo " ↑/↓ Navigate Space Toggle Enter Select Number Quick Select ←/Esc/B Back Q Quit" elif (( allow_back == 1 )); then echo " ↑/↓ Navigate Enter Select Number Quick Select ←/Esc/B Back Q Quit" elif (( allow_space == 1 )); then echo " ↑/↓ Navigate Space Toggle Enter Select Number Quick Select Q Quit" else echo " ↑/↓ Navigate Enter Select Number Quick Select Q Quit" fi MENU_INPUT="" MENU_SELECTED_INDEX="$selected" key="" if [[ "$UPDATE_STATUS" == "checking" ]]; then IFS= read -rsn1 -t 1 key || continue else IFS= read -rsn1 key fi case "$key" in "") MENU_INPUT="enter" MENU_SELECTION="${values_ref[$selected]}" return 0 ;; " ") if (( allow_space == 1 )); then MENU_INPUT="space" MENU_SELECTION="${values_ref[$selected]}" return 0 fi ;; [1-9]) index=$((10#$key - 1)) if (( index < ${#values_ref[@]} )); then selected="$index" MENU_SELECTED_INDEX="$selected" MENU_INPUT="number" 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 local selected_index=0 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 1 1 \ "$selected_index" selected_index="$MENU_SELECTED_INDEX" if [[ "$MENU_INPUT" == space && "$MENU_SELECTION" == apply ]]; then continue fi 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