#!/usr/bin/env bash # Cluster discovery and remote update helpers for TA-ProxMenu. TAPM_COROSYNC_CONFIG="${TAPM_COROSYNC_CONFIG:-/etc/pve/corosync.conf}" TAPM_CLUSTER_NODES_FROM_JSON() { python3 -c ' import json, re, sys try: payload = json.load(sys.stdin) except (json.JSONDecodeError, OSError): raise SystemExit(1) if isinstance(payload, dict): payload = payload.get("data", []) if not isinstance(payload, list): raise SystemExit(1) valid_name = re.compile(r"^[A-Za-z0-9][A-Za-z0-9.-]{0,62}$") rows = [] for item in payload: if not isinstance(item, dict): continue name = str(item.get("node", "")).strip() status = str(item.get("status", "unknown")).strip().lower() if valid_name.fullmatch(name): rows.append((name, status)) for name, status in sorted(rows): print(f"{name}\t{status}") ' } TAPM_CLUSTER_NODE_ROWS() { local node_json node_json="$(timeout 10 pvesh get /nodes --output-format json 2>/dev/null)" || return 1 printf '%s' "$node_json" | TAPM_CLUSTER_NODES_FROM_JSON } TAPM_LOCAL_CLUSTER_NODE() { local local_link='' local_link="$(readlink /etc/pve/local 2>/dev/null || true)" if [[ -n "$local_link" ]]; then basename "$local_link" else hostname -s fi } TAPM_UPDATE_REMOTE_NODE() { local node="$1" [[ "$node" =~ ^[A-Za-z0-9][A-Za-z0-9.-]{0,62}$ ]] || return 1 timeout 240 ssh \ -o BatchMode=yes \ -o ConnectTimeout=10 \ -o ServerAliveInterval=15 \ -o ServerAliveCountMax=2 \ "root@${node}" \ '/opt/idssys/ta-proxmenu/run.sh update --local-only' } INSTALL_CLUSTER_UPDATES() { local index local local_node local node local required_command local status local node_rows='' local cluster_failed=0 local -a cluster_nodes=() local -a cluster_statuses=() if [[ ! -s "$TAPM_COROSYNC_CONFIG" ]]; then INSTALL_LOCAL_UPDATES return $? fi for required_command in pvesh python3 ssh timeout; do if ! command -v "$required_command" >/dev/null 2>&1; then echo -e "${idsCL[Red]}Cluster update requires ${required_command}; no nodes were updated.${idsCL[Default]}" return 1 fi done node_rows="$(TAPM_CLUSTER_NODE_ROWS)" || { echo -e "${idsCL[Red]}Could not retrieve the Proxmox cluster node list; no nodes were updated.${idsCL[Default]}" return 1 } while IFS=$'\t' read -r node status; do [[ -n "$node" ]] || continue cluster_nodes+=("$node") cluster_statuses+=("$status") done <<<"$node_rows" if (( ${#cluster_nodes[@]} == 0 )); then echo -e "${idsCL[Red]}The Proxmox API returned no cluster nodes; no nodes were updated.${idsCL[Default]}" return 1 fi local_node="$(TAPM_LOCAL_CLUSTER_NODE)" echo -e "${idsCL[LightCyan]}Updating TA-ProxMenu across ${#cluster_nodes[@]} cluster node(s)...${idsCL[Default]}" for index in "${!cluster_nodes[@]}"; do node="${cluster_nodes[$index]}" status="${cluster_statuses[$index]}" if [[ "$node" == "$local_node" ]]; then continue fi if [[ "$status" != "online" ]]; then printf '\n' echo -e "${idsCL[LightYellow]}Skipping ${node}: node status is ${status}.${idsCL[Default]}" cluster_failed=1 continue fi printf '\n' echo -e "${idsCL[LightCyan]}Updating remote node ${node}...${idsCL[Default]}" if TAPM_UPDATE_REMOTE_NODE "$node"; then echo -e "${idsCL[Green]}Remote node ${node} is updated.${idsCL[Default]}" else echo -e "${idsCL[Red]}Remote node ${node} failed to update.${idsCL[Default]}" cluster_failed=1 fi done printf '\n' echo -e "${idsCL[LightCyan]}Updating local node ${local_node}...${idsCL[Default]}" if INSTALL_LOCAL_UPDATES; then echo -e "${idsCL[Green]}Local node ${local_node} is updated.${idsCL[Default]}" else echo -e "${idsCL[Red]}Local node ${local_node} failed to update.${idsCL[Default]}" cluster_failed=1 fi printf '\n' if (( cluster_failed == 0 )); then echo -e "${idsCL[Green]}TA-ProxMenu is updated on all ${#cluster_nodes[@]} cluster node(s).${idsCL[Default]}" return 0 fi echo -e "${idsCL[LightYellow]}Cluster update completed with failures; review the node messages above.${idsCL[Default]}" return 1 }