#!/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 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 } 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_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 } 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_REQUESTED_ACTION="$required_action" TAPM_REQUESTED_PACKAGE="$required_package" \ python3 -c 'import json, os, sys; json.dump({"code": os.environ["TAPM_CODE"], "host_fingerprint": os.environ["TAPM_FINGERPRINT"], "hostname": os.environ["TAPM_HOSTNAME"], "requested_action": os.environ["TAPM_REQUESTED_ACTION"], "requested_package": os.environ["TAPM_REQUESTED_PACKAGE"]}, sys.stdout)' | 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() { local installer local temp_dir echo if ! TAPM_CREATE_TEMP_DIR pulse; then FINISH_FAILED_ACTION return fi temp_dir="$TAPM_TEMP_DIR" installer="${temp_dir}/install.sh" if ! TAPM_DOWNLOAD_HTTPS \ 'https://github.com/rcourtman/Pulse/releases/latest/download/install.sh' \ "$installer" 'Pulse installer' || ! bash "$installer"; then TAPM_CLEAN_TEMP_DIR "$temp_dir" echo -e "${idsCL[LightRed]}Pulse installation failed.${idsCL[Default]}" FINISH_FAILED_ACTION return fi TAPM_CLEAN_TEMP_DIR "$temp_dir" echo echo -e "\n${idsCL[Green]}Pulse installer completed successfully.${idsCL[Default]}" 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_PROXMENUX() { local installer local temp_dir if ! TAPM_CREATE_TEMP_DIR proxmenux; then FINISH_FAILED_ACTION return fi temp_dir="$TAPM_TEMP_DIR" installer="${temp_dir}/install.sh" if ! TAPM_DOWNLOAD_HTTPS \ 'https://raw.githubusercontent.com/MacRimi/ProxMenux/main/install_proxmenux.sh' \ "$installer" 'ProxMenux installer' || ! bash "$installer" || [[ ! -x /usr/local/bin/menu ]]; then TAPM_CLEAN_TEMP_DIR "$temp_dir" echo -e "${idsCL[LightRed]}ProxMenux installation failed or could not be verified.${idsCL[Default]}" FINISH_FAILED_ACTION return fi TAPM_CLEAN_TEMP_DIR "$temp_dir" /usr/local/bin/menu } PROXMENUX_POST_INSTALL() { PMFLDR='/usr/local/share/proxmenux/scripts/post_install' [ ! -f "${PMFLDR}/customizable_post_install.sh" ] && INSTALL_PROXMENUX bash "${PMFLDR}/customizable_post_install.sh" touch /opt/.PROXMENUX_POST_INSTALL [ -s /etc/apt/sources.list ] && cat /dev/null > /etc/apt/sources.list } 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 } INSTALL_OMSA() { echo mkdir -p /tmp/omsa cd /tmp/omsa || return 1 apt install -y gnupg libcurl4t64 libncurses6 libxslt1.1 libgpm2 libtinfo6 mkdir -p /etc/apt/keyrings wget -qO - https://linux.dell.com/repo/pgp_pubkeys/0x1285491434D8786F.asc | gpg --dearmor -o /etc/apt/keyrings/linux.dell.com.gpg chmod +r /etc/apt/keyrings/linux.dell.com.gpg echo "deb [signed-by=/etc/apt/keyrings/linux.dell.com.gpg] http://linux.dell.com/repo/community/openmanage/11000/jammy jammy main" > /etc/apt/sources.list.d/linux.dell.com.list apt update wget -c http://archive.ubuntu.com/ubuntu/pool/universe/o/openwsman/libwsman-curl-client-transport1_2.6.5-0ubuntu16_amd64.deb wget -c http://archive.ubuntu.com/ubuntu/pool/universe/o/openwsman/libwsman-client4t64_2.6.5-0ubuntu16_amd64.deb wget -c http://archive.ubuntu.com/ubuntu/pool/universe/o/openwsman/libwsman1t64_2.6.5-0ubuntu16_amd64.deb # wget -c http://http.us.debian.org/debian/pool/main/libx/libxml2/libxml2-16_2.15.1+dfsg-2+b1_amd64.deb wget -c http://http.us.debian.org/debian/pool/main/libx/libxml2/libxml2-16_2.15.2+dfsg-0.1_amd64.deb wget -c http://archive.ubuntu.com/ubuntu/pool/universe/o/openwsman/libwsman-server1t64_2.6.5-0ubuntu16_amd64.deb wget -c http://archive.ubuntu.com/ubuntu/pool/universe/s/sblim-sfcc/libcimcclient0_2.2.8-0ubuntu2_amd64.deb wget -c http://archive.ubuntu.com/ubuntu/pool/universe/o/openwsman/openwsman_2.6.5-0ubuntu16_amd64.deb wget -c http://archive.ubuntu.com/ubuntu/pool/multiverse/c/cim-schema/cim-schema_2.48.0-0ubuntu1_all.deb wget -c http://archive.ubuntu.com/ubuntu/pool/universe/s/sblim-sfc-common/libsfcutil0_1.0.1-0ubuntu4_amd64.deb wget -c http://archive.ubuntu.com/ubuntu/pool/multiverse/s/sblim-sfcb/sfcb_1.4.9-0ubuntu7_amd64.deb wget -c http://archive.ubuntu.com/ubuntu/pool/universe/s/sblim-cmpi-devel/libcmpicppimpl0_2.0.3-0ubuntu2_amd64.deb wget -c http://ftp.us.debian.org/debian/pool/main/o/openssl/libssl1.1_1.1.1w-0+deb11u1_amd64.deb dpkg -i libwsman-curl-client-transport1_2.6.5-0ubuntu16_amd64.deb dpkg -i libwsman-client4t64_2.6.5-0ubuntu16_amd64.deb dpkg -i libxml2-16_2.15.2+dfsg-0.1_amd64.deb dpkg -i libwsman1t64_2.6.5-0ubuntu16_amd64.deb dpkg -i libwsman-server1t64_2.6.5-0ubuntu16_amd64.deb dpkg -i libcimcclient0_2.2.8-0ubuntu2_amd64.deb dpkg -i openwsman_2.6.5-0ubuntu16_amd64.deb dpkg -i cim-schema_2.48.0-0ubuntu1_all.deb dpkg -i libsfcutil0_1.0.1-0ubuntu4_amd64.deb dpkg -i sfcb_1.4.9-0ubuntu7_amd64.deb dpkg -i libcmpicppimpl0_2.0.3-0ubuntu2_amd64.deb dpkg -i libssl1.1_1.1.1w-0+deb11u1_amd64.deb apt install -y srvadmin-all /opt/dell/srvadmin/sbin/srvadmin-services.sh start rm -rf -- /tmp/omsa echo -e "\n${idsCL[Green]}Dell OMSA has been installed${idsCL[Default]}" echo -e "\n${idsCL[LightCyan]}Available at: ${idsCL[LightGreen]}https://${RNIP}:1311${idsCL[Default]}" FINISH_ACTION } DOWNLOAD_VIRTIO() { echo -e "\n${idsCL[LightCyan]}Current \"Stable\" version available for download: ${idsCL[White]}${VIRTIO_FILE}${idsCL[Default]}" if [ -f "${DLDIR}/${VIRTIO_FILE}" ]; then echo -en "\n${idsCL[LightRed]}Removing existing download ... " rm -f "${DLDIR}/${VIRTIO_FILE}" echo -e "${idsCL[Red]}Done${idsCL[Default]}" fi wget -q -P "$DLDIR" "$VIRTIO_DOWNLOAD_URL" & echo -e "\n${idsCL[LightCyan]}Downloading will continue in the background\n" FINISH_ACTION } DETECT_CPU(){ # if [ ! -f /etc/apt/sources.list.d/proxlb.list ]; then # echo "deb https://repo.gyptazy.com/stable /" > /etc/apt/sources.list.d/proxlb.list # wget -O /etc/apt/trusted.gpg.d/proxlb.asc https://repo.gyptazy.com/repository.gpg # apt-get update # fi if [ ! -f /etc/apt/sources.list.d/gyptazy.list ]; then curl https://git.gyptazy.com/api/packages/gyptazy/debian/repository.key -o /etc/apt/keyrings/gyptazy.asc echo "deb [signed-by=/etc/apt/keyrings/gyptazy.asc] https://packages.gyptazy.com/api/packages/gyptazy/debian trixie main" | sudo tee -a /etc/apt/sources.list.d/gyptazy.list apt update fi if [ "$(dpkg -l | awk '/proxclmc/ {print }'|wc -l)" -eq 0 ]; then apt -y install proxclmc fi echo proxclmc echo echo -en "${idsCL[LightCyan]}Would you like to set '${idsCL[LightGreen]}cpu: $(proxclmc --list-only)${idsCL[LightCyan]}' on all VMs (y/N)?${idsCL[Default]} " read -n 1 choice case "$choice" in [Yy]) sed -i "/cpu:/c cpu: $(proxclmc --list-only)" /etc/pve/nodes/*/qemu-server/*.conf echo echo -e "\n${idsCL[Green]}All VM's have been reconfigured\n${idsCL[LightCyan]}This will require the VM's to be powered off and then turned back on in order to take effect${idsCL[Default]}" FINISH_ACTION ;; *) echo;; esac } 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 -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 -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 } MAINTENANCE_MODE(){ local local_node local_node="$(hostname -s)" if ha-manager status | grep -F "$local_node" | grep -q "maintenance mode"; 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 -n 1 choice case "$choice" in [Nn]) echo;; *) echo if ha-manager status | grep -F "$local_node" | grep -q "maintenance mode"; then ha-manager crm-command node-maintenance disable "$local_node" echo -e "\n${idsCL[Green]}This host will be taken out of maintenance mode${idsCL[Default]}\n" else ha-manager crm-command node-maintenance enable "$local_node" echo -e "\n${idsCL[Green]}This host will be entered into maintenance mode${idsCL[Default]}\n" bash /opt/idssys/ta-proxmenu/inc/evacuate-proxmox-node.sh 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 } 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 LOAD_UPDATE_STATUS 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 } 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=( "post_install" "cpu" "virtio" "glances" "omsa" ) while true; do labels=("Run ProxMenux post-install configuration") [ -f /opt/.PROXMENUX_POST_INSTALL ] && labels[0]="Run ProxMenux post-install configuration (previously run)" labels+=("Detect CPU model for live migrations") if [ -f "${DLDIR}/${VIRTIO_FILE}" ]; then labels+=("VirtIO drivers (${VIRTIO_FILE} already downloaded)") elif compgen -G "${DLDIR}/virtio*.iso" >/dev/null; then labels+=("Download updated VirtIO drivers") else labels+=("Download current VirtIO drivers") fi command -v glances >/dev/null 2>&1 && labels+=("Glances CLI monitor (installed)") || labels+=("Install Glances CLI monitor") dpkg-query -W -f='${Status}' srvadmin-all 2>/dev/null | grep -q "install ok installed" && labels+=("Dell OpenManage Server Administrator (installed)") || labels+=("Install Dell OpenManage Server Administrator") SELECT_MENU "Host Setup" labels values case "$MENU_SELECTION" in post_install) PROXMENUX_POST_INSTALL;; cpu) DETECT_CPU;; virtio) DOWNLOAD_VIRTIO;; 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 while true; do cluster_resources="$(pvesh get /cluster/resources 2>/dev/null)" grep -qi pulse <<< "$cluster_resources" && labels=("Pulse monitoring (installed)") || labels=("Install Pulse monitoring") systemctl is-active --quiet ITSPlatform && labels+=("ConnectWise RMM agent (installed)") || labels+=("Install ConnectWise RMM agent") dpkg-query -W -f='${Status}' cyberprotect 2>/dev/null | grep -q "install ok installed" && labels+=("Acronis backup agent (installed)") || labels+=("Install Acronis backup agent") dpkg-query -W -f='${Status}' sentinelagent 2>/dev/null | grep -q "install ok installed" && labels+=("SentinelOne agent (installed)") || labels+=("Install SentinelOne agent") systemctl is-active --quiet 'connectwise*' && labels+=("ScreenConnect agent (installed)") || labels+=("Install ScreenConnect agent") 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") while true; do if ha-manager status | grep -F "$(hostname -s)" | grep -q "maintenance mode"; 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") SELECT_MENU "Cluster & Maintenance" labels values case "$MENU_SELECTION" in maintenance) MAINTENANCE_MODE;; services) SERVICE_RECOVERY_MENU;; keepalived) INSTALL_KEEPALIVE;; 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 -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 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 labels+=( "Check again now" "Git branch management" "Version and installation information" ) SELECT_MENU "Utilities" 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 -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" "Utilities" "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_POST_INSTALL;; proxmenux) [ ! -f /usr/local/bin/menu ] && INSTALL_PROXMENUX || /usr/local/bin/menu;; virtio) DOWNLOAD_VIRTIO;; sentinelone|s1) INSTALL_S1;; screenconnect) INSTALL_SCREENCONNECT;; restart) RESTART_PVE_SERVICES "${2:-}";; cpu) DETECT_CPU;; maintenance|mm) MAINTENANCE_MODE;; keepalived) INSTALL_KEEPALIVE;; *) MAIN_MENU;; esac else MAIN_MENU fi exit 0